mirror of
https://github.com/atom/atom.git
synced 2026-01-24 06:18:03 -05:00
Merge branch 'master' into Fank-patch-1
This commit is contained in:
@@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "1.18.8"
|
||||
"atom-package-manager": "1.18.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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' }
|
||||
|
||||
70
package.json
70
package.json
@@ -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": {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'name': 'Test Ruby'
|
||||
'scopeName': 'test.rb'
|
||||
'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)'
|
||||
'fileTypes': [
|
||||
'rb'
|
||||
]
|
||||
|
||||
@@ -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", ->
|
||||
|
||||
@@ -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]])
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
253
spec/tooltip-manager-spec.js
Normal file
253
spec/tooltip-manager-spec.js
Normal 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}))
|
||||
}
|
||||
@@ -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
216
spec/view-registry-spec.js
Normal 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 })
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -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()
|
||||
228
spec/window-event-handler-spec.js
Normal file
228
spec/window-event-handler-spec.js
Normal 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()
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
139
src/text-editor-utils.js
Normal 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]
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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
199
src/tooltip-manager.js
Normal 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>`
|
||||
}
|
||||
}
|
||||
@@ -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
259
src/view-registry.js
Normal 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() }
|
||||
}
|
||||
}
|
||||
@@ -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
253
src/window-event-handler.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user