Merge branch 'master' into Fank-patch-1

This commit is contained in:
Wliu
2017-10-30 15:10:23 +01:00
committed by GitHub
40 changed files with 2036 additions and 1676 deletions

View File

@@ -6,6 +6,6 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
"atom-package-manager": "1.18.8"
"atom-package-manager": "1.18.10"
}
}

View File

@@ -132,6 +132,7 @@
'ctrl-shift-w': 'editor:select-word'
'cmd-ctrl-left': 'editor:move-selection-left'
'cmd-ctrl-right': 'editor:move-selection-right'
'cmd-shift-V': 'editor:paste-without-reformatting'
# Emacs
'alt-f': 'editor:move-to-end-of-word'

View File

@@ -105,6 +105,7 @@
'alt-shift-right': 'editor:select-to-next-subword-boundary'
'alt-backspace': 'editor:delete-to-beginning-of-subword'
'alt-delete': 'editor:delete-to-end-of-subword'
'ctrl-shift-V': 'editor:paste-without-reformatting'
# Sublime Parity
'ctrl-a': 'core:select-all'

View File

@@ -110,6 +110,7 @@
'alt-shift-right': 'editor:select-to-next-subword-boundary'
'alt-backspace': 'editor:delete-to-beginning-of-subword'
'alt-delete': 'editor:delete-to-end-of-subword'
'ctrl-shift-V': 'editor:paste-without-reformatting'
# Sublime Parity
'ctrl-a': 'core:select-all'

View File

@@ -65,6 +65,7 @@
{ label: 'Copy', command: 'core:copy' }
{ label: 'Copy Path', command: 'editor:copy-path' }
{ label: 'Paste', command: 'core:paste' }
{ label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' }
{ label: 'Select All', command: 'core:select-all' }
{ type: 'separator' }
{ label: 'Toggle Comments', command: 'editor:toggle-line-comments' }

View File

@@ -38,6 +38,7 @@
{ label: 'C&opy', command: 'core:copy' }
{ label: 'Copy Pat&h', command: 'editor:copy-path' }
{ label: '&Paste', command: 'core:paste' }
{ label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' }
{ label: 'Select &All', command: 'core:select-all' }
{ type: 'separator' }
{ label: '&Toggle Comments', command: 'editor:toggle-line-comments' }

View File

@@ -46,6 +46,7 @@
{ label: '&Copy', command: 'core:copy' }
{ label: 'Copy Pat&h', command: 'editor:copy-path' }
{ label: '&Paste', command: 'core:paste' }
{ label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' }
{ label: 'Select &All', command: 'core:select-all' }
{ type: 'separator' }
{ label: '&Toggle Comments', command: 'editor:toggle-line-comments' }

View File

@@ -14,9 +14,10 @@
"license": "MIT",
"electronVersion": "1.6.15",
"dependencies": {
"@atom/nsfw": "^1.0.18",
"@atom/source-map-support": "^0.3.4",
"async": "0.2.6",
"atom-keymap": "8.2.7",
"atom-keymap": "8.2.8",
"atom-select-list": "^0.1.0",
"atom-ui": "0.4.1",
"babel-core": "5.8.38",
@@ -24,14 +25,14 @@
"chai": "3.5.0",
"chart.js": "^2.3.0",
"clear-cut": "^2.0.2",
"coffee-script": "1.11.1",
"coffee-script": "1.12.7",
"color": "^0.7.3",
"dedent": "^0.6.0",
"dedent": "^0.7.0",
"devtron": "1.3.0",
"etch": "^0.12.6",
"event-kit": "^2.4.0",
"find-parent-dir": "^0.3.0",
"first-mate": "7.0.9",
"first-mate": "7.1.0",
"focus-trap": "^2.3.0",
"fs-admin": "^0.1.6",
"fs-plus": "^3.0.1",
@@ -53,7 +54,6 @@
"mocha-multi-reporters": "^1.1.4",
"mock-spawn": "^0.2.6",
"normalize-package-data": "^2.0.0",
"nsfw": "^1.0.15",
"nslog": "^3",
"oniguruma": "6.2.1",
"pathwatcher": "8.0.1",
@@ -65,12 +65,12 @@
"scandal": "^3.1.0",
"scoped-property-store": "^0.17.0",
"scrollbar-style": "^3.2",
"season": "^6.0.1",
"season": "^6.0.2",
"semver": "^4.3.3",
"service-hub": "^0.7.4",
"sinon": "1.17.4",
"temp": "^0.8.3",
"text-buffer": "13.5.7",
"text-buffer": "13.7.1",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"winreg": "^1.2.1",
@@ -91,10 +91,10 @@
"solarized-light-syntax": "1.1.2",
"about": "1.7.8",
"archive-view": "0.63.4",
"autocomplete-atom-api": "0.10.3",
"autocomplete-css": "0.17.3",
"autocomplete-html": "0.8.2",
"autocomplete-plus": "2.36.8",
"autocomplete-atom-api": "0.10.5",
"autocomplete-css": "0.17.4",
"autocomplete-html": "0.8.3",
"autocomplete-plus": "2.37.2",
"autocomplete-snippets": "1.11.2",
"autoflow": "0.29.0",
"autosave": "0.24.6",
@@ -112,63 +112,63 @@
"github": "0.7.0",
"git-diff": "1.3.6",
"go-to-line": "0.32.1",
"grammar-selector": "0.49.6",
"grammar-selector": "0.49.8",
"image-view": "0.62.4",
"incompatible-packages": "0.27.3",
"keybinding-resolver": "0.38.0",
"line-ending-selector": "0.7.4",
"link": "0.31.3",
"markdown-preview": "0.159.15",
"markdown-preview": "0.159.17",
"metrics": "1.2.6",
"notifications": "0.69.2",
"open-on-github": "1.2.1",
"package-generator": "1.1.1",
"settings-view": "0.252.0",
"snippets": "1.1.5",
"settings-view": "0.252.2",
"snippets": "1.1.7",
"spell-check": "0.72.3",
"status-bar": "1.8.13",
"styleguide": "0.49.7",
"status-bar": "1.8.14",
"styleguide": "0.49.8",
"symbols-view": "0.118.1",
"tabs": "0.108.0",
"timecop": "0.36.0",
"tree-view": "0.220.0",
"tree-view": "0.221.0",
"update-package-dependencies": "0.12.0",
"welcome": "0.36.5",
"whitespace": "0.37.4",
"wrap-guide": "0.40.2",
"language-c": "0.58.1",
"language-clojure": "0.22.4",
"language-coffee-script": "0.49.1",
"language-coffee-script": "0.49.2",
"language-csharp": "0.14.3",
"language-css": "0.42.6",
"language-gfm": "0.90.1",
"language-css": "0.42.7",
"language-gfm": "0.90.2",
"language-git": "0.19.1",
"language-go": "0.44.2",
"language-html": "0.48.1",
"language-hyperlink": "0.16.2",
"language-java": "0.27.4",
"language-javascript": "0.127.5",
"language-go": "0.44.3",
"language-html": "0.48.2",
"language-hyperlink": "0.16.3",
"language-java": "0.27.5",
"language-javascript": "0.127.6",
"language-json": "0.19.1",
"language-less": "0.33.0",
"language-less": "0.34.0",
"language-make": "0.22.3",
"language-mustache": "0.14.3",
"language-mustache": "0.14.4",
"language-objective-c": "0.15.1",
"language-perl": "0.38.1",
"language-php": "0.42.1",
"language-php": "0.42.2",
"language-property-list": "0.9.1",
"language-python": "0.45.4",
"language-ruby": "0.71.3",
"language-python": "0.45.5",
"language-ruby": "0.71.4",
"language-ruby-on-rails": "0.25.2",
"language-sass": "0.61.1",
"language-shellscript": "0.25.3",
"language-sass": "0.61.2",
"language-shellscript": "0.25.4",
"language-source": "0.9.0",
"language-sql": "0.25.8",
"language-text": "0.7.3",
"language-todo": "0.29.2",
"language-todo": "0.29.3",
"language-toml": "0.18.1",
"language-typescript": "0.2.2",
"language-xml": "0.35.2",
"language-yaml": "0.31.0"
"language-yaml": "0.31.1"
},
"private": true,
"scripts": {

View File

@@ -27,47 +27,37 @@ module.exports = function (packagedAppPath) {
coreModules.has(modulePath) ||
(relativePath.startsWith(path.join('..', 'src')) && relativePath.endsWith('-element.js')) ||
relativePath.startsWith(path.join('..', 'node_modules', 'dugite')) ||
relativePath.endsWith(path.join('node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js')) ||
relativePath.endsWith(path.join('node_modules', 'fs-extra', 'lib', 'index.js')) ||
relativePath.endsWith(path.join('node_modules', 'graceful-fs', 'graceful-fs.js')) ||
relativePath.endsWith(path.join('node_modules', 'htmlparser2', 'lib', 'index.js')) ||
relativePath.endsWith(path.join('node_modules', 'minimatch', 'minimatch.js')) ||
relativePath === path.join('..', 'exports', 'atom.js') ||
relativePath === path.join('..', 'src', 'electron-shims.js') ||
relativePath === path.join('..', 'src', 'safe-clipboard.js') ||
relativePath === path.join('..', 'node_modules', 'atom-keymap', 'lib', 'command-event.js') ||
relativePath === path.join('..', 'node_modules', 'babel-core', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'cached-run-in-this-context', 'lib', 'main.js') ||
relativePath === path.join('..', 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js') ||
relativePath === path.join('..', 'node_modules', 'cson-parser', 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js') ||
relativePath === path.join('..', 'node_modules', 'decompress-zip', 'lib', 'decompress-zip.js') ||
relativePath === path.join('..', 'node_modules', 'debug', 'node.js') ||
relativePath === path.join('..', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'github', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'git-utils', 'src', 'git.js') ||
relativePath === path.join('..', 'node_modules', 'glob', 'glob.js') ||
relativePath === path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') ||
relativePath === path.join('..', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'markdown-preview', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'roaster', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'task-lists', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'iconv-lite', 'lib', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'less', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') ||
relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less-node', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'less', 'node_modules', 'graceful-fs', 'graceful-fs.js') ||
relativePath === path.join('..', 'node_modules', 'minimatch', 'minimatch.js') ||
relativePath === path.join('..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js') ||
relativePath === path.join('..', 'node_modules', 'nsfw', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'superstring', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') ||
relativePath === path.join('..', 'node_modules', 'request', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'resolve', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') ||
relativePath === path.join('..', 'node_modules', 'scandal', 'node_modules', 'minimatch', 'minimatch.js') ||
relativePath === path.join('..', 'node_modules', 'settings-view', 'node_modules', 'glob', 'glob.js') ||
relativePath === path.join('..', 'node_modules', 'settings-view', 'node_modules', 'minimatch', 'minimatch.js') ||
relativePath === path.join('..', 'node_modules', 'spellchecker', 'lib', 'spellchecker.js') ||
relativePath === path.join('..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'tar', 'tar.js') ||
relativePath === path.join('..', 'node_modules', 'temp', 'lib', 'temp.js') ||
relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') ||
relativePath === path.join('..', 'node_modules', 'tree-view', 'node_modules', 'minimatch', 'minimatch.js')
relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js')
)
}
}).then((snapshotScript) => {

View File

@@ -1,5 +1,6 @@
'name': 'Test Ruby'
'scopeName': 'test.rb'
'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)'
'fileTypes': [
'rb'
]

View File

@@ -120,6 +120,8 @@ describe "the `grammars` global", ->
atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true
atom.grammars.grammarForScopeName('test.rb').bundledPackage = false
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe 'source.ruby'
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe 'test.rb'
expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe 'test.rb'
describe "when there is no file path", ->

View File

@@ -103,6 +103,11 @@ describe "Selection", ->
selection.insertText("\r\n", autoIndent: true)
expect(buffer.lineForRow(2)).toBe " "
it "does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true", ->
selection.setBufferRange [[5, 0], [5, 0]]
selection.insertText(' foo\n bar\n', preserveTrailingLineIndentation: true, indentBasis: 1)
expect(buffer.lineForRow(6)).toBe(' bar')
describe ".fold()", ->
it "folds the buffer range spanned by the selection", ->
selection.setBufferRange([[0, 3], [1, 6]])

View File

@@ -58,7 +58,7 @@ if specPackagePath = FindParentDir.sync(testPaths[0], 'package.json')
if specDirectory = FindParentDir.sync(testPaths[0], 'fixtures')
specProjectPath = path.join(specDirectory, 'fixtures')
else
specProjectPath = path.join(__dirname, 'fixtures')
specProjectPath = require('os').tmpdir()
beforeEach ->
atom.project.setPaths([specProjectPath])

View File

@@ -1896,6 +1896,8 @@ describe('TextEditorComponent', () => {
const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'})
await component.getNextUpdatePromise()
const overlayComponent = component.overlayComponents.values().next().value
const overlayWrapper = overlayElement.parentElement
expect(overlayWrapper.classList.contains('a')).toBe(true)
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
@@ -1926,12 +1928,12 @@ describe('TextEditorComponent', () => {
await setScrollTop(component, 20)
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
overlayElement.style.height = 60 + 'px'
await component.getNextUpdatePromise()
await overlayComponent.getNextUpdatePromise()
expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4))
// Does not flip the overlay vertically if it would overflow the top of the window
overlayElement.style.height = 80 + 'px'
await component.getNextUpdatePromise()
await overlayComponent.getNextUpdatePromise()
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
// Can update overlay wrapper class

View File

@@ -1871,7 +1871,7 @@ describe "TextEditor", ->
expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]]
describe "when the 'preserveFolds' option is false (the default)", ->
it "removes folds that contain the selections", ->
it "removes folds that contain one or both of the selection's end points", ->
editor.setSelectedBufferRange([[0, 0], [0, 0]])
editor.foldBufferRowRange(1, 4)
editor.foldBufferRowRange(2, 3)
@@ -1884,6 +1884,9 @@ describe "TextEditor", ->
expect(editor.isFoldedAtScreenRow(6)).toBeFalsy()
expect(editor.isFoldedAtScreenRow(10)).toBeTruthy()
editor.setSelectedBufferRange([[10, 0], [12, 0]])
expect(editor.isFoldedAtScreenRow(10)).toBeTruthy()
describe "when the 'preserveFolds' option is true", ->
it "does not remove folds that contain the selections", ->
editor.setSelectedBufferRange([[0, 0], [0, 0]])
@@ -4222,6 +4225,19 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;")
expect(editor.getCursorBufferPosition()).toEqual([3, 13])
it "respects options that preserve the formatting of the pasted text", ->
editor.update({autoIndentOnPaste: true})
atom.clipboard.write("a(x);\n b(x);\r\nc(x);\n", indentBasis: 0)
editor.setCursorBufferPosition([5, 0])
editor.insertText(' ')
editor.pasteText({autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false})
expect(editor.lineTextForBufferRow(5)).toBe " a(x);"
expect(editor.lineTextForBufferRow(6)).toBe " b(x);"
expect(editor.buffer.lineEndingForRow(6)).toBe "\r\n"
expect(editor.lineTextForBufferRow(7)).toBe "c(x);"
expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();"
describe ".indentSelectedRows()", ->
describe "when nothing is selected", ->
describe "when softTabs is enabled", ->
@@ -4363,108 +4379,6 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(4)).toBe " }"
expect(editor.lineTextForBufferRow(5)).toBe " i=1"
describe ".toggleLineCommentsInSelection()", ->
it "toggles comments on the selected lines", ->
editor.setSelectedBufferRange([[4, 5], [7, 5]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {"
expect(buffer.lineForRow(5)).toBe " // current = items.shift();"
expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);"
expect(buffer.lineForRow(7)).toBe " // }"
expect(editor.getSelectedBufferRange()).toEqual [[4, 8], [7, 8]]
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {"
expect(buffer.lineForRow(5)).toBe " current = items.shift();"
expect(buffer.lineForRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);"
expect(buffer.lineForRow(7)).toBe " }"
it "does not comment the last line of a non-empty selection if it ends at column 0", ->
editor.setSelectedBufferRange([[4, 5], [7, 0]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {"
expect(buffer.lineForRow(5)).toBe " // current = items.shift();"
expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);"
expect(buffer.lineForRow(7)).toBe " }"
it "uncomments lines if all lines match the comment regex", ->
editor.setSelectedBufferRange([[0, 0], [0, 1]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(0)).toBe "// // var quicksort = function () {"
expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {"
expect(buffer.lineForRow(2)).toBe "// if (items.length <= 1) return items;"
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
expect(buffer.lineForRow(1)).toBe " var sort = function(items) {"
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
editor.setSelectedBufferRange([[0, 0], [0, Infinity]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
it "uncomments commented lines separated by an empty line", ->
editor.setSelectedBufferRange([[0, 0], [1, Infinity]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {"
buffer.insert([0, Infinity], '\n')
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
expect(buffer.lineForRow(1)).toBe ""
expect(buffer.lineForRow(2)).toBe " var sort = function(items) {"
it "preserves selection emptiness", ->
editor.setCursorBufferPosition([4, 0])
editor.toggleLineCommentsInSelection()
expect(editor.getLastSelection().isEmpty()).toBeTruthy()
it "does not explode if the current language mode has no comment regex", ->
editor = new TextEditor(buffer: new TextBuffer(text: 'hello'))
editor.setSelectedBufferRange([[0, 0], [0, 5]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(0)).toBe "hello"
it "does nothing for empty lines and null grammar", ->
runs ->
editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'))
editor.setCursorBufferPosition([10, 0])
editor.toggleLineCommentsInSelection()
expect(editor.buffer.lineForRow(10)).toBe ""
it "uncomments when the line lacks the trailing whitespace in the comment regex", ->
editor.setCursorBufferPosition([10, 0])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(10)).toBe "// "
expect(editor.getSelectedBufferRange()).toEqual [[10, 3], [10, 3]]
editor.backspace()
expect(buffer.lineForRow(10)).toBe "//"
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(10)).toBe ""
expect(editor.getSelectedBufferRange()).toEqual [[10, 0], [10, 0]]
it "uncomments when the line has leading whitespace", ->
editor.setCursorBufferPosition([10, 0])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(10)).toBe "// "
editor.moveToBeginningOfLine()
editor.insertText(" ")
editor.setSelectedBufferRange([[10, 0], [10, 0]])
editor.toggleLineCommentsInSelection()
expect(buffer.lineForRow(10)).toBe " "
describe ".undo() and .redo()", ->
it "undoes/redoes the last change", ->
editor.insertText("foo")

View File

@@ -2,6 +2,8 @@ const fs = require('fs')
const temp = require('temp').track()
const {Point, Range} = require('text-buffer')
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const TextBuffer = require('text-buffer')
const TextEditor = require('../src/text-editor')
describe('TextEditor', () => {
let editor
@@ -58,6 +60,276 @@ describe('TextEditor', () => {
})
})
describe('.toggleLineCommentsInSelection()', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-javascript')
editor = await atom.workspace.open('sample.js')
})
it('toggles comments on the selected lines', () => {
editor.setSelectedBufferRange([[4, 5], [7, 5]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
expect(editor.getSelectedBufferRange()).toEqual([[4, 8], [7, 8]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {')
expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();')
expect(editor.lineTextForBufferRow(6)).toBe(' current < pivot ? left.push(current) : right.push(current);')
expect(editor.lineTextForBufferRow(7)).toBe(' }')
})
it('does not comment the last line of a non-empty selection if it ends at column 0', () => {
editor.setSelectedBufferRange([[4, 5], [7, 0]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
expect(editor.lineTextForBufferRow(7)).toBe(' }')
})
it('uncomments lines if all lines match the comment regex', () => {
editor.setSelectedBufferRange([[0, 0], [0, 1]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(0)).toBe('// // var quicksort = function () {')
expect(editor.lineTextForBufferRow(1)).toBe('// var sort = function(items) {')
expect(editor.lineTextForBufferRow(2)).toBe('// if (items.length <= 1) return items;')
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
expect(editor.lineTextForBufferRow(1)).toBe(' var sort = function(items) {')
expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;')
editor.setSelectedBufferRange([[0, 0], [0, Infinity]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {')
})
it('uncomments commented lines separated by an empty line', () => {
editor.setSelectedBufferRange([[0, 0], [1, Infinity]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
expect(editor.lineTextForBufferRow(1)).toBe('// var sort = function(items) {')
editor.getBuffer().insert([0, Infinity], '\n')
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {')
expect(editor.lineTextForBufferRow(1)).toBe('')
expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {')
})
it('preserves selection emptiness', () => {
editor.setCursorBufferPosition([4, 0])
editor.toggleLineCommentsInSelection()
expect(editor.getLastSelection().isEmpty()).toBeTruthy()
})
it('does not explode if the current language mode has no comment regex', () => {
const editor = new TextEditor({buffer: new TextBuffer({text: 'hello'})})
editor.setSelectedBufferRange([[0, 0], [0, 5]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(0)).toBe('hello')
})
it('does nothing for empty lines and null grammar', () => {
editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'))
editor.setCursorBufferPosition([10, 0])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(10)).toBe('')
})
it('uncomments when the line lacks the trailing whitespace in the comment regex', () => {
editor.setCursorBufferPosition([10, 0])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(10)).toBe('// ')
expect(editor.getSelectedBufferRange()).toEqual([[10, 3], [10, 3]])
editor.backspace()
expect(editor.lineTextForBufferRow(10)).toBe('//')
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(10)).toBe('')
expect(editor.getSelectedBufferRange()).toEqual([[10, 0], [10, 0]])
})
it('uncomments when the line has leading whitespace', () => {
editor.setCursorBufferPosition([10, 0])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(10)).toBe('// ')
editor.moveToBeginningOfLine()
editor.insertText(' ')
editor.setSelectedBufferRange([[10, 0], [10, 0]])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(10)).toBe(' ')
})
})
describe('.toggleLineCommentsForBufferRows', () => {
describe('xml', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-xml')
editor = await atom.workspace.open('test.xml')
editor.setText('<!-- test -->')
})
it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => {
editor.toggleLineCommentsForBufferRows(0, 0)
expect(editor.lineTextForBufferRow(0)).toBe('test')
})
})
describe('less', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-less')
await atom.packages.activatePackage('language-css')
editor = await atom.workspace.open('sample.less')
})
it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => {
editor.toggleLineCommentsForBufferRows(0, 0)
expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;')
})
})
describe('css', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-css')
editor = await atom.workspace.open('css.css')
})
it('comments/uncomments lines in the given range', () => {
editor.toggleLineCommentsForBufferRows(0, 1)
expect(editor.lineTextForBufferRow(0)).toBe('/* body {')
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px; */')
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;')
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
editor.toggleLineCommentsForBufferRows(2, 2)
expect(editor.lineTextForBufferRow(0)).toBe('/* body {')
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px; */')
expect(editor.lineTextForBufferRow(2)).toBe(' /* width: 110%; */')
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
editor.toggleLineCommentsForBufferRows(0, 1)
expect(editor.lineTextForBufferRow(0)).toBe('body {')
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;')
expect(editor.lineTextForBufferRow(2)).toBe(' /* width: 110%; */')
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
})
it('uncomments lines with leading whitespace', () => {
editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /* width: 110%; */')
editor.toggleLineCommentsForBufferRows(2, 2)
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;')
})
it('uncomments lines with trailing whitespace', () => {
editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/* width: 110%; */ ')
editor.toggleLineCommentsForBufferRows(2, 2)
expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ')
})
it('uncomments lines with leading and trailing whitespace', () => {
editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /* width: 110%; */ ')
editor.toggleLineCommentsForBufferRows(2, 2)
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ')
})
})
describe('coffeescript', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-coffee-script')
editor = await atom.workspace.open('coffee.coffee')
})
it('comments/uncomments lines in the given range', () => {
editor.toggleLineCommentsForBufferRows(4, 6)
expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()')
expect(editor.lineTextForBufferRow(5)).toBe(' # left = []')
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
editor.toggleLineCommentsForBufferRows(4, 5)
expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()')
expect(editor.lineTextForBufferRow(5)).toBe(' left = []')
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
})
it('comments/uncomments empty lines', () => {
editor.toggleLineCommentsForBufferRows(4, 7)
expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()')
expect(editor.lineTextForBufferRow(5)).toBe(' # left = []')
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
expect(editor.lineTextForBufferRow(7)).toBe(' # ')
editor.toggleLineCommentsForBufferRows(4, 5)
expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()')
expect(editor.lineTextForBufferRow(5)).toBe(' left = []')
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
expect(editor.lineTextForBufferRow(7)).toBe(' # ')
})
})
describe('javascript', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-javascript')
editor = await atom.workspace.open('sample.js')
})
it('comments/uncomments lines in the given range', () => {
editor.toggleLineCommentsForBufferRows(4, 7)
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
editor.toggleLineCommentsForBufferRows(4, 5)
expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {')
expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();')
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
editor.setText('\tvar i;')
editor.toggleLineCommentsForBufferRows(0, 0)
expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;')
editor.setText('var i;')
editor.toggleLineCommentsForBufferRows(0, 0)
expect(editor.lineTextForBufferRow(0)).toBe('// var i;')
editor.setText(' var i;')
editor.toggleLineCommentsForBufferRows(0, 0)
expect(editor.lineTextForBufferRow(0)).toBe(' // var i;')
editor.setText(' ')
editor.toggleLineCommentsForBufferRows(0, 0)
expect(editor.lineTextForBufferRow(0)).toBe(' // ')
editor.setText(' a\n \n b')
editor.toggleLineCommentsForBufferRows(0, 2)
expect(editor.lineTextForBufferRow(0)).toBe(' // a')
expect(editor.lineTextForBufferRow(1)).toBe(' // ')
expect(editor.lineTextForBufferRow(2)).toBe(' // b')
editor.setText(' \n // var i;')
editor.toggleLineCommentsForBufferRows(0, 1)
expect(editor.lineTextForBufferRow(0)).toBe(' ')
expect(editor.lineTextForBufferRow(1)).toBe(' var i;')
})
})
})
describe('folding', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-javascript')
@@ -173,6 +445,26 @@ describe('TextEditor', () => {
})
})
describe('.foldCurrentRow()', () => {
it('creates a fold at the location of the last cursor', async () => {
editor = await atom.workspace.open()
editor.setText('\nif (x) {\n y()\n}')
editor.setCursorBufferPosition([1, 0])
expect(editor.getScreenLineCount()).toBe(4)
editor.foldCurrentRow()
expect(editor.getScreenLineCount()).toBe(3)
})
it('does nothing when the current row cannot be folded', async () => {
editor = await atom.workspace.open()
editor.setText('var x;\nx++\nx++')
editor.setCursorBufferPosition([0, 0])
expect(editor.getScreenLineCount()).toBe(3)
editor.foldCurrentRow()
expect(editor.getScreenLineCount()).toBe(3)
})
})
describe('.foldAllAtIndentLevel(indentLevel)', () => {
it('folds blocks of text at the given indentation level', async () => {
editor = await atom.workspace.open('sample.js', {autoIndent: false})

View File

@@ -643,186 +643,6 @@ describe('TokenizedBuffer', () => {
})
})
describe('.toggleLineCommentsForBufferRows', () => {
describe('xml', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-xml')
buffer = new TextBuffer('<!-- test -->')
tokenizedBuffer = new TokenizedBuffer({
buffer,
grammar: atom.grammars.grammarForScopeName('text.xml'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe('test')
})
})
describe('less', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-less')
await atom.packages.activatePackage('language-css')
buffer = await TextBuffer.load(require.resolve('./fixtures/sample.less'))
tokenizedBuffer = new TokenizedBuffer({
buffer,
grammar: atom.grammars.grammarForScopeName('source.css.less'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe('// @color: #4D926F;')
})
})
describe('css', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-css')
buffer = await TextBuffer.load(require.resolve('./fixtures/css.css'))
tokenizedBuffer = new TokenizedBuffer({
buffer,
grammar: atom.grammars.grammarForScopeName('source.css'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('comments/uncomments lines in the given range', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1)
expect(buffer.lineForRow(0)).toBe('/*body {')
expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;*/')
expect(buffer.lineForRow(2)).toBe(' width: 110%;')
expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;')
tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2)
expect(buffer.lineForRow(0)).toBe('/*body {')
expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;*/')
expect(buffer.lineForRow(2)).toBe(' /*width: 110%;*/')
expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1)
expect(buffer.lineForRow(0)).toBe('body {')
expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;')
expect(buffer.lineForRow(2)).toBe(' /*width: 110%;*/')
expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;')
})
it('uncomments lines with leading whitespace', () => {
buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/')
tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2)
expect(buffer.lineForRow(2)).toBe(' width: 110%;')
})
it('uncomments lines with trailing whitespace', () => {
buffer.setTextInRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ')
tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2)
expect(buffer.lineForRow(2)).toBe('width: 110%; ')
})
it('uncomments lines with leading and trailing whitespace', () => {
buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ')
tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2)
expect(buffer.lineForRow(2)).toBe(' width: 110%; ')
})
})
describe('coffeescript', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-coffee-script')
buffer = await TextBuffer.load(require.resolve('./fixtures/coffee.coffee'))
tokenizedBuffer = new TokenizedBuffer({
buffer,
tabLength: 2,
grammar: atom.grammars.grammarForScopeName('source.coffee'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('comments/uncomments lines in the given range', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 6)
expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()')
expect(buffer.lineForRow(5)).toBe(' # left = []')
expect(buffer.lineForRow(6)).toBe(' # right = []')
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5)
expect(buffer.lineForRow(4)).toBe(' pivot = items.shift()')
expect(buffer.lineForRow(5)).toBe(' left = []')
expect(buffer.lineForRow(6)).toBe(' # right = []')
})
it('comments/uncomments empty lines', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 7)
expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()')
expect(buffer.lineForRow(5)).toBe(' # left = []')
expect(buffer.lineForRow(6)).toBe(' # right = []')
expect(buffer.lineForRow(7)).toBe(' # ')
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5)
expect(buffer.lineForRow(4)).toBe(' pivot = items.shift()')
expect(buffer.lineForRow(5)).toBe(' left = []')
expect(buffer.lineForRow(6)).toBe(' # right = []')
expect(buffer.lineForRow(7)).toBe(' # ')
})
})
describe('javascript', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-javascript')
buffer = await TextBuffer.load(require.resolve('./fixtures/sample.js'))
tokenizedBuffer = new TokenizedBuffer({
buffer,
tabLength: 2,
grammar: atom.grammars.grammarForScopeName('source.js'),
scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config)
})
})
it('comments/uncomments lines in the given range', () => {
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 7)
expect(buffer.lineForRow(4)).toBe(' // while(items.length > 0) {')
expect(buffer.lineForRow(5)).toBe(' // current = items.shift();')
expect(buffer.lineForRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
expect(buffer.lineForRow(7)).toBe(' // }')
tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5)
expect(buffer.lineForRow(4)).toBe(' while(items.length > 0) {')
expect(buffer.lineForRow(5)).toBe(' current = items.shift();')
expect(buffer.lineForRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
expect(buffer.lineForRow(7)).toBe(' // }')
buffer.setText('\tvar i;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe('\t// var i;')
buffer.setText('var i;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe('// var i;')
buffer.setText(' var i;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe(' // var i;')
buffer.setText(' ')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0)
expect(buffer.lineForRow(0)).toBe(' // ')
buffer.setText(' a\n \n b')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 2)
expect(buffer.lineForRow(0)).toBe(' // a')
expect(buffer.lineForRow(1)).toBe(' // ')
expect(buffer.lineForRow(2)).toBe(' // b')
buffer.setText(' \n // var i;')
tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1)
expect(buffer.lineForRow(0)).toBe(' ')
expect(buffer.lineForRow(1)).toBe(' var i;')
})
})
})
describe('.isFoldableAtRow(row)', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')

View File

@@ -1,213 +0,0 @@
{CompositeDisposable} = require 'atom'
TooltipManager = require '../src/tooltip-manager'
Tooltip = require '../src/tooltip'
_ = require 'underscore-plus'
describe "TooltipManager", ->
[manager, element] = []
ctrlX = _.humanizeKeystroke("ctrl-x")
ctrlY = _.humanizeKeystroke("ctrl-y")
beforeEach ->
manager = new TooltipManager(keymapManager: atom.keymaps, viewRegistry: atom.views)
element = createElement 'foo'
createElement = (className) ->
el = document.createElement('div')
el.classList.add(className)
jasmine.attachToDOM(el)
el
mouseEnter = (element) ->
element.dispatchEvent(new CustomEvent('mouseenter', bubbles: false))
element.dispatchEvent(new CustomEvent('mouseover', bubbles: true))
mouseLeave = (element) ->
element.dispatchEvent(new CustomEvent('mouseleave', bubbles: false))
element.dispatchEvent(new CustomEvent('mouseout', bubbles: true))
hover = (element, fn) ->
mouseEnter(element)
advanceClock(manager.hoverDefaults.delay.show)
fn()
mouseLeave(element)
advanceClock(manager.hoverDefaults.delay.hide)
describe "::add(target, options)", ->
describe "when the trigger is 'hover' (the default)", ->
it "creates a tooltip when hovering over the target element", ->
manager.add element, title: "Title"
hover element, ->
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
it "displays tooltips immediately when hovering over new elements once a tooltip has been displayed once", ->
disposables = new CompositeDisposable
element1 = createElement('foo')
disposables.add(manager.add element1, title: 'Title')
element2 = createElement('bar')
disposables.add(manager.add element2, title: 'Title')
element3 = createElement('baz')
disposables.add(manager.add element3, title: 'Title')
hover element1, ->
expect(document.body.querySelector(".tooltip")).toBeNull()
mouseEnter(element2)
expect(document.body.querySelector(".tooltip")).not.toBeNull()
mouseLeave(element2)
advanceClock(manager.hoverDefaults.delay.hide)
expect(document.body.querySelector(".tooltip")).toBeNull()
advanceClock(Tooltip.FOLLOW_THROUGH_DURATION)
mouseEnter(element3)
expect(document.body.querySelector(".tooltip")).toBeNull()
advanceClock(manager.hoverDefaults.delay.show)
expect(document.body.querySelector(".tooltip")).not.toBeNull()
disposables.dispose()
describe "when the trigger is 'manual'", ->
it "creates a tooltip immediately and only hides it on dispose", ->
disposable = manager.add element, title: "Title", trigger: "manual"
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
disposable.dispose()
expect(document.body.querySelector(".tooltip")).toBeNull()
describe "when the trigger is 'click'", ->
it "shows and hides the tooltip when the target element is clicked", ->
disposable = manager.add element, title: "Title", trigger: "click"
expect(document.body.querySelector(".tooltip")).toBeNull()
element.click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
element.click()
expect(document.body.querySelector(".tooltip")).toBeNull()
# Hide the tooltip when clicking anywhere but inside the tooltip element
element.click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
document.body.querySelector(".tooltip").click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
document.body.querySelector(".tooltip").firstChild.click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
document.body.click()
expect(document.body.querySelector(".tooltip")).toBeNull()
# Tooltip can show again after hiding due to clicking outside of the tooltip
element.click()
expect(document.body.querySelector(".tooltip")).not.toBeNull()
element.click()
expect(document.body.querySelector(".tooltip")).toBeNull()
it "allows a custom item to be specified for the content of the tooltip", ->
tooltipElement = document.createElement('div')
manager.add element, item: {element: tooltipElement}
hover element, ->
expect(tooltipElement.closest(".tooltip")).not.toBeNull()
it "allows a custom class to be specified for the tooltip", ->
tooltipElement = document.createElement('div')
manager.add element, title: 'Title', class: 'custom-tooltip-class'
hover element, ->
expect(document.body.querySelector(".tooltip").classList.contains('custom-tooltip-class')).toBe(true)
it "allows jQuery elements to be passed as the target", ->
element2 = document.createElement('div')
jasmine.attachToDOM(element2)
fakeJqueryWrapper = [element, element2]
fakeJqueryWrapper.jquery = 'any-version'
disposable = manager.add fakeJqueryWrapper, title: "Title"
hover element, -> expect(document.body.querySelector(".tooltip")).toHaveText("Title")
expect(document.body.querySelector(".tooltip")).toBeNull()
hover element2, -> expect(document.body.querySelector(".tooltip")).toHaveText("Title")
expect(document.body.querySelector(".tooltip")).toBeNull()
disposable.dispose()
hover element, -> expect(document.body.querySelector(".tooltip")).toBeNull()
hover element2, -> expect(document.body.querySelector(".tooltip")).toBeNull()
describe "when a keyBindingCommand is specified", ->
describe "when a title is specified", ->
it "appends the key binding corresponding to the command to the title", ->
atom.keymaps.add 'test',
'.foo': 'ctrl-x ctrl-y': 'test-command'
'.bar': 'ctrl-x ctrl-z': 'test-command'
manager.add element, title: "Title", keyBindingCommand: 'test-command'
hover element, ->
tooltipElement = document.body.querySelector(".tooltip")
expect(tooltipElement).toHaveText "Title #{ctrlX} #{ctrlY}"
describe "when no title is specified", ->
it "shows the key binding corresponding to the command alone", ->
atom.keymaps.add 'test', '.foo': 'ctrl-x ctrl-y': 'test-command'
manager.add element, keyBindingCommand: 'test-command'
hover element, ->
tooltipElement = document.body.querySelector(".tooltip")
expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}"
describe "when a keyBindingTarget is specified", ->
it "looks up the key binding relative to the target", ->
atom.keymaps.add 'test',
'.bar': 'ctrl-x ctrl-z': 'test-command'
'.foo': 'ctrl-x ctrl-y': 'test-command'
manager.add element, keyBindingCommand: 'test-command', keyBindingTarget: element
hover element, ->
tooltipElement = document.body.querySelector(".tooltip")
expect(tooltipElement).toHaveText "#{ctrlX} #{ctrlY}"
it "does not display the keybinding if there is nothing mapped to the specified keyBindingCommand", ->
manager.add element, title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element
hover element, ->
tooltipElement = document.body.querySelector(".tooltip")
expect(tooltipElement.textContent).toBe "A Title"
describe "when .dispose() is called on the returned disposable", ->
it "no longer displays the tooltip on hover", ->
disposable = manager.add element, title: "Title"
hover element, ->
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
disposable.dispose()
hover element, ->
expect(document.body.querySelector(".tooltip")).toBeNull()
describe "when the window is resized", ->
it "hides the tooltips", ->
disposable = manager.add element, title: "Title"
hover element, ->
expect(document.body.querySelector(".tooltip")).not.toBeNull()
window.dispatchEvent(new CustomEvent('resize'))
expect(document.body.querySelector(".tooltip")).toBeNull()
disposable.dispose()
describe "findTooltips", ->
it "adds and remove tooltips correctly", ->
expect(manager.findTooltips(element).length).toBe(0)
disposable1 = manager.add element, title: "elem1"
expect(manager.findTooltips(element).length).toBe(1)
disposable2 = manager.add element, title: "elem2"
expect(manager.findTooltips(element).length).toBe(2)
disposable1.dispose()
expect(manager.findTooltips(element).length).toBe(1)
disposable2.dispose()
expect(manager.findTooltips(element).length).toBe(0)
it "lets us hide tooltips programmatically", ->
disposable = manager.add element, title: "Title"
hover element, ->
expect(document.body.querySelector(".tooltip")).not.toBeNull()
manager.findTooltips(element)[0].hide()
expect(document.body.querySelector(".tooltip")).toBeNull()
disposable.dispose()

View File

@@ -0,0 +1,253 @@
const {CompositeDisposable} = require('atom')
const TooltipManager = require('../src/tooltip-manager')
const Tooltip = require('../src/tooltip')
const _ = require('underscore-plus')
describe('TooltipManager', () => {
let manager, element
const ctrlX = _.humanizeKeystroke('ctrl-x')
const ctrlY = _.humanizeKeystroke('ctrl-y')
const hover = function (element, fn) {
mouseEnter(element)
advanceClock(manager.hoverDefaults.delay.show)
fn()
mouseLeave(element)
advanceClock(manager.hoverDefaults.delay.hide)
}
beforeEach(function () {
manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views})
element = createElement('foo')
})
describe('::add(target, options)', () => {
describe("when the trigger is 'hover' (the default)", () => {
it('creates a tooltip when hovering over the target element', () => {
manager.add(element, {title: 'Title'})
hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
})
it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', () => {
const disposables = new CompositeDisposable()
const element1 = createElement('foo')
disposables.add(manager.add(element1, {title: 'Title'}))
const element2 = createElement('bar')
disposables.add(manager.add(element2, {title: 'Title'}))
const element3 = createElement('baz')
disposables.add(manager.add(element3, {title: 'Title'}))
hover(element1, () => {})
expect(document.body.querySelector('.tooltip')).toBeNull()
mouseEnter(element2)
expect(document.body.querySelector('.tooltip')).not.toBeNull()
mouseLeave(element2)
advanceClock(manager.hoverDefaults.delay.hide)
expect(document.body.querySelector('.tooltip')).toBeNull()
advanceClock(Tooltip.FOLLOW_THROUGH_DURATION)
mouseEnter(element3)
expect(document.body.querySelector('.tooltip')).toBeNull()
advanceClock(manager.hoverDefaults.delay.show)
expect(document.body.querySelector('.tooltip')).not.toBeNull()
disposables.dispose()
})
})
describe("when the trigger is 'manual'", () =>
it('creates a tooltip immediately and only hides it on dispose', () => {
const disposable = manager.add(element, {title: 'Title', trigger: 'manual'})
expect(document.body.querySelector('.tooltip')).toHaveText('Title')
disposable.dispose()
expect(document.body.querySelector('.tooltip')).toBeNull()
})
)
describe("when the trigger is 'click'", () =>
it('shows and hides the tooltip when the target element is clicked', () => {
manager.add(element, {title: 'Title', trigger: 'click'})
expect(document.body.querySelector('.tooltip')).toBeNull()
element.click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
element.click()
expect(document.body.querySelector('.tooltip')).toBeNull()
// Hide the tooltip when clicking anywhere but inside the tooltip element
element.click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
document.body.querySelector('.tooltip').click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
document.body.querySelector('.tooltip').firstChild.click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
document.body.click()
expect(document.body.querySelector('.tooltip')).toBeNull()
// Tooltip can show again after hiding due to clicking outside of the tooltip
element.click()
expect(document.body.querySelector('.tooltip')).not.toBeNull()
element.click()
expect(document.body.querySelector('.tooltip')).toBeNull()
})
)
it('allows a custom item to be specified for the content of the tooltip', () => {
const tooltipElement = document.createElement('div')
manager.add(element, {item: {element: tooltipElement}})
hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull())
})
it('allows a custom class to be specified for the tooltip', () => {
manager.add(element, {title: 'Title', class: 'custom-tooltip-class'})
hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true))
})
it('allows jQuery elements to be passed as the target', () => {
const element2 = document.createElement('div')
jasmine.attachToDOM(element2)
const fakeJqueryWrapper = [element, element2]
fakeJqueryWrapper.jquery = 'any-version'
const disposable = manager.add(fakeJqueryWrapper, {title: 'Title'})
hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
expect(document.body.querySelector('.tooltip')).toBeNull()
hover(element2, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull())
hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull())
})
describe('when a keyBindingCommand is specified', () => {
describe('when a title is specified', () =>
it('appends the key binding corresponding to the command to the title', () => {
atom.keymaps.add('test', {
'.foo': { 'ctrl-x ctrl-y': 'test-command'
},
'.bar': { 'ctrl-x ctrl-z': 'test-command'
}
}
)
manager.add(element, {title: 'Title', keyBindingCommand: 'test-command'})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`)
})
})
)
describe('when no title is specified', () =>
it('shows the key binding corresponding to the command alone', () => {
atom.keymaps.add('test', {'.foo': {'ctrl-x ctrl-y': 'test-command'}})
manager.add(element, {keyBindingCommand: 'test-command'})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`)
})
})
)
describe('when a keyBindingTarget is specified', () => {
it('looks up the key binding relative to the target', () => {
atom.keymaps.add('test', {
'.bar': { 'ctrl-x ctrl-z': 'test-command'
},
'.foo': { 'ctrl-x ctrl-y': 'test-command'
}
}
)
manager.add(element, {keyBindingCommand: 'test-command', keyBindingTarget: element})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`)
})
})
it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', () => {
manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element})
hover(element, function () {
const tooltipElement = document.body.querySelector('.tooltip')
expect(tooltipElement.textContent).toBe('A Title')
})
})
})
})
describe('when .dispose() is called on the returned disposable', () =>
it('no longer displays the tooltip on hover', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title'))
disposable.dispose()
hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull())
})
)
describe('when the window is resized', () =>
it('hides the tooltips', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, function () {
expect(document.body.querySelector('.tooltip')).not.toBeNull()
window.dispatchEvent(new CustomEvent('resize'))
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
})
})
)
describe('findTooltips', () => {
it('adds and remove tooltips correctly', () => {
expect(manager.findTooltips(element).length).toBe(0)
const disposable1 = manager.add(element, {title: 'elem1'})
expect(manager.findTooltips(element).length).toBe(1)
const disposable2 = manager.add(element, {title: 'elem2'})
expect(manager.findTooltips(element).length).toBe(2)
disposable1.dispose()
expect(manager.findTooltips(element).length).toBe(1)
disposable2.dispose()
expect(manager.findTooltips(element).length).toBe(0)
})
it('lets us hide tooltips programmatically', () => {
const disposable = manager.add(element, {title: 'Title'})
hover(element, function () {
expect(document.body.querySelector('.tooltip')).not.toBeNull()
manager.findTooltips(element)[0].hide()
expect(document.body.querySelector('.tooltip')).toBeNull()
disposable.dispose()
})
})
})
})
})
function createElement (className) {
const el = document.createElement('div')
el.classList.add(className)
jasmine.attachToDOM(el)
return el
}
function mouseEnter (element) {
element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false}))
element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true}))
}
function mouseLeave (element) {
element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false}))
element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true}))
}

View File

@@ -1,163 +0,0 @@
ViewRegistry = require '../src/view-registry'
describe "ViewRegistry", ->
registry = null
beforeEach ->
registry = new ViewRegistry
afterEach ->
registry.clearDocumentRequests()
describe "::getView(object)", ->
describe "when passed a DOM node", ->
it "returns the given DOM node", ->
node = document.createElement('div')
expect(registry.getView(node)).toBe node
describe "when passed an object with an element property", ->
it "returns the element property if it's an instance of HTMLElement", ->
class TestComponent
constructor: -> @element = document.createElement('div')
component = new TestComponent
expect(registry.getView(component)).toBe component.element
describe "when passed an object with a getElement function", ->
it "returns the return value of getElement if it's an instance of HTMLElement", ->
class TestComponent
getElement: ->
@myElement ?= document.createElement('div')
component = new TestComponent
expect(registry.getView(component)).toBe component.myElement
describe "when passed a model object", ->
describe "when a view provider is registered matching the object's constructor", ->
it "constructs a view element and assigns the model on it", ->
class TestModel
class TestModelSubclass extends TestModel
class TestView
initialize: (@model) -> this
model = new TestModel
registry.addViewProvider TestModel, (model) ->
new TestView().initialize(model)
view = registry.getView(model)
expect(view instanceof TestView).toBe true
expect(view.model).toBe model
subclassModel = new TestModelSubclass
view2 = registry.getView(subclassModel)
expect(view2 instanceof TestView).toBe true
expect(view2.model).toBe subclassModel
describe "when a view provider is registered generically, and works with the object", ->
it "constructs a view element and assigns the model on it", ->
model = {a: 'b'}
registry.addViewProvider (model) ->
if model.a is 'b'
element = document.createElement('div')
element.className = 'test-element'
element
view = registry.getView({a: 'b'})
expect(view.className).toBe 'test-element'
expect(-> registry.getView({a: 'c'})).toThrow()
describe "when no view provider is registered for the object's constructor", ->
it "throws an exception", ->
expect(-> registry.getView(new Object)).toThrow()
describe "::addViewProvider(providerSpec)", ->
it "returns a disposable that can be used to remove the provider", ->
class TestModel
class TestView
initialize: (@model) -> this
disposable = registry.addViewProvider TestModel, (model) ->
new TestView().initialize(model)
expect(registry.getView(new TestModel) instanceof TestView).toBe true
disposable.dispose()
expect(-> registry.getView(new TestModel)).toThrow()
describe "::updateDocument(fn) and ::readDocument(fn)", ->
frameRequests = null
beforeEach ->
frameRequests = []
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> frameRequests.push(fn)
it "performs all pending writes before all pending reads on the next animation frame", ->
events = []
registry.updateDocument -> events.push('write 1')
registry.readDocument -> events.push('read 1')
registry.readDocument -> events.push('read 2')
registry.updateDocument -> events.push('write 2')
expect(events).toEqual []
expect(frameRequests.length).toBe 1
frameRequests[0]()
expect(events).toEqual ['write 1', 'write 2', 'read 1', 'read 2']
frameRequests = []
events = []
disposable = registry.updateDocument -> events.push('write 3')
registry.updateDocument -> events.push('write 4')
registry.readDocument -> events.push('read 3')
disposable.dispose()
expect(frameRequests.length).toBe 1
frameRequests[0]()
expect(events).toEqual ['write 4', 'read 3']
it "performs writes requested from read callbacks in the same animation frame", ->
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
events = []
registry.updateDocument -> events.push('write 1')
registry.readDocument ->
registry.updateDocument -> events.push('write from read 1')
events.push('read 1')
registry.readDocument ->
registry.updateDocument -> events.push('write from read 2')
events.push('read 2')
registry.updateDocument -> events.push('write 2')
expect(frameRequests.length).toBe 1
frameRequests[0]()
expect(frameRequests.length).toBe 1
expect(events).toEqual [
'write 1'
'write 2'
'read 1'
'read 2'
'write from read 1'
'write from read 2'
]
describe "::getNextUpdatePromise()", ->
it "returns a promise that resolves at the end of the next update cycle", ->
updateCalled = false
readCalled = false
waitsFor 'getNextUpdatePromise to resolve', (done) ->
registry.getNextUpdatePromise().then ->
expect(updateCalled).toBe true
expect(readCalled).toBe true
done()
registry.updateDocument -> updateCalled = true
registry.readDocument -> readCalled = true

216
spec/view-registry-spec.js Normal file
View File

@@ -0,0 +1,216 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const ViewRegistry = require('../src/view-registry')
describe('ViewRegistry', () => {
let registry = null
beforeEach(() => {
registry = new ViewRegistry()
})
afterEach(() => {
registry.clearDocumentRequests()
})
describe('::getView(object)', () => {
describe('when passed a DOM node', () =>
it('returns the given DOM node', () => {
const node = document.createElement('div')
expect(registry.getView(node)).toBe(node)
})
)
describe('when passed an object with an element property', () =>
it("returns the element property if it's an instance of HTMLElement", () => {
class TestComponent {
constructor () {
this.element = document.createElement('div')
}
}
const component = new TestComponent()
expect(registry.getView(component)).toBe(component.element)
})
)
describe('when passed an object with a getElement function', () =>
it("returns the return value of getElement if it's an instance of HTMLElement", () => {
class TestComponent {
getElement () {
if (this.myElement == null) {
this.myElement = document.createElement('div')
}
return this.myElement
}
}
const component = new TestComponent()
expect(registry.getView(component)).toBe(component.myElement)
})
)
describe('when passed a model object', () => {
describe("when a view provider is registered matching the object's constructor", () =>
it('constructs a view element and assigns the model on it', () => {
class TestModel {}
class TestModelSubclass extends TestModel {}
class TestView {
initialize (model) {
this.model = model
return this
}
}
const model = new TestModel()
registry.addViewProvider(TestModel, (model) =>
new TestView().initialize(model)
)
const view = registry.getView(model)
expect(view instanceof TestView).toBe(true)
expect(view.model).toBe(model)
const subclassModel = new TestModelSubclass()
const view2 = registry.getView(subclassModel)
expect(view2 instanceof TestView).toBe(true)
expect(view2.model).toBe(subclassModel)
})
)
describe('when a view provider is registered generically, and works with the object', () =>
it('constructs a view element and assigns the model on it', () => {
registry.addViewProvider((model) => {
if (model.a === 'b') {
const element = document.createElement('div')
element.className = 'test-element'
return element
}
})
const view = registry.getView({a: 'b'})
expect(view.className).toBe('test-element')
expect(() => registry.getView({a: 'c'})).toThrow()
})
)
describe("when no view provider is registered for the object's constructor", () =>
it('throws an exception', () => {
expect(() => registry.getView({})).toThrow()
})
)
})
})
describe('::addViewProvider(providerSpec)', () =>
it('returns a disposable that can be used to remove the provider', () => {
class TestModel {}
class TestView {
initialize (model) {
this.model = model
return this
}
}
const disposable = registry.addViewProvider(TestModel, (model) =>
new TestView().initialize(model)
)
expect(registry.getView(new TestModel()) instanceof TestView).toBe(true)
disposable.dispose()
expect(() => registry.getView(new TestModel())).toThrow()
})
)
describe('::updateDocument(fn) and ::readDocument(fn)', () => {
let frameRequests = null
beforeEach(() => {
frameRequests = []
spyOn(window, 'requestAnimationFrame').andCallFake(fn => frameRequests.push(fn))
})
it('performs all pending writes before all pending reads on the next animation frame', () => {
let events = []
registry.updateDocument(() => events.push('write 1'))
registry.readDocument(() => events.push('read 1'))
registry.readDocument(() => events.push('read 2'))
registry.updateDocument(() => events.push('write 2'))
expect(events).toEqual([])
expect(frameRequests.length).toBe(1)
frameRequests[0]()
expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2'])
frameRequests = []
events = []
const disposable = registry.updateDocument(() => events.push('write 3'))
registry.updateDocument(() => events.push('write 4'))
registry.readDocument(() => events.push('read 3'))
disposable.dispose()
expect(frameRequests.length).toBe(1)
frameRequests[0]()
expect(events).toEqual(['write 4', 'read 3'])
})
it('performs writes requested from read callbacks in the same animation frame', () => {
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
const events = []
registry.updateDocument(() => events.push('write 1'))
registry.readDocument(() => {
registry.updateDocument(() => events.push('write from read 1'))
events.push('read 1')
})
registry.readDocument(() => {
registry.updateDocument(() => events.push('write from read 2'))
events.push('read 2')
})
registry.updateDocument(() => events.push('write 2'))
expect(frameRequests.length).toBe(1)
frameRequests[0]()
expect(frameRequests.length).toBe(1)
expect(events).toEqual([
'write 1',
'write 2',
'read 1',
'read 2',
'write from read 1',
'write from read 2'
])
})
})
describe('::getNextUpdatePromise()', () =>
it('returns a promise that resolves at the end of the next update cycle', () => {
let updateCalled = false
let readCalled = false
waitsFor('getNextUpdatePromise to resolve', (done) => {
registry.getNextUpdatePromise().then(() => {
expect(updateCalled).toBe(true)
expect(readCalled).toBe(true)
done()
})
registry.updateDocument(() => { updateCalled = true })
registry.readDocument(() => { readCalled = true })
})
})
)
})

View File

@@ -1,209 +0,0 @@
KeymapManager = require 'atom-keymap'
TextEditor = require '../src/text-editor'
WindowEventHandler = require '../src/window-event-handler'
{ipcRenderer} = require 'electron'
describe "WindowEventHandler", ->
[windowEventHandler] = []
beforeEach ->
atom.uninstallWindowEventHandler()
spyOn(atom, 'hide')
initialPath = atom.project.getPaths()[0]
spyOn(atom, 'getLoadSettings').andCallFake ->
loadSettings = atom.getLoadSettings.originalValue.call(atom)
loadSettings.initialPath = initialPath
loadSettings
atom.project.destroy()
windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate})
windowEventHandler.initialize(window, document)
afterEach ->
windowEventHandler.unsubscribe()
atom.installWindowEventHandler()
describe "when the window is loaded", ->
it "doesn't have .is-blurred on the body tag", ->
return if process.platform is 'win32' #Win32TestFailures - can not steal focus
expect(document.body.className).not.toMatch("is-blurred")
describe "when the window is blurred", ->
beforeEach ->
window.dispatchEvent(new CustomEvent('blur'))
afterEach ->
document.body.classList.remove('is-blurred')
it "adds the .is-blurred class on the body", ->
expect(document.body.className).toMatch("is-blurred")
describe "when the window is focused again", ->
it "removes the .is-blurred class from the body", ->
window.dispatchEvent(new CustomEvent('focus'))
expect(document.body.className).not.toMatch("is-blurred")
describe "window:close event", ->
it "closes the window", ->
spyOn(atom, 'close')
window.dispatchEvent(new CustomEvent('window:close'))
expect(atom.close).toHaveBeenCalled()
describe "when a link is clicked", ->
it "opens the http/https links in an external application", ->
{shell} = require 'electron'
spyOn(shell, 'openExternal')
link = document.createElement('a')
linkChild = document.createElement('span')
link.appendChild(linkChild)
link.href = 'http://github.com'
jasmine.attachToDOM(link)
fakeEvent = {target: linkChild, currentTarget: link, preventDefault: (->)}
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).toHaveBeenCalled()
expect(shell.openExternal.argsForCall[0][0]).toBe "http://github.com"
shell.openExternal.reset()
link.href = 'https://github.com'
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).toHaveBeenCalled()
expect(shell.openExternal.argsForCall[0][0]).toBe "https://github.com"
shell.openExternal.reset()
link.href = ''
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).not.toHaveBeenCalled()
shell.openExternal.reset()
link.href = '#scroll-me'
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).not.toHaveBeenCalled()
describe "when a form is submitted", ->
it "prevents the default so that the window's URL isn't changed", ->
form = document.createElement('form')
jasmine.attachToDOM(form)
defaultPrevented = false
event = new CustomEvent('submit', bubbles: true)
event.preventDefault = -> defaultPrevented = true
form.dispatchEvent(event)
expect(defaultPrevented).toBe(true)
describe "core:focus-next and core:focus-previous", ->
describe "when there is no currently focused element", ->
it "focuses the element with the lowest/highest tabindex", ->
wrapperDiv = document.createElement('div')
wrapperDiv.innerHTML = """
<div>
<button tabindex="2"></button>
<input tabindex="1">
</div>
"""
elements = wrapperDiv.firstChild
jasmine.attachToDOM(elements)
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 1
document.body.focus()
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 2
describe "when a tabindex is set on the currently focused element", ->
it "focuses the element with the next highest/lowest tabindex, skipping disabled elements", ->
wrapperDiv = document.createElement('div')
wrapperDiv.innerHTML = """
<div>
<input tabindex="1">
<button tabindex="2"></button>
<button tabindex="5"></button>
<input tabindex="-1">
<input tabindex="3">
<button tabindex="7"></button>
<input tabindex="9" disabled>
</div>
"""
elements = wrapperDiv.firstChild
jasmine.attachToDOM(elements)
elements.querySelector('[tabindex="1"]').focus()
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 2
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 3
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 5
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 7
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
expect(document.activeElement.tabIndex).toBe 1
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 7
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 5
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 3
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 2
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 1
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
expect(document.activeElement.tabIndex).toBe 7
describe "when keydown events occur on the document", ->
it "dispatches the event via the KeymapManager and CommandRegistry", ->
dispatchedCommands = []
atom.commands.onWillDispatch (command) -> dispatchedCommands.push(command)
atom.commands.add '*', 'foo-command': ->
atom.keymaps.add 'source-name', '*': {'x': 'foo-command'}
event = KeymapManager.buildKeydownEvent('x', target: document.createElement('div'))
document.dispatchEvent(event)
expect(dispatchedCommands.length).toBe 1
expect(dispatchedCommands[0].type).toBe 'foo-command'
describe "native key bindings", ->
it "correctly dispatches them to active elements with the '.native-key-bindings' class", ->
webContentsSpy = jasmine.createSpyObj("webContents", ["copy", "paste"])
spyOn(atom.applicationDelegate, "getCurrentWindow").andReturn({
webContents: webContentsSpy
on: ->
})
nativeKeyBindingsInput = document.createElement("input")
nativeKeyBindingsInput.classList.add("native-key-bindings")
jasmine.attachToDOM(nativeKeyBindingsInput)
nativeKeyBindingsInput.focus()
atom.dispatchApplicationMenuCommand("core:copy")
atom.dispatchApplicationMenuCommand("core:paste")
expect(webContentsSpy.copy).toHaveBeenCalled()
expect(webContentsSpy.paste).toHaveBeenCalled()
webContentsSpy.copy.reset()
webContentsSpy.paste.reset()
normalInput = document.createElement("input")
jasmine.attachToDOM(normalInput)
normalInput.focus()
atom.dispatchApplicationMenuCommand("core:copy")
atom.dispatchApplicationMenuCommand("core:paste")
expect(webContentsSpy.copy).not.toHaveBeenCalled()
expect(webContentsSpy.paste).not.toHaveBeenCalled()

View File

@@ -0,0 +1,228 @@
const KeymapManager = require('atom-keymap')
const WindowEventHandler = require('../src/window-event-handler')
describe('WindowEventHandler', () => {
let windowEventHandler
beforeEach(() => {
atom.uninstallWindowEventHandler()
spyOn(atom, 'hide')
const initialPath = atom.project.getPaths()[0]
spyOn(atom, 'getLoadSettings').andCallFake(() => {
const loadSettings = atom.getLoadSettings.originalValue.call(atom)
loadSettings.initialPath = initialPath
return loadSettings
})
atom.project.destroy()
windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate})
windowEventHandler.initialize(window, document)
})
afterEach(() => {
windowEventHandler.unsubscribe()
atom.installWindowEventHandler()
})
describe('when the window is loaded', () =>
it("doesn't have .is-blurred on the body tag", () => {
if (process.platform === 'win32') { return } // Win32TestFailures - can not steal focus
expect(document.body.className).not.toMatch('is-blurred')
})
)
describe('when the window is blurred', () => {
beforeEach(() => window.dispatchEvent(new CustomEvent('blur')))
afterEach(() => document.body.classList.remove('is-blurred'))
it('adds the .is-blurred class on the body', () => expect(document.body.className).toMatch('is-blurred'))
describe('when the window is focused again', () =>
it('removes the .is-blurred class from the body', () => {
window.dispatchEvent(new CustomEvent('focus'))
expect(document.body.className).not.toMatch('is-blurred')
})
)
})
describe('window:close event', () =>
it('closes the window', () => {
spyOn(atom, 'close')
window.dispatchEvent(new CustomEvent('window:close'))
expect(atom.close).toHaveBeenCalled()
})
)
describe('when a link is clicked', () =>
it('opens the http/https links in an external application', () => {
const {shell} = require('electron')
spyOn(shell, 'openExternal')
const link = document.createElement('a')
const linkChild = document.createElement('span')
link.appendChild(linkChild)
link.href = 'http://github.com'
jasmine.attachToDOM(link)
const fakeEvent = {target: linkChild, currentTarget: link, preventDefault: () => {}}
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).toHaveBeenCalled()
expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com')
shell.openExternal.reset()
link.href = 'https://github.com'
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).toHaveBeenCalled()
expect(shell.openExternal.argsForCall[0][0]).toBe('https://github.com')
shell.openExternal.reset()
link.href = ''
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).not.toHaveBeenCalled()
shell.openExternal.reset()
link.href = '#scroll-me'
windowEventHandler.handleLinkClick(fakeEvent)
expect(shell.openExternal).not.toHaveBeenCalled()
})
)
describe('when a form is submitted', () =>
it("prevents the default so that the window's URL isn't changed", () => {
const form = document.createElement('form')
jasmine.attachToDOM(form)
let defaultPrevented = false
const event = new CustomEvent('submit', {bubbles: true})
event.preventDefault = () => { defaultPrevented = true }
form.dispatchEvent(event)
expect(defaultPrevented).toBe(true)
})
)
describe('core:focus-next and core:focus-previous', () => {
describe('when there is no currently focused element', () =>
it('focuses the element with the lowest/highest tabindex', () => {
const wrapperDiv = document.createElement('div')
wrapperDiv.innerHTML = `
<div>
<button tabindex="2"></button>
<input tabindex="1">
</div>
`.trim()
const elements = wrapperDiv.firstChild
jasmine.attachToDOM(elements)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(1)
document.body.focus()
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(2)
})
)
describe('when a tabindex is set on the currently focused element', () =>
it('focuses the element with the next highest/lowest tabindex, skipping disabled elements', () => {
const wrapperDiv = document.createElement('div')
wrapperDiv.innerHTML = `
<div>
<input tabindex="1">
<button tabindex="2"></button>
<button tabindex="5"></button>
<input tabindex="-1">
<input tabindex="3">
<button tabindex="7"></button>
<input tabindex="9" disabled>
</div>
`.trim()
const elements = wrapperDiv.firstChild
jasmine.attachToDOM(elements)
elements.querySelector('[tabindex="1"]').focus()
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(2)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(3)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(5)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(7)
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(1)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(7)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(5)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(3)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(2)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(1)
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
expect(document.activeElement.tabIndex).toBe(7)
})
)
})
describe('when keydown events occur on the document', () =>
it('dispatches the event via the KeymapManager and CommandRegistry', () => {
const dispatchedCommands = []
atom.commands.onWillDispatch(command => dispatchedCommands.push(command))
atom.commands.add('*', {'foo-command': () => {}})
atom.keymaps.add('source-name', {'*': {'x': 'foo-command'}})
const event = KeymapManager.buildKeydownEvent('x', {target: document.createElement('div')})
document.dispatchEvent(event)
expect(dispatchedCommands.length).toBe(1)
expect(dispatchedCommands[0].type).toBe('foo-command')
})
)
describe('native key bindings', () =>
it("correctly dispatches them to active elements with the '.native-key-bindings' class", () => {
const webContentsSpy = jasmine.createSpyObj('webContents', ['copy', 'paste'])
spyOn(atom.applicationDelegate, 'getCurrentWindow').andReturn({
webContents: webContentsSpy,
on: () => {}
})
const nativeKeyBindingsInput = document.createElement('input')
nativeKeyBindingsInput.classList.add('native-key-bindings')
jasmine.attachToDOM(nativeKeyBindingsInput)
nativeKeyBindingsInput.focus()
atom.dispatchApplicationMenuCommand('core:copy')
atom.dispatchApplicationMenuCommand('core:paste')
expect(webContentsSpy.copy).toHaveBeenCalled()
expect(webContentsSpy.paste).toHaveBeenCalled()
webContentsSpy.copy.reset()
webContentsSpy.paste.reset()
const normalInput = document.createElement('input')
jasmine.attachToDOM(normalInput)
normalInput.focus()
atom.dispatchApplicationMenuCommand('core:copy')
atom.dispatchApplicationMenuCommand('core:paste')
expect(webContentsSpy.copy).not.toHaveBeenCalled()
expect(webContentsSpy.paste).not.toHaveBeenCalled()
})
)
})

View File

@@ -1585,15 +1585,15 @@ i = /test/; #FIXME\
atom2.project.deserialize(atom.project.serialize())
atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers)
expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([
'CoffeeScript',
'CoffeeScript (Literate)',
'JSDoc',
'JavaScript',
'Null Grammar',
'Regular Expression Replacement (JavaScript)',
'Regular Expressions (JavaScript)',
'TODO'
expect(atom2.grammars.getGrammars().map(grammar => grammar.scopeName).sort()).toEqual([
'source.coffee',
'source.js',
'source.js.regexp',
'source.js.regexp.replacement',
'source.jsdoc',
'source.litcoffee',
'text.plain.null-grammar',
'text.todo'
])
atom2.destroy()

View File

@@ -89,7 +89,7 @@ module.exports = class CommandRegistry {
// DOM element, the command will be associated with just that element.
// * `commandName` A {String} containing the name of a command you want to
// handle such as `user:insert-date`.
// * `listener` A listener which handles the event. Either A {Function} to
// * `listener` A listener which handles the event. Either a {Function} to
// call when the given command is invoked on an element matching the
// selector, or an {Object} with a `didDispatch` property which is such a
// function.
@@ -97,7 +97,7 @@ module.exports = class CommandRegistry {
// The function (`listener` itself if it is a function, or the `didDispatch`
// method if `listener` is an object) will be called with `this` referencing
// the matching DOM node and the following argument:
// * `event` A standard DOM event instance. Call `stopPropagation` or
// * `event`: A standard DOM event instance. Call `stopPropagation` or
// `stopImmediatePropagation` to terminate bubbling early.
//
// Additionally, `listener` may have additional properties which are returned

View File

@@ -58,10 +58,10 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
let score = this.getGrammarPathScore(grammar, filePath)
if ((score > 0) && !grammar.bundledPackage) {
score += 0.25
score += 0.125
}
if (this.grammarMatchesContents(grammar, contents)) {
score += 0.125
score += 0.25
}
return score
}

View File

@@ -174,6 +174,11 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
'core:cut': -> @cutSelectedText()
'core:copy': -> @copySelectedText()
'core:paste': -> @pasteText()
'editor:paste-without-reformatting': -> @pasteText({
normalizeLineEndings: false,
autoIndent: false,
preserveTrailingLineIndentation: true
})
'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary()
'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary()
'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord()

View File

@@ -87,7 +87,7 @@ class Selection extends Model
setBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
options.reversed ?= @isReversed()
@editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
@editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) unless options.preserveFolds
@modifySelection =>
needsFlash = options.flash
delete options.flash if options.flash?
@@ -356,13 +356,19 @@ class Selection extends Model
#
# * `text` A {String} representing the text to add
# * `options` (optional) {Object} with keys:
# * `select` if `true`, selects the newly added text.
# * `autoIndent` if `true`, indents all inserted text appropriately.
# * `autoIndentNewline` if `true`, indent newline appropriately.
# * `autoDecreaseIndent` if `true`, decreases indent level appropriately
# * `select` If `true`, selects the newly added text.
# * `autoIndent` If `true`, indents all inserted text appropriately.
# * `autoIndentNewline` If `true`, indent newline appropriately.
# * `autoDecreaseIndent` If `true`, decreases indent level appropriately
# (for example, when a closing bracket is inserted).
# * `preserveTrailingLineIndentation` By default, when pasting multiple
# lines, Atom attempts to preserve the relative indent level between the
# first line and trailing lines, even if the indent level of the first
# line has changed from the copied text. If this option is `true`, this
# behavior is suppressed.
# level between the first lines and the trailing lines.
# * `normalizeLineEndings` (optional) {Boolean} (default: true)
# * `undo` if `skip`, skips the undo stack for this operation.
# * `undo` If `skip`, skips the undo stack for this operation.
insertText: (text, options={}) ->
oldBufferRange = @getBufferRange()
wasReversed = @isReversed()
@@ -373,7 +379,7 @@ class Selection extends Model
remainingLines = text.split('\n')
firstInsertedLine = remainingLines.shift()
if options.indentBasis?
if options.indentBasis? and not options.preserveTrailingLineIndentation
indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis
@adjustIndent(remainingLines, indentAdjustment)

View File

@@ -804,7 +804,15 @@ class TextEditorComponent {
key: overlayProps.element,
overlayComponents: this.overlayComponents,
measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element),
didResize: () => { this.updateSync() }
didResize: (overlayComponent) => {
this.updateOverlayToRender(overlayProps)
overlayComponent.update(Object.assign(
{
measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element)
},
overlayProps
))
}
},
overlayProps
))
@@ -1339,42 +1347,47 @@ class TextEditorComponent {
})
}
updateOverlayToRender (decoration) {
const windowInnerHeight = this.getWindowInnerHeight()
const windowInnerWidth = this.getWindowInnerWidth()
const contentClientRect = this.refs.content.getBoundingClientRect()
const {element, screenPosition, avoidOverflow} = decoration
const {row, column} = screenPosition
let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight()
let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
const clientRect = element.getBoundingClientRect()
this.overlayDimensionsByElement.set(element, clientRect)
if (avoidOverflow !== false) {
const computedStyle = window.getComputedStyle(element)
const elementTop = wrapperTop + parseInt(computedStyle.marginTop)
const elementBottom = elementTop + clientRect.height
const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom)
const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft)
const elementRight = elementLeft + clientRect.width
if (elementBottom > windowInnerHeight && flippedElementTop >= 0) {
wrapperTop -= (elementTop - flippedElementTop)
}
if (elementLeft < 0) {
wrapperLeft -= elementLeft
} else if (elementRight > windowInnerWidth) {
wrapperLeft -= (elementRight - windowInnerWidth)
}
}
decoration.pixelTop = Math.round(wrapperTop)
decoration.pixelLeft = Math.round(wrapperLeft)
}
updateOverlaysToRender () {
const overlayCount = this.decorationsToRender.overlays.length
if (overlayCount === 0) return null
const windowInnerHeight = this.getWindowInnerHeight()
const windowInnerWidth = this.getWindowInnerWidth()
const contentClientRect = this.refs.content.getBoundingClientRect()
for (let i = 0; i < overlayCount; i++) {
const decoration = this.decorationsToRender.overlays[i]
const {element, screenPosition, avoidOverflow} = decoration
const {row, column} = screenPosition
let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight()
let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
const clientRect = element.getBoundingClientRect()
this.overlayDimensionsByElement.set(element, clientRect)
if (avoidOverflow !== false) {
const computedStyle = window.getComputedStyle(element)
const elementTop = wrapperTop + parseInt(computedStyle.marginTop)
const elementBottom = elementTop + clientRect.height
const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom)
const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft)
const elementRight = elementLeft + clientRect.width
if (elementBottom > windowInnerHeight && flippedElementTop >= 0) {
wrapperTop -= (elementTop - flippedElementTop)
}
if (elementLeft < 0) {
wrapperLeft -= elementLeft
} else if (elementRight > windowInnerWidth) {
wrapperLeft -= (elementRight - windowInnerWidth)
}
}
decoration.pixelTop = Math.round(wrapperTop)
decoration.pixelLeft = Math.round(wrapperLeft)
this.updateOverlayToRender(decoration)
}
}
@@ -1758,7 +1771,7 @@ class TextEditorComponent {
if (target && target.matches('.fold-marker')) {
const bufferPosition = model.bufferPositionForScreenPosition(screenPosition)
model.destroyFoldsIntersectingBufferRange(Range(bufferPosition, bufferPosition))
model.destroyFoldsContainingBufferPositions([bufferPosition], false)
return
}
@@ -4202,7 +4215,7 @@ class OverlayComponent {
const {contentRect} = entries[0]
if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) {
this.resizeObserver.disconnect()
this.props.didResize()
this.props.didResize(this)
process.nextTick(() => { this.resizeObserver.observe(this.props.element) })
}
})
@@ -4215,15 +4228,30 @@ class OverlayComponent {
this.didDetach()
}
getNextUpdatePromise () {
if (!this.nextUpdatePromise) {
this.nextUpdatePromise = new Promise((resolve) => {
this.resolveNextUpdatePromise = () => {
this.nextUpdatePromise = null
this.resolveNextUpdatePromise = null
resolve()
}
})
}
return this.nextUpdatePromise
}
update (newProps) {
const oldProps = this.props
this.props = newProps
this.props = Object.assign({}, oldProps, newProps)
if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px'
if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px'
if (newProps.className !== oldProps.className) {
if (oldProps.className != null) this.element.classList.remove(oldProps.className)
if (newProps.className != null) this.element.classList.add(newProps.className)
}
if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise()
}
didAttach () {

View File

@@ -288,7 +288,7 @@ export default class TextEditorRegistry {
let currentScore = this.editorGrammarScores.get(editor)
if (currentScore == null || score > currentScore) {
editor.setGrammar(grammar, score)
editor.setGrammar(grammar)
this.editorGrammarScores.set(editor, score)
}
}

139
src/text-editor-utils.js Normal file
View File

@@ -0,0 +1,139 @@
// This file is temporary. We should gradually convert methods in `text-editor.coffee`
// from CoffeeScript to JavaScript and move them here, so that we can eventually convert
// the entire class to JavaScript.
const {Point, Range} = require('text-buffer')
const NON_WHITESPACE_REGEX = /\S/
module.exports = {
toggleLineCommentsForBufferRows (start, end) {
let {
commentStartString,
commentEndString
} = this.tokenizedBuffer.commentStringsForPosition(Point(start, 0))
if (!commentStartString) return
commentStartString = commentStartString.trim()
if (commentEndString) {
commentEndString = commentEndString.trim()
const startDelimiterColumnRange = columnRangeForStartDelimiter(
this.buffer.lineForRow(start),
commentStartString
)
if (startDelimiterColumnRange) {
const endDelimiterColumnRange = columnRangeForEndDelimiter(
this.buffer.lineForRow(end),
commentEndString
)
if (endDelimiterColumnRange) {
this.buffer.transact(() => {
this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]])
this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]])
})
}
} else {
this.buffer.transact(() => {
const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length
this.buffer.insert([start, indentLength], commentStartString + ' ')
this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString)
})
}
} else {
let hasCommentedLines = false
let hasUncommentedLines = false
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
if (NON_WHITESPACE_REGEX.test(line)) {
if (columnRangeForStartDelimiter(line, commentStartString)) {
hasCommentedLines = true
} else {
hasUncommentedLines = true
}
}
}
const shouldUncomment = hasCommentedLines && !hasUncommentedLines
if (shouldUncomment) {
for (let row = start; row <= end; row++) {
const columnRange = columnRangeForStartDelimiter(
this.buffer.lineForRow(row),
commentStartString
)
if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]])
}
} else {
let minIndentLevel = Infinity
let minBlankIndentLevel = Infinity
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
const indentLevel = this.indentLevelForLine(line)
if (NON_WHITESPACE_REGEX.test(line)) {
if (indentLevel < minIndentLevel) minIndentLevel = indentLevel
} else {
if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel
}
}
minIndentLevel = Number.isFinite(minIndentLevel)
? minIndentLevel
: Number.isFinite(minBlankIndentLevel)
? minBlankIndentLevel
: 0
const tabLength = this.getTabLength()
const indentString = ' '.repeat(tabLength * minIndentLevel)
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
if (NON_WHITESPACE_REGEX.test(line)) {
const indentColumn = columnForIndentLevel(line, minIndentLevel, this.getTabLength())
this.buffer.insert(Point(row, indentColumn), commentStartString + ' ')
} else {
this.buffer.setTextInRange(
new Range(new Point(row, 0), new Point(row, Infinity)),
indentString + commentStartString + ' '
)
}
}
}
}
}
}
function columnForIndentLevel (line, indentLevel, tabLength) {
let column = 0
let indentLength = 0
const goalIndentLength = indentLevel * tabLength
while (indentLength < goalIndentLength) {
const char = line[column]
if (char === '\t') {
indentLength += tabLength - (indentLength % tabLength)
} else if (char === ' ') {
indentLength++
} else {
break
}
column++
}
return column
}
function columnRangeForStartDelimiter (line, delimiter) {
const startColumn = line.search(NON_WHITESPACE_REGEX)
if (startColumn === -1) return null
if (!line.startsWith(delimiter, startColumn)) return null
let endColumn = startColumn + delimiter.length
if (line[endColumn] === ' ') endColumn++
return [startColumn, endColumn]
}
function columnRangeForEndDelimiter (line, delimiter) {
let startColumn = line.lastIndexOf(delimiter)
if (startColumn === -1) return null
const endColumn = startColumn + delimiter.length
if (NON_WHITESPACE_REGEX.test(line.slice(endColumn))) return null
if (line[startColumn - 1] === ' ') startColumn--
return [startColumn, endColumn]
}

View File

@@ -9,6 +9,8 @@ TokenizedBuffer = require './tokenized-buffer'
Cursor = require './cursor'
Model = require './model'
Selection = require './selection'
TextEditorUtils = require './text-editor-utils'
TextMateScopeSelector = require('first-mate').ScopeSelector
GutterContainer = require './gutter-container'
TextEditorComponent = null
@@ -123,6 +125,8 @@ class TextEditor extends Model
Object.defineProperty(@prototype, 'languageMode', get: -> @tokenizedBuffer)
Object.assign(@prototype, TextEditorUtils)
@deserialize: (state, atomEnvironment) ->
# TODO: Return null on version mismatch when 1.8.0 has been out for a while
if state.version isnt @prototype.serializationVersion and state.displayBuffer?
@@ -2495,8 +2499,9 @@ class TextEditor extends Model
#
# Returns the added {Selection}.
addSelectionForBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
unless options.preserveFolds
@destroyFoldsIntersectingBufferRange(bufferRange)
@displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true)
@selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false})
@getLastSelection().autoscroll() unless options.autoscroll is false
@getLastSelection()
@@ -3247,12 +3252,13 @@ class TextEditor extends Model
# corresponding clipboard selection text.
#
# * `options` (optional) See {Selection::insertText}.
pasteText: (options={}) ->
pasteText: (options) ->
options = Object.assign({}, options)
{text: clipboardText, metadata} = @constructor.clipboard.readWithMetadata()
return false unless @emitWillInsertTextEvent(clipboardText)
metadata ?= {}
options.autoIndent = @shouldAutoIndentOnPaste()
options.autoIndent ?= @shouldAutoIndentOnPaste()
@mutateSelectedText (selection, index) =>
if metadata.selections?.length is @getSelections().length
@@ -3310,14 +3316,13 @@ class TextEditor extends Model
# level.
foldCurrentRow: ->
{row} = @getCursorBufferPosition()
range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity))
@displayLayer.foldBufferRange(range)
if range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity))
@displayLayer.foldBufferRange(range)
# Essential: Unfold the most recent cursor's row by one level.
unfoldCurrentRow: ->
{row} = @getCursorBufferPosition()
position = Point(row, Infinity)
@displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position))
@displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false)
# Essential: Fold the given row in buffer coordinates based on its indentation
# level.
@@ -3346,7 +3351,7 @@ class TextEditor extends Model
# * `bufferRow` A {Number}
unfoldBufferRow: (bufferRow) ->
position = Point(bufferRow, Infinity)
@displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position))
@displayLayer.destroyFoldsContainingBufferPositions([position])
# Extended: For each selection, fold the rows it intersects.
foldSelectedLines: ->
@@ -3445,6 +3450,10 @@ class TextEditor extends Model
destroyFoldsIntersectingBufferRange: (bufferRange) ->
@displayLayer.destroyFoldsIntersectingBufferRange(bufferRange)
# Remove any {Fold}s found that contain the given array of buffer positions.
destroyFoldsContainingBufferPositions: (bufferPositions, excludeEndpoints) ->
@displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints)
###
Section: Gutters
###
@@ -3621,9 +3630,6 @@ class TextEditor extends Model
getNonWordCharacters: (scopes) ->
@scopedSettingsDelegate?.getNonWordCharacters?(scopes) ? @nonWordCharacters
getCommentStrings: (scopes) ->
@scopedSettingsDelegate?.getCommentStrings?(scopes)
###
Section: Event Handlers
###
@@ -3886,8 +3892,6 @@ class TextEditor extends Model
toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row)
toggleLineCommentsForBufferRows: (start, end) -> @tokenizedBuffer.toggleLineCommentsForBufferRows(start, end)
rowRangeForParagraphAtBufferRow: (bufferRow) ->
return unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(bufferRow))

View File

@@ -163,99 +163,12 @@ class TokenizedBuffer {
Section - Comments
*/
toggleLineCommentsForBufferRows (start, end) {
const scope = this.scopeDescriptorForPosition([start, 0])
const commentStrings = this.commentStringsForScopeDescriptor(scope)
if (!commentStrings) return
const {commentStartString, commentEndString} = commentStrings
if (!commentStartString) return
const commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
const commentStartRegex = new OnigRegExp(`^(\\s*)(${commentStartRegexString})`)
if (commentEndString) {
const shouldUncomment = commentStartRegex.testSync(this.buffer.lineForRow(start))
if (shouldUncomment) {
const commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?')
const commentEndRegex = new OnigRegExp(`(${commentEndRegexString})(\\s*)$`)
const startMatch = commentStartRegex.searchSync(this.buffer.lineForRow(start))
const endMatch = commentEndRegex.searchSync(this.buffer.lineForRow(end))
if (startMatch && endMatch) {
this.buffer.transact(() => {
const columnStart = startMatch[1].length
const columnEnd = columnStart + startMatch[2].length
this.buffer.setTextInRange([[start, columnStart], [start, columnEnd]], '')
const endLength = this.buffer.lineLengthForRow(end) - endMatch[2].length
const endColumn = endLength - endMatch[1].length
return this.buffer.setTextInRange([[end, endColumn], [end, endLength]], '')
})
}
} else {
this.buffer.transact(() => {
const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length
this.buffer.insert([start, indentLength], commentStartString)
this.buffer.insert([end, this.buffer.lineLengthForRow(end)], commentEndString)
})
}
commentStringsForPosition (position) {
if (this.scopedSettingsDelegate) {
const scope = this.scopeDescriptorForPosition(position)
return this.scopedSettingsDelegate.getCommentStrings(scope)
} else {
let hasCommentedLines = false
let hasUncommentedLines = false
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
if (NON_WHITESPACE_REGEX.test(line)) {
if (commentStartRegex.testSync(line)) {
hasCommentedLines = true
} else {
hasUncommentedLines = true
}
}
}
const shouldUncomment = hasCommentedLines && !hasUncommentedLines
if (shouldUncomment) {
for (let row = start; row <= end; row++) {
const match = commentStartRegex.searchSync(this.buffer.lineForRow(row))
if (match) {
const columnStart = match[1].length
const columnEnd = columnStart + match[2].length
this.buffer.setTextInRange([[row, columnStart], [row, columnEnd]], '')
}
}
} else {
let minIndentLevel = Infinity
let minBlankIndentLevel = Infinity
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
const indentLevel = this.indentLevelForLine(line)
if (NON_WHITESPACE_REGEX.test(line)) {
if (indentLevel < minIndentLevel) minIndentLevel = indentLevel
} else {
if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel
}
}
minIndentLevel = Number.isFinite(minIndentLevel)
? minIndentLevel
: Number.isFinite(minBlankIndentLevel)
? minBlankIndentLevel
: 0
const tabLength = this.getTabLength()
const indentString = ' '.repeat(tabLength * minIndentLevel)
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
if (NON_WHITESPACE_REGEX.test(line)) {
const indentColumn = this.columnForIndentLevel(line, minIndentLevel)
this.buffer.insert(Point(row, indentColumn), commentStartString)
} else {
this.buffer.setTextInRange(
new Range(new Point(row, 0), new Point(row, Infinity)),
indentString + commentStartString
)
}
}
}
return {}
}
}
@@ -594,24 +507,6 @@ class TokenizedBuffer {
return scopes
}
columnForIndentLevel (line, indentLevel, tabLength = this.tabLength) {
let column = 0
let indentLength = 0
const goalIndentLength = indentLevel * tabLength
while (indentLength < goalIndentLength) {
const char = line[column]
if (char === '\t') {
indentLength += tabLength - (indentLength % tabLength)
} else if (char === ' ') {
indentLength++
} else {
break
}
column++
}
return column
}
indentLevelForLine (line, tabLength = this.tabLength) {
let indentLength = 0
for (let i = 0, {length} = line; i < length; i++) {
@@ -841,12 +736,6 @@ class TokenizedBuffer {
}
}
commentStringsForScopeDescriptor (scopes) {
if (this.scopedSettingsDelegate) {
return this.scopedSettingsDelegate.getCommentStrings(scopes)
}
}
regexForPattern (pattern) {
if (pattern) {
if (!this.regexesByPattern[pattern]) {

View File

@@ -1,176 +0,0 @@
_ = require 'underscore-plus'
{Disposable, CompositeDisposable} = require 'event-kit'
Tooltip = null
# Essential: Associates tooltips with HTML elements.
#
# You can get the `TooltipManager` via `atom.tooltips`.
#
# ## Examples
#
# The essence of displaying a tooltip
#
# ```coffee
# # display it
# disposable = atom.tooltips.add(div, {title: 'This is a tooltip'})
#
# # remove it
# disposable.dispose()
# ```
#
# In practice there are usually multiple tooltips. So we add them to a
# CompositeDisposable
#
# ```coffee
# {CompositeDisposable} = require 'atom'
# subscriptions = new CompositeDisposable
#
# div1 = document.createElement('div')
# div2 = document.createElement('div')
# subscriptions.add atom.tooltips.add(div1, {title: 'This is a tooltip'})
# subscriptions.add atom.tooltips.add(div2, {title: 'Another tooltip'})
#
# # remove them all
# subscriptions.dispose()
# ```
#
# You can display a key binding in the tooltip as well with the
# `keyBindingCommand` option.
#
# ```coffee
# disposable = atom.tooltips.add @caseOptionButton,
# title: "Match Case"
# keyBindingCommand: 'find-and-replace:toggle-case-option'
# keyBindingTarget: @findEditor.element
# ```
module.exports =
class TooltipManager
defaults:
trigger: 'hover'
container: 'body'
html: true
placement: 'auto top'
viewportPadding: 2
hoverDefaults:
{delay: {show: 1000, hide: 100}}
constructor: ({@keymapManager, @viewRegistry}) ->
@tooltips = new Map()
# Essential: Add a tooltip to the given element.
#
# * `target` An `HTMLElement`
# * `options` An object with one or more of the following options:
# * `title` A {String} or {Function} to use for the text in the tip. If
# a function is passed, `this` will be set to the `target` element. This
# option is mutually exclusive with the `item` option.
# * `html` A {Boolean} affecting the interpretation of the `title` option.
# If `true` (the default), the `title` string will be interpreted as HTML.
# Otherwise it will be interpreted as plain text.
# * `item` A view (object with an `.element` property) or a DOM element
# containing custom content for the tooltip. This option is mutually
# exclusive with the `title` option.
# * `class` A {String} with a class to apply to the tooltip element to
# enable custom styling.
# * `placement` A {String} or {Function} returning a string to indicate
# the position of the tooltip relative to `element`. Can be `'top'`,
# `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is
# specified, it will dynamically reorient the tooltip. For example, if
# placement is `'auto left'`, the tooltip will display to the left when
# possible, otherwise it will display right.
# When a function is used to determine the placement, it is called with
# the tooltip DOM node as its first argument and the triggering element
# DOM node as its second. The `this` context is set to the tooltip
# instance.
# * `trigger` A {String} indicating how the tooltip should be displayed.
# Choose from one of the following options:
# * `'hover'` Show the tooltip when the mouse hovers over the element.
# This is the default.
# * `'click'` Show the tooltip when the element is clicked. The tooltip
# will be hidden after clicking the element again or anywhere else
# outside of the tooltip itself.
# * `'focus'` Show the tooltip when the element is focused.
# * `'manual'` Show the tooltip immediately and only hide it when the
# returned disposable is disposed.
# * `delay` An object specifying the show and hide delay in milliseconds.
# Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and
# otherwise defaults to `0` for both values.
# * `keyBindingCommand` A {String} containing a command name. If you specify
# this option and a key binding exists that matches the command, it will
# be appended to the title or rendered alone if no title is specified.
# * `keyBindingTarget` An `HTMLElement` on which to look up the key binding.
# If this option is not supplied, the first of all matching key bindings
# for the given command will be rendered.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# tooltip.
add: (target, options) ->
if target.jquery
disposable = new CompositeDisposable
disposable.add @add(element, options) for element in target
return disposable
Tooltip ?= require './tooltip'
{keyBindingCommand, keyBindingTarget} = options
if keyBindingCommand?
bindings = @keymapManager.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget)
keystroke = getKeystroke(bindings)
if options.title? and keystroke?
options.title += " " + getKeystroke(bindings)
else if keystroke?
options.title = getKeystroke(bindings)
delete options.selector
options = _.defaults(options, @defaults)
if options.trigger is 'hover'
options = _.defaults(options, @hoverDefaults)
tooltip = new Tooltip(target, options, @viewRegistry)
if not @tooltips.has(target)
@tooltips.set(target, [])
@tooltips.get(target).push(tooltip)
hideTooltip = ->
tooltip.leave(currentTarget: target)
tooltip.hide()
window.addEventListener('resize', hideTooltip)
disposable = new Disposable =>
window.removeEventListener('resize', hideTooltip)
hideTooltip()
tooltip.destroy()
if @tooltips.has(target)
tooltipsForTarget = @tooltips.get(target)
index = tooltipsForTarget.indexOf(tooltip)
if index isnt -1
tooltipsForTarget.splice(index, 1)
if tooltipsForTarget.length is 0
@tooltips.delete(target)
disposable
# Extended: Find the tooltips that have been applied to the given element.
#
# * `target` The `HTMLElement` to find tooltips on.
#
# Returns an {Array} of `Tooltip` objects that match the `target`.
findTooltips: (target) ->
if @tooltips.has(target)
@tooltips.get(target).slice()
else
[]
humanizeKeystrokes = (keystroke) ->
keystrokes = keystroke.split(' ')
keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes)
keystrokes.join(' ')
getKeystroke = (bindings) ->
if bindings?.length
"<span class=\"keystroke\">#{humanizeKeystrokes(bindings[0].keystrokes)}</span>"

199
src/tooltip-manager.js Normal file
View File

@@ -0,0 +1,199 @@
const _ = require('underscore-plus')
const {Disposable, CompositeDisposable} = require('event-kit')
let Tooltip = null
// Essential: Associates tooltips with HTML elements.
//
// You can get the `TooltipManager` via `atom.tooltips`.
//
// ## Examples
//
// The essence of displaying a tooltip
//
// ```javascript
// // display it
// const disposable = atom.tooltips.add(div, {title: 'This is a tooltip'})
//
// // remove it
// disposable.dispose()
// ```
//
// In practice there are usually multiple tooltips. So we add them to a
// CompositeDisposable
//
// ```javascript
// const {CompositeDisposable} = require('atom')
// const subscriptions = new CompositeDisposable()
//
// const div1 = document.createElement('div')
// const div2 = document.createElement('div')
// subscriptions.add(atom.tooltips.add(div1, {title: 'This is a tooltip'}))
// subscriptions.add(atom.tooltips.add(div2, {title: 'Another tooltip'}))
//
// // remove them all
// subscriptions.dispose()
// ```
//
// You can display a key binding in the tooltip as well with the
// `keyBindingCommand` option.
//
// ```javascript
// disposable = atom.tooltips.add(this.caseOptionButton, {
// title: 'Match Case',
// keyBindingCommand: 'find-and-replace:toggle-case-option',
// keyBindingTarget: this.findEditor.element
// })
// ```
module.exports =
class TooltipManager {
constructor ({keymapManager, viewRegistry}) {
this.defaults = {
trigger: 'hover',
container: 'body',
html: true,
placement: 'auto top',
viewportPadding: 2
}
this.hoverDefaults = {
delay: {show: 1000, hide: 100}
}
this.keymapManager = keymapManager
this.viewRegistry = viewRegistry
this.tooltips = new Map()
}
// Essential: Add a tooltip to the given element.
//
// * `target` An `HTMLElement`
// * `options` An object with one or more of the following options:
// * `title` A {String} or {Function} to use for the text in the tip. If
// a function is passed, `this` will be set to the `target` element. This
// option is mutually exclusive with the `item` option.
// * `html` A {Boolean} affecting the interpretation of the `title` option.
// If `true` (the default), the `title` string will be interpreted as HTML.
// Otherwise it will be interpreted as plain text.
// * `item` A view (object with an `.element` property) or a DOM element
// containing custom content for the tooltip. This option is mutually
// exclusive with the `title` option.
// * `class` A {String} with a class to apply to the tooltip element to
// enable custom styling.
// * `placement` A {String} or {Function} returning a string to indicate
// the position of the tooltip relative to `element`. Can be `'top'`,
// `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is
// specified, it will dynamically reorient the tooltip. For example, if
// placement is `'auto left'`, the tooltip will display to the left when
// possible, otherwise it will display right.
// When a function is used to determine the placement, it is called with
// the tooltip DOM node as its first argument and the triggering element
// DOM node as its second. The `this` context is set to the tooltip
// instance.
// * `trigger` A {String} indicating how the tooltip should be displayed.
// Choose from one of the following options:
// * `'hover'` Show the tooltip when the mouse hovers over the element.
// This is the default.
// * `'click'` Show the tooltip when the element is clicked. The tooltip
// will be hidden after clicking the element again or anywhere else
// outside of the tooltip itself.
// * `'focus'` Show the tooltip when the element is focused.
// * `'manual'` Show the tooltip immediately and only hide it when the
// returned disposable is disposed.
// * `delay` An object specifying the show and hide delay in milliseconds.
// Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and
// otherwise defaults to `0` for both values.
// * `keyBindingCommand` A {String} containing a command name. If you specify
// this option and a key binding exists that matches the command, it will
// be appended to the title or rendered alone if no title is specified.
// * `keyBindingTarget` An `HTMLElement` on which to look up the key binding.
// If this option is not supplied, the first of all matching key bindings
// for the given command will be rendered.
//
// Returns a {Disposable} on which `.dispose()` can be called to remove the
// tooltip.
add (target, options) {
if (target.jquery) {
const disposable = new CompositeDisposable()
for (const element of target) { disposable.add(this.add(element, options)) }
return disposable
}
if (Tooltip == null) { Tooltip = require('./tooltip') }
const {keyBindingCommand, keyBindingTarget} = options
if (keyBindingCommand != null) {
const bindings = this.keymapManager.findKeyBindings({command: keyBindingCommand, target: keyBindingTarget})
const keystroke = getKeystroke(bindings)
if ((options.title != null) && (keystroke != null)) {
options.title += ` ${getKeystroke(bindings)}`
} else if (keystroke != null) {
options.title = getKeystroke(bindings)
}
}
delete options.selector
options = _.defaults(options, this.defaults)
if (options.trigger === 'hover') {
options = _.defaults(options, this.hoverDefaults)
}
const tooltip = new Tooltip(target, options, this.viewRegistry)
if (!this.tooltips.has(target)) {
this.tooltips.set(target, [])
}
this.tooltips.get(target).push(tooltip)
const hideTooltip = function () {
tooltip.leave({currentTarget: target})
tooltip.hide()
}
window.addEventListener('resize', hideTooltip)
const disposable = new Disposable(() => {
window.removeEventListener('resize', hideTooltip)
hideTooltip()
tooltip.destroy()
if (this.tooltips.has(target)) {
const tooltipsForTarget = this.tooltips.get(target)
const index = tooltipsForTarget.indexOf(tooltip)
if (index !== -1) {
tooltipsForTarget.splice(index, 1)
}
if (tooltipsForTarget.length === 0) {
this.tooltips.delete(target)
}
}
})
return disposable
}
// Extended: Find the tooltips that have been applied to the given element.
//
// * `target` The `HTMLElement` to find tooltips on.
//
// Returns an {Array} of `Tooltip` objects that match the `target`.
findTooltips (target) {
if (this.tooltips.has(target)) {
return this.tooltips.get(target).slice()
} else {
return []
}
}
}
function humanizeKeystrokes (keystroke) {
let keystrokes = keystroke.split(' ')
keystrokes = (keystrokes.map((stroke) => _.humanizeKeystroke(stroke)))
return keystrokes.join(' ')
}
function getKeystroke (bindings) {
if (bindings && bindings.length) {
return `<span class="keystroke">${humanizeKeystrokes(bindings[0].keystrokes)}</span>`
}
}

View File

@@ -1,201 +0,0 @@
Grim = require 'grim'
{Disposable} = require 'event-kit'
_ = require 'underscore-plus'
AnyConstructor = Symbol('any-constructor')
# Essential: `ViewRegistry` handles the association between model and view
# types in Atom. We call this association a View Provider. As in, for a given
# model, this class can provide a view via {::getView}, as long as the
# model/view association was registered via {::addViewProvider}
#
# If you're adding your own kind of pane item, a good strategy for all but the
# simplest items is to separate the model and the view. The model handles
# application logic and is the primary point of API interaction. The view
# just handles presentation.
#
# Note: Models can be any object, but must implement a `getTitle()` function
# if they are to be displayed in a {Pane}
#
# View providers inform the workspace how your model objects should be
# presented in the DOM. A view provider must always return a DOM node, which
# makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/)
# an ideal tool for implementing views in Atom.
#
# You can access the `ViewRegistry` object via `atom.views`.
module.exports =
class ViewRegistry
animationFrameRequest: null
documentReadInProgress: false
constructor: (@atomEnvironment) ->
@clear()
clear: ->
@views = new WeakMap
@providers = []
@clearDocumentRequests()
# Essential: Add a provider that will be used to construct views in the
# workspace's view layer based on model objects in its model layer.
#
# ## Examples
#
# Text editors are divided into a model and a view layer, so when you interact
# with methods like `atom.workspace.getActiveTextEditor()` you're only going
# to get the model object. We display text editors on screen by teaching the
# workspace what view constructor it should use to represent them:
#
# ```coffee
# atom.views.addViewProvider TextEditor, (textEditor) ->
# textEditorElement = new TextEditorElement
# textEditorElement.initialize(textEditor)
# textEditorElement
# ```
#
# * `modelConstructor` (optional) Constructor {Function} for your model. If
# a constructor is given, the `createView` function will only be used
# for model objects inheriting from that constructor. Otherwise, it will
# will be called for any object.
# * `createView` Factory {Function} that is passed an instance of your model
# and must return a subclass of `HTMLElement` or `undefined`. If it returns
# `undefined`, then the registry will continue to search for other view
# providers.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# added provider.
addViewProvider: (modelConstructor, createView) ->
if arguments.length is 1
switch typeof modelConstructor
when 'function'
provider = {createView: modelConstructor, modelConstructor: AnyConstructor}
when 'object'
Grim.deprecate("atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.")
provider = modelConstructor
else
throw new TypeError("Arguments to addViewProvider must be functions")
else
provider = {modelConstructor, createView}
@providers.push(provider)
new Disposable =>
@providers = @providers.filter (p) -> p isnt provider
getViewProviderCount: ->
@providers.length
# Essential: Get the view associated with an object in the workspace.
#
# If you're just *using* the workspace, you shouldn't need to access the view
# layer, but view layer access may be necessary if you want to perform DOM
# manipulation that isn't supported via the model API.
#
# ## View Resolution Algorithm
#
# The view associated with the object is resolved using the following
# sequence
#
# 1. Is the object an instance of `HTMLElement`? If true, return the object.
# 2. Does the object have a method named `getElement` that returns an
# instance of `HTMLElement`? If true, return that value.
# 3. Does the object have a property named `element` with a value which is
# an instance of `HTMLElement`? If true, return the property value.
# 4. Is the object a jQuery object, indicated by the presence of a `jquery`
# property? If true, return the root DOM element (i.e. `object[0]`).
# 5. Has a view provider been registered for the object? If true, use the
# provider to create a view associated with the object, and return the
# view.
#
# If no associated view is returned by the sequence an error is thrown.
#
# Returns a DOM element.
getView: (object) ->
return unless object?
if view = @views.get(object)
view
else
view = @createView(object)
@views.set(object, view)
view
createView: (object) ->
if object instanceof HTMLElement
return object
if typeof object?.getElement is 'function'
element = object.getElement()
if element instanceof HTMLElement
return element
if object?.element instanceof HTMLElement
return object.element
if object?.jquery
return object[0]
for provider in @providers
if provider.modelConstructor is AnyConstructor
if element = provider.createView(object, @atomEnvironment)
return element
continue
if object instanceof provider.modelConstructor
if element = provider.createView?(object, @atomEnvironment)
return element
if viewConstructor = provider.viewConstructor
element = new viewConstructor
element.initialize?(object) ? element.setModel?(object)
return element
if viewConstructor = object?.getViewClass?()
view = new viewConstructor(object)
return view[0]
throw new Error("Can't create a view for #{object.constructor.name} instance. Please register a view provider.")
updateDocument: (fn) ->
@documentWriters.push(fn)
@requestDocumentUpdate() unless @documentReadInProgress
new Disposable =>
@documentWriters = @documentWriters.filter (writer) -> writer isnt fn
readDocument: (fn) ->
@documentReaders.push(fn)
@requestDocumentUpdate()
new Disposable =>
@documentReaders = @documentReaders.filter (reader) -> reader isnt fn
getNextUpdatePromise: ->
@nextUpdatePromise ?= new Promise (resolve) =>
@resolveNextUpdatePromise = resolve
clearDocumentRequests: ->
@documentReaders = []
@documentWriters = []
@nextUpdatePromise = null
@resolveNextUpdatePromise = null
if @animationFrameRequest?
cancelAnimationFrame(@animationFrameRequest)
@animationFrameRequest = null
requestDocumentUpdate: ->
@animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate)
performDocumentUpdate: =>
resolveNextUpdatePromise = @resolveNextUpdatePromise
@animationFrameRequest = null
@nextUpdatePromise = null
@resolveNextUpdatePromise = null
writer() while writer = @documentWriters.shift()
@documentReadInProgress = true
reader() while reader = @documentReaders.shift()
@documentReadInProgress = false
# process updates requested as a result of reads
writer() while writer = @documentWriters.shift()
resolveNextUpdatePromise?()

259
src/view-registry.js Normal file
View File

@@ -0,0 +1,259 @@
const Grim = require('grim')
const {Disposable} = require('event-kit')
const AnyConstructor = Symbol('any-constructor')
// Essential: `ViewRegistry` handles the association between model and view
// types in Atom. We call this association a View Provider. As in, for a given
// model, this class can provide a view via {::getView}, as long as the
// model/view association was registered via {::addViewProvider}
//
// If you're adding your own kind of pane item, a good strategy for all but the
// simplest items is to separate the model and the view. The model handles
// application logic and is the primary point of API interaction. The view
// just handles presentation.
//
// Note: Models can be any object, but must implement a `getTitle()` function
// if they are to be displayed in a {Pane}
//
// View providers inform the workspace how your model objects should be
// presented in the DOM. A view provider must always return a DOM node, which
// makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/)
// an ideal tool for implementing views in Atom.
//
// You can access the `ViewRegistry` object via `atom.views`.
module.exports =
class ViewRegistry {
constructor (atomEnvironment) {
this.animationFrameRequest = null
this.documentReadInProgress = false
this.performDocumentUpdate = this.performDocumentUpdate.bind(this)
this.atomEnvironment = atomEnvironment
this.clear()
}
clear () {
this.views = new WeakMap()
this.providers = []
this.clearDocumentRequests()
}
// Essential: Add a provider that will be used to construct views in the
// workspace's view layer based on model objects in its model layer.
//
// ## Examples
//
// Text editors are divided into a model and a view layer, so when you interact
// with methods like `atom.workspace.getActiveTextEditor()` you're only going
// to get the model object. We display text editors on screen by teaching the
// workspace what view constructor it should use to represent them:
//
// ```coffee
// atom.views.addViewProvider TextEditor, (textEditor) ->
// textEditorElement = new TextEditorElement
// textEditorElement.initialize(textEditor)
// textEditorElement
// ```
//
// * `modelConstructor` (optional) Constructor {Function} for your model. If
// a constructor is given, the `createView` function will only be used
// for model objects inheriting from that constructor. Otherwise, it will
// will be called for any object.
// * `createView` Factory {Function} that is passed an instance of your model
// and must return a subclass of `HTMLElement` or `undefined`. If it returns
// `undefined`, then the registry will continue to search for other view
// providers.
//
// Returns a {Disposable} on which `.dispose()` can be called to remove the
// added provider.
addViewProvider (modelConstructor, createView) {
let provider
if (arguments.length === 1) {
switch (typeof modelConstructor) {
case 'function':
provider = {createView: modelConstructor, modelConstructor: AnyConstructor}
break
case 'object':
Grim.deprecate('atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.')
provider = modelConstructor
break
default:
throw new TypeError('Arguments to addViewProvider must be functions')
}
} else {
provider = {modelConstructor, createView}
}
this.providers.push(provider)
return new Disposable(() => {
this.providers = this.providers.filter(p => p !== provider)
})
}
getViewProviderCount () {
return this.providers.length
}
// Essential: Get the view associated with an object in the workspace.
//
// If you're just *using* the workspace, you shouldn't need to access the view
// layer, but view layer access may be necessary if you want to perform DOM
// manipulation that isn't supported via the model API.
//
// ## View Resolution Algorithm
//
// The view associated with the object is resolved using the following
// sequence
//
// 1. Is the object an instance of `HTMLElement`? If true, return the object.
// 2. Does the object have a method named `getElement` that returns an
// instance of `HTMLElement`? If true, return that value.
// 3. Does the object have a property named `element` with a value which is
// an instance of `HTMLElement`? If true, return the property value.
// 4. Is the object a jQuery object, indicated by the presence of a `jquery`
// property? If true, return the root DOM element (i.e. `object[0]`).
// 5. Has a view provider been registered for the object? If true, use the
// provider to create a view associated with the object, and return the
// view.
//
// If no associated view is returned by the sequence an error is thrown.
//
// Returns a DOM element.
getView (object) {
if (object == null) { return }
let view = this.views.get(object)
if (!view) {
view = this.createView(object)
this.views.set(object, view)
}
return view
}
createView (object) {
if (object instanceof HTMLElement) { return object }
let element
if (object && (typeof object.getElement === 'function')) {
element = object.getElement()
if (element instanceof HTMLElement) {
return element
}
}
if (object && object.element instanceof HTMLElement) {
return object.element
}
if (object && object.jquery) {
return object[0]
}
for (let provider of this.providers) {
if (provider.modelConstructor === AnyConstructor) {
element = provider.createView(object, this.atomEnvironment)
if (element) { return element }
continue
}
if (object instanceof provider.modelConstructor) {
element = provider.createView && provider.createView(object, this.atomEnvironment)
if (element) { return element }
let ViewConstructor = provider.viewConstructor
if (ViewConstructor) {
element = new ViewConstructor()
if (element.initialize) {
element.initialize(object)
} else if (element.setModel) {
element.setModel(object)
}
return element
}
}
}
if (object && object.getViewClass) {
let ViewConstructor = object.getViewClass()
if (ViewConstructor) {
const view = new ViewConstructor(object)
return view[0]
}
}
throw new Error(`Can't create a view for ${object.constructor.name} instance. Please register a view provider.`)
}
updateDocument (fn) {
this.documentWriters.push(fn)
if (!this.documentReadInProgress) { this.requestDocumentUpdate() }
return new Disposable(() => {
this.documentWriters = this.documentWriters.filter(writer => writer !== fn)
})
}
readDocument (fn) {
this.documentReaders.push(fn)
this.requestDocumentUpdate()
return new Disposable(() => {
this.documentReaders = this.documentReaders.filter(reader => reader !== fn)
})
}
getNextUpdatePromise () {
if (this.nextUpdatePromise == null) {
this.nextUpdatePromise = new Promise(resolve => {
this.resolveNextUpdatePromise = resolve
})
}
return this.nextUpdatePromise
}
clearDocumentRequests () {
this.documentReaders = []
this.documentWriters = []
this.nextUpdatePromise = null
this.resolveNextUpdatePromise = null
if (this.animationFrameRequest != null) {
cancelAnimationFrame(this.animationFrameRequest)
this.animationFrameRequest = null
}
}
requestDocumentUpdate () {
if (this.animationFrameRequest == null) {
this.animationFrameRequest = requestAnimationFrame(this.performDocumentUpdate)
}
}
performDocumentUpdate () {
const { resolveNextUpdatePromise } = this
this.animationFrameRequest = null
this.nextUpdatePromise = null
this.resolveNextUpdatePromise = null
var writer = this.documentWriters.shift()
while (writer) {
writer()
writer = this.documentWriters.shift()
}
var reader = this.documentReaders.shift()
this.documentReadInProgress = true
while (reader) {
reader()
reader = this.documentReaders.shift()
}
this.documentReadInProgress = false
// process updates requested as a result of reads
writer = this.documentWriters.shift()
while (writer) {
writer()
writer = this.documentWriters.shift()
}
if (resolveNextUpdatePromise) { resolveNextUpdatePromise() }
}
}

View File

@@ -1,189 +0,0 @@
{Disposable, CompositeDisposable} = require 'event-kit'
listen = require './delegated-listener'
# Handles low-level events related to the @window.
module.exports =
class WindowEventHandler
constructor: ({@atomEnvironment, @applicationDelegate}) ->
@reloadRequested = false
@subscriptions = new CompositeDisposable
@handleNativeKeybindings()
initialize: (@window, @document) ->
@subscriptions.add @atomEnvironment.commands.add @window,
'window:toggle-full-screen': @handleWindowToggleFullScreen
'window:close': @handleWindowClose
'window:reload': @handleWindowReload
'window:toggle-dev-tools': @handleWindowToggleDevTools
if process.platform in ['win32', 'linux']
@subscriptions.add @atomEnvironment.commands.add @window,
'window:toggle-menu-bar': @handleWindowToggleMenuBar
@subscriptions.add @atomEnvironment.commands.add @document,
'core:focus-next': @handleFocusNext
'core:focus-previous': @handleFocusPrevious
@addEventListener(@window, 'beforeunload', @handleWindowBeforeunload)
@addEventListener(@window, 'focus', @handleWindowFocus)
@addEventListener(@window, 'blur', @handleWindowBlur)
@addEventListener(@document, 'keyup', @handleDocumentKeyEvent)
@addEventListener(@document, 'keydown', @handleDocumentKeyEvent)
@addEventListener(@document, 'drop', @handleDocumentDrop)
@addEventListener(@document, 'dragover', @handleDocumentDragover)
@addEventListener(@document, 'contextmenu', @handleDocumentContextmenu)
@subscriptions.add listen(@document, 'click', 'a', @handleLinkClick)
@subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit)
@subscriptions.add(@applicationDelegate.onDidEnterFullScreen(@handleEnterFullScreen))
@subscriptions.add(@applicationDelegate.onDidLeaveFullScreen(@handleLeaveFullScreen))
# Wire commands that should be handled by Chromium for elements with the
# `.native-key-bindings` class.
handleNativeKeybindings: ->
bindCommandToAction = (command, action) =>
@subscriptions.add @atomEnvironment.commands.add(
'.native-key-bindings',
command,
((event) => @applicationDelegate.getCurrentWindow().webContents[action]()),
false
)
bindCommandToAction('core:copy', 'copy')
bindCommandToAction('core:paste', 'paste')
bindCommandToAction('core:undo', 'undo')
bindCommandToAction('core:redo', 'redo')
bindCommandToAction('core:select-all', 'selectAll')
bindCommandToAction('core:cut', 'cut')
unsubscribe: ->
@subscriptions.dispose()
on: (target, eventName, handler) ->
target.on(eventName, handler)
@subscriptions.add(new Disposable ->
target.removeListener(eventName, handler)
)
addEventListener: (target, eventName, handler) ->
target.addEventListener(eventName, handler)
@subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler)))
handleDocumentKeyEvent: (event) =>
@atomEnvironment.keymaps.handleKeyboardEvent(event)
event.stopImmediatePropagation()
handleDrop: (event) ->
event.preventDefault()
event.stopPropagation()
handleDragover: (event) ->
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'none'
eachTabIndexedElement: (callback) ->
for element in @document.querySelectorAll('[tabindex]')
continue if element.disabled
continue unless element.tabIndex >= 0
callback(element, element.tabIndex)
return
handleFocusNext: =>
focusedTabIndex = @document.activeElement.tabIndex ? -Infinity
nextElement = null
nextTabIndex = Infinity
lowestElement = null
lowestTabIndex = Infinity
@eachTabIndexedElement (element, tabIndex) ->
if tabIndex < lowestTabIndex
lowestTabIndex = tabIndex
lowestElement = element
if focusedTabIndex < tabIndex < nextTabIndex
nextTabIndex = tabIndex
nextElement = element
if nextElement?
nextElement.focus()
else if lowestElement?
lowestElement.focus()
handleFocusPrevious: =>
focusedTabIndex = @document.activeElement.tabIndex ? Infinity
previousElement = null
previousTabIndex = -Infinity
highestElement = null
highestTabIndex = -Infinity
@eachTabIndexedElement (element, tabIndex) ->
if tabIndex > highestTabIndex
highestTabIndex = tabIndex
highestElement = element
if focusedTabIndex > tabIndex > previousTabIndex
previousTabIndex = tabIndex
previousElement = element
if previousElement?
previousElement.focus()
else if highestElement?
highestElement.focus()
handleWindowFocus: ->
@document.body.classList.remove('is-blurred')
handleWindowBlur: =>
@document.body.classList.add('is-blurred')
@atomEnvironment.storeWindowDimensions()
handleEnterFullScreen: =>
@document.body.classList.add("fullscreen")
handleLeaveFullScreen: =>
@document.body.classList.remove("fullscreen")
handleWindowBeforeunload: (event) =>
if not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused()
@atomEnvironment.hide()
@reloadRequested = false
@atomEnvironment.storeWindowDimensions()
@atomEnvironment.unloadEditorWindow()
@atomEnvironment.destroy()
handleWindowToggleFullScreen: =>
@atomEnvironment.toggleFullScreen()
handleWindowClose: =>
@atomEnvironment.close()
handleWindowReload: =>
@reloadRequested = true
@atomEnvironment.reload()
handleWindowToggleDevTools: =>
@atomEnvironment.toggleDevTools()
handleWindowToggleMenuBar: =>
@atomEnvironment.config.set('core.autoHideMenuBar', not @atomEnvironment.config.get('core.autoHideMenuBar'))
if @atomEnvironment.config.get('core.autoHideMenuBar')
detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command"
@atomEnvironment.notifications.addInfo('Menu bar hidden', {detail})
handleLinkClick: (event) =>
event.preventDefault()
uri = event.currentTarget?.getAttribute('href')
if uri and uri[0] isnt '#' and /^https?:\/\//.test(uri)
@applicationDelegate.openExternal(uri)
handleFormSubmit: (event) ->
# Prevent form submits from changing the current window's URL
event.preventDefault()
handleDocumentContextmenu: (event) =>
event.preventDefault()
@atomEnvironment.contextMenu.showForEvent(event)

253
src/window-event-handler.js Normal file
View File

@@ -0,0 +1,253 @@
const {Disposable, CompositeDisposable} = require('event-kit')
const listen = require('./delegated-listener')
// Handles low-level events related to the `window`.
module.exports =
class WindowEventHandler {
constructor ({atomEnvironment, applicationDelegate}) {
this.handleDocumentKeyEvent = this.handleDocumentKeyEvent.bind(this)
this.handleFocusNext = this.handleFocusNext.bind(this)
this.handleFocusPrevious = this.handleFocusPrevious.bind(this)
this.handleWindowBlur = this.handleWindowBlur.bind(this)
this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this)
this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this)
this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this)
this.handleWindowToggleFullScreen = this.handleWindowToggleFullScreen.bind(this)
this.handleWindowClose = this.handleWindowClose.bind(this)
this.handleWindowReload = this.handleWindowReload.bind(this)
this.handleWindowToggleDevTools = this.handleWindowToggleDevTools.bind(this)
this.handleWindowToggleMenuBar = this.handleWindowToggleMenuBar.bind(this)
this.handleLinkClick = this.handleLinkClick.bind(this)
this.handleDocumentContextmenu = this.handleDocumentContextmenu.bind(this)
this.atomEnvironment = atomEnvironment
this.applicationDelegate = applicationDelegate
this.reloadRequested = false
this.subscriptions = new CompositeDisposable()
this.handleNativeKeybindings()
}
initialize (window, document) {
this.window = window
this.document = document
this.subscriptions.add(this.atomEnvironment.commands.add(this.window, {
'window:toggle-full-screen': this.handleWindowToggleFullScreen,
'window:close': this.handleWindowClose,
'window:reload': this.handleWindowReload,
'window:toggle-dev-tools': this.handleWindowToggleDevTools
}))
if (['win32', 'linux'].includes(process.platform)) {
this.subscriptions.add(this.atomEnvironment.commands.add(this.window,
{'window:toggle-menu-bar': this.handleWindowToggleMenuBar})
)
}
this.subscriptions.add(this.atomEnvironment.commands.add(this.document, {
'core:focus-next': this.handleFocusNext,
'core:focus-previous': this.handleFocusPrevious
}))
this.addEventListener(this.window, 'beforeunload', this.handleWindowBeforeunload)
this.addEventListener(this.window, 'focus', this.handleWindowFocus)
this.addEventListener(this.window, 'blur', this.handleWindowBlur)
this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent)
this.addEventListener(this.document, 'keydown', this.handleDocumentKeyEvent)
this.addEventListener(this.document, 'drop', this.handleDocumentDrop)
this.addEventListener(this.document, 'dragover', this.handleDocumentDragover)
this.addEventListener(this.document, 'contextmenu', this.handleDocumentContextmenu)
this.subscriptions.add(listen(this.document, 'click', 'a', this.handleLinkClick))
this.subscriptions.add(listen(this.document, 'submit', 'form', this.handleFormSubmit))
this.subscriptions.add(this.applicationDelegate.onDidEnterFullScreen(this.handleEnterFullScreen))
this.subscriptions.add(this.applicationDelegate.onDidLeaveFullScreen(this.handleLeaveFullScreen))
}
// Wire commands that should be handled by Chromium for elements with the
// `.native-key-bindings` class.
handleNativeKeybindings () {
const bindCommandToAction = (command, action) => {
this.subscriptions.add(
this.atomEnvironment.commands.add(
'.native-key-bindings',
command,
event => this.applicationDelegate.getCurrentWindow().webContents[action](),
false
)
)
}
bindCommandToAction('core:copy', 'copy')
bindCommandToAction('core:paste', 'paste')
bindCommandToAction('core:undo', 'undo')
bindCommandToAction('core:redo', 'redo')
bindCommandToAction('core:select-all', 'selectAll')
bindCommandToAction('core:cut', 'cut')
}
unsubscribe () {
this.subscriptions.dispose()
}
on (target, eventName, handler) {
target.on(eventName, handler)
this.subscriptions.add(new Disposable(function () {
target.removeListener(eventName, handler)
}))
}
addEventListener (target, eventName, handler) {
target.addEventListener(eventName, handler)
this.subscriptions.add(new Disposable(function () {
target.removeEventListener(eventName, handler)
}))
}
handleDocumentKeyEvent (event) {
this.atomEnvironment.keymaps.handleKeyboardEvent(event)
event.stopImmediatePropagation()
}
handleDrop (event) {
event.preventDefault()
event.stopPropagation()
}
handleDragover (event) {
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'none'
}
eachTabIndexedElement (callback) {
for (let element of this.document.querySelectorAll('[tabindex]')) {
if (element.disabled) { continue }
if (!(element.tabIndex >= 0)) { continue }
callback(element, element.tabIndex)
}
}
handleFocusNext () {
const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : -Infinity
let nextElement = null
let nextTabIndex = Infinity
let lowestElement = null
let lowestTabIndex = Infinity
this.eachTabIndexedElement(function (element, tabIndex) {
if (tabIndex < lowestTabIndex) {
lowestTabIndex = tabIndex
lowestElement = element
}
if (focusedTabIndex < tabIndex && tabIndex < nextTabIndex) {
nextTabIndex = tabIndex
nextElement = element
}
})
if (nextElement != null) {
nextElement.focus()
} else if (lowestElement != null) {
lowestElement.focus()
}
}
handleFocusPrevious () {
const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : Infinity
let previousElement = null
let previousTabIndex = -Infinity
let highestElement = null
let highestTabIndex = -Infinity
this.eachTabIndexedElement(function (element, tabIndex) {
if (tabIndex > highestTabIndex) {
highestTabIndex = tabIndex
highestElement = element
}
if (focusedTabIndex > tabIndex && tabIndex > previousTabIndex) {
previousTabIndex = tabIndex
previousElement = element
}
})
if (previousElement != null) {
previousElement.focus()
} else if (highestElement != null) {
highestElement.focus()
}
}
handleWindowFocus () {
this.document.body.classList.remove('is-blurred')
}
handleWindowBlur () {
this.document.body.classList.add('is-blurred')
this.atomEnvironment.storeWindowDimensions()
}
handleEnterFullScreen () {
this.document.body.classList.add('fullscreen')
}
handleLeaveFullScreen () {
this.document.body.classList.remove('fullscreen')
}
handleWindowBeforeunload (event) {
if (!this.reloadRequested && !this.atomEnvironment.inSpecMode() && this.atomEnvironment.getCurrentWindow().isWebViewFocused()) {
this.atomEnvironment.hide()
}
this.reloadRequested = false
this.atomEnvironment.storeWindowDimensions()
this.atomEnvironment.unloadEditorWindow()
this.atomEnvironment.destroy()
}
handleWindowToggleFullScreen () {
this.atomEnvironment.toggleFullScreen()
}
handleWindowClose () {
this.atomEnvironment.close()
}
handleWindowReload () {
this.reloadRequested = true
this.atomEnvironment.reload()
}
handleWindowToggleDevTools () {
this.atomEnvironment.toggleDevTools()
}
handleWindowToggleMenuBar () {
this.atomEnvironment.config.set('core.autoHideMenuBar', !this.atomEnvironment.config.get('core.autoHideMenuBar'))
if (this.atomEnvironment.config.get('core.autoHideMenuBar')) {
const detail = 'To toggle, press the Alt key or execute the window:toggle-menu-bar command'
this.atomEnvironment.notifications.addInfo('Menu bar hidden', {detail})
}
}
handleLinkClick (event) {
event.preventDefault()
const uri = event.currentTarget && event.currentTarget.getAttribute('href')
if (uri && (uri[0] !== '#') && /^https?:\/\//.test(uri)) {
this.applicationDelegate.openExternal(uri)
}
}
handleFormSubmit (event) {
// Prevent form submits from changing the current window's URL
event.preventDefault()
}
handleDocumentContextmenu (event) {
event.preventDefault()
this.atomEnvironment.contextMenu.showForEvent(event)
}
}

View File

@@ -659,7 +659,7 @@ module.exports = class Workspace extends Model {
// changing or closing tabs and ensures critical UI feedback, like changing the
// highlighted tab, gets priority over work that can be done asynchronously.
//
// * `callback` {Function} to be called when the active pane item stopts
// * `callback` {Function} to be called when the active pane item stops
// changing.
// * `item` The active pane item.
//
@@ -1050,10 +1050,10 @@ module.exports = class Workspace extends Model {
// Essential: Search the workspace for items matching the given URI and hide them.
//
// * `itemOrURI` (optional) The item to hide or a {String} containing the URI
// * `itemOrURI` The item to hide or a {String} containing the URI
// of the item to hide.
//
// Returns a {boolean} indicating whether any items were found (and hidden).
// Returns a {Boolean} indicating whether any items were found (and hidden).
hide (itemOrURI) {
let foundItems = false