From 3c14b8d77199a5c22702cf8e28ed6f3172c3129c Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 6 Apr 2016 17:04:16 -0700 Subject: [PATCH 001/301] Add MessageRegistry The MessageRegistry is similar to the CommandRegistry in that it maps string message types to callback functions; however, messages are dispatched to Atom from outside the application using URIs, and so should be more restrictive about what they allow. Signed-off-by: Katrina Uychaco --- spec/message-registry-spec.coffee | 84 +++++++++++++++ src/application-delegate.coffee | 8 ++ src/atom-environment.coffee | 7 ++ src/command-registry.coffee | 8 ++ src/main-process/atom-application.coffee | 27 ++++- src/main-process/atom-window.coffee | 3 + src/message-registry.coffee | 131 +++++++++++++++++++++++ 7 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 spec/message-registry-spec.coffee create mode 100644 src/message-registry.coffee diff --git a/spec/message-registry-spec.coffee b/spec/message-registry-spec.coffee new file mode 100644 index 000000000..3752719e7 --- /dev/null +++ b/spec/message-registry-spec.coffee @@ -0,0 +1,84 @@ +MessageRegistry = require '../src/message-registry' + +describe 'MessageRegistry', -> + [registry] = [] + + beforeEach -> + registry = new MessageRegistry + + describe '::add', -> + it 'throws an error when the listener is not a function', -> + badAdder = -> registry.add 'package:message', 'not a function' + expect(badAdder).toThrow() + + describe 'the returned disosable', -> + it 'removes the callback', -> + spy = jasmine.createSpy('callback') + disposable = registry.add 'package:message', spy + disposable.dispose() + registry.dispatch 'atom://atom/package:message' + expect(spy).not.toHaveBeenCalled() + + it 'removes only the associated callback', -> + spy1 = jasmine.createSpy('callback 1') + spy2 = jasmine.createSpy('callback 2') + registry.add 'package:message', spy1 + disposable = registry.add 'package:message', spy2 + disposable.dispose() + registry.dispatch 'atom://atom/package:message' + expect(spy1).toHaveBeenCalledWith('package:message', {}) + expect(spy2).not.toHaveBeenCalledWith('package:message', {}) + + it 'removes all callbacks when created via ::add(object)', -> + spy1 = jasmine.createSpy('callback 1') + spy2 = jasmine.createSpy('callback 2') + disposable = registry.add + 'package:message1': spy1 + 'package:message2': spy2 + disposable.dispose() + registry.dispatch 'atom://atom/package:message1' + registry.dispatch 'atom://atom/package:message2' + expect(spy1).not.toHaveBeenCalled() + expect(spy2).not.toHaveBeenCalled() + + describe '::dispatch', -> + describe 'when a single callback is registered', -> + [spy1, spy2] = [] + + beforeEach -> + spy1 = jasmine.createSpy('callback1 ') + spy2 = jasmine.createSpy('callback 2') + + it 'invokes callbacks for matching messages', -> + registry.add 'package:message', spy1 + registry.add 'package:other-message', spy2 + registry.dispatch 'atom://atom/package:message' + expect(spy1).toHaveBeenCalledWith 'package:message', {} + expect(spy2).not.toHaveBeenCalled() + + describe 'when multiple callbacks are registered', -> + [spy1, spy2, spy3] = [] + + beforeEach -> + spy1 = jasmine.createSpy('callback 1') + spy2 = jasmine.createSpy('callback 2') + spy3 = jasmine.createSpy('callback 3') + + it 'invokes all the registered callbacks for matching messages', -> + registry.add 'package:message', spy1 + registry.add 'package:message', spy2 + registry.add 'package:other-message', spy3 + registry.dispatch 'atom://atom/package:message' + expect(spy1).toHaveBeenCalledWith('package:message', {}) + expect(spy2).toHaveBeenCalledWith('package:message', {}) + expect(spy3).not.toHaveBeenCalled() + + describe 'when a message with params is dispatched', -> + it 'invokes the callback with the given params', -> + spy = jasmine.createSpy('callback') + registry.add 'package:message', spy + registry.dispatch 'atom://atom/package:message?one=1&2=two' + expectedParams = + one: '1' + 2: 'two' + expect(spy).toHaveBeenCalledWith('package:message', expectedParams) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index aee02ee8e..217c4c389 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -235,6 +235,14 @@ class ApplicationDelegate new Disposable -> ipcRenderer.removeListener('context-command', outerCallback) + onUrlMessage: (callback) -> + outerCallback = (event, args...) -> + callback(args...) + + ipcRenderer.on('url-message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('url-message', outerCallback) + didCancelWindowUnload: -> ipcRenderer.send('did-cancel-window-unload') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7b3edee0a..912b307c6 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -22,6 +22,7 @@ Config = require './config' KeymapManager = require './keymap-extensions' TooltipManager = require './tooltip-manager' CommandRegistry = require './command-registry' +MessageRegistry = require './message-registry' GrammarRegistry = require './grammar-registry' StyleManager = require './style-manager' PackageManager = require './package-manager' @@ -160,6 +161,8 @@ class AtomEnvironment extends Model @commands = new CommandRegistry @commands.attach(@window) + @messages = new MessageRegistry + @grammars = new GrammarRegistry({@config}) @styles = new StyleManager({@configDirPath}) @@ -668,6 +671,7 @@ class AtomEnvironment extends Model @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this))) @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this))) @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this))) + @disposables.add(@applicationDelegate.onUrlMessage(@dispatchUrlMessage.bind(this))) @listenForUpdates() @registerDefaultTargetForKeymaps() @@ -938,6 +942,9 @@ class AtomEnvironment extends Model dispatchContextMenuCommand: (command, args...) -> @commands.dispatch(@contextMenu.activeElement, command, args) + dispatchUrlMessage: (uri) -> + @messages.dispatch(uri) + openLocations: (locations) -> needsProjectPaths = @project?.getPaths().length is 0 diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 955a1b540..19dcd3f2a 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -49,6 +49,7 @@ class CommandRegistry @clear() clear: -> + @urlWhitelistedCommands = {} @registeredCommands = {} @selectorBasedListenersByCommandName = {} @inlineListenersByCommandName = {} @@ -108,6 +109,9 @@ class CommandRegistry else @addInlineListener(target, commandName, callback) + whitelistUrlCommand: (commandName) -> + @urlWhitelistedCommands[commandName] = true + addSelectorBasedListener: (selector, commandName, callback) -> @selectorBasedListenersByCommandName[commandName] ?= [] listenersForCommand = @selectorBasedListenersByCommandName[commandName] @@ -183,6 +187,10 @@ class CommandRegistry Object.defineProperty(event, 'target', value: target) @handleCommandEvent(event) + dispatchFromUrl: (target, commandName, detail) -> + if @urlWhitelistedCommands[commandName] + @dispatch(target, commandName, detail) + # Public: Invoke the given callback before dispatching a command event. # # * `callback` {Function} to be called before dispatching each command diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index ba2fce4a4..3e38f7110 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -11,6 +11,7 @@ path = require 'path' os = require 'os' net = require 'net' url = require 'url' +querystring = require 'querystring' {EventEmitter} = require 'events' _ = require 'underscore-plus' FindParentDir = null @@ -550,6 +551,29 @@ class AtomApplication # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. openUrl: ({urlToOpen, devMode, safeMode, env}) -> + parsedUrl = url.parse(urlToOpen) + if parsedUrl.host is "atom" + @openWithCommandFromUrl(urlToOpen, devMode, safeMode, env) + else + @openPackageUrlMain(parsedUrl.host, devMode, safeMode, env) + + openWithCommandFromUrl: (url, devMode, safeMode, env) -> + resourcePath = @resourcePath + if devMode + try + windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) + resourcePath = @devResourcePath + + windowInitializationScript ?= require.resolve('../initialize-application-window') + if @lastFocusedWindow? + @lastFocusedWindow.sendUrlMessage url + else + windowDimensions = @getDimensionsForNewWindow() + @lastFocusedWindow = new AtomWindow({resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) + @lastFocusedWindow.on 'window:loaded', => + @lastFocusedWindow.sendUrlMessage url + + openPackageUrlMain: (packageName, devMode, safeMode, env) -> unless @packages? PackageManager = require '../package-manager' @packages = new PackageManager @@ -557,7 +581,6 @@ class AtomApplication devMode: devMode resourcePath: @resourcePath - packageName = url.parse(urlToOpen).host pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName if pack? if pack.urlMain @@ -568,7 +591,7 @@ class AtomApplication else console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}" else - console.log "Opening unknown url: #{urlToOpen}" + console.log "Opening unknown url: #{urlToOpen}" # TODO: should this forward the URL to the workspace? # Opens up a new {AtomWindow} to run specs within. # diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 34999b44e..798fd78ac 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -194,6 +194,9 @@ class AtomWindow unless global.atomApplication.sendCommandToFirstResponder(command) @sendCommandToBrowserWindow(command, args...) + sendUrlMessage: (url) -> + @browserWindow.webContents.send 'url-message', url + sendCommandToBrowserWindow: (command, args...) -> action = if args[0]?.contextCommand then 'context-command' else 'command' @browserWindow.webContents.send action, command, args... diff --git a/src/message-registry.coffee b/src/message-registry.coffee new file mode 100644 index 000000000..f90dc3e1c --- /dev/null +++ b/src/message-registry.coffee @@ -0,0 +1,131 @@ +querystring = require 'querystring' +url = require 'url' +{Disposable, CompositeDisposable} = require 'event-kit' + +# Public: Associates listener functions with messages from outside the +# application. You can access a global instance of this class via +# `atom.messages`. +# +# The global message registry is similar to the {CommandRegistry} in that it +# maps messages, identified by strings, to listener functions; however, unlike +# commands, messages can originate from outside the application, and thus the +# range of actions that messages can trigger should be more limited. +# +# Message names must follow the `namespace:action` pattern, where `namespace` +# will typically be the name of your package, and `action` describes the +# behavior of your command. If either part consists of multiple words, these +# must be separated by hyphens. E.g. `awesome-package:turn-it-up-to-eleven`. All +# words should be lowercased. +# +# Messages are exposed to applications outside Atom via special URIs that begin +# with `atom://atom/`. For example, a message named `package:show-pane` could +# be triggered by visiting `atom://atom/package:show-pane`. Additional +# parameters can be passed via query string parameters. +# +# Since messages can originate from outside the application, you should avoid +# registering messages for operations that can be destructive to the user's +# environment; for example, a message to open the install page for a package is +# fine, but a message that immediately installs a package is not. +# +# ## Example +# +# Here is a message that could open a specific panel in a package's view: +# +# ```coffee +# atom.messages.add 'package:show-panel', (message, params) -> +# packageView.showPanel(params.panel) +# ``` +# +# Such a message could be triggered by visiting the associated URL: +# +# ``` +# atom://atom/package:show-panel?panel=help +# ``` +module.exports = +class MessageRegistry + constructor: -> + @clear() + + clear: -> + @listenersByMessageName = {} + + # Public: Add one or more message listeners. + # + # ## Arguments: Registering One Message + # + # * `messageName` A {String} containing the name of a message you want to + # handle such as `package:show-panel`. + # * `callback` A {Function} to call when the given message is activated. + # * `message` An {String} containing the message that triggered this + # callback. + # * `params` An {Object} containing any key-value pairs passed to the + # message via query string parameters. The values will always be {String}s. + # + # ## Arguments: Registering Multiple Messages + # + # * `messages` An {Object} mapping message names like `package:show-panel` + # to listener {Function}s. + # + # Returns a {Disposable} on which `.dispose()` can be called to remove the + # added message handler(s). + add: (messageName, callback) -> + if typeof messageName is 'object' + messages = messageName + disposable = new CompositeDisposable + for messageName, callback of messages + disposable.add @add(messageName, callback) + return disposable + + if typeof callback isnt 'function' + throw new Error("Can't register a message with a non-function callback") + + @addListener(messageName, callback) + + addListener: (messageName, callback) -> + messageListeners = @listenersByMessageName[messageName] + + if typeof messageListeners is 'function' + @listenersByMessageName[messageName] = [ + messageListeners, + callback + ] + else if messageListeners? + messageListeners.push(callback) + else + @listenersByMessageName[messageName] = callback + + new Disposable => + @removeListener(messageName, callback) + + removeListener: (messageName, callback) -> + messageListeners = @listenersByMessageName[messageName] + + if callback? and messageListeners is callback + delete @listenersByMessageName[messageName] + else + messageListeners.splice(messageListeners.indexOf(callback), 1) + + # Public: Simulates the dispatch of a given message URI. + # + # This can be useful for testing when you want to simulate a mesasge being + # passed from outside Atom. + # + # * `uri` {String} The URI to dispatch. URIs are expected to be in the form + # `atom://atom/package:message?param=value&other=more`, where + # `package:message?param=value&other=more` describes the message to + # dispatch. + dispatch: (uri) -> + parsedUri = url.parse(uri) + return unless parsedUri.host is 'atom' + + path = parsedUri.pathname or '' + messageName = path.substr(1) + + listeners = @listenersByMessageName[messageName] + return unless listeners? + + params = querystring.parse(parsedUri.query) + if typeof listeners is 'function' + listeners(messageName, params) + else + listeners.forEach (l) -> l(messageName, params) From 3acd6d83e81408ac2be1b1de8e470c34bbbd31a0 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 7 Apr 2016 15:58:21 -0700 Subject: [PATCH 002/301] :fire: Unused import --- src/main-process/atom-application.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 3e38f7110..3fb75c896 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -11,7 +11,6 @@ path = require 'path' os = require 'os' net = require 'net' url = require 'url' -querystring = require 'querystring' {EventEmitter} = require 'events' _ = require 'underscore-plus' FindParentDir = null From 1b7ab00aa1a3881872e91df56253d1729faaade8 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 23 May 2016 15:05:24 -0700 Subject: [PATCH 003/301] :fire: Unused CommandRegistry::dispatchFromUrl and friends --- src/command-registry.coffee | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 19dcd3f2a..955a1b540 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -49,7 +49,6 @@ class CommandRegistry @clear() clear: -> - @urlWhitelistedCommands = {} @registeredCommands = {} @selectorBasedListenersByCommandName = {} @inlineListenersByCommandName = {} @@ -109,9 +108,6 @@ class CommandRegistry else @addInlineListener(target, commandName, callback) - whitelistUrlCommand: (commandName) -> - @urlWhitelistedCommands[commandName] = true - addSelectorBasedListener: (selector, commandName, callback) -> @selectorBasedListenersByCommandName[commandName] ?= [] listenersForCommand = @selectorBasedListenersByCommandName[commandName] @@ -187,10 +183,6 @@ class CommandRegistry Object.defineProperty(event, 'target', value: target) @handleCommandEvent(event) - dispatchFromUrl: (target, commandName, detail) -> - if @urlWhitelistedCommands[commandName] - @dispatch(target, commandName, detail) - # Public: Invoke the given callback before dispatching a command event. # # * `callback` {Function} to be called before dispatching each command From bf4985e265470291f90ca507f3968a3e4fda70c2 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 17:33:42 -0700 Subject: [PATCH 004/301] :fire: MessageRegistry --- spec/message-registry-spec.coffee | 84 ------------------- src/message-registry.coffee | 131 ------------------------------ 2 files changed, 215 deletions(-) delete mode 100644 spec/message-registry-spec.coffee delete mode 100644 src/message-registry.coffee diff --git a/spec/message-registry-spec.coffee b/spec/message-registry-spec.coffee deleted file mode 100644 index 3752719e7..000000000 --- a/spec/message-registry-spec.coffee +++ /dev/null @@ -1,84 +0,0 @@ -MessageRegistry = require '../src/message-registry' - -describe 'MessageRegistry', -> - [registry] = [] - - beforeEach -> - registry = new MessageRegistry - - describe '::add', -> - it 'throws an error when the listener is not a function', -> - badAdder = -> registry.add 'package:message', 'not a function' - expect(badAdder).toThrow() - - describe 'the returned disosable', -> - it 'removes the callback', -> - spy = jasmine.createSpy('callback') - disposable = registry.add 'package:message', spy - disposable.dispose() - registry.dispatch 'atom://atom/package:message' - expect(spy).not.toHaveBeenCalled() - - it 'removes only the associated callback', -> - spy1 = jasmine.createSpy('callback 1') - spy2 = jasmine.createSpy('callback 2') - registry.add 'package:message', spy1 - disposable = registry.add 'package:message', spy2 - disposable.dispose() - registry.dispatch 'atom://atom/package:message' - expect(spy1).toHaveBeenCalledWith('package:message', {}) - expect(spy2).not.toHaveBeenCalledWith('package:message', {}) - - it 'removes all callbacks when created via ::add(object)', -> - spy1 = jasmine.createSpy('callback 1') - spy2 = jasmine.createSpy('callback 2') - disposable = registry.add - 'package:message1': spy1 - 'package:message2': spy2 - disposable.dispose() - registry.dispatch 'atom://atom/package:message1' - registry.dispatch 'atom://atom/package:message2' - expect(spy1).not.toHaveBeenCalled() - expect(spy2).not.toHaveBeenCalled() - - describe '::dispatch', -> - describe 'when a single callback is registered', -> - [spy1, spy2] = [] - - beforeEach -> - spy1 = jasmine.createSpy('callback1 ') - spy2 = jasmine.createSpy('callback 2') - - it 'invokes callbacks for matching messages', -> - registry.add 'package:message', spy1 - registry.add 'package:other-message', spy2 - registry.dispatch 'atom://atom/package:message' - expect(spy1).toHaveBeenCalledWith 'package:message', {} - expect(spy2).not.toHaveBeenCalled() - - describe 'when multiple callbacks are registered', -> - [spy1, spy2, spy3] = [] - - beforeEach -> - spy1 = jasmine.createSpy('callback 1') - spy2 = jasmine.createSpy('callback 2') - spy3 = jasmine.createSpy('callback 3') - - it 'invokes all the registered callbacks for matching messages', -> - registry.add 'package:message', spy1 - registry.add 'package:message', spy2 - registry.add 'package:other-message', spy3 - registry.dispatch 'atom://atom/package:message' - expect(spy1).toHaveBeenCalledWith('package:message', {}) - expect(spy2).toHaveBeenCalledWith('package:message', {}) - expect(spy3).not.toHaveBeenCalled() - - describe 'when a message with params is dispatched', -> - it 'invokes the callback with the given params', -> - spy = jasmine.createSpy('callback') - registry.add 'package:message', spy - registry.dispatch 'atom://atom/package:message?one=1&2=two' - expectedParams = - one: '1' - 2: 'two' - expect(spy).toHaveBeenCalledWith('package:message', expectedParams) diff --git a/src/message-registry.coffee b/src/message-registry.coffee deleted file mode 100644 index f90dc3e1c..000000000 --- a/src/message-registry.coffee +++ /dev/null @@ -1,131 +0,0 @@ -querystring = require 'querystring' -url = require 'url' -{Disposable, CompositeDisposable} = require 'event-kit' - -# Public: Associates listener functions with messages from outside the -# application. You can access a global instance of this class via -# `atom.messages`. -# -# The global message registry is similar to the {CommandRegistry} in that it -# maps messages, identified by strings, to listener functions; however, unlike -# commands, messages can originate from outside the application, and thus the -# range of actions that messages can trigger should be more limited. -# -# Message names must follow the `namespace:action` pattern, where `namespace` -# will typically be the name of your package, and `action` describes the -# behavior of your command. If either part consists of multiple words, these -# must be separated by hyphens. E.g. `awesome-package:turn-it-up-to-eleven`. All -# words should be lowercased. -# -# Messages are exposed to applications outside Atom via special URIs that begin -# with `atom://atom/`. For example, a message named `package:show-pane` could -# be triggered by visiting `atom://atom/package:show-pane`. Additional -# parameters can be passed via query string parameters. -# -# Since messages can originate from outside the application, you should avoid -# registering messages for operations that can be destructive to the user's -# environment; for example, a message to open the install page for a package is -# fine, but a message that immediately installs a package is not. -# -# ## Example -# -# Here is a message that could open a specific panel in a package's view: -# -# ```coffee -# atom.messages.add 'package:show-panel', (message, params) -> -# packageView.showPanel(params.panel) -# ``` -# -# Such a message could be triggered by visiting the associated URL: -# -# ``` -# atom://atom/package:show-panel?panel=help -# ``` -module.exports = -class MessageRegistry - constructor: -> - @clear() - - clear: -> - @listenersByMessageName = {} - - # Public: Add one or more message listeners. - # - # ## Arguments: Registering One Message - # - # * `messageName` A {String} containing the name of a message you want to - # handle such as `package:show-panel`. - # * `callback` A {Function} to call when the given message is activated. - # * `message` An {String} containing the message that triggered this - # callback. - # * `params` An {Object} containing any key-value pairs passed to the - # message via query string parameters. The values will always be {String}s. - # - # ## Arguments: Registering Multiple Messages - # - # * `messages` An {Object} mapping message names like `package:show-panel` - # to listener {Function}s. - # - # Returns a {Disposable} on which `.dispose()` can be called to remove the - # added message handler(s). - add: (messageName, callback) -> - if typeof messageName is 'object' - messages = messageName - disposable = new CompositeDisposable - for messageName, callback of messages - disposable.add @add(messageName, callback) - return disposable - - if typeof callback isnt 'function' - throw new Error("Can't register a message with a non-function callback") - - @addListener(messageName, callback) - - addListener: (messageName, callback) -> - messageListeners = @listenersByMessageName[messageName] - - if typeof messageListeners is 'function' - @listenersByMessageName[messageName] = [ - messageListeners, - callback - ] - else if messageListeners? - messageListeners.push(callback) - else - @listenersByMessageName[messageName] = callback - - new Disposable => - @removeListener(messageName, callback) - - removeListener: (messageName, callback) -> - messageListeners = @listenersByMessageName[messageName] - - if callback? and messageListeners is callback - delete @listenersByMessageName[messageName] - else - messageListeners.splice(messageListeners.indexOf(callback), 1) - - # Public: Simulates the dispatch of a given message URI. - # - # This can be useful for testing when you want to simulate a mesasge being - # passed from outside Atom. - # - # * `uri` {String} The URI to dispatch. URIs are expected to be in the form - # `atom://atom/package:message?param=value&other=more`, where - # `package:message?param=value&other=more` describes the message to - # dispatch. - dispatch: (uri) -> - parsedUri = url.parse(uri) - return unless parsedUri.host is 'atom' - - path = parsedUri.pathname or '' - messageName = path.substr(1) - - listeners = @listenersByMessageName[messageName] - return unless listeners? - - params = querystring.parse(parsedUri.query) - if typeof listeners is 'function' - listeners(messageName, params) - else - listeners.forEach (l) -> l(messageName, params) From 0ae64c37deb99ad658bb4b90d32e09d138d071de Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 17:33:51 -0700 Subject: [PATCH 005/301] Add UrlHandlerRegistry --- spec/url-handler-registry-spec.js | 38 ++++++++++++ src/url-handler-registry.js | 99 +++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 spec/url-handler-registry-spec.js create mode 100644 src/url-handler-registry.js diff --git a/spec/url-handler-registry-spec.js b/spec/url-handler-registry-spec.js new file mode 100644 index 000000000..2845927ac --- /dev/null +++ b/spec/url-handler-registry-spec.js @@ -0,0 +1,38 @@ +/** @babel */ + +import {it} from './async-spec-helpers' + +import UrlHandlerRegistry from '../src/url-handler-registry' + +describe('UrlHandlerRegistry', () => { + let registry = new UrlHandlerRegistry() + + it('handles URLs on a per-host basis', () => { + const testPackageSpy = jasmine.createSpy() + const otherPackageSpy = jasmine.createSpy() + registry.registerHostHandler('test-package', testPackageSpy) + registry.registerHostHandler('other-package', otherPackageSpy) + + registry.handleUrl("atom://yet-another-package/path") + expect(testPackageSpy).not.toHaveBeenCalled() + expect(otherPackageSpy).not.toHaveBeenCalled() + + registry.handleUrl("atom://test-package/path") + expect(testPackageSpy).toHaveBeenCalledWith("atom://test-package/path") + expect(otherPackageSpy).not.toHaveBeenCalled() + + registry.handleUrl("atom://other-package/path") + expect(otherPackageSpy).toHaveBeenCalledWith("atom://other-package/path") + }) + + it('refuses to handle bad URLs', () => { + [ + 'atom:package/path', + 'atom:8080://package/path', + 'user:pass@atom://package/path', + 'smth://package/path' + ].forEach(uri => { + expect(() => registry.handleUrl(uri)).toThrow() + }) + }) +}) diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js new file mode 100644 index 000000000..19ab768de --- /dev/null +++ b/src/url-handler-registry.js @@ -0,0 +1,99 @@ +const url = require('url') +const {Disposable} = require('event-kit') + +// Public: Associates listener functions with URLs from outside the application. +// +// The global URL handler registry maps URLs to listener functions. URLs are mapped +// based on the hostname of the URL; the format is atom://package/command?args. +// The "core" package name is reserved for URLs handled by Atom core (it is not possible +// to register a package with the name "core"). +// +// Because URL handling can be triggered from outside the application (e.g. from +// the user's browser), package authors should take great care to ensure that malicious +// activities cannot be performed by an attacker. A good rule to follow is that +// **URL handlers should not take action on behalf of the user**. For example, clicking +// a link to open a pane item that prompts the user to install a package is okay; +// automatically installing the package right away is not. +// +// Packages can register their desire to handle URLs via a special key in their +// `package.json` called "urlHandlers". The value of this key should be an object +// that contains, at minimum, a key named "method". This is the name of the method +// on your package object that Atom will call when it receives a URL your package +// is responsible for handling. It will pass the full URL as the only argument, and you +// are free to do your own URL parsing to handle it. +// +// If your package can defer activation until a URL it needs to handle is triggered, +// you can additionally specify the `"defer": true` option in your "urlHandlers" object. +// When Atom receives a request for a URL in your package's namespace, it will activate your +// pacakge and then call `methodName` on it as before. +// +// If your package specifies a deprecated `urlMain` property, you cannot register URL handlers +// via the `urlHandlers` key. +// +// ## Example +// +// Here is a message that could open a specific panel in a package's view: +// +// `package.json`: +// +// ```javascript +// { +// "name": "my-package", +// "main": "./lib/my-package.js", +// "urlHandlers": { +// "method": "handleUrl", +// "deferActivation": true, +// } +// } +// ``` +// +// `lib/my-package.json` +// +// ```javascript +// module.exports = { +// activate: function() { +// // code to activate your package +// } +// +// handleUrl(url) { +// // parse and handle url +// } +// } +// ``` +// +// In this example, when Atom handles `atom://my-package/something`, it will activate your +// package and then call `handleUrl` passing in the string `"atom://my-package/something"` +module.exports = +class UrlHandlerRegistry { + constructor () { + this.registrations = new Map() + } + + registerHostHandler (host, callback) { + if (typeof callback !== 'function') { + throw new Error('Cannot register a URL host handler with a non-function callback') + } + + if (this.registrations.has(host)) { + throw new Error(`There is already a URL host handler for the host ${host}`) + } else { + this.registrations.set(host, callback) + } + + return new Disposable(() => { + this.registrations.delete(host) + }) + } + + handleUrl (uri) { + const {protocol, slashes, auth, port, host} = url.parse(uri) + if (protocol !== 'atom:' || slashes !== true || auth || port) { + throw new Error(`UrlHandlerRegistry#handleUrl asked to handle an invalid URL: ${uri}`) + } + + const registration = this.registrations.get(host) + if (registration) { + registration(uri) + } + } +} From 7c1d6ec07cea357aa73ae863dec9e47d9328f43a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 17:40:29 -0700 Subject: [PATCH 006/301] Replace MessageRegistry with UrlHandlerRegistry --- src/atom-environment.coffee | 9 +++++---- src/package-manager.js | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index b282d5330..24554cded 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -22,7 +22,7 @@ Config = require './config' KeymapManager = require './keymap-extensions' TooltipManager = require './tooltip-manager' CommandRegistry = require './command-registry' -MessageRegistry = require './message-registry' +UrlHandlerRegistry = require './url-handler-registry' GrammarRegistry = require './grammar-registry' {HistoryManager, HistoryProject} = require './history-manager' ReopenProjectMenuManager = require './reopen-project-menu-manager' @@ -148,13 +148,14 @@ class AtomEnvironment extends Model @keymaps = new KeymapManager({notificationManager: @notifications}) @tooltips = new TooltipManager(keymapManager: @keymaps, viewRegistry: @views) @commands = new CommandRegistry - @messages = new MessageRegistry + @urlHandlerRegistry = new UrlHandlerRegistry @grammars = new GrammarRegistry({@config}) @styles = new StyleManager() @packages = new PackageManager({ @config, styleManager: @styles, commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications, - grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views + grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views, + urlHandlerRegistry: @urlHandlerRegistry }) @themes = new ThemeManager({ packageManager: @packages, @config, styleManager: @styles, @@ -1073,7 +1074,7 @@ class AtomEnvironment extends Model @commands.dispatch(@contextMenu.activeElement, command, args) dispatchUrlMessage: (uri) -> - @messages.dispatch(uri) + @urlHandlerRegistry.handleUrl(uri) openLocations: (locations) -> needsProjectPaths = @project?.getPaths().length is 0 diff --git a/src/package-manager.js b/src/package-manager.js index b52e29cad..7f7c8ee03 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -31,7 +31,8 @@ module.exports = class PackageManager { constructor (params) { ({ config: this.config, styleManager: this.styleManager, notificationManager: this.notificationManager, keymapManager: this.keymapManager, - commandRegistry: this.commandRegistry, grammarRegistry: this.grammarRegistry, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry + commandRegistry: this.commandRegistry, grammarRegistry: this.grammarRegistry, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry, + urlHandlerRegistry: this.urlHandlerRegistry } = params) this.emitter = new Emitter() From 208132fb52a5a01a18f8d758eb7864cdb42f0ba1 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 17:46:57 -0700 Subject: [PATCH 007/301] Refactor AtomApplication URL opening code --- src/main-process/atom-application.coffee | 33 ++++++++++++------------ 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index a613f86a9..f924541fa 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -649,12 +649,15 @@ class AtomApplication # :safeMode - Boolean to control the opened window's safe mode. openUrl: ({urlToOpen, devMode, safeMode, env}) -> parsedUrl = url.parse(urlToOpen) - if parsedUrl.host is "atom" - @openWithCommandFromUrl(urlToOpen, devMode, safeMode, env) - else - @openPackageUrlMain(parsedUrl.host, devMode, safeMode, env) + return unless parsedUrl.protocol is "atom:" - openWithCommandFromUrl: (url, devMode, safeMode, env) -> + pack = @findPackageWithName(parsedUrl.host) + if pack?.urlMain + @openPackageUrlMain(urlToOpen, devMode, safeMode, env) + else + @openWithAtomUrl(urlToOpen, devMode, safeMode, env) + + openWithAtomUrl: (url, devMode, safeMode, env) -> resourcePath = @resourcePath if devMode try @@ -670,7 +673,7 @@ class AtomApplication @lastFocusedWindow.on 'window:loaded', => @lastFocusedWindow.sendUrlMessage url - openPackageUrlMain: (packageName, devMode, safeMode, env) -> + findPackageWithName: (packageName) -> unless @packages? PackageManager = require '../package-manager' @packages = new PackageManager({}) @@ -679,17 +682,13 @@ class AtomApplication devMode: devMode resourcePath: @resourcePath - pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName - if pack? - if pack.urlMain - packagePath = @packages.resolvePackagePath(packageName) - windowInitializationScript = path.resolve(packagePath, pack.urlMain) - windowDimensions = @getDimensionsForNewWindow() - new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) - else - console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}" - else - console.log "Opening unknown url: #{urlToOpen}" # TODO: should this forward the URL to the workspace? + _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName + + openPackageUrlMain: (urlToOpen, devMode, safeMode, env) -> + packagePath = @packages.resolvePackagePath(packageName) + windowInitializationScript = path.resolve(packagePath, pack.urlMain) + windowDimensions = @getDimensionsForNewWindow() + new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) # Opens up a new {AtomWindow} to run specs within. # From 40ae9a3698c97b353818b16163f31d0c9e5434f7 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 17:49:36 -0700 Subject: [PATCH 008/301] Make it private --- src/url-handler-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js index 19ab768de..74f350674 100644 --- a/src/url-handler-registry.js +++ b/src/url-handler-registry.js @@ -1,7 +1,7 @@ const url = require('url') const {Disposable} = require('event-kit') -// Public: Associates listener functions with URLs from outside the application. +// Private: Associates listener functions with URLs from outside the application. // // The global URL handler registry maps URLs to listener functions. URLs are mapped // based on the hostname of the URL; the format is atom://package/command?args. From 2996d9ddeddbe656da137b04ca956f7bd560dbf9 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 17:49:46 -0700 Subject: [PATCH 009/301] Fix package.json key --- src/url-handler-registry.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js index 74f350674..3ddeb7530 100644 --- a/src/url-handler-registry.js +++ b/src/url-handler-registry.js @@ -16,19 +16,19 @@ const {Disposable} = require('event-kit') // automatically installing the package right away is not. // // Packages can register their desire to handle URLs via a special key in their -// `package.json` called "urlHandlers". The value of this key should be an object +// `package.json` called "urlHandler". The value of this key should be an object // that contains, at minimum, a key named "method". This is the name of the method // on your package object that Atom will call when it receives a URL your package // is responsible for handling. It will pass the full URL as the only argument, and you // are free to do your own URL parsing to handle it. // // If your package can defer activation until a URL it needs to handle is triggered, -// you can additionally specify the `"defer": true` option in your "urlHandlers" object. +// you can additionally specify the `"deferActivation": true` option in your "urlHandler" object. // When Atom receives a request for a URL in your package's namespace, it will activate your // pacakge and then call `methodName` on it as before. // // If your package specifies a deprecated `urlMain` property, you cannot register URL handlers -// via the `urlHandlers` key. +// via the `urlHandler` key. // // ## Example // @@ -40,7 +40,7 @@ const {Disposable} = require('event-kit') // { // "name": "my-package", // "main": "./lib/my-package.js", -// "urlHandlers": { +// "urlHandler": { // "method": "handleUrl", // "deferActivation": true, // } From 3174ecbc13565851969b9d245af86abbf369a7e7 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 17:55:52 -0700 Subject: [PATCH 010/301] More :memo: --- src/url-handler-registry.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js index 3ddeb7530..dfb2947a0 100644 --- a/src/url-handler-registry.js +++ b/src/url-handler-registry.js @@ -22,17 +22,19 @@ const {Disposable} = require('event-kit') // is responsible for handling. It will pass the full URL as the only argument, and you // are free to do your own URL parsing to handle it. // -// If your package can defer activation until a URL it needs to handle is triggered, -// you can additionally specify the `"deferActivation": true` option in your "urlHandler" object. -// When Atom receives a request for a URL in your package's namespace, it will activate your -// pacakge and then call `methodName` on it as before. +// By default, Atom will defer activation of your package until a URL it needs to handle +// is triggered. If you need your package to activate right away, you can add +// `"deferActivation": false` to your "urlHandler" configuration object. When activation +// is deferred, once Atom receives a request for a URL in your package's namespace, it will +// activate your pacakge and then call `methodName` on it as before. // // If your package specifies a deprecated `urlMain` property, you cannot register URL handlers // via the `urlHandler` key. // // ## Example // -// Here is a message that could open a specific panel in a package's view: +// Here is a sample package that will be activated and have its `handleUrl` method called +// when a URL beginning with `atom://my-package` is triggered: // // `package.json`: // @@ -41,8 +43,7 @@ const {Disposable} = require('event-kit') // "name": "my-package", // "main": "./lib/my-package.js", // "urlHandler": { -// "method": "handleUrl", -// "deferActivation": true, +// "method": "handleUrl" // } // } // ``` @@ -60,9 +61,6 @@ const {Disposable} = require('event-kit') // } // } // ``` -// -// In this example, when Atom handles `atom://my-package/something`, it will activate your -// package and then call `handleUrl` passing in the string `"atom://my-package/something"` module.exports = class UrlHandlerRegistry { constructor () { From 006612c9e4b36f962df2d45d418b28f233e1a7f7 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 18:11:40 -0700 Subject: [PATCH 011/301] Ensure initial packages are loaded before dispatching URLs --- src/atom-environment.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 24554cded..37106084e 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1074,7 +1074,12 @@ class AtomEnvironment extends Model @commands.dispatch(@contextMenu.activeElement, command, args) dispatchUrlMessage: (uri) -> - @urlHandlerRegistry.handleUrl(uri) + if @packages.hasLoadedInitialPackages() + @urlHandlerRegistry.handleUrl(uri) + else + sub = @packages.onDidLoadInitialPackages -> + sub.dispose() + @urlHandlerRegistry.handleUrl(uri) openLocations: (locations) -> needsProjectPaths = @project?.getPaths().length is 0 From a7b52ee9cbb9675c800fb7393ba44952abf9b979 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 18:52:18 -0700 Subject: [PATCH 012/301] Fix missing devMode param in AtomApplication#findPackageWithName --- src/main-process/atom-application.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index f924541fa..2cd4729c1 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -651,7 +651,7 @@ class AtomApplication parsedUrl = url.parse(urlToOpen) return unless parsedUrl.protocol is "atom:" - pack = @findPackageWithName(parsedUrl.host) + pack = @findPackageWithName(parsedUrl.host, devMode) if pack?.urlMain @openPackageUrlMain(urlToOpen, devMode, safeMode, env) else @@ -673,7 +673,7 @@ class AtomApplication @lastFocusedWindow.on 'window:loaded', => @lastFocusedWindow.sendUrlMessage url - findPackageWithName: (packageName) -> + findPackageWithName: (packageName, devMode) -> unless @packages? PackageManager = require '../package-manager' @packages = new PackageManager({}) From b4f73f254df5b7128aba3f41df183dc20d8f4228 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 18:53:46 -0700 Subject: [PATCH 013/301] Hook up package URL handlers --- src/package-manager.js | 4 ++++ src/package.coffee | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/package-manager.js b/src/package-manager.js index 7f7c8ee03..d9984e40c 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -648,6 +648,10 @@ module.exports = class PackageManager { }) } + registerUrlHandlerForPackage (packageName, handler) { + return this.urlHandlerRegistry.registerHostHandler(packageName, handler) + } + // another type of package manager can handle other package types. // See ThemeManager registerPackageActivator (activator, types) { diff --git a/src/package.coffee b/src/package.coffee index fdd89bc74..d5e13b6b9 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -84,6 +84,7 @@ class Package @loadMenus() @registerDeserializerMethods() @activateCoreStartupServices() + @registerUrlHandler() @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @requireMainModule() @settingsPromise = @loadSettings() @@ -114,6 +115,7 @@ class Package @loadStylesheets() @registerDeserializerMethods() @activateCoreStartupServices() + @registerUrlHandler() @registerTranspilerConfig() @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @settingsPromise = @loadSettings() @@ -318,6 +320,21 @@ class Package @activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) return + registerUrlHandler: -> + handlerConfig = @getUrlHandler() + if methodName = handlerConfig?.method + @urlHandlerSubscription = @packageManager.registerUrlHandlerForPackage @name, (url) => + @handleUrl(url, methodName) + + unregisterUrlHandler: -> + @urlHandlerSubscription?.dispose() + + handleUrl: (url, methodName) -> + @activate().then => + @mainModule[methodName]?(url) + unless @mainActivated + @activateNow() + registerTranspilerConfig: -> if @metadata.atomTranspilers CompileCache.addTranspilerConfigForPath(@path, @name, @metadata, @metadata.atomTranspilers) @@ -595,7 +612,7 @@ class Package @mainModulePath = fs.resolveExtension(mainModulePath, ["", CompileCache.supportedExtensions...]) activationShouldBeDeferred: -> - @hasActivationCommands() or @hasActivationHooks() + @hasActivationCommands() or @hasActivationHooks() or @hasDeferredUrlHandler() hasActivationHooks: -> @getActivationHooks()?.length > 0 @@ -605,6 +622,9 @@ class Package return true if commands.length > 0 false + hasDeferredUrlHandler: -> + @getUrlHandler() and @getUrlHandler().deferActivation isnt false + subscribeToDeferredActivation: -> @subscribeToActivationCommands() @subscribeToActivationHooks() @@ -673,6 +693,9 @@ class Package @activationHooks = _.uniq(@activationHooks) + getUrlHandler: -> + @metadata?.urlHandler + # Does the given module path contain native code? isNativeModule: (modulePath) -> try From f66e4c21d8fa545e97e6bc4fb49933d66b7e4275 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 19:01:41 -0700 Subject: [PATCH 014/301] Correctly initialize AtomWindow --- src/main-process/atom-application.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 2cd4729c1..f1a1e578e 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -669,7 +669,7 @@ class AtomApplication @lastFocusedWindow.sendUrlMessage url else windowDimensions = @getDimensionsForNewWindow() - @lastFocusedWindow = new AtomWindow({resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) + @lastFocusedWindow = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) @lastFocusedWindow.on 'window:loaded', => @lastFocusedWindow.sendUrlMessage url From 3c5d471ec7935a29a18c6e4c0239d4e6293c64c3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 19 Sep 2017 10:41:53 -0700 Subject: [PATCH 015/301] Fix bugs in urlMain handling --- src/main-process/atom-application.coffee | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index f1a1e578e..2191b108f 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -653,7 +653,7 @@ class AtomApplication pack = @findPackageWithName(parsedUrl.host, devMode) if pack?.urlMain - @openPackageUrlMain(urlToOpen, devMode, safeMode, env) + @openPackageUrlMain(parsedUrl.host, urlToOpen, devMode, safeMode, env) else @openWithAtomUrl(urlToOpen, devMode, safeMode, env) @@ -674,6 +674,15 @@ class AtomApplication @lastFocusedWindow.sendUrlMessage url findPackageWithName: (packageName, devMode) -> + _.find @getPackageManager().getAvailablePackageMetadata(), ({name}) -> name is packageName + + openPackageUrlMain: (packageName, urlToOpen, devMode, safeMode, env) -> + packagePath = @getPackageManager().resolvePackagePath(packageName) + windowInitializationScript = path.resolve(packagePath, pack.urlMain) + windowDimensions = @getDimensionsForNewWindow() + new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) + + getPackageManager: -> unless @packages? PackageManager = require '../package-manager' @packages = new PackageManager({}) @@ -682,13 +691,8 @@ class AtomApplication devMode: devMode resourcePath: @resourcePath - _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName + @packages - openPackageUrlMain: (urlToOpen, devMode, safeMode, env) -> - packagePath = @packages.resolvePackagePath(packageName) - windowInitializationScript = path.resolve(packagePath, pack.urlMain) - windowDimensions = @getDimensionsForNewWindow() - new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) # Opens up a new {AtomWindow} to run specs within. # From 1bcf2e246c0647557d855388f04d47515036d4f6 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 19 Sep 2017 10:45:01 -0700 Subject: [PATCH 016/301] Just in case --- src/application-delegate.coffee | 2 +- src/atom-environment.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 6353c16f6..5efd62fe4 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -233,7 +233,7 @@ class ApplicationDelegate new Disposable -> ipcRenderer.removeListener('context-command', outerCallback) - onUrlMessage: (callback) -> + onURLMessage: (callback) -> outerCallback = (event, args...) -> callback(args...) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 37106084e..f532ffcea 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -693,7 +693,7 @@ class AtomEnvironment extends Model @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this))) @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this))) @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this))) - @disposables.add(@applicationDelegate.onUrlMessage(@dispatchUrlMessage.bind(this))) + @disposables.add(@applicationDelegate.onURLMessage(@dispatchUrlMessage.bind(this))) @disposables.add @applicationDelegate.onDidRequestUnload => @saveState({isUnloading: true}) .catch(console.error) From ed423672efa4fdff5fc748381ce5f4126b36da35 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 19 Sep 2017 12:22:58 -0700 Subject: [PATCH 017/301] Getting the package manager requires devMode --- src/main-process/atom-application.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 2191b108f..2c5f81d24 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -674,15 +674,15 @@ class AtomApplication @lastFocusedWindow.sendUrlMessage url findPackageWithName: (packageName, devMode) -> - _.find @getPackageManager().getAvailablePackageMetadata(), ({name}) -> name is packageName + _.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName openPackageUrlMain: (packageName, urlToOpen, devMode, safeMode, env) -> - packagePath = @getPackageManager().resolvePackagePath(packageName) + packagePath = @getPackageManager(devMode).resolvePackagePath(packageName) windowInitializationScript = path.resolve(packagePath, pack.urlMain) windowDimensions = @getDimensionsForNewWindow() new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) - getPackageManager: -> + getPackageManager: (devMode) -> unless @packages? PackageManager = require '../package-manager' @packages = new PackageManager({}) From d81f48b1b29ab751f78eed24b826fb10915288e2 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 19 Sep 2017 13:47:46 -0700 Subject: [PATCH 018/301] :white_check_mark: --- src/main-process/atom-application.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 2c5f81d24..ebde3b40a 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -653,7 +653,7 @@ class AtomApplication pack = @findPackageWithName(parsedUrl.host, devMode) if pack?.urlMain - @openPackageUrlMain(parsedUrl.host, urlToOpen, devMode, safeMode, env) + @openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env) else @openWithAtomUrl(urlToOpen, devMode, safeMode, env) @@ -676,9 +676,9 @@ class AtomApplication findPackageWithName: (packageName, devMode) -> _.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName - openPackageUrlMain: (packageName, urlToOpen, devMode, safeMode, env) -> + openPackageUrlMain: (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) -> packagePath = @getPackageManager(devMode).resolvePackagePath(packageName) - windowInitializationScript = path.resolve(packagePath, pack.urlMain) + windowInitializationScript = path.resolve(packagePath, packageUrlMain) windowDimensions = @getDimensionsForNewWindow() new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) From 7ac071d62269812b37e59daf29680597440a88e8 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 21 Sep 2017 14:32:23 -0700 Subject: [PATCH 019/301] Add ability to register Atom as default protocol client for atom:// URIs --- src/atom-environment.coffee | 4 +++ src/config-schema.js | 9 ++++++ src/protocol-handler-installer.js | 49 +++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/protocol-handler-installer.js diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 08a3a5dc3..850ff6b30 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -32,6 +32,7 @@ ThemeManager = require './theme-manager' MenuManager = require './menu-manager' ContextMenuManager = require './context-menu-manager' CommandInstaller = require './command-installer' +ProtocolHandlerInstaller = require './protocol-handler-installer' Project = require './project' TitleBar = require './title-bar' Workspace = require './workspace' @@ -169,6 +170,7 @@ class AtomEnvironment extends Model @project = new Project({notificationManager: @notifications, packageManager: @packages, @config, @applicationDelegate}) @commandInstaller = new CommandInstaller(@applicationDelegate) + @protocolHandlerInstaller = new ProtocolHandlerInstaller() @textEditors = new TextEditorRegistry({ @config, grammarRegistry: @grammars, assert: @assert.bind(this), @@ -235,6 +237,7 @@ class AtomEnvironment extends Model @themes.initialize({@configDirPath, resourcePath, safeMode, devMode}) @commandInstaller.initialize(@getVersion()) + @protocolHandlerInstaller.initialize(@config) @autoUpdater.initialize() @config.load() @@ -353,6 +356,7 @@ class AtomEnvironment extends Model @stylesElement.remove() @config.unobserveUserConfig() @autoUpdater.destroy() + @protocolHandlerInstaller.destroy() @uninstallWindowEventHandler() diff --git a/src/config-schema.js b/src/config-schema.js index 00fb8bbe3..188fe1f70 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -519,6 +519,15 @@ if (['win32', 'linux'].includes(process.platform)) { } } +if (['win32', 'darwin'].includes(process.platform)) { + configSchema.core.properties.defaultProtocolHandler = { + title: 'Open atom:// URIs', + type: 'boolean', + default: true, + description: 'Register Atom as the default handler for atom:// URIs' + } +} + if (process.platform === 'darwin') { configSchema.core.properties.titleBar = { type: 'string', diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js new file mode 100644 index 000000000..a19368794 --- /dev/null +++ b/src/protocol-handler-installer.js @@ -0,0 +1,49 @@ +const {CompositeDisposable} = require('event-kit') + +const {remote} = require('electron') + +module.exports = +class ProtocolHandlerInstaller { + constructor () { + this.subscriptions = new CompositeDisposable() + this.supported = ['win32', 'darwin'].includes(process.platform) + } + + initialize (config) { + this.config = config + + this.subscriptions.add( + this.config.observe('core.defaultProtocolHandler', this.onValueChange.bind(this)) + ) + } + + onValueChange (shouldBeProtocolHandler) { + this.isProtocolHandler = remote.app.isDefaultProtocolClient('atom', process.execPath, ['--url-handler']) + if (!this.isProtocolHandler && shouldBeProtocolHandler) { + this.installProtocolHandler() + } else if (this.isProtocolHandler && !shouldBeProtocolHandler) { + this.uninstallProtocolHandler() + } + } + + installProtocolHandler () { + // This Electron API is only available on Windows and macOS. There might be some + // hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440 + if (this.supported) { + return remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--url-handler']) + } + } + + uninstallProtocolHandler () { + // On macOS, this sets the first supported application that is not Atom + // as the new default protocol client; if there are none, it seems we remain + // the default client. See https://github.com/electron/electron/pull/5440 + if (this.supported) { + return remote.app.removeAsDefaultProtocolClient('atom', process.execPath, ['--url-handler']) + } + } + + destroy () { + this.subscriptions.dispose() + } +} From c52d517d134af15b58f110aba4986b9bf7fe5de4 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 21 Sep 2017 14:50:41 -0700 Subject: [PATCH 020/301] Limit argument parsing when --url-handler is set --- spec/main-process/parse-command-line.test.js | 27 ++++++++++++++++++++ src/main-process/parse-command-line.js | 24 ++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 spec/main-process/parse-command-line.test.js diff --git a/spec/main-process/parse-command-line.test.js b/spec/main-process/parse-command-line.test.js new file mode 100644 index 000000000..8b33ebea9 --- /dev/null +++ b/spec/main-process/parse-command-line.test.js @@ -0,0 +1,27 @@ +/** @babel */ + +import parseCommandLine from '../../src/main-process/parse-command-line' + +describe('parseCommandLine', function () { + describe('when --url-handler is not passed', function () { + it('parses arguments as normal', function () { + const args = parseCommandLine(['-d', '--safe', '--test', 'atom://test/url', 'atom://other/url', '/some/path']) + assert.isTrue(args.devMode) + assert.isTrue(args.safeMode) + assert.isTrue(args.test) + assert.deepEqual(args.urlsToOpen, ['atom://test/url', 'atom://other/url']) + assert.deepEqual(args.pathsToOpen, ['/some/path']) + }) + }) + + describe('when --url-handler is passed', function () { + it('ignores other arguments and limits to one URL', function () { + const args = parseCommandLine(['-d', '--url-handler', '--safe', '--test', 'atom://test/url', 'atom://other/url', '/some/path']) + assert.isUndefined(args.devMode) + assert.isUndefined(args.safeMode) + assert.isUndefined(args.test) + assert.deepEqual(args.urlsToOpen, ['atom://test/url']) + assert.deepEqual(args.pathsToOpen, []) + }) + }) +}) diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 6c5349437..c2e91d737 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -58,8 +58,18 @@ module.exports = function parseCommandLine (processArgs) { options.string('user-data-dir') options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.') options.boolean('enable-electron-logging').describe('enable-electron-logging', 'Enable low-level logging messages from Electron.') + options.boolean('url-handler') - const args = options.argv + let args = options.argv + + // If --url-handler is set, then we parse NOTHING else + if (args.urlHandler) { + args = { + urlHandler: true, + 'url-handler': true, + _: args._ + } + } if (args.help) { process.stdout.write(options.help()) @@ -101,8 +111,8 @@ module.exports = function parseCommandLine (processArgs) { const userDataDir = args['user-data-dir'] const profileStartup = args['profile-startup'] const clearWindowState = args['clear-window-state'] - const pathsToOpen = [] - const urlsToOpen = [] + let pathsToOpen = [] + let urlsToOpen = [] let devMode = args['dev'] let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom') let resourcePath = null @@ -115,6 +125,14 @@ module.exports = function parseCommandLine (processArgs) { } } + // When performing as a URL handler, only accept one URL and no paths + if (args.urlHandler) { + pathsToOpen = [] + if (urlsToOpen.length > 1) { + urlsToOpen.length = 1 + } + } + if (args['resource-path']) { devMode = true devResourcePath = args['resource-path'] From 6227203de7b9ddd84b9c12c67e6d94b2fc473d1d Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 21 Sep 2017 14:55:19 -0700 Subject: [PATCH 021/301] Do this different --- src/main-process/parse-command-line.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index c2e91d737..6f7ffa9fd 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -67,7 +67,7 @@ module.exports = function parseCommandLine (processArgs) { args = { urlHandler: true, 'url-handler': true, - _: args._ + _: args._.slice(0, 1) } } @@ -125,14 +125,6 @@ module.exports = function parseCommandLine (processArgs) { } } - // When performing as a URL handler, only accept one URL and no paths - if (args.urlHandler) { - pathsToOpen = [] - if (urlsToOpen.length > 1) { - urlsToOpen.length = 1 - } - } - if (args['resource-path']) { devMode = true devResourcePath = args['resource-path'] From eecd524788a4b5e959b8b99d4036d8f72374b92b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 21 Sep 2017 14:57:33 -0700 Subject: [PATCH 022/301] Differenter --- spec/main-process/parse-command-line.test.js | 4 ++-- src/main-process/parse-command-line.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/main-process/parse-command-line.test.js b/spec/main-process/parse-command-line.test.js index 8b33ebea9..b91ad866f 100644 --- a/spec/main-process/parse-command-line.test.js +++ b/spec/main-process/parse-command-line.test.js @@ -5,7 +5,7 @@ import parseCommandLine from '../../src/main-process/parse-command-line' describe('parseCommandLine', function () { describe('when --url-handler is not passed', function () { it('parses arguments as normal', function () { - const args = parseCommandLine(['-d', '--safe', '--test', 'atom://test/url', 'atom://other/url', '/some/path']) + const args = parseCommandLine(['-d', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url']) assert.isTrue(args.devMode) assert.isTrue(args.safeMode) assert.isTrue(args.test) @@ -16,7 +16,7 @@ describe('parseCommandLine', function () { describe('when --url-handler is passed', function () { it('ignores other arguments and limits to one URL', function () { - const args = parseCommandLine(['-d', '--url-handler', '--safe', '--test', 'atom://test/url', 'atom://other/url', '/some/path']) + const args = parseCommandLine(['-d', '--url-handler', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url']) assert.isUndefined(args.devMode) assert.isUndefined(args.safeMode) assert.isUndefined(args.test) diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 6f7ffa9fd..67d238883 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -67,7 +67,7 @@ module.exports = function parseCommandLine (processArgs) { args = { urlHandler: true, 'url-handler': true, - _: args._.slice(0, 1) + _: args._.filter(str => str.startsWith('atom://')).slice(0, 1) } } From a93ebf24658d8af770e6a5a235216ffeecabaf69 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 21 Sep 2017 15:33:40 -0700 Subject: [PATCH 023/301] We should parse the URI --- src/url-handler-registry.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js index dfb2947a0..3ea624617 100644 --- a/src/url-handler-registry.js +++ b/src/url-handler-registry.js @@ -19,8 +19,8 @@ const {Disposable} = require('event-kit') // `package.json` called "urlHandler". The value of this key should be an object // that contains, at minimum, a key named "method". This is the name of the method // on your package object that Atom will call when it receives a URL your package -// is responsible for handling. It will pass the full URL as the only argument, and you -// are free to do your own URL parsing to handle it. +// is responsible for handling. It will pass the parsed URL as the only argument (by using +// [Node's `url.parse(uri, true)`](https://nodejs.org/docs/latest/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost)) // // By default, Atom will defer activation of your package until a URL it needs to handle // is triggered. If you need your package to activate right away, you can add @@ -91,7 +91,7 @@ class UrlHandlerRegistry { const registration = this.registrations.get(host) if (registration) { - registration(uri) + registration(url.parse(uri, true)) } } } From dae51719f1e73b7eb420899a03d7f9ef3d36ee71 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 25 Sep 2017 14:18:51 -0700 Subject: [PATCH 024/301] Change up protocol handler installation --- src/atom-environment.coffee | 2 +- src/config-schema.js | 28 ++++++--- src/protocol-handler-installer.js | 100 +++++++++++++++++++++++------- 3 files changed, 96 insertions(+), 34 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 850ff6b30..0036e35b3 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -237,7 +237,7 @@ class AtomEnvironment extends Model @themes.initialize({@configDirPath, resourcePath, safeMode, devMode}) @commandInstaller.initialize(@getVersion()) - @protocolHandlerInstaller.initialize(@config) + @protocolHandlerInstaller.initialize(@config, @notifications) @autoUpdater.initialize() @config.load() diff --git a/src/config-schema.js b/src/config-schema.js index 188fe1f70..7a286658a 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -55,6 +55,25 @@ const configSchema = { } } }, + uriHandlerRegistration: { + type: 'string', + default: 'prompt', + description: 'When should Atom register itself as the default handler for atom:// URIs', + enum: [ + { + value: 'prompt', + description: 'Prompt to register Atom as the default atom:// URI handler' + }, + { + value: 'always', + description: 'Always become the default atom:// URI handler automatically' + }, + { + value: 'never', + description: 'Never become the default atom:// URI handler' + } + ] + }, themes: { type: 'array', default: ['one-dark-ui', 'one-dark-syntax'], @@ -519,15 +538,6 @@ if (['win32', 'linux'].includes(process.platform)) { } } -if (['win32', 'darwin'].includes(process.platform)) { - configSchema.core.properties.defaultProtocolHandler = { - title: 'Open atom:// URIs', - type: 'boolean', - default: true, - description: 'Register Atom as the default handler for atom:// URIs' - } -} - if (process.platform === 'darwin') { configSchema.core.properties.titleBar = { type: 'string', diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js index a19368794..e4eee7a4d 100644 --- a/src/protocol-handler-installer.js +++ b/src/protocol-handler-installer.js @@ -2,45 +2,97 @@ const {CompositeDisposable} = require('event-kit') const {remote} = require('electron') +function isSupported () { + return ['win32', 'darwin'].includes(process.platform) +} + +function isDefaultProtocolClient () { + return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--url-handler']) +} + +function setAsDefaultProtocolClient () { + // This Electron API is only available on Windows and macOS. There might be some + // hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440 + return isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--url-handler']) +} + module.exports = class ProtocolHandlerInstaller { constructor () { this.subscriptions = new CompositeDisposable() - this.supported = ['win32', 'darwin'].includes(process.platform) } - initialize (config) { + initialize (config, notifications) { this.config = config + this.notifications = notifications - this.subscriptions.add( - this.config.observe('core.defaultProtocolHandler', this.onValueChange.bind(this)) - ) + this.subscriptions.add(this.config.observe('core.uriHandlerRegistration', this.onValueChange.bind(this))) } - onValueChange (shouldBeProtocolHandler) { - this.isProtocolHandler = remote.app.isDefaultProtocolClient('atom', process.execPath, ['--url-handler']) - if (!this.isProtocolHandler && shouldBeProtocolHandler) { - this.installProtocolHandler() - } else if (this.isProtocolHandler && !shouldBeProtocolHandler) { - this.uninstallProtocolHandler() + onValueChange () { + if (!isDefaultProtocolClient()) { + const behaviorWhenNotProtocolClient = this.config.get('core.uriHandlerRegistration') + switch (behaviorWhenNotProtocolClient) { + case 'prompt': + this.promptToBecomeProtocolClient() + break + case 'always': + setAsDefaultProtocolClient() + break + case 'never': + default: + // Do nothing + } } } - installProtocolHandler () { - // This Electron API is only available on Windows and macOS. There might be some - // hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440 - if (this.supported) { - return remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--url-handler']) - } - } + promptToBecomeProtocolClient () { + let notification - uninstallProtocolHandler () { - // On macOS, this sets the first supported application that is not Atom - // as the new default protocol client; if there are none, it seems we remain - // the default client. See https://github.com/electron/electron/pull/5440 - if (this.supported) { - return remote.app.removeAsDefaultProtocolClient('atom', process.execPath, ['--url-handler']) + const accept = () => { + notification.dismiss() + setAsDefaultProtocolClient() } + const acceptAlways = () => { + this.config.set('core.uriHandlerRegistration', 'always') + return accept() + } + const decline = () => { + notification.dismiss() + } + const declineAlways = () => { + this.config.set('core.uriHandlerRegistration', 'never') + return decline() + } + + notification = this.notifications.addInfo('Register as default atom:// URI handler?', { + dismissable: true, + icon: 'link', + description: 'Atom is not currently set as the defaut handler for atom:// URIs. Would you like Atom to handle ' + + 'atom:// URIs?', + buttons: [ + { + text: 'Yes', + className: 'btn btn-info btn-primary', + onDidClick: accept + }, + { + text: 'Yes, Always', + className: 'btn btn-info', + onDidClick: acceptAlways + }, + { + text: 'No', + className: 'btn btn-info', + onDidClick: decline + }, + { + text: 'No, Never', + className: 'btn btn-info', + onDidClick: declineAlways + } + ] + }) } destroy () { From 5de1c9b9fefcb026407cc315ebd19808ed189b20 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 27 Sep 2017 12:37:12 -0700 Subject: [PATCH 025/301] Refactor ProtocolHandlerInstaller Only prompt to become default client on initialization --- src/protocol-handler-installer.js | 85 ++++++++++++++----------------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js index e4eee7a4d..eaedf0dea 100644 --- a/src/protocol-handler-installer.js +++ b/src/protocol-handler-installer.js @@ -1,71 +1,66 @@ -const {CompositeDisposable} = require('event-kit') - const {remote} = require('electron') -function isSupported () { - return ['win32', 'darwin'].includes(process.platform) -} - -function isDefaultProtocolClient () { - return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--url-handler']) -} - -function setAsDefaultProtocolClient () { - // This Electron API is only available on Windows and macOS. There might be some - // hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440 - return isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--url-handler']) -} +const SETTING = 'core.uriHandlerRegistration' +const PROMPT = 'prompt' +const ALWAYS = 'always' +const NEVER = 'never' module.exports = class ProtocolHandlerInstaller { - constructor () { - this.subscriptions = new CompositeDisposable() + isSupported () { + return ['win32', 'darwin'].includes(process.platform) + } + + isDefaultProtocolClient () { + return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--url-handler']) + } + + setAsDefaultProtocolClient () { + // This Electron API is only available on Windows and macOS. There might be some + // hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440 + return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--url-handler']) } initialize (config, notifications) { - this.config = config - this.notifications = notifications + if (!this.isSupported()) { + return false + } - this.subscriptions.add(this.config.observe('core.uriHandlerRegistration', this.onValueChange.bind(this))) - } - - onValueChange () { - if (!isDefaultProtocolClient()) { - const behaviorWhenNotProtocolClient = this.config.get('core.uriHandlerRegistration') + if (!this.isDefaultProtocolClient()) { + const behaviorWhenNotProtocolClient = config.get(SETTING) switch (behaviorWhenNotProtocolClient) { - case 'prompt': - this.promptToBecomeProtocolClient() + case PROMPT: + this.promptToBecomeProtocolClient(config, notifications) break - case 'always': - setAsDefaultProtocolClient() + case ALWAYS: + this.setAsDefaultProtocolClient() break - case 'never': + case NEVER: default: // Do nothing } } } - promptToBecomeProtocolClient () { + promptToBecomeProtocolClient (config, notifications) { let notification + const withSetting = (value, fn) => { + return function () { + config.set(SETTING, value) + fn() + } + } + const accept = () => { notification.dismiss() - setAsDefaultProtocolClient() - } - const acceptAlways = () => { - this.config.set('core.uriHandlerRegistration', 'always') - return accept() + this.setAsDefaultProtocolClient() } const decline = () => { notification.dismiss() } - const declineAlways = () => { - this.config.set('core.uriHandlerRegistration', 'never') - return decline() - } - notification = this.notifications.addInfo('Register as default atom:// URI handler?', { + notification = notifications.addInfo('Register as default atom:// URI handler?', { dismissable: true, icon: 'link', description: 'Atom is not currently set as the defaut handler for atom:// URIs. Would you like Atom to handle ' + @@ -79,7 +74,7 @@ class ProtocolHandlerInstaller { { text: 'Yes, Always', className: 'btn btn-info', - onDidClick: acceptAlways + onDidClick: withSetting(ALWAYS, accept) }, { text: 'No', @@ -89,13 +84,9 @@ class ProtocolHandlerInstaller { { text: 'No, Never', className: 'btn btn-info', - onDidClick: declineAlways + onDidClick: withSetting(NEVER, decline) } ] }) } - - destroy () { - this.subscriptions.dispose() - } } From ce8553767456b928fa4d905bffe724bd18b820b9 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 27 Sep 2017 12:40:21 -0700 Subject: [PATCH 026/301] Fix spec --- spec/url-handler-registry-spec.js | 12 +++++++----- src/url-handler-registry.js | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/spec/url-handler-registry-spec.js b/spec/url-handler-registry-spec.js index 2845927ac..0a5042262 100644 --- a/spec/url-handler-registry-spec.js +++ b/spec/url-handler-registry-spec.js @@ -1,5 +1,7 @@ /** @babel */ +import url from 'url' + import {it} from './async-spec-helpers' import UrlHandlerRegistry from '../src/url-handler-registry' @@ -13,16 +15,16 @@ describe('UrlHandlerRegistry', () => { registry.registerHostHandler('test-package', testPackageSpy) registry.registerHostHandler('other-package', otherPackageSpy) - registry.handleUrl("atom://yet-another-package/path") + registry.handleUrl('atom://yet-another-package/path') expect(testPackageSpy).not.toHaveBeenCalled() expect(otherPackageSpy).not.toHaveBeenCalled() - registry.handleUrl("atom://test-package/path") - expect(testPackageSpy).toHaveBeenCalledWith("atom://test-package/path") + registry.handleUrl('atom://test-package/path') + expect(testPackageSpy).toHaveBeenCalledWith(url.parse('atom://test-package/path', true), 'atom://test-package/path') expect(otherPackageSpy).not.toHaveBeenCalled() - registry.handleUrl("atom://other-package/path") - expect(otherPackageSpy).toHaveBeenCalledWith("atom://other-package/path") + registry.handleUrl('atom://other-package/path') + expect(otherPackageSpy).toHaveBeenCalledWith(url.parse('atom://other-package/path', true), 'atom://other-package/path') }) it('refuses to handle bad URLs', () => { diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js index 3ea624617..3115506e7 100644 --- a/src/url-handler-registry.js +++ b/src/url-handler-registry.js @@ -19,8 +19,9 @@ const {Disposable} = require('event-kit') // `package.json` called "urlHandler". The value of this key should be an object // that contains, at minimum, a key named "method". This is the name of the method // on your package object that Atom will call when it receives a URL your package -// is responsible for handling. It will pass the parsed URL as the only argument (by using +// is responsible for handling. It will pass the parsed URL as the first argument (by using // [Node's `url.parse(uri, true)`](https://nodejs.org/docs/latest/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost)) +// and the raw URL as the second argument. // // By default, Atom will defer activation of your package until a URL it needs to handle // is triggered. If you need your package to activate right away, you can add @@ -84,14 +85,15 @@ class UrlHandlerRegistry { } handleUrl (uri) { - const {protocol, slashes, auth, port, host} = url.parse(uri) + const parsed = url.parse(uri, true) + const {protocol, slashes, auth, port, host} = parsed if (protocol !== 'atom:' || slashes !== true || auth || port) { throw new Error(`UrlHandlerRegistry#handleUrl asked to handle an invalid URL: ${uri}`) } const registration = this.registrations.get(host) if (registration) { - registration(url.parse(uri, true)) + registration(parsed, uri) } } } From 67df9d5eff0703ae2128f8b5e292e46febe4781d Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 27 Sep 2017 13:00:42 -0700 Subject: [PATCH 027/301] Add history to UrlHandlerRegistry --- spec/url-handler-registry-spec.js | 37 ++++++++++++++++++++++++++++++- src/atom-environment.coffee | 1 + src/url-handler-registry.js | 37 +++++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/spec/url-handler-registry-spec.js b/spec/url-handler-registry-spec.js index 0a5042262..3488a94fc 100644 --- a/spec/url-handler-registry-spec.js +++ b/spec/url-handler-registry-spec.js @@ -7,7 +7,11 @@ import {it} from './async-spec-helpers' import UrlHandlerRegistry from '../src/url-handler-registry' describe('UrlHandlerRegistry', () => { - let registry = new UrlHandlerRegistry() + let registry + + beforeEach(() => { + registry = new UrlHandlerRegistry(5) + }) it('handles URLs on a per-host basis', () => { const testPackageSpy = jasmine.createSpy() @@ -27,6 +31,37 @@ describe('UrlHandlerRegistry', () => { expect(otherPackageSpy).toHaveBeenCalledWith(url.parse('atom://other-package/path', true), 'atom://other-package/path') }) + it('keeps track of the most recent URIs', () => { + const spy1 = jasmine.createSpy() + const spy2 = jasmine.createSpy() + const changeSpy = jasmine.createSpy() + registry.registerHostHandler('one', spy1) + registry.registerHostHandler('two', spy2) + registry.onHistoryChange(changeSpy) + + const urls = [ + 'atom://one/something?asdf=1', + 'atom://fake/nothing', + 'atom://two/other/stuff', + 'atom://one/more/thing', + 'atom://two/more/stuff' + ] + + urls.forEach(u => registry.handleUrl(u)) + + expect(changeSpy.callCount).toBe(5) + expect(registry.getRecentlyHandledUrls()).toEqual(urls.map((u, idx) => { + return {id: idx + 1, url: u, handled: !u.match(/fake/), host: url.parse(u).host} + }).reverse()) + + registry.handleUrl('atom://another/url') + expect(changeSpy.callCount).toBe(6) + const history = registry.getRecentlyHandledUrls() + expect(history.length).toBe(5) + expect(history[0].url).toBe('atom://another/url') + expect(history[4].url).toBe(urls[1]) + }) + it('refuses to handle bad URLs', () => { [ 'atom:package/path', diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 0036e35b3..a7178aac7 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -357,6 +357,7 @@ class AtomEnvironment extends Model @config.unobserveUserConfig() @autoUpdater.destroy() @protocolHandlerInstaller.destroy() + @urlHandlerRegistry.destroy() @uninstallWindowEventHandler() diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js index 3115506e7..608ce2810 100644 --- a/src/url-handler-registry.js +++ b/src/url-handler-registry.js @@ -1,5 +1,5 @@ const url = require('url') -const {Disposable} = require('event-kit') +const {Emitter, Disposable} = require('event-kit') // Private: Associates listener functions with URLs from outside the application. // @@ -64,8 +64,13 @@ const {Disposable} = require('event-kit') // ``` module.exports = class UrlHandlerRegistry { - constructor () { + constructor (maxHistoryLength = 50) { this.registrations = new Map() + this.history = [] + this.maxHistoryLength = maxHistoryLength + this._id = 0 + + this.emitter = new Emitter() } registerHostHandler (host, callback) { @@ -92,8 +97,32 @@ class UrlHandlerRegistry { } const registration = this.registrations.get(host) - if (registration) { - registration(parsed, uri) + const historyEntry = {id: ++this._id, url: uri, handled: false, host} + try { + if (registration) { + historyEntry.handled = true + registration(parsed, uri) + } + } finally { + this.history.unshift(historyEntry) + if (this.history.length > this.maxHistoryLength) { + this.history.length = this.maxHistoryLength + } + this.emitter.emit('history-change') } } + + getRecentlyHandledUrls () { + return this.history + } + + onHistoryChange (cb) { + return this.emitter.on('history-change', cb) + } + + destroy () { + this.emitter.dispose() + this.registrations = new Map() + this._id = 0 + } } From 0b62381d51d05c8f7017af4e358173b7db994b2a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 3 Oct 2017 12:47:01 -0700 Subject: [PATCH 028/301] :fire: unused destroy call --- src/atom-environment.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index a7178aac7..7bbbdd78e 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -356,7 +356,6 @@ class AtomEnvironment extends Model @stylesElement.remove() @config.unobserveUserConfig() @autoUpdater.destroy() - @protocolHandlerInstaller.destroy() @urlHandlerRegistry.destroy() @uninstallWindowEventHandler() From eb4357ce8777b0263960ce0efe4758f00e3ced25 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 3 Oct 2017 13:20:48 -0700 Subject: [PATCH 029/301] Add tests for packages with URI handlers --- .../packages/package-with-url-handler/index.js | 5 +++++ .../package-with-url-handler/package.json | 6 ++++++ spec/package-manager-spec.js | 15 +++++++++++++++ src/package.coffee | 8 ++++---- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 spec/fixtures/packages/package-with-url-handler/index.js create mode 100644 spec/fixtures/packages/package-with-url-handler/package.json diff --git a/spec/fixtures/packages/package-with-url-handler/index.js b/spec/fixtures/packages/package-with-url-handler/index.js new file mode 100644 index 000000000..3e6391be4 --- /dev/null +++ b/spec/fixtures/packages/package-with-url-handler/index.js @@ -0,0 +1,5 @@ +module.exports = { + activate: () => null, + deactivate: () => null, + handleUrl: () => null, +} diff --git a/spec/fixtures/packages/package-with-url-handler/package.json b/spec/fixtures/packages/package-with-url-handler/package.json new file mode 100644 index 000000000..4ecbdb23b --- /dev/null +++ b/spec/fixtures/packages/package-with-url-handler/package.json @@ -0,0 +1,6 @@ +{ + "name": "package-with-url-handler", + "urlHandler": { + "method": "handleUrl" + } +} diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 1d949859d..2c88c4fbb 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -1,4 +1,5 @@ const path = require('path') +const url = require('url') const Package = require('../src/package') const PackageManager = require('../src/package-manager') const temp = require('temp').track() @@ -1038,6 +1039,20 @@ describe('PackageManager', () => { }) }) + + describe("URL handler registration", () => { + it("registers the package's specified URL handler", async () => { + const uri = 'atom://package-with-url-handler/some/url?with=args' + const mod = require('./fixtures/packages/package-with-url-handler') + spyOn(mod, 'handleUrl') + spyOn(atom.packages, 'hasLoadedInitialPackages').andReturn(true) + const activationPromise = atom.packages.activatePackage('package-with-url-handler') + atom.dispatchUrlMessage(uri) + await activationPromise + expect(mod.handleUrl).toHaveBeenCalledWith(url.parse(uri, true), uri) + }) + }) + describe('service registration', () => { it("registers the package's provided and consumed services", async () => { const consumerModule = require('./fixtures/packages/package-with-consumed-services') diff --git a/src/package.coffee b/src/package.coffee index 42647acb5..815d0b537 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -323,15 +323,15 @@ class Package registerUrlHandler: -> handlerConfig = @getUrlHandler() if methodName = handlerConfig?.method - @urlHandlerSubscription = @packageManager.registerUrlHandlerForPackage @name, (url) => - @handleUrl(url, methodName) + @urlHandlerSubscription = @packageManager.registerUrlHandlerForPackage @name, (args...) => + @handleUrl(methodName, args) unregisterUrlHandler: -> @urlHandlerSubscription?.dispose() - handleUrl: (url, methodName) -> + handleUrl: (methodName, args) -> @activate().then => - @mainModule[methodName]?(url) + @mainModule[methodName]?.apply(@mainModule, args) unless @mainActivated @activateNow() From 785ade897ecda6a73d87b9a95b6e3f83aaf97eb7 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 5 Oct 2017 09:55:33 -0400 Subject: [PATCH 030/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/gutter.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gutter.coffee | 95 ---------------------------------------- src/gutter.js | 107 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 95 deletions(-) delete mode 100644 src/gutter.coffee create mode 100644 src/gutter.js diff --git a/src/gutter.coffee b/src/gutter.coffee deleted file mode 100644 index 4521eeeb2..000000000 --- a/src/gutter.coffee +++ /dev/null @@ -1,95 +0,0 @@ -{Emitter} = require 'event-kit' -CustomGutterComponent = null - -DefaultPriority = -100 - -# Extended: Represents a gutter within a {TextEditor}. -# -# See {TextEditor::addGutter} for information on creating a gutter. -module.exports = -class Gutter - constructor: (gutterContainer, options) -> - @gutterContainer = gutterContainer - @name = options?.name - @priority = options?.priority ? DefaultPriority - @visible = options?.visible ? true - - @emitter = new Emitter - - ### - Section: Gutter Destruction - ### - - # Essential: Destroys the gutter. - destroy: -> - if @name is 'line-number' - throw new Error('The line-number gutter cannot be destroyed.') - else - @gutterContainer.removeGutter(this) - @emitter.emit 'did-destroy' - @emitter.dispose() - - ### - Section: Event Subscription - ### - - # Essential: Calls your `callback` when the gutter's visibility changes. - # - # * `callback` {Function} - # * `gutter` The gutter whose visibility changed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeVisible: (callback) -> - @emitter.on 'did-change-visible', callback - - # Essential: Calls your `callback` when the gutter is destroyed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - ### - Section: Visibility - ### - - # Essential: Hide the gutter. - hide: -> - if @visible - @visible = false - @gutterContainer.scheduleComponentUpdate() - @emitter.emit 'did-change-visible', this - - # Essential: Show the gutter. - show: -> - if not @visible - @visible = true - @gutterContainer.scheduleComponentUpdate() - @emitter.emit 'did-change-visible', this - - # Essential: Determine whether the gutter is visible. - # - # Returns a {Boolean}. - isVisible: -> - @visible - - # Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves, - # is invalidated, or is destroyed, the decoration will be updated to reflect - # the marker's state. - # - # ## Arguments - # - # * `marker` A {DisplayMarker} you want this decoration to follow. - # * `decorationParams` An {Object} representing the decoration. It is passed - # to {TextEditor::decorateMarker} as its `decorationParams` and so supports - # all options documented there. - # * `type` __Caveat__: set to `'line-number'` if this is the line-number - # gutter, `'gutter'` otherwise. This cannot be overridden. - # - # Returns a {Decoration} object - decorateMarker: (marker, options) -> - @gutterContainer.addGutterDecoration(this, marker, options) - - getElement: -> - @element ?= document.createElement('div') diff --git a/src/gutter.js b/src/gutter.js new file mode 100644 index 000000000..3bf7a72ea --- /dev/null +++ b/src/gutter.js @@ -0,0 +1,107 @@ +const {Emitter} = require('event-kit') + +const DefaultPriority = -100 + +// Extended: Represents a gutter within a {TextEditor}. +// +// See {TextEditor::addGutter} for information on creating a gutter. +module.exports = class Gutter { + constructor (gutterContainer, options) { + this.gutterContainer = gutterContainer + this.name = options && options.name + this.priority = (options && options.priority != null) ? options.priority : DefaultPriority + this.visible = (options && options.visible != null) ? options.visible : true + + this.emitter = new Emitter() + } + + /* + Section: Gutter Destruction + */ + + // Essential: Destroys the gutter. + destroy () { + if (this.name === 'line-number') { + throw new Error('The line-number gutter cannot be destroyed.') + } else { + this.gutterContainer.removeGutter(this) + this.emitter.emit('did-destroy') + this.emitter.dispose() + } + } + + /* + Section: Event Subscription + */ + + // Essential: Calls your `callback` when the gutter's visibility changes. + // + // * `callback` {Function} + // * `gutter` The gutter whose visibility changed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeVisible (callback) { + return this.emitter.on('did-change-visible', callback) + } + + // Essential: Calls your `callback` when the gutter is destroyed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + /* + Section: Visibility + */ + + // Essential: Hide the gutter. + hide () { + if (this.visible) { + this.visible = false + this.gutterContainer.scheduleComponentUpdate() + this.emitter.emit('did-change-visible', this) + } + } + + // Essential: Show the gutter. + show () { + if (!this.visible) { + this.visible = true + this.gutterContainer.scheduleComponentUpdate() + this.emitter.emit('did-change-visible', this) + } + } + + // Essential: Determine whether the gutter is visible. + // + // Returns a {Boolean}. + isVisible () { + return this.visible + } + + // Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves, + // is invalidated, or is destroyed, the decoration will be updated to reflect + // the marker's state. + // + // ## Arguments + // + // * `marker` A {DisplayMarker} you want this decoration to follow. + // * `decorationParams` An {Object} representing the decoration. It is passed + // to {TextEditor::decorateMarker} as its `decorationParams` and so supports + // all options documented there. + // * `type` __Caveat__: set to `'line-number'` if this is the line-number + // gutter, `'gutter'` otherwise. This cannot be overridden. + // + // Returns a {Decoration} object + decorateMarker (marker, options) { + return this.gutterContainer.addGutterDecoration(this, marker, options) + } + + getElement () { + if (this.element == null) this.element = document.createElement('div') + return this.element + } +} From 51df9a308a9da909ddd04d0b2ffa50c69fe7fcf7 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 5 Oct 2017 10:01:34 -0400 Subject: [PATCH 031/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/gutter-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/gutter-spec.coffee | 70 ----------------------------------- spec/gutter-spec.js | 82 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 70 deletions(-) delete mode 100644 spec/gutter-spec.coffee create mode 100644 spec/gutter-spec.js diff --git a/spec/gutter-spec.coffee b/spec/gutter-spec.coffee deleted file mode 100644 index 47c5983f6..000000000 --- a/spec/gutter-spec.coffee +++ /dev/null @@ -1,70 +0,0 @@ -Gutter = require '../src/gutter' - -describe 'Gutter', -> - fakeGutterContainer = { - scheduleComponentUpdate: -> - } - name = 'name' - - describe '::hide', -> - it 'hides the gutter if it is visible.', -> - options = - name: name - visible: true - gutter = new Gutter fakeGutterContainer, options - events = [] - gutter.onDidChangeVisible (gutter) -> - events.push gutter.isVisible() - - expect(gutter.isVisible()).toBe true - gutter.hide() - expect(gutter.isVisible()).toBe false - expect(events).toEqual [false] - gutter.hide() - expect(gutter.isVisible()).toBe false - # An event should only be emitted when the visibility changes. - expect(events.length).toBe 1 - - describe '::show', -> - it 'shows the gutter if it is hidden.', -> - options = - name: name - visible: false - gutter = new Gutter fakeGutterContainer, options - events = [] - gutter.onDidChangeVisible (gutter) -> - events.push gutter.isVisible() - - expect(gutter.isVisible()).toBe false - gutter.show() - expect(gutter.isVisible()).toBe true - expect(events).toEqual [true] - gutter.show() - expect(gutter.isVisible()).toBe true - # An event should only be emitted when the visibility changes. - expect(events.length).toBe 1 - - describe '::destroy', -> - [mockGutterContainer, mockGutterContainerRemovedGutters] = [] - - beforeEach -> - mockGutterContainerRemovedGutters = [] - mockGutterContainer = removeGutter: (destroyedGutter) -> - mockGutterContainerRemovedGutters.push destroyedGutter - - it 'removes the gutter from its container.', -> - gutter = new Gutter mockGutterContainer, {name} - gutter.destroy() - expect(mockGutterContainerRemovedGutters).toEqual([gutter]) - - it 'calls all callbacks registered on ::onDidDestroy.', -> - gutter = new Gutter mockGutterContainer, {name} - didDestroy = false - gutter.onDidDestroy -> - didDestroy = true - gutter.destroy() - expect(didDestroy).toBe true - - it 'does not allow destroying the line-number gutter', -> - gutter = new Gutter mockGutterContainer, {name: 'line-number'} - expect(gutter.destroy).toThrow() diff --git a/spec/gutter-spec.js b/spec/gutter-spec.js new file mode 100644 index 000000000..4ae23db3e --- /dev/null +++ b/spec/gutter-spec.js @@ -0,0 +1,82 @@ +const Gutter = require('../src/gutter') + +describe('Gutter', () => { + const fakeGutterContainer = { + scheduleComponentUpdate () {} + } + const name = 'name' + + describe('::hide', () => + it('hides the gutter if it is visible.', () => { + const options = { + name, + visible: true + } + const gutter = new Gutter(fakeGutterContainer, options) + const events = [] + gutter.onDidChangeVisible(gutter => events.push(gutter.isVisible())) + + expect(gutter.isVisible()).toBe(true) + gutter.hide() + expect(gutter.isVisible()).toBe(false) + expect(events).toEqual([false]) + gutter.hide() + expect(gutter.isVisible()).toBe(false) + // An event should only be emitted when the visibility changes. + expect(events.length).toBe(1) + }) + ) + + describe('::show', () => + it('shows the gutter if it is hidden.', () => { + const options = { + name, + visible: false + } + const gutter = new Gutter(fakeGutterContainer, options) + const events = [] + gutter.onDidChangeVisible(gutter => events.push(gutter.isVisible())) + + expect(gutter.isVisible()).toBe(false) + gutter.show() + expect(gutter.isVisible()).toBe(true) + expect(events).toEqual([true]) + gutter.show() + expect(gutter.isVisible()).toBe(true) + // An event should only be emitted when the visibility changes. + expect(events.length).toBe(1) + }) + ) + + describe('::destroy', () => { + let mockGutterContainer, mockGutterContainerRemovedGutters + + beforeEach(() => { + mockGutterContainerRemovedGutters = [] + mockGutterContainer = { + removeGutter (destroyedGutter) { + mockGutterContainerRemovedGutters.push(destroyedGutter) + } + } + }) + + it('removes the gutter from its container.', () => { + const gutter = new Gutter(mockGutterContainer, {name}) + gutter.destroy() + expect(mockGutterContainerRemovedGutters).toEqual([gutter]) + }) + + it('calls all callbacks registered on ::onDidDestroy.', () => { + const gutter = new Gutter(mockGutterContainer, {name}) + let didDestroy = false + gutter.onDidDestroy(() => { didDestroy = true }) + gutter.destroy() + expect(didDestroy).toBe(true) + }) + + it('does not allow destroying the line-number gutter', () => { + const gutter = new Gutter(mockGutterContainer, {name: 'line-number'}) + expect(gutter.destroy).toThrow() + }) + }) +}) From 6015ba0096efbac4caac59954dfbf60230b4a451 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 5 Oct 2017 13:46:38 -0600 Subject: [PATCH 032/301] :arrow_up: autocomplete-plus@2.36.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f420deba9..1107eb925 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.2", + "autocomplete-plus": "2.36.3", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From 92913b48e53c40ec4b73f76f0457d556aa6dfc87 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 5 Oct 2017 14:16:26 -0600 Subject: [PATCH 033/301] :arrow_up: text-buffer@13.5.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1107eb925..434b0b4dc 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.3", + "text-buffer": "13.5.4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From e2f1dd7bba86819a2ec6215ac7b16260c18b5cd9 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 5 Oct 2017 14:46:58 -0600 Subject: [PATCH 034/301] :arrow_up: text-buffer@13.5.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 434b0b4dc..ab049624b 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.4", + "text-buffer": "13.5.5", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From e60916579e9219e86e742df76fb325385caea0ef Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 5 Oct 2017 15:23:54 -0600 Subject: [PATCH 035/301] :arrow_up: autocomplete-plus@2.36.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab049624b..e810fcf39 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.3", + "autocomplete-plus": "2.36.4", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From 835efd3d68ed7308846d2bcba2687fb03a65846a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 5 Oct 2017 15:01:46 -0600 Subject: [PATCH 036/301] Clear the dimensions cache after updating the soft wrap column Updating the soft wrap column could cause us to compute different values for derived dimensions, so any dimensions that were cached *in the process* of updating the soft wrap column need to be cleared. --- src/text-editor-component.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5667a733e..8dda2297d 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2118,6 +2118,7 @@ class TextEditorComponent { // rendered start row accurately. 😥 this.populateVisibleRowRange(renderedStartRow) this.props.model.setEditorWidthInChars(this.getScrollContainerClientWidthInBaseCharacters()) + this.derivedDimensionsCache = {} this.suppressUpdates = false } From 683cdeac27658f2c6732f2c55955c50dc1b58598 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 6 Oct 2017 12:37:18 +0200 Subject: [PATCH 037/301] Always revert to composition checkpoint, even if input is disabled Previously, if the user opened the IME menu while input was disabled, we would create a composition checkpoint without reverting to it after the composition ended. When enabling input again, the first keystroke would cause any buffer change that occurred between the IME composition and the keystroke to be reverted. With this commit we will always revert and delete the composition checkpoint as soon as the composition ends, regardless of whether the input is enabled or not. --- src/text-editor-component.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 8dda2297d..a51dd6465 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1595,30 +1595,30 @@ class TextEditorComponent { } didTextInput (event) { - if (!this.isInputEnabled()) return - - event.stopPropagation() - - // WARNING: If we call preventDefault on the input of a space character, - // then the browser interprets the spacebar keypress as a page-down command, - // causing spaces to scroll elements containing editors. This is impossible - // to test. - if (event.data !== ' ') event.preventDefault() - if (this.compositionCheckpoint) { this.props.model.revertToCheckpoint(this.compositionCheckpoint) this.compositionCheckpoint = null } - // If the input event is fired while the accented character menu is open it - // means that the user has chosen one of the accented alternatives. Thus, we - // will replace the original non accented character with the selected - // alternative. - if (this.accentedCharacterMenuIsOpen) { - this.props.model.selectLeft() - } + if (this.isInputEnabled()) { + event.stopPropagation() - this.props.model.insertText(event.data, {groupUndo: true}) + // WARNING: If we call preventDefault on the input of a space character, + // then the browser interprets the spacebar keypress as a page-down command, + // causing spaces to scroll elements containing editors. This is impossible + // to test. + if (event.data !== ' ') event.preventDefault() + + // If the input event is fired while the accented character menu is open it + // means that the user has chosen one of the accented alternatives. Thus, we + // will replace the original non accented character with the selected + // alternative. + if (this.accentedCharacterMenuIsOpen) { + this.props.model.selectLeft() + } + + this.props.model.insertText(event.data, {groupUndo: true}) + } } // We need to get clever to detect when the accented character menu is From 440316b45cbb583990842c991d6385c67de1a352 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 6 Oct 2017 09:23:34 -0400 Subject: [PATCH 038/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/gutter-container.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gutter-container.coffee | 87 ----------------------------- src/gutter-container.js | 108 ++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 87 deletions(-) delete mode 100644 src/gutter-container.coffee create mode 100644 src/gutter-container.js diff --git a/src/gutter-container.coffee b/src/gutter-container.coffee deleted file mode 100644 index 677fa4521..000000000 --- a/src/gutter-container.coffee +++ /dev/null @@ -1,87 +0,0 @@ -{Emitter} = require 'event-kit' -Gutter = require './gutter' - -module.exports = -class GutterContainer - constructor: (textEditor) -> - @gutters = [] - @textEditor = textEditor - @emitter = new Emitter - - scheduleComponentUpdate: -> - @textEditor.scheduleComponentUpdate() - - destroy: -> - # Create a copy, because `Gutter::destroy` removes the gutter from - # GutterContainer's @gutters. - guttersToDestroy = @gutters.slice(0) - for gutter in guttersToDestroy - gutter.destroy() if gutter.name isnt 'line-number' - @gutters = [] - @emitter.dispose() - - addGutter: (options) -> - options = options ? {} - gutterName = options.name - if gutterName is null - throw new Error('A name is required to create a gutter.') - if @gutterWithName(gutterName) - throw new Error('Tried to create a gutter with a name that is already in use.') - newGutter = new Gutter(this, options) - - inserted = false - # Insert the gutter into the gutters array, sorted in ascending order by 'priority'. - # This could be optimized, but there are unlikely to be many gutters. - for i in [0...@gutters.length] - if @gutters[i].priority >= newGutter.priority - @gutters.splice(i, 0, newGutter) - inserted = true - break - if not inserted - @gutters.push newGutter - @scheduleComponentUpdate() - @emitter.emit 'did-add-gutter', newGutter - return newGutter - - getGutters: -> - @gutters.slice() - - gutterWithName: (name) -> - for gutter in @gutters - if gutter.name is name then return gutter - null - - observeGutters: (callback) -> - callback(gutter) for gutter in @getGutters() - @onDidAddGutter callback - - onDidAddGutter: (callback) -> - @emitter.on 'did-add-gutter', callback - - onDidRemoveGutter: (callback) -> - @emitter.on 'did-remove-gutter', callback - - ### - Section: Private Methods - ### - - # Processes the destruction of the gutter. Throws an error if this gutter is - # not within this gutterContainer. - removeGutter: (gutter) -> - index = @gutters.indexOf(gutter) - if index > -1 - @gutters.splice(index, 1) - @scheduleComponentUpdate() - @emitter.emit 'did-remove-gutter', gutter.name - else - throw new Error 'The given gutter cannot be removed because it is not ' + - 'within this GutterContainer.' - - # The public interface is Gutter::decorateMarker or TextEditor::decorateMarker. - addGutterDecoration: (gutter, marker, options) -> - if gutter.name is 'line-number' - options.type = 'line-number' - else - options.type = 'gutter' - options.gutterName = gutter.name - @textEditor.decorateMarker(marker, options) diff --git a/src/gutter-container.js b/src/gutter-container.js new file mode 100644 index 000000000..3faece073 --- /dev/null +++ b/src/gutter-container.js @@ -0,0 +1,108 @@ +const {Emitter} = require('event-kit') +const Gutter = require('./gutter') + +module.exports = class GutterContainer { + constructor (textEditor) { + this.gutters = [] + this.textEditor = textEditor + this.emitter = new Emitter() + } + + scheduleComponentUpdate () { + this.textEditor.scheduleComponentUpdate() + } + + destroy () { + // Create a copy, because `Gutter::destroy` removes the gutter from + // GutterContainer's @gutters. + const guttersToDestroy = this.gutters.slice(0) + for (let gutter of guttersToDestroy) { + if (gutter.name !== 'line-number') { gutter.destroy() } + } + this.gutters = [] + this.emitter.dispose() + } + + addGutter (options) { + options = options || {} + const gutterName = options.name + if (gutterName === null) { + throw new Error('A name is required to create a gutter.') + } + if (this.gutterWithName(gutterName)) { + throw new Error('Tried to create a gutter with a name that is already in use.') + } + const newGutter = new Gutter(this, options) + + let inserted = false + // Insert the gutter into the gutters array, sorted in ascending order by 'priority'. + // This could be optimized, but there are unlikely to be many gutters. + for (let i = 0; i < this.gutters.length; i++) { + if (this.gutters[i].priority >= newGutter.priority) { + this.gutters.splice(i, 0, newGutter) + inserted = true + break + } + } + if (!inserted) { + this.gutters.push(newGutter) + } + this.scheduleComponentUpdate() + this.emitter.emit('did-add-gutter', newGutter) + return newGutter + } + + getGutters () { + return this.gutters.slice() + } + + gutterWithName (name) { + for (let gutter of this.gutters) { + if (gutter.name === name) { return gutter } + } + return null + } + + observeGutters (callback) { + for (let gutter of this.getGutters()) { callback(gutter) } + return this.onDidAddGutter(callback) + } + + onDidAddGutter (callback) { + return this.emitter.on('did-add-gutter', callback) + } + + onDidRemoveGutter (callback) { + return this.emitter.on('did-remove-gutter', callback) + } + + /* + Section: Private Methods + */ + + // Processes the destruction of the gutter. Throws an error if this gutter is + // not within this gutterContainer. + removeGutter (gutter) { + const index = this.gutters.indexOf(gutter) + if (index > -1) { + this.gutters.splice(index, 1) + this.scheduleComponentUpdate() + this.emitter.emit('did-remove-gutter', gutter.name) + } else { + throw new Error('The given gutter cannot be removed because it is not ' + + 'within this GutterContainer.' + ) + } + } + + // The public interface is Gutter::decorateMarker or TextEditor::decorateMarker. + addGutterDecoration (gutter, marker, options) { + if (gutter.name === 'line-number') { + options.type = 'line-number' + } else { + options.type = 'gutter' + } + options.gutterName = gutter.name + return this.textEditor.decorateMarker(marker, options) + } +} From d546037863bd8cdc02a008bcaeed76c4cfe3b4f5 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 6 Oct 2017 09:31:01 -0400 Subject: [PATCH 039/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/gutter-container-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/gutter-container-spec.coffee | 64 ------------------------- spec/gutter-container-spec.js | 77 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 64 deletions(-) delete mode 100644 spec/gutter-container-spec.coffee create mode 100644 spec/gutter-container-spec.js diff --git a/spec/gutter-container-spec.coffee b/spec/gutter-container-spec.coffee deleted file mode 100644 index dc4af0b8c..000000000 --- a/spec/gutter-container-spec.coffee +++ /dev/null @@ -1,64 +0,0 @@ -Gutter = require '../src/gutter' -GutterContainer = require '../src/gutter-container' - -describe 'GutterContainer', -> - gutterContainer = null - fakeTextEditor = { - scheduleComponentUpdate: -> - } - - beforeEach -> - gutterContainer = new GutterContainer fakeTextEditor - - describe 'when initialized', -> - it 'it has no gutters', -> - expect(gutterContainer.getGutters().length).toBe 0 - - describe '::addGutter', -> - it 'creates a new gutter', -> - newGutter = gutterContainer.addGutter {'test-gutter', priority: 1} - expect(gutterContainer.getGutters()).toEqual [newGutter] - expect(newGutter.priority).toBe 1 - - it 'throws an error if the provided gutter name is already in use', -> - name = 'test-gutter' - gutterContainer.addGutter {name} - expect(gutterContainer.addGutter.bind(null, {name})).toThrow() - - it 'keeps added gutters sorted by ascending priority', -> - gutter1 = gutterContainer.addGutter {name: 'first', priority: 1} - gutter3 = gutterContainer.addGutter {name: 'third', priority: 3} - gutter2 = gutterContainer.addGutter {name: 'second', priority: 2} - expect(gutterContainer.getGutters()).toEqual [gutter1, gutter2, gutter3] - - describe '::removeGutter', -> - removedGutters = null - - beforeEach -> - gutterContainer = new GutterContainer fakeTextEditor - removedGutters = [] - gutterContainer.onDidRemoveGutter (gutterName) -> - removedGutters.push gutterName - - it 'removes the gutter if it is contained by this GutterContainer', -> - gutter = gutterContainer.addGutter {'test-gutter'} - expect(gutterContainer.getGutters()).toEqual [gutter] - gutterContainer.removeGutter gutter - expect(gutterContainer.getGutters().length).toBe 0 - expect(removedGutters).toEqual [gutter.name] - - it 'throws an error if the gutter is not within this GutterContainer', -> - fakeOtherTextEditor = {} - otherGutterContainer = new GutterContainer fakeOtherTextEditor - gutter = new Gutter 'gutter-name', otherGutterContainer - expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow() - - describe '::destroy', -> - it 'clears its array of gutters and destroys custom gutters', -> - newGutter = gutterContainer.addGutter {'test-gutter', priority: 1} - newGutterSpy = jasmine.createSpy() - newGutter.onDidDestroy(newGutterSpy) - - gutterContainer.destroy() - expect(newGutterSpy).toHaveBeenCalled() - expect(gutterContainer.getGutters()).toEqual [] diff --git a/spec/gutter-container-spec.js b/spec/gutter-container-spec.js new file mode 100644 index 000000000..f41f1d220 --- /dev/null +++ b/spec/gutter-container-spec.js @@ -0,0 +1,77 @@ +const Gutter = require('../src/gutter') +const GutterContainer = require('../src/gutter-container') + +describe('GutterContainer', () => { + let gutterContainer = null + const fakeTextEditor = { + scheduleComponentUpdate () {} + } + + beforeEach(() => { + gutterContainer = new GutterContainer(fakeTextEditor) + }) + + describe('when initialized', () => + it('it has no gutters', () => { + expect(gutterContainer.getGutters().length).toBe(0) + }) + ) + + describe('::addGutter', () => { + it('creates a new gutter', () => { + const newGutter = gutterContainer.addGutter({'test-gutter': 'test-gutter', priority: 1}) + expect(gutterContainer.getGutters()).toEqual([newGutter]) + expect(newGutter.priority).toBe(1) + }) + + it('throws an error if the provided gutter name is already in use', () => { + const name = 'test-gutter' + gutterContainer.addGutter({name}) + expect(gutterContainer.addGutter.bind(null, {name})).toThrow() + }) + + it('keeps added gutters sorted by ascending priority', () => { + const gutter1 = gutterContainer.addGutter({name: 'first', priority: 1}) + const gutter3 = gutterContainer.addGutter({name: 'third', priority: 3}) + const gutter2 = gutterContainer.addGutter({name: 'second', priority: 2}) + expect(gutterContainer.getGutters()).toEqual([gutter1, gutter2, gutter3]) + }) + }) + + describe('::removeGutter', () => { + let removedGutters + + beforeEach(function () { + gutterContainer = new GutterContainer(fakeTextEditor) + removedGutters = [] + gutterContainer.onDidRemoveGutter(gutterName => removedGutters.push(gutterName)) + }) + + it('removes the gutter if it is contained by this GutterContainer', () => { + const gutter = gutterContainer.addGutter({'test-gutter': 'test-gutter'}) + expect(gutterContainer.getGutters()).toEqual([gutter]) + gutterContainer.removeGutter(gutter) + expect(gutterContainer.getGutters().length).toBe(0) + expect(removedGutters).toEqual([gutter.name]) + }) + + it('throws an error if the gutter is not within this GutterContainer', () => { + const fakeOtherTextEditor = {} + const otherGutterContainer = new GutterContainer(fakeOtherTextEditor) + const gutter = new Gutter('gutter-name', otherGutterContainer) + expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow() + }) + }) + + describe('::destroy', () => + it('clears its array of gutters and destroys custom gutters', () => { + const newGutter = gutterContainer.addGutter({'test-gutter': 'test-gutter', priority: 1}) + const newGutterSpy = jasmine.createSpy() + newGutter.onDidDestroy(newGutterSpy) + + gutterContainer.destroy() + expect(newGutterSpy).toHaveBeenCalled() + expect(gutterContainer.getGutters()).toEqual([]) + }) +) +}) From cbbd0c42a3a87300adc6aea3aff4ff3d1c13579b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Oct 2017 10:08:44 -0600 Subject: [PATCH 040/301] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e810fcf39..187bd0e20 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.5", + "text-buffer": "13.5.6", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From c698576c8f37dfd037b7a815c9bcce45c786a68e Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 6 Oct 2017 10:43:01 -0700 Subject: [PATCH 041/301] Remove test*.html from packaged app output --- script/lib/include-path-in-packaged-app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/lib/include-path-in-packaged-app.js b/script/lib/include-path-in-packaged-app.js index 1705c3457..603f14da0 100644 --- a/script/lib/include-path-in-packaged-app.js +++ b/script/lib/include-path-in-packaged-app.js @@ -71,7 +71,8 @@ const EXCLUDE_REGEXPS_SOURCES = [ 'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + 'examples?' + escapeRegExp(path.sep), 'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.md$', 'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.d\\.ts$', - 'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.js\\.map$' + 'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.js\\.map$', + '.*' + escapeRegExp(path.sep) + 'test.*\\.html$' ] // Ignore spec directories in all bundled packages From 65efc99ef9fcf55cce7bc163c706202c4e4f8bb9 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 6 Oct 2017 16:57:51 -0600 Subject: [PATCH 042/301] :arrow_up: autocomplete-plus@2.36.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 187bd0e20..3552e7af1 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.4", + "autocomplete-plus": "2.36.5", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From a00f619643b75907adb4d011664cdd203cb3391a Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 7 Oct 2017 08:30:44 -0400 Subject: [PATCH 043/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/grammar-registry.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/grammar-registry.coffee | 130 --------------------------- src/grammar-registry.js | 171 ++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 130 deletions(-) delete mode 100644 src/grammar-registry.coffee create mode 100644 src/grammar-registry.js diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee deleted file mode 100644 index a2341c967..000000000 --- a/src/grammar-registry.coffee +++ /dev/null @@ -1,130 +0,0 @@ -_ = require 'underscore-plus' -FirstMate = require 'first-mate' -Token = require './token' -fs = require 'fs-plus' -Grim = require 'grim' - -PathSplitRegex = new RegExp("[/.]") - -# Extended: Syntax class holding the grammars used for tokenizing. -# -# An instance of this class is always available as the `atom.grammars` global. -# -# The Syntax class also contains properties for things such as the -# language-specific comment regexes. See {::getProperty} for more details. -module.exports = -class GrammarRegistry extends FirstMate.GrammarRegistry - constructor: ({@config}={}) -> - super(maxTokensPerLine: 100, maxLineLength: 1000) - - createToken: (value, scopes) -> new Token({value, scopes}) - - # Extended: Select a grammar for the given file path and file contents. - # - # This picks the best match by checking the file path and contents against - # each grammar. - # - # * `filePath` A {String} file path. - # * `fileContents` A {String} of text for the file path. - # - # Returns a {Grammar}, never null. - selectGrammar: (filePath, fileContents) -> - @selectGrammarWithScore(filePath, fileContents).grammar - - selectGrammarWithScore: (filePath, fileContents) -> - bestMatch = null - highestScore = -Infinity - for grammar in @grammars - score = @getGrammarScore(grammar, filePath, fileContents) - if score > highestScore or not bestMatch? - bestMatch = grammar - highestScore = score - {grammar: bestMatch, score: highestScore} - - # Extended: Returns a {Number} representing how well the grammar matches the - # `filePath` and `contents`. - getGrammarScore: (grammar, filePath, contents) -> - contents = fs.readFileSync(filePath, 'utf8') if not contents? and fs.isFileSync(filePath) - - score = @getGrammarPathScore(grammar, filePath) - if score > 0 and not grammar.bundledPackage - score += 0.25 - if @grammarMatchesContents(grammar, contents) - score += 0.125 - score - - getGrammarPathScore: (grammar, filePath) -> - return -1 unless filePath - filePath = filePath.replace(/\\/g, '/') if process.platform is 'win32' - - pathComponents = filePath.toLowerCase().split(PathSplitRegex) - pathScore = -1 - - fileTypes = grammar.fileTypes - if customFileTypes = @config.get('core.customFileTypes')?[grammar.scopeName] - fileTypes = fileTypes.concat(customFileTypes) - - for fileType, i in fileTypes - fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex) - pathSuffix = pathComponents[-fileTypeComponents.length..-1] - if _.isEqual(pathSuffix, fileTypeComponents) - pathScore = Math.max(pathScore, fileType.length) - if i >= grammar.fileTypes.length - pathScore += 0.5 - - pathScore - - grammarMatchesContents: (grammar, contents) -> - return false unless contents? and grammar.firstLineRegex? - - escaped = false - numberOfNewlinesInRegex = 0 - for character in grammar.firstLineRegex.source - switch character - when '\\' - escaped = not escaped - when 'n' - numberOfNewlinesInRegex++ if escaped - escaped = false - else - escaped = false - lines = contents.split('\n') - grammar.firstLineRegex.testSync(lines[0..numberOfNewlinesInRegex].join('\n')) - - # Deprecated: Get the grammar override for the given file path. - # - # * `filePath` A {String} file path. - # - # Returns a {String} such as `"source.js"`. - grammarOverrideForPath: (filePath) -> - Grim.deprecate 'Use atom.textEditors.getGrammarOverride(editor) instead' - if editor = getEditorForPath(filePath) - atom.textEditors.getGrammarOverride(editor) - - # Deprecated: Set the grammar override for the given file path. - # - # * `filePath` A non-empty {String} file path. - # * `scopeName` A {String} such as `"source.js"`. - # - # Returns undefined - setGrammarOverrideForPath: (filePath, scopeName) -> - Grim.deprecate 'Use atom.textEditors.setGrammarOverride(editor, scopeName) instead' - if editor = getEditorForPath(filePath) - atom.textEditors.setGrammarOverride(editor, scopeName) - return - - # Deprecated: Remove the grammar override for the given file path. - # - # * `filePath` A {String} file path. - # - # Returns undefined. - clearGrammarOverrideForPath: (filePath) -> - Grim.deprecate 'Use atom.textEditors.clearGrammarOverride(editor) instead' - if editor = getEditorForPath(filePath) - atom.textEditors.clearGrammarOverride(editor) - return - -getEditorForPath = (filePath) -> - if filePath? - atom.workspace.getTextEditors().find (editor) -> - editor.getPath() is filePath diff --git a/src/grammar-registry.js b/src/grammar-registry.js new file mode 100644 index 000000000..b1de16ba1 --- /dev/null +++ b/src/grammar-registry.js @@ -0,0 +1,171 @@ +const _ = require('underscore-plus') +const FirstMate = require('first-mate') +const Token = require('./token') +const fs = require('fs-plus') +const Grim = require('grim') + +const PathSplitRegex = new RegExp('[/.]') + +// Extended: Syntax class holding the grammars used for tokenizing. +// +// An instance of this class is always available as the `atom.grammars` global. +// +// The Syntax class also contains properties for things such as the +// language-specific comment regexes. See {::getProperty} for more details. +module.exports = +class GrammarRegistry extends FirstMate.GrammarRegistry { + constructor ({config} = {}) { + super({maxTokensPerLine: 100, maxLineLength: 1000}) + this.config = config + } + + createToken (value, scopes) { + return new Token({value, scopes}) + } + + // Extended: Select a grammar for the given file path and file contents. + // + // This picks the best match by checking the file path and contents against + // each grammar. + // + // * `filePath` A {String} file path. + // * `fileContents` A {String} of text for the file path. + // + // Returns a {Grammar}, never null. + selectGrammar (filePath, fileContents) { + return this.selectGrammarWithScore(filePath, fileContents).grammar + } + + selectGrammarWithScore (filePath, fileContents) { + let bestMatch = null + let highestScore = -Infinity + for (let grammar of this.grammars) { + const score = this.getGrammarScore(grammar, filePath, fileContents) + if ((score > highestScore) || (bestMatch == null)) { + bestMatch = grammar + highestScore = score + } + } + return {grammar: bestMatch, score: highestScore} + } + + // Extended: Returns a {Number} representing how well the grammar matches the + // `filePath` and `contents`. + getGrammarScore (grammar, filePath, contents) { + if ((contents == null) && fs.isFileSync(filePath)) { + contents = fs.readFileSync(filePath, 'utf8') + } + + let score = this.getGrammarPathScore(grammar, filePath) + if ((score > 0) && !grammar.bundledPackage) { + score += 0.25 + } + if (this.grammarMatchesContents(grammar, contents)) { + score += 0.125 + } + return score + } + + getGrammarPathScore (grammar, filePath) { + if (!filePath) { return -1 } + if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') } + + const pathComponents = filePath.toLowerCase().split(PathSplitRegex) + let pathScore = -1 + + let customFileTypes + if (this.config.get('core.customFileTypes')) { + customFileTypes = this.config.get('core.customFileTypes')[grammar.scopeName] + } + + let { fileTypes } = grammar + if (customFileTypes) { + fileTypes = fileTypes.concat(customFileTypes) + } + + for (let i = 0; i < fileTypes.length; i++) { + const fileType = fileTypes[i] + const fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex) + const pathSuffix = pathComponents.slice(-fileTypeComponents.length) + if (_.isEqual(pathSuffix, fileTypeComponents)) { + pathScore = Math.max(pathScore, fileType.length) + if (i >= grammar.fileTypes.length) { + pathScore += 0.5 + } + } + } + + return pathScore + } + + grammarMatchesContents (grammar, contents) { + if ((contents == null) || (grammar.firstLineRegex == null)) { return false } + + let escaped = false + let numberOfNewlinesInRegex = 0 + for (let character of grammar.firstLineRegex.source) { + switch (character) { + case '\\': + escaped = !escaped + break + case 'n': + if (escaped) { numberOfNewlinesInRegex++ } + escaped = false + break + default: + escaped = false + } + } + const lines = contents.split('\n') + return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n')) + } + + // Deprecated: Get the grammar override for the given file path. + // + // * `filePath` A {String} file path. + // + // Returns a {String} such as `"source.js"`. + grammarOverrideForPath (filePath) { + Grim.deprecate('Use atom.textEditors.getGrammarOverride(editor) instead') + + const editor = getEditorForPath(filePath) + if (editor) { + return atom.textEditors.getGrammarOverride(editor) + } + } + + // Deprecated: Set the grammar override for the given file path. + // + // * `filePath` A non-empty {String} file path. + // * `scopeName` A {String} such as `"source.js"`. + // + // Returns undefined. + setGrammarOverrideForPath (filePath, scopeName) { + Grim.deprecate('Use atom.textEditors.setGrammarOverride(editor, scopeName) instead') + + const editor = getEditorForPath(filePath) + if (editor) { + atom.textEditors.setGrammarOverride(editor, scopeName) + } + } + + // Deprecated: Remove the grammar override for the given file path. + // + // * `filePath` A {String} file path. + // + // Returns undefined. + clearGrammarOverrideForPath (filePath) { + Grim.deprecate('Use atom.textEditors.clearGrammarOverride(editor) instead') + + const editor = getEditorForPath(filePath) + if (editor) { + atom.textEditors.clearGrammarOverride(editor) + } + } +} + +function getEditorForPath (filePath) { + if (filePath != null) { + return atom.workspace.getTextEditors().find(editor => editor.getPath() === filePath) + } +} From 359e6b9a9afe9c97633d27c9850b246bc1647638 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Sat, 7 Oct 2017 12:59:30 -0700 Subject: [PATCH 044/301] Add indentation to force YAML to leave certain lines the way they are --- .github/stale.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 2adc475b5..4888a3bb6 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -14,18 +14,18 @@ staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: > Thanks for your contribution! - + This issue has been automatically marked as stale because it has not had recent activity. Because the Atom team treats their issues [as their backlog](https://en.wikipedia.org/wiki/Scrum_(software_development)#Product_backlog), stale issues are closed. If you would like this issue to remain open: - - 1. Verify that you can still reproduce the issue in the latest version of Atom - 1. Comment that the issue is still reproducible and include: - * What version of Atom you reproduced the issue on - * What OS and version you reproduced the issue on - * What steps you followed to reproduce the issue - + + 1. Verify that you can still reproduce the issue in the latest version of Atom + 1. Comment that the issue is still reproducible and include: + * What version of Atom you reproduced the issue on + * What OS and version you reproduced the issue on + * What steps you followed to reproduce the issue + Issues that are labeled as triaged will not be automatically marked as stale. # Comment to post when removing the stale label. Set to `false` to disable unmarkComment: false From b0079265fd09904659a60b182e0e7990df5f2b75 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Sun, 8 Oct 2017 20:38:05 +0200 Subject: [PATCH 045/301] :arrow_up: atom-keymap@8.2.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3552e7af1..955f57e07 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "@atom/source-map-support": "^0.3.4", "async": "0.2.6", - "atom-keymap": "8.2.6", + "atom-keymap": "8.2.7", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", From bdfa61e8411fe708794e45bdc39370608eabdca2 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Sun, 8 Oct 2017 21:42:34 -0700 Subject: [PATCH 046/301] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 955f57e07..7456e925d 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.9", + "settings-view": "0.251.10", "snippets": "1.1.4", "spell-check": "0.72.2", "status-bar": "1.8.13", From 87d38c0a4d3820d894acbc8d3d1dce8d4f1e17f0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 9 Oct 2017 12:22:17 -0700 Subject: [PATCH 047/301] Return a Point from cursor word methods Fixes #15847 --- src/cursor.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cursor.js b/src/cursor.js index 1425f5b49..6cd0cc623 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -528,7 +528,7 @@ class Cursor extends Model { let result for (let range of ranges) { if (position.isLessThanOrEqual(range.start)) break - if (allowPrevious || position.isLessThanOrEqual(range.end)) result = range.start + if (allowPrevious || position.isLessThanOrEqual(range.end)) result = Point.fromObject(range.start) } return result || (allowPrevious ? new Point(0, 0) : position) @@ -559,7 +559,7 @@ class Cursor extends Model { for (let range of ranges) { if (position.isLessThan(range.start) && !allowNext) break - if (position.isLessThan(range.end)) return range.end + if (position.isLessThan(range.end)) return Point.fromObject(range.end) } return allowNext ? this.editor.getEofBufferPosition() : position @@ -597,9 +597,10 @@ class Cursor extends Model { options.wordRegex || this.wordRegExp(), new Range(new Point(position.row, 0), new Point(position.row, Infinity)) ) - return ranges.find(range => + const range = ranges.find(range => range.end.column >= position.column && range.start.column <= position.column - ) || new Range(position, position) + ) + return range ? Range.fromObject(range) : new Range(position, position) } // Public: Returns the buffer Range for the current line. From 36435964bbad6729bb7747130c58342a876e7fcf Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 9 Oct 2017 16:11:01 -0700 Subject: [PATCH 048/301] :arrow_up: markdown-preview --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7456e925d..927795bbb 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.14", + "markdown-preview": "0.159.15", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From 52873ef3b2f1d7044d08a10b49ad35c707b4d62c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 Oct 2017 17:15:36 -0600 Subject: [PATCH 049/301] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 927795bbb..54921a044 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.5", + "autocomplete-plus": "2.36.6", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From 3f0f72ad0b3d33490a02ddcf25c4ed49b5a1f75e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 10 Oct 2017 12:41:02 +0200 Subject: [PATCH 050/301] :arrow_up: electron-link This fixes the DevTools slowness we were observing in Atom. For more information, see https://github.com/atom/electron-link/commit/7f5555c33ec22f03c094c6c7f53f3a54bbcb3f93. --- script/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/package.json b/script/package.json index c766806a1..4cf1bfb8c 100644 --- a/script/package.json +++ b/script/package.json @@ -9,7 +9,7 @@ "csslint": "1.0.2", "donna": "1.0.16", "electron-chromedriver": "~1.6", - "electron-link": "0.1.1", + "electron-link": "0.1.2", "electron-mksnapshot": "~1.6", "electron-packager": "7.3.0", "electron-winstaller": "2.6.3", From c51b07e40f8dac089b4c4007ab9cadc6dbbbf206 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 10 Oct 2017 15:47:19 -0700 Subject: [PATCH 051/301] :arrow_up: spell-check --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54921a044..c285c53c4 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "package-generator": "1.1.1", "settings-view": "0.251.10", "snippets": "1.1.4", - "spell-check": "0.72.2", + "spell-check": "0.72.3", "status-bar": "1.8.13", "styleguide": "0.49.7", "symbols-view": "0.118.1", From 2ca2dfd841c735df19b9b529825dfa1d62125114 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Tue, 10 Oct 2017 18:30:09 -0600 Subject: [PATCH 052/301] :arrow_up: autocomplete-plus@2.36.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c285c53c4..e444860fa 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.6", + "autocomplete-plus": "2.36.7", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From 7853e3cd8cbc07b8abd57a350939028074ba1242 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 11 Oct 2017 09:42:53 +0200 Subject: [PATCH 053/301] Don't throw when destroying block decorations inside marker change event --- spec/text-editor-component-spec.js | 18 ++++++++++++++++++ src/text-editor-component.js | 1 + 2 files changed, 19 insertions(+) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 029cfee19..dbfd170f6 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2541,6 +2541,24 @@ describe('TextEditorComponent', () => { ]) }) + it('does not throw exceptions when destroying a block decoration inside a marker change event (regression)', async () => { + const {editor, component} = buildComponent({rowsPerTile: 3}) + + const marker = editor.markScreenPosition([2, 0]) + marker.onDidChange(() => { marker.destroy() }) + const item = document.createElement('div') + editor.decorateMarker(marker, {type: 'block', item}) + + await component.getNextUpdatePromise() + expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 2)) + + marker.setBufferRange([[0, 0], [0, 0]]) + expect(marker.isDestroyed()).toBe(true) + + await component.getNextUpdatePromise() + expect(item.parentElement).toBeNull() + }) + it('does not attempt to render block decorations located outside the visible range', async () => { const {editor, component} = buildComponent({autoHeight: false, rowsPerTile: 2}) await setEditorHeightInLines(component, 2) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index a51dd6465..ad7048708 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2523,6 +2523,7 @@ class TextEditorComponent { didDestroyDisposable.dispose() if (wasValid) { + wasValid = false this.blockDecorationsToMeasure.delete(decoration) this.heightsByBlockDecoration.delete(decoration) this.blockDecorationsByElement.delete(element) From 9ce189a695aa6e60c64be0c2f462dec3ce0076f8 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 11 Oct 2017 09:36:49 -0700 Subject: [PATCH 054/301] :arrow_up: language-typescript --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e444860fa..650e5c59f 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "language-text": "0.7.3", "language-todo": "0.29.2", "language-toml": "0.18.1", - "language-typescript": "0.2.1", + "language-typescript": "0.2.2", "language-xml": "0.35.2", "language-yaml": "0.31.0" }, From 2fc852f407064f3b55f5c95f19e6833b0b34f591 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 11 Oct 2017 20:06:09 +0200 Subject: [PATCH 055/301] :arrow_up: snippets --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 650e5c59f..51811a58d 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.251.10", - "snippets": "1.1.4", + "snippets": "1.1.5", "spell-check": "0.72.3", "status-bar": "1.8.13", "styleguide": "0.49.7", From 1fe2548ab96ab632a4010832060e10711ab241de Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Wed, 11 Oct 2017 13:21:10 -0700 Subject: [PATCH 056/301] :arrow_up: electron@1.6.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 51811a58d..6deacb619 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.6.14", + "electronVersion": "1.6.15", "dependencies": { "@atom/source-map-support": "^0.3.4", "async": "0.2.6", From 763297df82db33120621fca36ae43c80185b4062 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 12 Oct 2017 21:26:52 -0600 Subject: [PATCH 057/301] decaffeinate decoration.coffee --- src/decoration.coffee | 178 ------------------------------------ src/decoration.js | 203 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 178 deletions(-) delete mode 100644 src/decoration.coffee create mode 100644 src/decoration.js diff --git a/src/decoration.coffee b/src/decoration.coffee deleted file mode 100644 index f18733f6e..000000000 --- a/src/decoration.coffee +++ /dev/null @@ -1,178 +0,0 @@ -_ = require 'underscore-plus' -{Emitter} = require 'event-kit' - -idCounter = 0 -nextId = -> idCounter++ - -# Applies changes to a decorationsParam {Object} to make it possible to -# differentiate decorations on custom gutters versus the line-number gutter. -translateDecorationParamsOldToNew = (decorationParams) -> - if decorationParams.type is 'line-number' - decorationParams.gutterName = 'line-number' - decorationParams - -# Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is -# basically a visual representation of a marker. It allows you to add CSS -# classes to line numbers in the gutter, lines, and add selection-line regions -# around marked ranges of text. -# -# {Decoration} objects are not meant to be created directly, but created with -# {TextEditor::decorateMarker}. eg. -# -# ```coffee -# range = editor.getSelectedBufferRange() # any range you like -# marker = editor.markBufferRange(range) -# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'}) -# ``` -# -# Best practice for destroying the decoration is by destroying the {DisplayMarker}. -# -# ```coffee -# marker.destroy() -# ``` -# -# You should only use {Decoration::destroy} when you still need or do not own -# the marker. -module.exports = -class Decoration - # Private: Check if the `decorationProperties.type` matches `type` - # - # * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}` - # * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also - # be an {Array} of {String}s, where it will return true if the decoration's - # type matches any in the array. - # - # Returns {Boolean} - # Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a - # 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'. - @isType: (decorationProperties, type) -> - # 'line-number' is a special case of 'gutter'. - if _.isArray(decorationProperties.type) - return true if type in decorationProperties.type - if type is 'gutter' - return true if 'line-number' in decorationProperties.type - return false - else - if type is 'gutter' - return true if decorationProperties.type in ['gutter', 'line-number'] - else - type is decorationProperties.type - - ### - Section: Construction and Destruction - ### - - constructor: (@marker, @decorationManager, properties) -> - @emitter = new Emitter - @id = nextId() - @setProperties properties - @destroyed = false - @markerDestroyDisposable = @marker.onDidDestroy => @destroy() - - # Essential: Destroy this marker decoration. - # - # You can also destroy the marker if you own it, which will destroy this - # decoration. - destroy: -> - return if @destroyed - @markerDestroyDisposable.dispose() - @markerDestroyDisposable = null - @destroyed = true - @decorationManager.didDestroyMarkerDecoration(this) - @emitter.emit 'did-destroy' - @emitter.dispose() - - isDestroyed: -> @destroyed - - ### - Section: Event Subscription - ### - - # Essential: When the {Decoration} is updated via {Decoration::update}. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldProperties` {Object} the old parameters the decoration used to have - # * `newProperties` {Object} the new parameters the decoration now has - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeProperties: (callback) -> - @emitter.on 'did-change-properties', callback - - # Essential: Invoke the given callback when the {Decoration} is destroyed - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - ### - Section: Decoration Details - ### - - # Essential: An id unique across all {Decoration} objects - getId: -> @id - - # Essential: Returns the marker associated with this {Decoration} - getMarker: -> @marker - - # Public: Check if this decoration is of type `type` - # - # * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also - # be an {Array} of {String}s, where it will return true if the decoration's - # type matches any in the array. - # - # Returns {Boolean} - isType: (type) -> - Decoration.isType(@properties, type) - - ### - Section: Properties - ### - - # Essential: Returns the {Decoration}'s properties. - getProperties: -> - @properties - - # Essential: Update the marker with new Properties. Allows you to change the decoration's class. - # - # ## Examples - # - # ```coffee - # decoration.update({type: 'line-number', class: 'my-new-class'}) - # ``` - # - # * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}` - setProperties: (newProperties) -> - return if @destroyed - oldProperties = @properties - @properties = translateDecorationParamsOldToNew(newProperties) - if newProperties.type? - @decorationManager.decorationDidChangeType(this) - @decorationManager.emitDidUpdateDecorations() - @emitter.emit 'did-change-properties', {oldProperties, newProperties} - - ### - Section: Utility - ### - - inspect: -> - "" - - ### - Section: Private methods - ### - - matchesPattern: (decorationPattern) -> - return false unless decorationPattern? - for key, value of decorationPattern - return false if @properties[key] isnt value - true - - flash: (klass, duration=500) -> - @properties.flashRequested = true - @properties.flashClass = klass - @properties.flashDuration = duration - @decorationManager.emitDidUpdateDecorations() - @emitter.emit 'did-flash' diff --git a/src/decoration.js b/src/decoration.js new file mode 100644 index 000000000..29b2ee5d0 --- /dev/null +++ b/src/decoration.js @@ -0,0 +1,203 @@ +const _ = require('underscore-plus') +const {Emitter} = require('event-kit') + +let idCounter = 0 +const nextId = () => idCounter++ + +// Applies changes to a decorationsParam {Object} to make it possible to +// differentiate decorations on custom gutters versus the line-number gutter. +const translateDecorationParamsOldToNew = function(decorationParams) { + if (decorationParams.type === 'line-number') { + decorationParams.gutterName = 'line-number' + } + return decorationParams +} + +// Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is +// basically a visual representation of a marker. It allows you to add CSS +// classes to line numbers in the gutter, lines, and add selection-line regions +// around marked ranges of text. +// +// {Decoration} objects are not meant to be created directly, but created with +// {TextEditor::decorateMarker}. eg. +// +// ```coffee +// range = editor.getSelectedBufferRange() # any range you like +// marker = editor.markBufferRange(range) +// decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'}) +// ``` +// +// Best practice for destroying the decoration is by destroying the {DisplayMarker}. +// +// ```coffee +// marker.destroy() +// ``` +// +// You should only use {Decoration::destroy} when you still need or do not own +// the marker. +module.exports = +class Decoration { + // Private: Check if the `decorationProperties.type` matches `type` + // + // * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}` + // * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also + // be an {Array} of {String}s, where it will return true if the decoration's + // type matches any in the array. + // + // Returns {Boolean} + // Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a + // 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'. + static isType(decorationProperties, type) { + // 'line-number' is a special case of 'gutter'. + if (_.isArray(decorationProperties.type)) { + if (decorationProperties.type.includes(type)) + return true + + if (type === 'gutter' && decorationProperties.type.includes('line-number')) + return true + + return false + } else { + if (type === 'gutter') { + return ['gutter', 'line-number'].includes(decorationProperties.type) + } else { + return type === decorationProperties.type + } + } + } + + /* + Section: Construction and Destruction + */ + + constructor(marker, decorationManager, properties) { + this.marker = marker + this.decorationManager = decorationManager + this.emitter = new Emitter() + this.id = nextId() + this.setProperties(properties) + this.destroyed = false + this.markerDestroyDisposable = this.marker.onDidDestroy(() => this.destroy()) + } + + // Essential: Destroy this marker decoration. + // + // You can also destroy the marker if you own it, which will destroy this + // decoration. + destroy() { + if (this.destroyed) { return } + this.markerDestroyDisposable.dispose() + this.markerDestroyDisposable = null + this.destroyed = true + this.decorationManager.didDestroyMarkerDecoration(this) + this.emitter.emit('did-destroy') + return this.emitter.dispose() + } + + isDestroyed() { return this.destroyed } + + /* + Section: Event Subscription + */ + + // Essential: When the {Decoration} is updated via {Decoration::update}. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldProperties` {Object} the old parameters the decoration used to have + // * `newProperties` {Object} the new parameters the decoration now has + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeProperties(callback) { + return this.emitter.on('did-change-properties', callback) + } + + // Essential: Invoke the given callback when the {Decoration} is destroyed + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy(callback) { + return this.emitter.once('did-destroy', callback) + } + + /* + Section: Decoration Details + */ + + // Essential: An id unique across all {Decoration} objects + getId() { return this.id } + + // Essential: Returns the marker associated with this {Decoration} + getMarker() { return this.marker } + + // Public: Check if this decoration is of type `type` + // + // * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also + // be an {Array} of {String}s, where it will return true if the decoration's + // type matches any in the array. + // + // Returns {Boolean} + isType(type) { + return Decoration.isType(this.properties, type) + } + + /* + Section: Properties + */ + + // Essential: Returns the {Decoration}'s properties. + getProperties() { + return this.properties + } + + // Essential: Update the marker with new Properties. Allows you to change the decoration's class. + // + // ## Examples + // + // ```coffee + // decoration.update({type: 'line-number', class: 'my-new-class'}) + // ``` + // + // * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}` + setProperties(newProperties) { + if (this.destroyed) { return } + const oldProperties = this.properties + this.properties = translateDecorationParamsOldToNew(newProperties) + if (newProperties.type != null) { + this.decorationManager.decorationDidChangeType(this) + } + this.decorationManager.emitDidUpdateDecorations() + return this.emitter.emit('did-change-properties', {oldProperties, newProperties}) + } + + /* + Section: Utility + */ + + inspect() { + return `` + } + + /* + Section: Private methods + */ + + matchesPattern(decorationPattern) { + if (decorationPattern == null) { return false } + for (let key in decorationPattern) { + const value = decorationPattern[key] + if (this.properties[key] !== value) { return false } + } + return true + } + + flash(klass, duration) { + if (duration == null) { duration = 500 } + this.properties.flashRequested = true + this.properties.flashClass = klass + this.properties.flashDuration = duration + this.decorationManager.emitDidUpdateDecorations() + return this.emitter.emit('did-flash') + } +} From ae65b49b9bab92c27f5d6c037f579168184a9537 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 12 Oct 2017 23:46:53 -0600 Subject: [PATCH 058/301] fix lint errors --- src/decoration.js | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/decoration.js b/src/decoration.js index 29b2ee5d0..731935506 100644 --- a/src/decoration.js +++ b/src/decoration.js @@ -6,7 +6,7 @@ const nextId = () => idCounter++ // Applies changes to a decorationsParam {Object} to make it possible to // differentiate decorations on custom gutters versus the line-number gutter. -const translateDecorationParamsOldToNew = function(decorationParams) { +const translateDecorationParamsOldToNew = function (decorationParams) { if (decorationParams.type === 'line-number') { decorationParams.gutterName = 'line-number' } @@ -47,14 +47,16 @@ class Decoration { // Returns {Boolean} // Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a // 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'. - static isType(decorationProperties, type) { + static isType (decorationProperties, type) { // 'line-number' is a special case of 'gutter'. if (_.isArray(decorationProperties.type)) { - if (decorationProperties.type.includes(type)) + if (decorationProperties.type.includes(type)) { return true + } - if (type === 'gutter' && decorationProperties.type.includes('line-number')) + if (type === 'gutter' && decorationProperties.type.includes('line-number')) { return true + } return false } else { @@ -70,7 +72,7 @@ class Decoration { Section: Construction and Destruction */ - constructor(marker, decorationManager, properties) { + constructor (marker, decorationManager, properties) { this.marker = marker this.decorationManager = decorationManager this.emitter = new Emitter() @@ -84,7 +86,7 @@ class Decoration { // // You can also destroy the marker if you own it, which will destroy this // decoration. - destroy() { + destroy () { if (this.destroyed) { return } this.markerDestroyDisposable.dispose() this.markerDestroyDisposable = null @@ -94,7 +96,7 @@ class Decoration { return this.emitter.dispose() } - isDestroyed() { return this.destroyed } + isDestroyed () { return this.destroyed } /* Section: Event Subscription @@ -108,7 +110,7 @@ class Decoration { // * `newProperties` {Object} the new parameters the decoration now has // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeProperties(callback) { + onDidChangeProperties (callback) { return this.emitter.on('did-change-properties', callback) } @@ -117,7 +119,7 @@ class Decoration { // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy(callback) { + onDidDestroy (callback) { return this.emitter.once('did-destroy', callback) } @@ -126,10 +128,10 @@ class Decoration { */ // Essential: An id unique across all {Decoration} objects - getId() { return this.id } + getId () { return this.id } // Essential: Returns the marker associated with this {Decoration} - getMarker() { return this.marker } + getMarker () { return this.marker } // Public: Check if this decoration is of type `type` // @@ -138,7 +140,7 @@ class Decoration { // type matches any in the array. // // Returns {Boolean} - isType(type) { + isType (type) { return Decoration.isType(this.properties, type) } @@ -147,7 +149,7 @@ class Decoration { */ // Essential: Returns the {Decoration}'s properties. - getProperties() { + getProperties () { return this.properties } @@ -160,7 +162,7 @@ class Decoration { // ``` // // * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}` - setProperties(newProperties) { + setProperties (newProperties) { if (this.destroyed) { return } const oldProperties = this.properties this.properties = translateDecorationParamsOldToNew(newProperties) @@ -175,7 +177,7 @@ class Decoration { Section: Utility */ - inspect() { + inspect () { return `` } @@ -183,7 +185,7 @@ class Decoration { Section: Private methods */ - matchesPattern(decorationPattern) { + matchesPattern (decorationPattern) { if (decorationPattern == null) { return false } for (let key in decorationPattern) { const value = decorationPattern[key] @@ -192,7 +194,7 @@ class Decoration { return true } - flash(klass, duration) { + flash (klass, duration) { if (duration == null) { duration = 500 } this.properties.flashRequested = true this.properties.flashClass = klass From 8952cd315d44b7730ed686c2be14a9dc32d1e529 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 13 Oct 2017 13:21:10 -0600 Subject: [PATCH 059/301] :arrow_up: text-buffer@13.5.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6deacb619..8ce5537ed 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.6", + "text-buffer": "13.5.7", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 25b7ddb328ab396e7ed53cad0775fdd0eba5ac74 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 11:58:42 -0400 Subject: [PATCH 060/301] =?UTF-8?q?=E2=98=A0=EF=B8=8F=E2=98=95=20Decaffein?= =?UTF-8?q?ate=20src/project.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply results of running: ``` $ decaffeinate --keep-commonjs --prefer-const --loose-default-params --loose-for-expressions --loose-for-of --loose-includes' $ standard --fix src/project.js ``` --- src/project.coffee | 565 ----------------------------------- src/project.js | 714 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 714 insertions(+), 565 deletions(-) delete mode 100644 src/project.coffee create mode 100644 src/project.js diff --git a/src/project.coffee b/src/project.coffee deleted file mode 100644 index ab41f9eb3..000000000 --- a/src/project.coffee +++ /dev/null @@ -1,565 +0,0 @@ -path = require 'path' - -_ = require 'underscore-plus' -fs = require 'fs-plus' -{Emitter, Disposable} = require 'event-kit' -TextBuffer = require 'text-buffer' -{watchPath} = require('./path-watcher') - -DefaultDirectoryProvider = require './default-directory-provider' -Model = require './model' -GitRepositoryProvider = require './git-repository-provider' - -# Extended: Represents a project that's opened in Atom. -# -# An instance of this class is always available as the `atom.project` global. -module.exports = -class Project extends Model - ### - Section: Construction and Destruction - ### - - constructor: ({@notificationManager, packageManager, config, @applicationDelegate}) -> - @emitter = new Emitter - @buffers = [] - @rootDirectories = [] - @repositories = [] - @directoryProviders = [] - @defaultDirectoryProvider = new DefaultDirectoryProvider() - @repositoryPromisesByPath = new Map() - @repositoryProviders = [new GitRepositoryProvider(this, config)] - @loadPromisesByPath = {} - @watcherPromisesByPath = {} - @retiredBufferIDs = new Set() - @retiredBufferPaths = new Set() - @consumeServices(packageManager) - - destroyed: -> - buffer.destroy() for buffer in @buffers.slice() - repository?.destroy() for repository in @repositories.slice() - watcher.dispose() for _, watcher in @watcherPromisesByPath - @rootDirectories = [] - @repositories = [] - - reset: (packageManager) -> - @emitter.dispose() - @emitter = new Emitter - - buffer?.destroy() for buffer in @buffers - @buffers = [] - @setPaths([]) - @loadPromisesByPath = {} - @retiredBufferIDs = new Set() - @retiredBufferPaths = new Set() - @consumeServices(packageManager) - - destroyUnretainedBuffers: -> - buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() - return - - ### - Section: Serialization - ### - - deserialize: (state) -> - @retiredBufferIDs = new Set() - @retiredBufferPaths = new Set() - - handleBufferState = (bufferState) => - bufferState.shouldDestroyOnFileDelete ?= -> atom.config.get('core.closeDeletedFileTabs') - - # Use a little guilty knowledge of the way TextBuffers are serialized. - # This allows TextBuffers that have never been saved (but have filePaths) to be deserialized, but prevents - # TextBuffers backed by files that have been deleted from being saved. - bufferState.mustExist = bufferState.digestWhenLastPersisted isnt false - - TextBuffer.deserialize(bufferState).catch (err) => - @retiredBufferIDs.add(bufferState.id) - @retiredBufferPaths.add(bufferState.filePath) - null - - bufferPromises = (handleBufferState(bufferState) for bufferState in state.buffers) - - Promise.all(bufferPromises).then (buffers) => - @buffers = buffers.filter(Boolean) - @subscribeToBuffer(buffer) for buffer in @buffers - @setPaths(state.paths or [], mustExist: true, exact: true) - - serialize: (options={}) -> - deserializer: 'Project' - paths: @getPaths() - buffers: _.compact(@buffers.map (buffer) -> - if buffer.isRetained() - isUnloading = options.isUnloading is true - buffer.serialize({markerLayers: isUnloading, history: isUnloading}) - ) - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when the project paths change. - # - # * `callback` {Function} to be called after the project paths change. - # * `projectPaths` An {Array} of {String} project paths. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePaths: (callback) -> - @emitter.on 'did-change-paths', callback - - # Public: Invoke the given callback when a text buffer is added to the - # project. - # - # * `callback` {Function} to be called when a text buffer is added. - # * `buffer` A {TextBuffer} item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddBuffer: (callback) -> - @emitter.on 'did-add-buffer', callback - - # Public: Invoke the given callback with all current and future text - # buffers in the project. - # - # * `callback` {Function} to be called with current and future text buffers. - # * `buffer` A {TextBuffer} item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeBuffers: (callback) -> - callback(buffer) for buffer in @getBuffers() - @onDidAddBuffer callback - - # Extended: Invoke a callback when a filesystem change occurs within any open - # project path. - # - # ```js - # const disposable = atom.project.onDidChangeFiles(events => { - # for (const event of events) { - # // "created", "modified", "deleted", or "renamed" - # console.log(`Event action: ${event.type}`) - # - # // absolute path to the filesystem entry that was touched - # console.log(`Event path: ${event.path}`) - # - # if (event.type === 'renamed') { - # console.log(`.. renamed from: ${event.oldPath}`) - # } - # } - # } - # - # disposable.dispose() - # ``` - # - # To watch paths outside of open projects, use the `watchPaths` function instead; see {PathWatcher}. - # - # When writing tests against functionality that uses this method, be sure to wait for the - # {Promise} returned by {getWatcherPromise()} before manipulating the filesystem to ensure that - # the watcher is receiving events. - # - # * `callback` {Function} to be called with batches of filesystem events reported by - # the operating system. - # * `events` An {Array} of objects that describe a batch of filesystem events. - # * `action` {String} describing the filesystem action that occurred. One of `"created"`, - # `"modified"`, `"deleted"`, or `"renamed"`. - # * `path` {String} containing the absolute path to the filesystem entry - # that was acted upon. - # * `oldPath` For rename events, {String} containing the filesystem entry's - # former absolute path. - # - # Returns a {Disposable} to manage this event subscription. - onDidChangeFiles: (callback) -> - @emitter.on 'did-change-files', callback - - ### - Section: Accessing the git repository - ### - - # Public: Get an {Array} of {GitRepository}s associated with the project's - # directories. - # - # This method will be removed in 2.0 because it does synchronous I/O. - # Prefer the following, which evaluates to a {Promise} that resolves to an - # {Array} of {Repository} objects: - # ``` - # Promise.all(atom.project.getDirectories().map( - # atom.project.repositoryForDirectory.bind(atom.project))) - # ``` - getRepositories: -> @repositories - - # Public: Get the repository for a given directory asynchronously. - # - # * `directory` {Directory} for which to get a {Repository}. - # - # Returns a {Promise} that resolves with either: - # * {Repository} if a repository can be created for the given directory - # * `null` if no repository can be created for the given directory. - repositoryForDirectory: (directory) -> - pathForDirectory = directory.getRealPathSync() - promise = @repositoryPromisesByPath.get(pathForDirectory) - unless promise - promises = @repositoryProviders.map (provider) -> - provider.repositoryForDirectory(directory) - promise = Promise.all(promises).then (repositories) => - repo = _.find(repositories, (repo) -> repo?) ? null - - # If no repository is found, remove the entry in for the directory in - # @repositoryPromisesByPath in case some other RepositoryProvider is - # registered in the future that could supply a Repository for the - # directory. - @repositoryPromisesByPath.delete(pathForDirectory) unless repo? - repo?.onDidDestroy?(=> @repositoryPromisesByPath.delete(pathForDirectory)) - repo - @repositoryPromisesByPath.set(pathForDirectory, promise) - promise - - ### - Section: Managing Paths - ### - - # Public: Get an {Array} of {String}s containing the paths of the project's - # directories. - getPaths: -> rootDirectory.getPath() for rootDirectory in @rootDirectories - - # Public: Set the paths of the project's directories. - # - # * `projectPaths` {Array} of {String} paths. - # * `options` An optional {Object} that may contain the following keys: - # * `mustExist` If `true`, throw an Error if any `projectPaths` do not exist. Any remaining `projectPaths` that - # do exist will still be added to the project. Default: `false`. - # * `exact` If `true`, only add a `projectPath` if it names an existing directory. If `false` and any `projectPath` - # is a file or does not exist, its parent directory will be added instead. Default: `false`. - setPaths: (projectPaths, options = {}) -> - repository?.destroy() for repository in @repositories - @rootDirectories = [] - @repositories = [] - - watcher.then((w) -> w.dispose()) for _, watcher in @watcherPromisesByPath - @watcherPromisesByPath = {} - - missingProjectPaths = [] - for projectPath in projectPaths - try - @addPath projectPath, emitEvent: false, mustExist: true, exact: options.exact is true - catch e - if e.missingProjectPaths? - missingProjectPaths.push e.missingProjectPaths... - else - throw e - - @emitter.emit 'did-change-paths', projectPaths - - if options.mustExist is true and missingProjectPaths.length > 0 - err = new Error "One or more project directories do not exist" - err.missingProjectPaths = missingProjectPaths - throw err - - # Public: Add a path to the project's list of root paths - # - # * `projectPath` {String} The path to the directory to add. - # * `options` An optional {Object} that may contain the following keys: - # * `mustExist` If `true`, throw an Error if the `projectPath` does not exist. If `false`, a `projectPath` that does - # not exist is ignored. Default: `false`. - # * `exact` If `true`, only add `projectPath` if it names an existing directory. If `false`, if `projectPath` is a - # a file or does not exist, its parent directory will be added instead. - addPath: (projectPath, options = {}) -> - directory = @getDirectoryForProjectPath(projectPath) - - ok = true - ok = ok and directory.getPath() is projectPath if options.exact is true - ok = ok and directory.existsSync() - - unless ok - if options.mustExist is true - err = new Error "Project directory #{directory} does not exist" - err.missingProjectPaths = [projectPath] - throw err - else - return - - for existingDirectory in @getDirectories() - return if existingDirectory.getPath() is directory.getPath() - - @rootDirectories.push(directory) - @watcherPromisesByPath[directory.getPath()] = watchPath directory.getPath(), {}, (events) => - # Stop event delivery immediately on removal of a rootDirectory, even if its watcher - # promise has yet to resolve at the time of removal - if @rootDirectories.includes directory - @emitter.emit 'did-change-files', events - - for root, watcherPromise in @watcherPromisesByPath - unless @rootDirectories.includes root - watcherPromise.then (watcher) -> watcher.dispose() - - repo = null - for provider in @repositoryProviders - break if repo = provider.repositoryForDirectorySync?(directory) - @repositories.push(repo ? null) - - unless options.emitEvent is false - @emitter.emit 'did-change-paths', @getPaths() - - getDirectoryForProjectPath: (projectPath) -> - directory = null - for provider in @directoryProviders - break if directory = provider.directoryForURISync?(projectPath) - directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath) - directory - - # Extended: Access a {Promise} that resolves when the filesystem watcher associated with a project - # root directory is ready to begin receiving events. - # - # This is especially useful in test cases, where it's important to know that the watcher is - # ready before manipulating the filesystem to produce events. - # - # * `projectPath` {String} One of the project's root directories. - # - # Returns a {Promise} that resolves with the {PathWatcher} associated with this project root - # once it has initialized and is ready to start sending events. The Promise will reject with - # an error instead if `projectPath` is not currently a root directory. - getWatcherPromise: (projectPath) -> - @watcherPromisesByPath[projectPath] or - Promise.reject(new Error("#{projectPath} is not a project root")) - - # Public: remove a path from the project's list of root paths. - # - # * `projectPath` {String} The path to remove. - removePath: (projectPath) -> - # The projectPath may be a URI, in which case it should not be normalized. - unless projectPath in @getPaths() - projectPath = @defaultDirectoryProvider.normalizePath(projectPath) - - indexToRemove = null - for directory, i in @rootDirectories - if directory.getPath() is projectPath - indexToRemove = i - break - - if indexToRemove? - [removedDirectory] = @rootDirectories.splice(indexToRemove, 1) - [removedRepository] = @repositories.splice(indexToRemove, 1) - removedRepository?.destroy() unless removedRepository in @repositories - @watcherPromisesByPath[projectPath]?.then (w) -> w.dispose() - delete @watcherPromisesByPath[projectPath] - @emitter.emit "did-change-paths", @getPaths() - true - else - false - - # Public: Get an {Array} of {Directory}s associated with this project. - getDirectories: -> - @rootDirectories - - resolvePath: (uri) -> - return unless uri - - if uri?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme - uri - else - if fs.isAbsolute(uri) - @defaultDirectoryProvider.normalizePath(fs.resolveHome(uri)) - # TODO: what should we do here when there are multiple directories? - else if projectPath = @getPaths()[0] - @defaultDirectoryProvider.normalizePath(fs.resolveHome(path.join(projectPath, uri))) - else - undefined - - relativize: (fullPath) -> - @relativizePath(fullPath)[1] - - # Public: Get the path to the project directory that contains the given path, - # and the relative path from that project directory to the given path. - # - # * `fullPath` {String} An absolute path. - # - # Returns an {Array} with two elements: - # * `projectPath` The {String} path to the project directory that contains the - # given path, or `null` if none is found. - # * `relativePath` {String} The relative path from the project directory to - # the given path. - relativizePath: (fullPath) -> - result = [null, fullPath] - if fullPath? - for rootDirectory in @rootDirectories - relativePath = rootDirectory.relativize(fullPath) - if relativePath?.length < result[1].length - result = [rootDirectory.getPath(), relativePath] - result - - # Public: Determines whether the given path (real or symbolic) is inside the - # project's directory. - # - # This method does not actually check if the path exists, it just checks their - # locations relative to each other. - # - # ## Examples - # - # Basic operation - # - # ```coffee - # # Project's root directory is /foo/bar - # project.contains('/foo/bar/baz') # => true - # project.contains('/usr/lib/baz') # => false - # ``` - # - # Existence of the path is not required - # - # ```coffee - # # Project's root directory is /foo/bar - # fs.existsSync('/foo/bar/baz') # => false - # project.contains('/foo/bar/baz') # => true - # ``` - # - # * `pathToCheck` {String} path - # - # Returns whether the path is inside the project's root directory. - contains: (pathToCheck) -> - @rootDirectories.some (dir) -> dir.contains(pathToCheck) - - ### - Section: Private - ### - - consumeServices: ({serviceHub}) -> - serviceHub.consume( - 'atom.directory-provider', - '^0.1.0', - (provider) => - @directoryProviders.unshift(provider) - new Disposable => - @directoryProviders.splice(@directoryProviders.indexOf(provider), 1) - ) - - serviceHub.consume( - 'atom.repository-provider', - '^0.1.0', - (provider) => - @repositoryProviders.unshift(provider) - @setPaths(@getPaths()) if null in @repositories - new Disposable => - @repositoryProviders.splice(@repositoryProviders.indexOf(provider), 1) - ) - - # Retrieves all the {TextBuffer}s in the project; that is, the - # buffers for all open files. - # - # Returns an {Array} of {TextBuffer}s. - getBuffers: -> - @buffers.slice() - - # Is the buffer for the given path modified? - isPathModified: (filePath) -> - @findBufferForPath(@resolvePath(filePath))?.isModified() - - findBufferForPath: (filePath) -> - _.find @buffers, (buffer) -> buffer.getPath() is filePath - - findBufferForId: (id) -> - _.find @buffers, (buffer) -> buffer.getId() is id - - # Only to be used in specs - bufferForPathSync: (filePath) -> - absoluteFilePath = @resolvePath(filePath) - return null if @retiredBufferPaths.has absoluteFilePath - existingBuffer = @findBufferForPath(absoluteFilePath) if filePath - existingBuffer ? @buildBufferSync(absoluteFilePath) - - # Only to be used when deserializing - bufferForIdSync: (id) -> - return null if @retiredBufferIDs.has id - existingBuffer = @findBufferForId(id) if id - existingBuffer ? @buildBufferSync() - - # Given a file path, this retrieves or creates a new {TextBuffer}. - # - # If the `filePath` already has a `buffer`, that value is used instead. Otherwise, - # `text` is used as the contents of the new buffer. - # - # * `filePath` A {String} representing a path. If `null`, an "Untitled" buffer is created. - # - # Returns a {Promise} that resolves to the {TextBuffer}. - bufferForPath: (absoluteFilePath) -> - existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath? - if existingBuffer - Promise.resolve(existingBuffer) - else - @buildBuffer(absoluteFilePath) - - shouldDestroyBufferOnFileDelete: -> - atom.config.get('core.closeDeletedFileTabs') - - # Still needed when deserializing a tokenized buffer - buildBufferSync: (absoluteFilePath) -> - params = {shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete} - if absoluteFilePath? - buffer = TextBuffer.loadSync(absoluteFilePath, params) - else - buffer = new TextBuffer(params) - @addBuffer(buffer) - buffer - - # Given a file path, this sets its {TextBuffer}. - # - # * `absoluteFilePath` A {String} representing a path. - # * `text` The {String} text to use as a buffer. - # - # Returns a {Promise} that resolves to the {TextBuffer}. - buildBuffer: (absoluteFilePath) -> - params = {shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete} - if absoluteFilePath? - promise = - @loadPromisesByPath[absoluteFilePath] ?= - TextBuffer.load(absoluteFilePath, params).catch (error) => - delete @loadPromisesByPath[absoluteFilePath] - throw error - else - promise = Promise.resolve(new TextBuffer(params)) - promise.then (buffer) => - delete @loadPromisesByPath[absoluteFilePath] - @addBuffer(buffer) - buffer - - - addBuffer: (buffer, options={}) -> - @addBufferAtIndex(buffer, @buffers.length, options) - - addBufferAtIndex: (buffer, index, options={}) -> - @buffers.splice(index, 0, buffer) - @subscribeToBuffer(buffer) - @emitter.emit 'did-add-buffer', buffer - buffer - - # Removes a {TextBuffer} association from the project. - # - # Returns the removed {TextBuffer}. - removeBuffer: (buffer) -> - index = @buffers.indexOf(buffer) - @removeBufferAtIndex(index) unless index is -1 - - removeBufferAtIndex: (index, options={}) -> - [buffer] = @buffers.splice(index, 1) - buffer?.destroy() - - eachBuffer: (args...) -> - subscriber = args.shift() if args.length > 1 - callback = args.shift() - - callback(buffer) for buffer in @getBuffers() - if subscriber - subscriber.subscribe this, 'buffer-created', (buffer) -> callback(buffer) - else - @on 'buffer-created', (buffer) -> callback(buffer) - - subscribeToBuffer: (buffer) -> - buffer.onWillSave ({path}) => @applicationDelegate.emitWillSavePath(path) - buffer.onDidSave ({path}) => @applicationDelegate.emitDidSavePath(path) - buffer.onDidDestroy => @removeBuffer(buffer) - buffer.onDidChangePath => - unless @getPaths().length > 0 - @setPaths([path.dirname(buffer.getPath())]) - buffer.onWillThrowWatchError ({error, handle}) => - handle() - @notificationManager.addWarning """ - Unable to read file after file `#{error.eventType}` event. - Make sure you have permission to access `#{buffer.getPath()}`. - """, - detail: error.message - dismissable: true diff --git a/src/project.js b/src/project.js new file mode 100644 index 000000000..448f2b87c --- /dev/null +++ b/src/project.js @@ -0,0 +1,714 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Project +const path = require('path') + +let _ = require('underscore-plus') +const fs = require('fs-plus') +const {Emitter, Disposable} = require('event-kit') +const TextBuffer = require('text-buffer') +const {watchPath} = require('./path-watcher') + +const DefaultDirectoryProvider = require('./default-directory-provider') +const Model = require('./model') +const GitRepositoryProvider = require('./git-repository-provider') + +// Extended: Represents a project that's opened in Atom. +// +// An instance of this class is always available as the `atom.project` global. +module.exports = +(Project = class Project extends Model { + /* + Section: Construction and Destruction + */ + + constructor ({notificationManager, packageManager, config, applicationDelegate}) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super() } + let thisFn = (() => { this }).toString() + let thisName = thisFn.slice(thisFn.indexOf('{') + 1, thisFn.indexOf(';')).trim() + eval(`${thisName} = this;`) + } + this.notificationManager = notificationManager + this.applicationDelegate = applicationDelegate + this.emitter = new Emitter() + this.buffers = [] + this.rootDirectories = [] + this.repositories = [] + this.directoryProviders = [] + this.defaultDirectoryProvider = new DefaultDirectoryProvider() + this.repositoryPromisesByPath = new Map() + this.repositoryProviders = [new GitRepositoryProvider(this, config)] + this.loadPromisesByPath = {} + this.watcherPromisesByPath = {} + this.retiredBufferIDs = new Set() + this.retiredBufferPaths = new Set() + this.consumeServices(packageManager) + } + + destroyed () { + for (let buffer of this.buffers.slice()) { buffer.destroy() } + for (let repository of this.repositories.slice()) { + if (repository != null) { + repository.destroy() + } + } + for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.dispose() } + this.rootDirectories = [] + return this.repositories = [] + } + + reset (packageManager) { + this.emitter.dispose() + this.emitter = new Emitter() + + for (let buffer of this.buffers) { + if (buffer != null) { + buffer.destroy() + } + } + this.buffers = [] + this.setPaths([]) + this.loadPromisesByPath = {} + this.retiredBufferIDs = new Set() + this.retiredBufferPaths = new Set() + return this.consumeServices(packageManager) + } + + destroyUnretainedBuffers () { + for (let buffer of this.getBuffers()) { if (!buffer.isRetained()) { buffer.destroy() } } + } + + /* + Section: Serialization + */ + + deserialize (state) { + let bufferState + this.retiredBufferIDs = new Set() + this.retiredBufferPaths = new Set() + + const handleBufferState = bufferState => { + if (bufferState.shouldDestroyOnFileDelete == null) { bufferState.shouldDestroyOnFileDelete = () => atom.config.get('core.closeDeletedFileTabs') } + + // Use a little guilty knowledge of the way TextBuffers are serialized. + // This allows TextBuffers that have never been saved (but have filePaths) to be deserialized, but prevents + // TextBuffers backed by files that have been deleted from being saved. + bufferState.mustExist = bufferState.digestWhenLastPersisted !== false + + return TextBuffer.deserialize(bufferState).catch(err => { + this.retiredBufferIDs.add(bufferState.id) + this.retiredBufferPaths.add(bufferState.filePath) + return null + }) + } + + const bufferPromises = ((() => { + const result = [] + for (bufferState of state.buffers) { + result.push(handleBufferState(bufferState)) + } + return result + })()) + + return Promise.all(bufferPromises).then(buffers => { + this.buffers = buffers.filter(Boolean) + for (let buffer of this.buffers) { this.subscribeToBuffer(buffer) } + return this.setPaths(state.paths || [], {mustExist: true, exact: true}) + }) + } + + serialize (options = {}) { + return { + deserializer: 'Project', + paths: this.getPaths(), + buffers: _.compact(this.buffers.map(function (buffer) { + if (buffer.isRetained()) { + const isUnloading = options.isUnloading === true + return buffer.serialize({markerLayers: isUnloading, history: isUnloading}) + } + })) + } + } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when the project paths change. + // + // * `callback` {Function} to be called after the project paths change. + // * `projectPaths` An {Array} of {String} project paths. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePaths (callback) { + return this.emitter.on('did-change-paths', callback) + } + + // Public: Invoke the given callback when a text buffer is added to the + // project. + // + // * `callback` {Function} to be called when a text buffer is added. + // * `buffer` A {TextBuffer} item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddBuffer (callback) { + return this.emitter.on('did-add-buffer', callback) + } + + // Public: Invoke the given callback with all current and future text + // buffers in the project. + // + // * `callback` {Function} to be called with current and future text buffers. + // * `buffer` A {TextBuffer} item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeBuffers (callback) { + for (let buffer of this.getBuffers()) { callback(buffer) } + return this.onDidAddBuffer(callback) + } + + // Extended: Invoke a callback when a filesystem change occurs within any open + // project path. + // + // ```js + // const disposable = atom.project.onDidChangeFiles(events => { + // for (const event of events) { + // // "created", "modified", "deleted", or "renamed" + // console.log(`Event action: ${event.type}`) + // + // // absolute path to the filesystem entry that was touched + // console.log(`Event path: ${event.path}`) + // + // if (event.type === 'renamed') { + // console.log(`.. renamed from: ${event.oldPath}`) + // } + // } + // } + // + // disposable.dispose() + // ``` + // + // To watch paths outside of open projects, use the `watchPaths` function instead; see {PathWatcher}. + // + // When writing tests against functionality that uses this method, be sure to wait for the + // {Promise} returned by {getWatcherPromise()} before manipulating the filesystem to ensure that + // the watcher is receiving events. + // + // * `callback` {Function} to be called with batches of filesystem events reported by + // the operating system. + // * `events` An {Array} of objects that describe a batch of filesystem events. + // * `action` {String} describing the filesystem action that occurred. One of `"created"`, + // `"modified"`, `"deleted"`, or `"renamed"`. + // * `path` {String} containing the absolute path to the filesystem entry + // that was acted upon. + // * `oldPath` For rename events, {String} containing the filesystem entry's + // former absolute path. + // + // Returns a {Disposable} to manage this event subscription. + onDidChangeFiles (callback) { + return this.emitter.on('did-change-files', callback) + } + + /* + Section: Accessing the git repository + */ + + // Public: Get an {Array} of {GitRepository}s associated with the project's + // directories. + // + // This method will be removed in 2.0 because it does synchronous I/O. + // Prefer the following, which evaluates to a {Promise} that resolves to an + // {Array} of {Repository} objects: + // ``` + // Promise.all(atom.project.getDirectories().map( + // atom.project.repositoryForDirectory.bind(atom.project))) + // ``` + getRepositories () { return this.repositories } + + // Public: Get the repository for a given directory asynchronously. + // + // * `directory` {Directory} for which to get a {Repository}. + // + // Returns a {Promise} that resolves with either: + // * {Repository} if a repository can be created for the given directory + // * `null` if no repository can be created for the given directory. + repositoryForDirectory (directory) { + const pathForDirectory = directory.getRealPathSync() + let promise = this.repositoryPromisesByPath.get(pathForDirectory) + if (!promise) { + const promises = this.repositoryProviders.map(provider => provider.repositoryForDirectory(directory)) + promise = Promise.all(promises).then(repositories => { + let left + const repo = (left = _.find(repositories, repo => repo != null)) != null ? left : null + + // If no repository is found, remove the entry in for the directory in + // @repositoryPromisesByPath in case some other RepositoryProvider is + // registered in the future that could supply a Repository for the + // directory. + if (repo == null) { this.repositoryPromisesByPath.delete(pathForDirectory) } + __guardMethod__(repo, 'onDidDestroy', o => o.onDidDestroy(() => this.repositoryPromisesByPath.delete(pathForDirectory))) + return repo + }) + this.repositoryPromisesByPath.set(pathForDirectory, promise) + } + return promise + } + + /* + Section: Managing Paths + */ + + // Public: Get an {Array} of {String}s containing the paths of the project's + // directories. + getPaths () { return this.rootDirectories.map((rootDirectory) => rootDirectory.getPath()) } + + // Public: Set the paths of the project's directories. + // + // * `projectPaths` {Array} of {String} paths. + // * `options` An optional {Object} that may contain the following keys: + // * `mustExist` If `true`, throw an Error if any `projectPaths` do not exist. Any remaining `projectPaths` that + // do exist will still be added to the project. Default: `false`. + // * `exact` If `true`, only add a `projectPath` if it names an existing directory. If `false` and any `projectPath` + // is a file or does not exist, its parent directory will be added instead. Default: `false`. + setPaths (projectPaths, options = {}) { + for (let repository of this.repositories) { + if (repository != null) { + repository.destroy() + } + } + this.rootDirectories = [] + this.repositories = [] + + for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.then(w => w.dispose()) } + this.watcherPromisesByPath = {} + + const missingProjectPaths = [] + for (let projectPath of projectPaths) { + try { + this.addPath(projectPath, {emitEvent: false, mustExist: true, exact: options.exact === true}) + } catch (e) { + if (e.missingProjectPaths != null) { + missingProjectPaths.push(...Array.from(e.missingProjectPaths || [])) + } else { + throw e + } + } + } + + this.emitter.emit('did-change-paths', projectPaths) + + if ((options.mustExist === true) && (missingProjectPaths.length > 0)) { + const err = new Error('One or more project directories do not exist') + err.missingProjectPaths = missingProjectPaths + throw err + } + } + + // Public: Add a path to the project's list of root paths + // + // * `projectPath` {String} The path to the directory to add. + // * `options` An optional {Object} that may contain the following keys: + // * `mustExist` If `true`, throw an Error if the `projectPath` does not exist. If `false`, a `projectPath` that does + // not exist is ignored. Default: `false`. + // * `exact` If `true`, only add `projectPath` if it names an existing directory. If `false`, if `projectPath` is a + // a file or does not exist, its parent directory will be added instead. + addPath (projectPath, options = {}) { + const directory = this.getDirectoryForProjectPath(projectPath) + + let ok = true + if (options.exact === true) { ok = ok && (directory.getPath() === projectPath) } + ok = ok && directory.existsSync() + + if (!ok) { + if (options.mustExist === true) { + const err = new Error(`Project directory ${directory} does not exist`) + err.missingProjectPaths = [projectPath] + throw err + } else { + return + } + } + + for (let existingDirectory of this.getDirectories()) { + if (existingDirectory.getPath() === directory.getPath()) { return } + } + + this.rootDirectories.push(directory) + this.watcherPromisesByPath[directory.getPath()] = watchPath(directory.getPath(), {}, events => { + // Stop event delivery immediately on removal of a rootDirectory, even if its watcher + // promise has yet to resolve at the time of removal + if (this.rootDirectories.includes(directory)) { + return this.emitter.emit('did-change-files', events) + } + }) + + for (let watcherPromise = 0; watcherPromise < this.watcherPromisesByPath.length; watcherPromise++) { + const root = this.watcherPromisesByPath[watcherPromise] + if (!this.rootDirectories.includes(root)) { + watcherPromise.then(watcher => watcher.dispose()) + } + } + + let repo = null + for (let provider of this.repositoryProviders) { + if (repo = typeof provider.repositoryForDirectorySync === 'function' ? provider.repositoryForDirectorySync(directory) : undefined) { break } + } + this.repositories.push(repo != null ? repo : null) + + if (options.emitEvent !== false) { + return this.emitter.emit('did-change-paths', this.getPaths()) + } + } + + getDirectoryForProjectPath (projectPath) { + let directory = null + for (let provider of this.directoryProviders) { + if (directory = typeof provider.directoryForURISync === 'function' ? provider.directoryForURISync(projectPath) : undefined) { break } + } + if (directory == null) { directory = this.defaultDirectoryProvider.directoryForURISync(projectPath) } + return directory + } + + // Extended: Access a {Promise} that resolves when the filesystem watcher associated with a project + // root directory is ready to begin receiving events. + // + // This is especially useful in test cases, where it's important to know that the watcher is + // ready before manipulating the filesystem to produce events. + // + // * `projectPath` {String} One of the project's root directories. + // + // Returns a {Promise} that resolves with the {PathWatcher} associated with this project root + // once it has initialized and is ready to start sending events. The Promise will reject with + // an error instead if `projectPath` is not currently a root directory. + getWatcherPromise (projectPath) { + return this.watcherPromisesByPath[projectPath] || + Promise.reject(new Error(`${projectPath} is not a project root`)) + } + + // Public: remove a path from the project's list of root paths. + // + // * `projectPath` {String} The path to remove. + removePath (projectPath) { + // The projectPath may be a URI, in which case it should not be normalized. + let needle + if ((needle = projectPath, !this.getPaths().includes(needle))) { + projectPath = this.defaultDirectoryProvider.normalizePath(projectPath) + } + + let indexToRemove = null + for (let i = 0; i < this.rootDirectories.length; i++) { + const directory = this.rootDirectories[i] + if (directory.getPath() === projectPath) { + indexToRemove = i + break + } + } + + if (indexToRemove != null) { + const [removedDirectory] = Array.from(this.rootDirectories.splice(indexToRemove, 1)) + const [removedRepository] = Array.from(this.repositories.splice(indexToRemove, 1)) + if (!this.repositories.includes(removedRepository)) { + if (removedRepository != null) { + removedRepository.destroy() + } + } + if (this.watcherPromisesByPath[projectPath] != null) { + this.watcherPromisesByPath[projectPath].then(w => w.dispose()) + } + delete this.watcherPromisesByPath[projectPath] + this.emitter.emit('did-change-paths', this.getPaths()) + return true + } else { + return false + } + } + + // Public: Get an {Array} of {Directory}s associated with this project. + getDirectories () { + return this.rootDirectories + } + + resolvePath (uri) { + if (!uri) { return } + + if ((uri != null ? uri.match(/[A-Za-z0-9+-.]+:\/\//) : undefined)) { // leave path alone if it has a scheme + return uri + } else { + let projectPath + if (fs.isAbsolute(uri)) { + return this.defaultDirectoryProvider.normalizePath(fs.resolveHome(uri)) + // TODO: what should we do here when there are multiple directories? + } else if ((projectPath = this.getPaths()[0])) { + return this.defaultDirectoryProvider.normalizePath(fs.resolveHome(path.join(projectPath, uri))) + } else { + return undefined + } + } + } + + relativize (fullPath) { + return this.relativizePath(fullPath)[1] + } + + // Public: Get the path to the project directory that contains the given path, + // and the relative path from that project directory to the given path. + // + // * `fullPath` {String} An absolute path. + // + // Returns an {Array} with two elements: + // * `projectPath` The {String} path to the project directory that contains the + // given path, or `null` if none is found. + // * `relativePath` {String} The relative path from the project directory to + // the given path. + relativizePath (fullPath) { + let result = [null, fullPath] + if (fullPath != null) { + for (let rootDirectory of this.rootDirectories) { + const relativePath = rootDirectory.relativize(fullPath) + if ((relativePath != null ? relativePath.length : undefined) < result[1].length) { + result = [rootDirectory.getPath(), relativePath] + } + } + } + return result + } + + // Public: Determines whether the given path (real or symbolic) is inside the + // project's directory. + // + // This method does not actually check if the path exists, it just checks their + // locations relative to each other. + // + // ## Examples + // + // Basic operation + // + // ```coffee + // # Project's root directory is /foo/bar + // project.contains('/foo/bar/baz') # => true + // project.contains('/usr/lib/baz') # => false + // ``` + // + // Existence of the path is not required + // + // ```coffee + // # Project's root directory is /foo/bar + // fs.existsSync('/foo/bar/baz') # => false + // project.contains('/foo/bar/baz') # => true + // ``` + // + // * `pathToCheck` {String} path + // + // Returns whether the path is inside the project's root directory. + contains (pathToCheck) { + return this.rootDirectories.some(dir => dir.contains(pathToCheck)) + } + + /* + Section: Private + */ + + consumeServices ({serviceHub}) { + serviceHub.consume( + 'atom.directory-provider', + '^0.1.0', + provider => { + this.directoryProviders.unshift(provider) + return new Disposable(() => { + return this.directoryProviders.splice(this.directoryProviders.indexOf(provider), 1) + }) + }) + + return serviceHub.consume( + 'atom.repository-provider', + '^0.1.0', + provider => { + this.repositoryProviders.unshift(provider) + if (this.repositories.includes(null)) { this.setPaths(this.getPaths()) } + return new Disposable(() => { + return this.repositoryProviders.splice(this.repositoryProviders.indexOf(provider), 1) + }) + }) + } + + // Retrieves all the {TextBuffer}s in the project; that is, the + // buffers for all open files. + // + // Returns an {Array} of {TextBuffer}s. + getBuffers () { + return this.buffers.slice() + } + + // Is the buffer for the given path modified? + isPathModified (filePath) { + return __guard__(this.findBufferForPath(this.resolvePath(filePath)), x => x.isModified()) + } + + findBufferForPath (filePath) { + return _.find(this.buffers, buffer => buffer.getPath() === filePath) + } + + findBufferForId (id) { + return _.find(this.buffers, buffer => buffer.getId() === id) + } + + // Only to be used in specs + bufferForPathSync (filePath) { + let existingBuffer + const absoluteFilePath = this.resolvePath(filePath) + if (this.retiredBufferPaths.has(absoluteFilePath)) { return null } + if (filePath) { existingBuffer = this.findBufferForPath(absoluteFilePath) } + return existingBuffer != null ? existingBuffer : this.buildBufferSync(absoluteFilePath) + } + + // Only to be used when deserializing + bufferForIdSync (id) { + let existingBuffer + if (this.retiredBufferIDs.has(id)) { return null } + if (id) { existingBuffer = this.findBufferForId(id) } + return existingBuffer != null ? existingBuffer : this.buildBufferSync() + } + + // Given a file path, this retrieves or creates a new {TextBuffer}. + // + // If the `filePath` already has a `buffer`, that value is used instead. Otherwise, + // `text` is used as the contents of the new buffer. + // + // * `filePath` A {String} representing a path. If `null`, an "Untitled" buffer is created. + // + // Returns a {Promise} that resolves to the {TextBuffer}. + bufferForPath (absoluteFilePath) { + let existingBuffer + if (absoluteFilePath != null) { existingBuffer = this.findBufferForPath(absoluteFilePath) } + if (existingBuffer) { + return Promise.resolve(existingBuffer) + } else { + return this.buildBuffer(absoluteFilePath) + } + } + + shouldDestroyBufferOnFileDelete () { + return atom.config.get('core.closeDeletedFileTabs') + } + + // Still needed when deserializing a tokenized buffer + buildBufferSync (absoluteFilePath) { + let buffer + const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + if (absoluteFilePath != null) { + buffer = TextBuffer.loadSync(absoluteFilePath, params) + } else { + buffer = new TextBuffer(params) + } + this.addBuffer(buffer) + return buffer + } + + // Given a file path, this sets its {TextBuffer}. + // + // * `absoluteFilePath` A {String} representing a path. + // * `text` The {String} text to use as a buffer. + // + // Returns a {Promise} that resolves to the {TextBuffer}. + buildBuffer (absoluteFilePath) { + let promise + const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + if (absoluteFilePath != null) { + promise = + this.loadPromisesByPath[absoluteFilePath] != null ? this.loadPromisesByPath[absoluteFilePath] : (this.loadPromisesByPath[absoluteFilePath] = + TextBuffer.load(absoluteFilePath, params).catch(error => { + delete this.loadPromisesByPath[absoluteFilePath] + throw error + })) + } else { + promise = Promise.resolve(new TextBuffer(params)) + } + return promise.then(buffer => { + delete this.loadPromisesByPath[absoluteFilePath] + this.addBuffer(buffer) + return buffer + }) + } + + addBuffer (buffer, options = {}) { + return this.addBufferAtIndex(buffer, this.buffers.length, options) + } + + addBufferAtIndex (buffer, index, options = {}) { + this.buffers.splice(index, 0, buffer) + this.subscribeToBuffer(buffer) + this.emitter.emit('did-add-buffer', buffer) + return buffer + } + + // Removes a {TextBuffer} association from the project. + // + // Returns the removed {TextBuffer}. + removeBuffer (buffer) { + const index = this.buffers.indexOf(buffer) + if (index !== -1) { return this.removeBufferAtIndex(index) } + } + + removeBufferAtIndex (index, options = {}) { + const [buffer] = Array.from(this.buffers.splice(index, 1)) + return (buffer != null ? buffer.destroy() : undefined) + } + + eachBuffer (...args) { + let subscriber + if (args.length > 1) { subscriber = args.shift() } + const callback = args.shift() + + for (let buffer of this.getBuffers()) { callback(buffer) } + if (subscriber) { + return subscriber.subscribe(this, 'buffer-created', buffer => callback(buffer)) + } else { + return this.on('buffer-created', buffer => callback(buffer)) + } + } + + subscribeToBuffer (buffer) { + buffer.onWillSave(({path}) => this.applicationDelegate.emitWillSavePath(path)) + buffer.onDidSave(({path}) => this.applicationDelegate.emitDidSavePath(path)) + buffer.onDidDestroy(() => this.removeBuffer(buffer)) + buffer.onDidChangePath(() => { + if (!(this.getPaths().length > 0)) { + return this.setPaths([path.dirname(buffer.getPath())]) + } + }) + return buffer.onWillThrowWatchError(({error, handle}) => { + handle() + return this.notificationManager.addWarning(`\ +Unable to read file after file \`${error.eventType}\` event. +Make sure you have permission to access \`${buffer.getPath()}\`.\ +`, { + detail: error.message, + dismissable: true +} + ) + }) + } +}) + +function __guardMethod__ (obj, methodName, transform) { + if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { + return transform(obj, methodName) + } else { + return undefined + } +} +function __guard__ (value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined +} From cab8824aaedee5ab54333633b05b389402490d32 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:47:00 -0400 Subject: [PATCH 061/301] :necktie: Fix linter violations --- src/project.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/project.js b/src/project.js index 448f2b87c..4a481f5e6 100644 --- a/src/project.js +++ b/src/project.js @@ -10,7 +10,6 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let Project const path = require('path') let _ = require('underscore-plus') @@ -27,7 +26,7 @@ const GitRepositoryProvider = require('./git-repository-provider') // // An instance of this class is always available as the `atom.project` global. module.exports = -(Project = class Project extends Model { +class Project extends Model { /* Section: Construction and Destruction */ @@ -66,7 +65,7 @@ module.exports = } for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.dispose() } this.rootDirectories = [] - return this.repositories = [] + this.repositories = [] } reset (packageManager) { @@ -107,7 +106,7 @@ module.exports = // TextBuffers backed by files that have been deleted from being saved. bufferState.mustExist = bufferState.digestWhenLastPersisted !== false - return TextBuffer.deserialize(bufferState).catch(err => { + return TextBuffer.deserialize(bufferState).catch((_) => { this.retiredBufferIDs.add(bufferState.id) this.retiredBufferPaths.add(bufferState.filePath) return null @@ -363,7 +362,10 @@ module.exports = let repo = null for (let provider of this.repositoryProviders) { - if (repo = typeof provider.repositoryForDirectorySync === 'function' ? provider.repositoryForDirectorySync(directory) : undefined) { break } + if (provider.repositoryForDirectorySync) { + repo = provider.repositoryForDirectorySync(directory) + } + if (repo) { break } } this.repositories.push(repo != null ? repo : null) @@ -375,9 +377,14 @@ module.exports = getDirectoryForProjectPath (projectPath) { let directory = null for (let provider of this.directoryProviders) { - if (directory = typeof provider.directoryForURISync === 'function' ? provider.directoryForURISync(projectPath) : undefined) { break } + if (typeof provider.directoryForURISync === 'function') { + directory = provider.directoryForURISync(projectPath) + if (directory) break + } + } + if (directory == null) { + directory = this.defaultDirectoryProvider.directoryForURISync(projectPath) } - if (directory == null) { directory = this.defaultDirectoryProvider.directoryForURISync(projectPath) } return directory } @@ -417,7 +424,7 @@ module.exports = } if (indexToRemove != null) { - const [removedDirectory] = Array.from(this.rootDirectories.splice(indexToRemove, 1)) + this.rootDirectories.splice(indexToRemove, 1) const [removedRepository] = Array.from(this.repositories.splice(indexToRemove, 1)) if (!this.repositories.includes(removedRepository)) { if (removedRepository != null) { @@ -700,7 +707,7 @@ Make sure you have permission to access \`${buffer.getPath()}\`.\ ) }) } -}) +} function __guardMethod__ (obj, methodName, transform) { if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { From dd6359b507583c6bc4e5c021a2f4dd0a61365f1c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:00:09 -0400 Subject: [PATCH 062/301] Remove Babel/TypeScript constructor workaround --- src/project.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/project.js b/src/project.js index 4a481f5e6..1909b0b94 100644 --- a/src/project.js +++ b/src/project.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS001: Remove Babel/TypeScript constructor workaround * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ @@ -32,13 +31,7 @@ class Project extends Model { */ constructor ({notificationManager, packageManager, config, applicationDelegate}) { - { - // Hack: trick Babel/TypeScript into allowing this before super. - if (false) { super() } - let thisFn = (() => { this }).toString() - let thisName = thisFn.slice(thisFn.indexOf('{') + 1, thisFn.indexOf(';')).trim() - eval(`${thisName} = this;`) - } + super() this.notificationManager = notificationManager this.applicationDelegate = applicationDelegate this.emitter = new Emitter() From 8f40af16a96e314d722f925cd362cbfc2533e60b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:10:32 -0400 Subject: [PATCH 063/301] :art: --- src/project.js | 85 ++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/project.js b/src/project.js index 1909b0b94..32538fc02 100644 --- a/src/project.js +++ b/src/project.js @@ -52,9 +52,7 @@ class Project extends Model { destroyed () { for (let buffer of this.buffers.slice()) { buffer.destroy() } for (let repository of this.repositories.slice()) { - if (repository != null) { - repository.destroy() - } + if (repository != null) repository.destroy() } for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.dispose() } this.rootDirectories = [] @@ -66,9 +64,7 @@ class Project extends Model { this.emitter = new Emitter() for (let buffer of this.buffers) { - if (buffer != null) { - buffer.destroy() - } + if (buffer != null) buffer.destroy() } this.buffers = [] this.setPaths([]) @@ -79,7 +75,9 @@ class Project extends Model { } destroyUnretainedBuffers () { - for (let buffer of this.getBuffers()) { if (!buffer.isRetained()) { buffer.destroy() } } + for (let buffer of this.getBuffers()) { + if (!buffer.isRetained()) buffer.destroy() + } } /* @@ -91,8 +89,10 @@ class Project extends Model { this.retiredBufferIDs = new Set() this.retiredBufferPaths = new Set() - const handleBufferState = bufferState => { - if (bufferState.shouldDestroyOnFileDelete == null) { bufferState.shouldDestroyOnFileDelete = () => atom.config.get('core.closeDeletedFileTabs') } + const handleBufferState = (bufferState) => { + if (bufferState.shouldDestroyOnFileDelete == null) { + bufferState.shouldDestroyOnFileDelete = () => atom.config.get('core.closeDeletedFileTabs') + } // Use a little guilty knowledge of the way TextBuffers are serialized. // This allows TextBuffers that have never been saved (but have filePaths) to be deserialized, but prevents @@ -116,7 +116,9 @@ class Project extends Model { return Promise.all(bufferPromises).then(buffers => { this.buffers = buffers.filter(Boolean) - for (let buffer of this.buffers) { this.subscribeToBuffer(buffer) } + for (let buffer of this.buffers) { + this.subscribeToBuffer(buffer) + } return this.setPaths(state.paths || [], {mustExist: true, exact: true}) }) } @@ -227,7 +229,9 @@ class Project extends Model { // Promise.all(atom.project.getDirectories().map( // atom.project.repositoryForDirectory.bind(atom.project))) // ``` - getRepositories () { return this.repositories } + getRepositories () { + return this.repositories + } // Public: Get the repository for a given directory asynchronously. // @@ -245,7 +249,7 @@ class Project extends Model { let left const repo = (left = _.find(repositories, repo => repo != null)) != null ? left : null - // If no repository is found, remove the entry in for the directory in + // If no repository is found, remove the entry for the directory in // @repositoryPromisesByPath in case some other RepositoryProvider is // registered in the future that could supply a Repository for the // directory. @@ -264,7 +268,9 @@ class Project extends Model { // Public: Get an {Array} of {String}s containing the paths of the project's // directories. - getPaths () { return this.rootDirectories.map((rootDirectory) => rootDirectory.getPath()) } + getPaths () { + return this.rootDirectories.map((rootDirectory) => rootDirectory.getPath()) + } // Public: Set the paths of the project's directories. // @@ -276,9 +282,7 @@ class Project extends Model { // is a file or does not exist, its parent directory will be added instead. Default: `false`. setPaths (projectPaths, options = {}) { for (let repository of this.repositories) { - if (repository != null) { - repository.destroy() - } + if (repository != null) repository.destroy() } this.rootDirectories = [] this.repositories = [] @@ -320,7 +324,9 @@ class Project extends Model { const directory = this.getDirectoryForProjectPath(projectPath) let ok = true - if (options.exact === true) { ok = ok && (directory.getPath() === projectPath) } + if (options.exact === true) { + ok = (directory.getPath() === projectPath) + } ok = ok && directory.existsSync() if (!ok) { @@ -420,9 +426,7 @@ class Project extends Model { this.rootDirectories.splice(indexToRemove, 1) const [removedRepository] = Array.from(this.repositories.splice(indexToRemove, 1)) if (!this.repositories.includes(removedRepository)) { - if (removedRepository != null) { - removedRepository.destroy() - } + if (removedRepository) removedRepository.destroy() } if (this.watcherPromisesByPath[projectPath] != null) { this.watcherPromisesByPath[projectPath].then(w => w.dispose()) @@ -566,17 +570,19 @@ class Project extends Model { // Only to be used in specs bufferForPathSync (filePath) { - let existingBuffer const absoluteFilePath = this.resolvePath(filePath) if (this.retiredBufferPaths.has(absoluteFilePath)) { return null } + + let existingBuffer if (filePath) { existingBuffer = this.findBufferForPath(absoluteFilePath) } return existingBuffer != null ? existingBuffer : this.buildBufferSync(absoluteFilePath) } // Only to be used when deserializing bufferForIdSync (id) { - let existingBuffer if (this.retiredBufferIDs.has(id)) { return null } + + let existingBuffer if (id) { existingBuffer = this.findBufferForId(id) } return existingBuffer != null ? existingBuffer : this.buildBufferSync() } @@ -605,8 +611,9 @@ class Project extends Model { // Still needed when deserializing a tokenized buffer buildBufferSync (absoluteFilePath) { - let buffer const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + + let buffer if (absoluteFilePath != null) { buffer = TextBuffer.loadSync(absoluteFilePath, params) } else { @@ -623,15 +630,18 @@ class Project extends Model { // // Returns a {Promise} that resolves to the {TextBuffer}. buildBuffer (absoluteFilePath) { - let promise const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + + let promise if (absoluteFilePath != null) { - promise = - this.loadPromisesByPath[absoluteFilePath] != null ? this.loadPromisesByPath[absoluteFilePath] : (this.loadPromisesByPath[absoluteFilePath] = - TextBuffer.load(absoluteFilePath, params).catch(error => { - delete this.loadPromisesByPath[absoluteFilePath] - throw error - })) + if (this.loadPromisesByPath[absoluteFilePath] == null) { + this.loadPromisesByPath[absoluteFilePath] = + TextBuffer.load(absoluteFilePath, params).catch(error => { + delete this.loadPromisesByPath[absoluteFilePath] + throw error + }) + } + promise = this.loadPromisesByPath[absoluteFilePath] } else { promise = Promise.resolve(new TextBuffer(params)) } @@ -690,14 +700,13 @@ class Project extends Model { }) return buffer.onWillThrowWatchError(({error, handle}) => { handle() - return this.notificationManager.addWarning(`\ -Unable to read file after file \`${error.eventType}\` event. -Make sure you have permission to access \`${buffer.getPath()}\`.\ -`, { - detail: error.message, - dismissable: true -} - ) + const message = + `Unable to read file after file \`${error.eventType}\` event.` + + `Make sure you have permission to access \`${buffer.getPath()}\`.` + this.notificationManager.addWarning(message, { + detail: error.message, + dismissable: true + }) }) } } From 0c35c26805f7ba8390b1250e9aaab75ecdb20661 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Sat, 14 Oct 2017 19:05:54 -0600 Subject: [PATCH 064/301] fix infinite overlay resizing loop --- src/text-editor-component.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index ad7048708..a7bbd99d4 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -4202,7 +4202,7 @@ class OverlayComponent { if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) { this.resizeObserver.disconnect() this.props.didResize() - process.nextTick(() => { this.resizeObserver.observe(this.element) }) + process.nextTick(() => { this.resizeObserver.observe(this.props.element) }) } }) this.didAttach() @@ -4226,7 +4226,7 @@ class OverlayComponent { } didAttach () { - this.resizeObserver.observe(this.element) + this.resizeObserver.observe(this.props.element) } didDetach () { From c1b0afe96981b7232ee8fcc605d19119b76cc9f6 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:34:25 -0400 Subject: [PATCH 065/301] Remove unnecessary code created because of implicit returns --- src/project.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/project.js b/src/project.js index 32538fc02..b176679c0 100644 --- a/src/project.js +++ b/src/project.js @@ -1,7 +1,6 @@ /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS104: Avoid inline assignments * DS204: Change includes calls to have a more natural evaluation order @@ -71,7 +70,7 @@ class Project extends Model { this.loadPromisesByPath = {} this.retiredBufferIDs = new Set() this.retiredBufferPaths = new Set() - return this.consumeServices(packageManager) + this.consumeServices(packageManager) } destroyUnretainedBuffers () { @@ -119,7 +118,7 @@ class Project extends Model { for (let buffer of this.buffers) { this.subscribeToBuffer(buffer) } - return this.setPaths(state.paths || [], {mustExist: true, exact: true}) + this.setPaths(state.paths || [], {mustExist: true, exact: true}) }) } @@ -348,7 +347,7 @@ class Project extends Model { // Stop event delivery immediately on removal of a rootDirectory, even if its watcher // promise has yet to resolve at the time of removal if (this.rootDirectories.includes(directory)) { - return this.emitter.emit('did-change-files', events) + this.emitter.emit('did-change-files', events) } }) @@ -369,7 +368,7 @@ class Project extends Model { this.repositories.push(repo != null ? repo : null) if (options.emitEvent !== false) { - return this.emitter.emit('did-change-paths', this.getPaths()) + this.emitter.emit('did-change-paths', this.getPaths()) } } @@ -695,10 +694,10 @@ class Project extends Model { buffer.onDidDestroy(() => this.removeBuffer(buffer)) buffer.onDidChangePath(() => { if (!(this.getPaths().length > 0)) { - return this.setPaths([path.dirname(buffer.getPath())]) + this.setPaths([path.dirname(buffer.getPath())]) } }) - return buffer.onWillThrowWatchError(({error, handle}) => { + buffer.onWillThrowWatchError(({error, handle}) => { handle() const message = `Unable to read file after file \`${error.eventType}\` event.` + From 94a552149d09b442be47679ecd50d14abcc7364f Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:36:47 -0400 Subject: [PATCH 066/301] Remove unnecessary use of Array.from --- src/project.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/project.js b/src/project.js index b176679c0..8d5d5e957 100644 --- a/src/project.js +++ b/src/project.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS103: Rewrite code to no longer use __guard__ * DS104: Avoid inline assignments * DS204: Change includes calls to have a more natural evaluation order @@ -295,7 +294,7 @@ class Project extends Model { this.addPath(projectPath, {emitEvent: false, mustExist: true, exact: options.exact === true}) } catch (e) { if (e.missingProjectPaths != null) { - missingProjectPaths.push(...Array.from(e.missingProjectPaths || [])) + missingProjectPaths.push(...e.missingProjectPaths) } else { throw e } @@ -423,7 +422,7 @@ class Project extends Model { if (indexToRemove != null) { this.rootDirectories.splice(indexToRemove, 1) - const [removedRepository] = Array.from(this.repositories.splice(indexToRemove, 1)) + const [removedRepository] = this.repositories.splice(indexToRemove, 1) if (!this.repositories.includes(removedRepository)) { if (removedRepository) removedRepository.destroy() } @@ -671,7 +670,7 @@ class Project extends Model { } removeBufferAtIndex (index, options = {}) { - const [buffer] = Array.from(this.buffers.splice(index, 1)) + const [buffer] = this.buffers.splice(index, 1) return (buffer != null ? buffer.destroy() : undefined) } From 99aaafed1b8d39713b7e033d68b248f710778fa2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:39:40 -0400 Subject: [PATCH 067/301] DS103: Rewrite code to no longer use __guard__ --- src/project.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/project.js b/src/project.js index 8d5d5e957..144e3d7c4 100644 --- a/src/project.js +++ b/src/project.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS103: Rewrite code to no longer use __guard__ * DS104: Avoid inline assignments * DS204: Change includes calls to have a more natural evaluation order * DS205: Consider reworking code to avoid use of IIFEs @@ -251,8 +250,12 @@ class Project extends Model { // @repositoryPromisesByPath in case some other RepositoryProvider is // registered in the future that could supply a Repository for the // directory. - if (repo == null) { this.repositoryPromisesByPath.delete(pathForDirectory) } - __guardMethod__(repo, 'onDidDestroy', o => o.onDidDestroy(() => this.repositoryPromisesByPath.delete(pathForDirectory))) + if (repo == null) this.repositoryPromisesByPath.delete(pathForDirectory) + + if (repo && repo.onDidDestroy) { + repo.onDidDestroy(() => this.repositoryPromisesByPath.delete(pathForDirectory)) + } + return repo }) this.repositoryPromisesByPath.set(pathForDirectory, promise) @@ -555,7 +558,8 @@ class Project extends Model { // Is the buffer for the given path modified? isPathModified (filePath) { - return __guard__(this.findBufferForPath(this.resolvePath(filePath)), x => x.isModified()) + const bufferForPath = this.findBufferForPath(this.resolvePath(filePath)) + return bufferForPath && bufferForPath.isModified() } findBufferForPath (filePath) { @@ -708,14 +712,3 @@ class Project extends Model { }) } } - -function __guardMethod__ (obj, methodName, transform) { - if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { - return transform(obj, methodName) - } else { - return undefined - } -} -function __guard__ (value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined -} From b2571e8976b084e466f336c192fd0a2e4b5491e3 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:53:11 -0400 Subject: [PATCH 068/301] Avoid inline assignments --- src/project.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/project.js b/src/project.js index 144e3d7c4..f960cf2bc 100644 --- a/src/project.js +++ b/src/project.js @@ -409,8 +409,7 @@ class Project extends Model { // * `projectPath` {String} The path to remove. removePath (projectPath) { // The projectPath may be a URI, in which case it should not be normalized. - let needle - if ((needle = projectPath, !this.getPaths().includes(needle))) { + if (!this.getPaths().includes(projectPath)) { projectPath = this.defaultDirectoryProvider.normalizePath(projectPath) } From 48625584e4fb766e5d169887e671fa9657ac0c17 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:57:03 -0400 Subject: [PATCH 069/301] :art: Use shorter variations of null checks --- src/project.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/project.js b/src/project.js index f960cf2bc..123401b56 100644 --- a/src/project.js +++ b/src/project.js @@ -241,10 +241,11 @@ class Project extends Model { const pathForDirectory = directory.getRealPathSync() let promise = this.repositoryPromisesByPath.get(pathForDirectory) if (!promise) { - const promises = this.repositoryProviders.map(provider => provider.repositoryForDirectory(directory)) - promise = Promise.all(promises).then(repositories => { - let left - const repo = (left = _.find(repositories, repo => repo != null)) != null ? left : null + const promises = this.repositoryProviders.map((provider) => + provider.repositoryForDirectory(directory) + ) + promise = Promise.all(promises).then((repositories) => { + const repo = repositories.find((repo) => repo != null) || null // If no repository is found, remove the entry for the directory in // @repositoryPromisesByPath in case some other RepositoryProvider is @@ -447,7 +448,7 @@ class Project extends Model { resolvePath (uri) { if (!uri) { return } - if ((uri != null ? uri.match(/[A-Za-z0-9+-.]+:\/\//) : undefined)) { // leave path alone if it has a scheme + if (uri.match(/[A-Za-z0-9+-.]+:\/\//)) { // leave path alone if it has a scheme return uri } else { let projectPath @@ -481,7 +482,7 @@ class Project extends Model { if (fullPath != null) { for (let rootDirectory of this.rootDirectories) { const relativePath = rootDirectory.relativize(fullPath) - if ((relativePath != null ? relativePath.length : undefined) < result[1].length) { + if ((relativePath != null) && (relativePath.length < result[1].length)) { result = [rootDirectory.getPath(), relativePath] } } From 8df4cfbe58ee009ec532b27198a64e54f97d5f5e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 17:27:06 -0400 Subject: [PATCH 070/301] :art: --- src/project.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/project.js b/src/project.js index 123401b56..5de586c1d 100644 --- a/src/project.js +++ b/src/project.js @@ -1,11 +1,3 @@ -/* - * decaffeinate suggestions: - * DS104: Avoid inline assignments - * DS204: Change includes calls to have a more natural evaluation order - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const path = require('path') let _ = require('underscore-plus') @@ -82,7 +74,6 @@ class Project extends Model { */ deserialize (state) { - let bufferState this.retiredBufferIDs = new Set() this.retiredBufferPaths = new Set() @@ -103,13 +94,10 @@ class Project extends Model { }) } - const bufferPromises = ((() => { - const result = [] - for (bufferState of state.buffers) { - result.push(handleBufferState(bufferState)) - } - return result - })()) + const bufferPromises = [] + for (let bufferState of state.buffers) { + bufferPromises.push(handleBufferState(bufferState)) + } return Promise.all(bufferPromises).then(buffers => { this.buffers = buffers.filter(Boolean) From 790e2025490026fb3eb4128cf9c9c683e6f481bb Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 17:46:46 -0400 Subject: [PATCH 071/301] :art: --- src/project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/project.js b/src/project.js index 5de586c1d..5f401dbd2 100644 --- a/src/project.js +++ b/src/project.js @@ -1,6 +1,6 @@ const path = require('path') -let _ = require('underscore-plus') +const _ = require('underscore-plus') const fs = require('fs-plus') const {Emitter, Disposable} = require('event-kit') const TextBuffer = require('text-buffer') From 5937b95b4969c89689fbbfdec3f77e40a01b9eb9 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 17:43:58 -0400 Subject: [PATCH 072/301] Fix loop contructs that got borked by `decaffeinate` --- src/project.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/project.js b/src/project.js index 5f401dbd2..4f95cf851 100644 --- a/src/project.js +++ b/src/project.js @@ -43,7 +43,9 @@ class Project extends Model { for (let repository of this.repositories.slice()) { if (repository != null) repository.destroy() } - for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.dispose() } + for (let path in this.watcherPromisesByPath) { + this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) + } this.rootDirectories = [] this.repositories = [] } @@ -277,7 +279,9 @@ class Project extends Model { this.rootDirectories = [] this.repositories = [] - for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.then(w => w.dispose()) } + for (let path in this.watcherPromisesByPath) { + this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) + } this.watcherPromisesByPath = {} const missingProjectPaths = [] @@ -342,10 +346,9 @@ class Project extends Model { } }) - for (let watcherPromise = 0; watcherPromise < this.watcherPromisesByPath.length; watcherPromise++) { - const root = this.watcherPromisesByPath[watcherPromise] - if (!this.rootDirectories.includes(root)) { - watcherPromise.then(watcher => watcher.dispose()) + for (let path in this.watcherPromisesByPath) { + if (!this.rootDirectories.includes(path)) { + this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) } } From 5ec9d0f1347eda1aecea72ab00348a56eb3d0ec6 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 09:59:41 -0400 Subject: [PATCH 073/301] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug=20disposing=20?= =?UTF-8?q?watchers=20in=20Project::addPath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `this.rootDirectories` is an Array of Directory objects. `path` is a String. Therefore, `this.rootDirectories.includes(path)` will always evaluate to `false`. We instead need to look for an entry in `this.rootDirectories` where the Directory object's path is equal to the given path. --- src/project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/project.js b/src/project.js index 4f95cf851..b99a60e85 100644 --- a/src/project.js +++ b/src/project.js @@ -347,7 +347,7 @@ class Project extends Model { }) for (let path in this.watcherPromisesByPath) { - if (!this.rootDirectories.includes(path)) { + if (!this.rootDirectories.find(dir => dir.getPath() === path)) { this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) } } From a4ea46c57e078ae691d9aa9781948c39253ed4c9 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 16 Oct 2017 11:04:56 -0400 Subject: [PATCH 074/301] Rename local variable /xref: https://github.com/atom/atom/pull/15898#discussion_r144850427 --- src/project.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/project.js b/src/project.js index b99a60e85..48541c395 100644 --- a/src/project.js +++ b/src/project.js @@ -346,9 +346,9 @@ class Project extends Model { } }) - for (let path in this.watcherPromisesByPath) { - if (!this.rootDirectories.find(dir => dir.getPath() === path)) { - this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) + for (let watchedPath in this.watcherPromisesByPath) { + if (!this.rootDirectories.find(dir => dir.getPath() === watchedPath)) { + this.watcherPromisesByPath[watchedPath].then(watcher => { watcher.dispose() }) } } From 2ae8b5d46cef9b051bccebe47361e75889f1b873 Mon Sep 17 00:00:00 2001 From: Laura Murphy-Clarkin Date: Mon, 16 Oct 2017 19:27:06 +0100 Subject: [PATCH 075/301] Link to more accurate local dev instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Flight Manual contains more accurate instructions for local development on official Atom packages. I was caught out by this (as you can see in https://github.com/atom/bracket-matcher/issues/306) so I'm changing it for future contributors. 😊 I think it's better to just link to the Flight Manual rather than maintaining the instructions in two places. --- CONTRIBUTING.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6ee13d47..77c1889ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ Here's a list of the big ones: * [apm](https://github.com/atom/apm) - the `apm` command line tool (Atom Package Manager). You should use this repository for any contributions related to the `apm` tool and to publishing packages. * [atom.io](https://github.com/atom/atom.io) - the repository for feedback on the [Atom.io website](https://atom.io) and the [Atom.io package API](https://github.com/atom/atom/blob/master/docs/apm-rest-api.md) used by [apm](https://github.com/atom/apm). -There are many more, but this list should be a good starting point. For more information on how to work with Atom's official packages, see [Contributing to Atom Packages](http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/). +There are many more, but this list should be a good starting point. For more information on how to work with Atom's official packages, see [Contributing to Atom Packages][contributing-to-official-atom-packages]. Also, because Atom is so extensible, it's possible that a feature you've become accustomed to in Atom or an issue you're encountering isn't coming from a bundled package at all, but rather a [community package](https://atom.io/packages) you've installed. Each community package has its own repository too, the [Atom FAQ](https://discuss.atom.io/c/faq) has instructions on how to [contact the maintainers of any Atom community package or theme.](https://discuss.atom.io/t/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it/25581) @@ -199,16 +199,7 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F #### Local development -All packages can be developed locally, by checking out the corresponding repository and registering the package to Atom with `apm`: - -``` -$ git clone url-to-git-repository -$ cd path-to-package/ -$ apm link -d -$ atom -d . -``` - -By running Atom with the `-d` flag, you signal it to run with development packages installed. `apm link` makes sure that your local repository is loaded by Atom. +All packages can be developed locally. For instructions on how to do this, see [Contributing to Official Atom Packages][contributing-to-official-atom-packages] in the [Atom Flight Manual](http://flight-manual.atom.io). ### Pull Requests @@ -500,3 +491,4 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and [beginner]:https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc [help-wanted]:https://github.com/issues?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc+-label%3Abeginner +[contributing-to-official-atom-packages]:http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/ From 9b61114c0f8a6ab1afa2c12f339362692ed299f7 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Mon, 16 Oct 2017 21:05:41 +0200 Subject: [PATCH 076/301] Update contributing-to-packages.md --- docs/contributing-to-packages.md | 54 +------------------------------- 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/docs/contributing-to-packages.md b/docs/contributing-to-packages.md index 4576635ff..67933dc26 100644 --- a/docs/contributing-to-packages.md +++ b/docs/contributing-to-packages.md @@ -1,53 +1 @@ -# Contributing to Official Atom Packages - -If you think you know which package is causing the issue you are reporting, feel -free to open up the issue in that specific repository instead. When in doubt -just open the issue here but be aware that it may get closed here and reopened -in the proper package's repository. - -## Hacking on Packages - -### Cloning - -The first step is creating your own clone. - -For example, if you want to make changes to the `tree-view` package, fork the repo on your github account, then clone it: - -``` -> git clone git@github.com:your-username/tree-view.git -``` - -Next install all the dependencies: - -``` -> cd tree-view -> apm install -Installing modules ✓ -``` - -Now you can link it to development mode so when you run an Atom window with `atom --dev`, you will use your fork instead of the built in package: - -``` -> apm link -d -``` - -### Running in Development Mode - -Editing a package in Atom is a bit of a circular experience: you're using Atom -to modify itself. What happens if you temporarily break something? You don't -want the version of Atom you're using to edit to become useless in the process. -For this reason, you'll only want to load packages in **development mode** while -you are working on them. You'll perform your editing in **stable mode**, only -switching to development mode to test your changes. - -To open a development mode window, use the "Application: Open Dev" command. -You can also run dev mode from the command line with `atom --dev`. - -To load your package in development mode, create a symlink to it in -`~/.atom/dev/packages`. This occurs automatically when you clone the package -with `apm develop`. You can also run `apm link --dev` and `apm unlink --dev` -from the package directory to create and remove dev-mode symlinks. - -### Installing Dependencies - -You'll want to keep dependencies up to date by running `apm update` after pulling any upstream changes. +See http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/ From de66cf218ab30b64cc7df36db20475d6fd10a10d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 16 Oct 2017 17:53:45 -0700 Subject: [PATCH 077/301] :arrow_up: github@0.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8ce5537ed..d974a6dac 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", "fuzzy-finder": "1.6.1", - "github": "0.6.3", + "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.6", From 65af9e953be79ab23d4da34ca503046833b6244d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 17 Oct 2017 18:50:20 +0200 Subject: [PATCH 078/301] Stop dragging only when user interacts with keyboard Previously, we used to prevent the user from dragging the selection further when the buffer was about to change. This was problematic because any change in the buffer, even one that was performed "automatically" by a package, would cancel the dragging action and result in a confusing experience for the user. On the other hand, we want to prevent users from accidentally selecting text when they perform an edit (see #15217, #15405). This commit addresses both concerns by canceling the dragging as soon as the user interacts with the keyboard, instead of canceling the dragging when the buffer is about to change. One downside of this approach is that it changes the behavior of pressing a keystroke that does not result in a buffer change, e.g. Shift, Arrow Keys, etc. Signed-off-by: Jason Rudolph --- spec/text-editor-component-spec.js | 10 ++++++++-- src/text-editor-component.js | 13 +++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index dbfd170f6..41d770212 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4422,7 +4422,7 @@ describe('TextEditorComponent', () => { expect(dragEvents).toEqual([]) }) - it('calls `didStopDragging` if the buffer changes while dragging', async () => { + it('calls `didStopDragging` if the user interacts with the keyboard while dragging', async () => { const {component, editor} = buildComponent() let dragging = false @@ -4435,8 +4435,14 @@ describe('TextEditorComponent', () => { await getNextAnimationFramePromise() expect(dragging).toBe(true) - editor.delete() + // Buffer changes don't cause dragging to be stopped. + editor.insertText('X') + expect(dragging).toBe(true) + + // Keyboard interaction prevents users from dragging further. + component.didKeydown({code: 'KeyX'}) expect(dragging).toBe(false) + window.dispatchEvent(new MouseEvent('mousemove')) await getNextAnimationFramePromise() expect(dragging).toBe(false) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index a7bbd99d4..5ff96eec5 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1638,6 +1638,11 @@ class TextEditorComponent { // keypress, meaning we're *holding* the _same_ key we intially pressed. // Got that? didKeydown (event) { + // Stop dragging when user interacts with the keyboard. This prevents + // unwanted selections in the case edits are performed while selecting text + // at the same time. + if (this.stopDragging) this.stopDragging() + if (this.lastKeydownBeforeKeypress != null) { if (this.lastKeydownBeforeKeypress.code === event.code) { this.accentedCharacterMenuIsOpen = true @@ -1862,7 +1867,6 @@ class TextEditorComponent { handleMouseDragUntilMouseUp ({didDrag, didStopDragging}) { let dragging = false let lastMousemoveEvent - let bufferWillChangeDisposable const animationFrameLoop = () => { window.requestAnimationFrame(() => { @@ -1882,9 +1886,9 @@ class TextEditorComponent { } function didMouseUp () { + this.stopDragging = null window.removeEventListener('mousemove', didMouseMove) window.removeEventListener('mouseup', didMouseUp, {capture: true}) - bufferWillChangeDisposable.dispose() if (dragging) { dragging = false didStopDragging() @@ -1893,10 +1897,7 @@ class TextEditorComponent { window.addEventListener('mousemove', didMouseMove) window.addEventListener('mouseup', didMouseUp, {capture: true}) - // Simulate a mouse-up event if the buffer is about to change. This prevents - // unwanted selections when users perform edits while holding the left mouse - // button at the same time. - bufferWillChangeDisposable = this.props.model.getBuffer().onWillChange(didMouseUp) + this.stopDragging = didMouseUp } autoscrollOnMouseDrag ({clientX, clientY}, verticalOnly = false) { From 894f5471e6fbb8790bfedc49df5aad5ebc3aa3f9 Mon Sep 17 00:00:00 2001 From: anon Date: Wed, 18 Oct 2017 22:09:11 +0200 Subject: [PATCH 079/301] textChange(CoreSettings): remove redundancy --- src/config-schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config-schema.js b/src/config-schema.js index 00fb8bbe3..b2a286151 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -17,7 +17,7 @@ const configSchema = { type: 'boolean', default: true, title: 'Exclude VCS Ignored Paths', - description: 'Files and directories ignored by the current project\'s VCS system will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders.' + description: 'Files and directories ignored by the current project\'s VCS will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders.' }, followSymlinks: { type: 'boolean', From 641898ed2a4624173e56f6d66c695d506434b7a2 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 17 Oct 2017 21:55:02 +0200 Subject: [PATCH 080/301] :arrow_up: tree-view@0.219.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d974a6dac..b9acf0492 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.107.4", "timecop": "0.36.0", - "tree-view": "0.218.0", + "tree-view": "0.219.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.4", From 7aa79bc3a1ae2787fdb23fd70306545180ce2395 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 17 Oct 2017 12:59:21 -0700 Subject: [PATCH 081/301] Unregister package URL handlers when deactivating --- src/package.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/package.coffee b/src/package.coffee index 815d0b537..19df43b3c 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -521,6 +521,7 @@ class Package @activationCommandSubscriptions?.dispose() @activationHookSubscriptions?.dispose() @configSchemaRegisteredOnActivate = false + @unregisterUrlHandler() @deactivateResources() @deactivateKeymaps() From 2b70f57405f734ec08743216f18527ce57435787 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 17 Oct 2017 14:41:50 -0700 Subject: [PATCH 082/301] :art: return undefined --- src/protocol-handler-installer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js index eaedf0dea..b3acfe8f8 100644 --- a/src/protocol-handler-installer.js +++ b/src/protocol-handler-installer.js @@ -23,7 +23,7 @@ class ProtocolHandlerInstaller { initialize (config, notifications) { if (!this.isSupported()) { - return false + return } if (!this.isDefaultProtocolClient()) { From 2504118d8bc6dee9c2478c1b0b9512d0ef70f04c Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 17 Oct 2017 14:42:00 -0700 Subject: [PATCH 083/301] :keyboard: fix typo --- src/url-handler-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js index 608ce2810..1f64826f3 100644 --- a/src/url-handler-registry.js +++ b/src/url-handler-registry.js @@ -49,7 +49,7 @@ const {Emitter, Disposable} = require('event-kit') // } // ``` // -// `lib/my-package.json` +// `lib/my-package.js` // // ```javascript // module.exports = { From e02337265a1fa0f71490811892f17613e26227b9 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 17 Oct 2017 14:42:11 -0700 Subject: [PATCH 084/301] Reset history when destroying UrlHandlerRegistry --- src/url-handler-registry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/url-handler-registry.js b/src/url-handler-registry.js index 1f64826f3..f8d421833 100644 --- a/src/url-handler-registry.js +++ b/src/url-handler-registry.js @@ -123,6 +123,7 @@ class UrlHandlerRegistry { destroy () { this.emitter.dispose() this.registrations = new Map() + this.history = [] this._id = 0 } } From 5e43084cd3af022526229a1e0ba7fac5552bf34b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 17 Oct 2017 15:23:10 -0700 Subject: [PATCH 085/301] url -> URI --- .../index.js | 2 +- .../package-with-uri-handler/package.json | 6 ++ .../package-with-url-handler/package.json | 6 -- spec/main-process/parse-command-line.test.js | 6 +- spec/package-manager-spec.js | 16 ++--- ...y-spec.js => uri-handler-registry-spec.js} | 32 +++++----- src/application-delegate.coffee | 6 +- src/atom-environment.coffee | 16 ++--- src/main-process/atom-application.coffee | 4 +- src/main-process/atom-window.coffee | 4 +- src/main-process/parse-command-line.js | 10 ++-- src/package-manager.js | 4 +- src/package.coffee | 10 ++-- src/protocol-handler-installer.js | 4 +- ...er-registry.js => uri-handler-registry.js} | 58 +++++++++---------- 15 files changed, 92 insertions(+), 92 deletions(-) rename spec/fixtures/packages/{package-with-url-handler => package-with-uri-handler}/index.js (73%) create mode 100644 spec/fixtures/packages/package-with-uri-handler/package.json delete mode 100644 spec/fixtures/packages/package-with-url-handler/package.json rename spec/{url-handler-registry-spec.js => uri-handler-registry-spec.js} (67%) rename src/{url-handler-registry.js => uri-handler-registry.js} (67%) diff --git a/spec/fixtures/packages/package-with-url-handler/index.js b/spec/fixtures/packages/package-with-uri-handler/index.js similarity index 73% rename from spec/fixtures/packages/package-with-url-handler/index.js rename to spec/fixtures/packages/package-with-uri-handler/index.js index 3e6391be4..5d31dca98 100644 --- a/spec/fixtures/packages/package-with-url-handler/index.js +++ b/spec/fixtures/packages/package-with-uri-handler/index.js @@ -1,5 +1,5 @@ module.exports = { activate: () => null, deactivate: () => null, - handleUrl: () => null, + handleURI: () => null, } diff --git a/spec/fixtures/packages/package-with-uri-handler/package.json b/spec/fixtures/packages/package-with-uri-handler/package.json new file mode 100644 index 000000000..60160e36b --- /dev/null +++ b/spec/fixtures/packages/package-with-uri-handler/package.json @@ -0,0 +1,6 @@ +{ + "name": "package-with-uri-handler", + "uriHandler": { + "method": "handleURI" + } +} diff --git a/spec/fixtures/packages/package-with-url-handler/package.json b/spec/fixtures/packages/package-with-url-handler/package.json deleted file mode 100644 index 4ecbdb23b..000000000 --- a/spec/fixtures/packages/package-with-url-handler/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "package-with-url-handler", - "urlHandler": { - "method": "handleUrl" - } -} diff --git a/spec/main-process/parse-command-line.test.js b/spec/main-process/parse-command-line.test.js index b91ad866f..0cd1f5b13 100644 --- a/spec/main-process/parse-command-line.test.js +++ b/spec/main-process/parse-command-line.test.js @@ -3,7 +3,7 @@ import parseCommandLine from '../../src/main-process/parse-command-line' describe('parseCommandLine', function () { - describe('when --url-handler is not passed', function () { + describe('when --uri-handler is not passed', function () { it('parses arguments as normal', function () { const args = parseCommandLine(['-d', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url']) assert.isTrue(args.devMode) @@ -14,9 +14,9 @@ describe('parseCommandLine', function () { }) }) - describe('when --url-handler is passed', function () { + describe('when --uri-handler is passed', function () { it('ignores other arguments and limits to one URL', function () { - const args = parseCommandLine(['-d', '--url-handler', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url']) + const args = parseCommandLine(['-d', '--uri-handler', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url']) assert.isUndefined(args.devMode) assert.isUndefined(args.safeMode) assert.isUndefined(args.test) diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 2c88c4fbb..0b26bf839 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -1040,16 +1040,16 @@ describe('PackageManager', () => { }) - describe("URL handler registration", () => { - it("registers the package's specified URL handler", async () => { - const uri = 'atom://package-with-url-handler/some/url?with=args' - const mod = require('./fixtures/packages/package-with-url-handler') - spyOn(mod, 'handleUrl') + describe("URI handler registration", () => { + it("registers the package's specified URI handler", async () => { + const uri = 'atom://package-with-uri-handler/some/url?with=args' + const mod = require('./fixtures/packages/package-with-uri-handler') + spyOn(mod, 'handleURI') spyOn(atom.packages, 'hasLoadedInitialPackages').andReturn(true) - const activationPromise = atom.packages.activatePackage('package-with-url-handler') - atom.dispatchUrlMessage(uri) + const activationPromise = atom.packages.activatePackage('package-with-uri-handler') + atom.dispatchURIMessage(uri) await activationPromise - expect(mod.handleUrl).toHaveBeenCalledWith(url.parse(uri, true), uri) + expect(mod.handleURI).toHaveBeenCalledWith(url.parse(uri, true), uri) }) }) diff --git a/spec/url-handler-registry-spec.js b/spec/uri-handler-registry-spec.js similarity index 67% rename from spec/url-handler-registry-spec.js rename to spec/uri-handler-registry-spec.js index 3488a94fc..d2da93087 100644 --- a/spec/url-handler-registry-spec.js +++ b/spec/uri-handler-registry-spec.js @@ -4,30 +4,30 @@ import url from 'url' import {it} from './async-spec-helpers' -import UrlHandlerRegistry from '../src/url-handler-registry' +import URIHandlerRegistry from '../src/uri-handler-registry' -describe('UrlHandlerRegistry', () => { +describe('URIHandlerRegistry', () => { let registry beforeEach(() => { - registry = new UrlHandlerRegistry(5) + registry = new URIHandlerRegistry(5) }) - it('handles URLs on a per-host basis', () => { + it('handles URIs on a per-host basis', () => { const testPackageSpy = jasmine.createSpy() const otherPackageSpy = jasmine.createSpy() registry.registerHostHandler('test-package', testPackageSpy) registry.registerHostHandler('other-package', otherPackageSpy) - registry.handleUrl('atom://yet-another-package/path') + registry.handleURI('atom://yet-another-package/path') expect(testPackageSpy).not.toHaveBeenCalled() expect(otherPackageSpy).not.toHaveBeenCalled() - registry.handleUrl('atom://test-package/path') + registry.handleURI('atom://test-package/path') expect(testPackageSpy).toHaveBeenCalledWith(url.parse('atom://test-package/path', true), 'atom://test-package/path') expect(otherPackageSpy).not.toHaveBeenCalled() - registry.handleUrl('atom://other-package/path') + registry.handleURI('atom://other-package/path') expect(otherPackageSpy).toHaveBeenCalledWith(url.parse('atom://other-package/path', true), 'atom://other-package/path') }) @@ -39,7 +39,7 @@ describe('UrlHandlerRegistry', () => { registry.registerHostHandler('two', spy2) registry.onHistoryChange(changeSpy) - const urls = [ + const uris = [ 'atom://one/something?asdf=1', 'atom://fake/nothing', 'atom://two/other/stuff', @@ -47,19 +47,19 @@ describe('UrlHandlerRegistry', () => { 'atom://two/more/stuff' ] - urls.forEach(u => registry.handleUrl(u)) + uris.forEach(u => registry.handleURI(u)) expect(changeSpy.callCount).toBe(5) - expect(registry.getRecentlyHandledUrls()).toEqual(urls.map((u, idx) => { - return {id: idx + 1, url: u, handled: !u.match(/fake/), host: url.parse(u).host} + expect(registry.getRecentlyHandledURIs()).toEqual(uris.map((u, idx) => { + return {id: idx + 1, uri: u, handled: !u.match(/fake/), host: url.parse(u).host} }).reverse()) - registry.handleUrl('atom://another/url') + registry.handleURI('atom://another/url') expect(changeSpy.callCount).toBe(6) - const history = registry.getRecentlyHandledUrls() + const history = registry.getRecentlyHandledURIs() expect(history.length).toBe(5) - expect(history[0].url).toBe('atom://another/url') - expect(history[4].url).toBe(urls[1]) + expect(history[0].uri).toBe('atom://another/url') + expect(history[4].uri).toBe(uris[1]) }) it('refuses to handle bad URLs', () => { @@ -69,7 +69,7 @@ describe('UrlHandlerRegistry', () => { 'user:pass@atom://package/path', 'smth://package/path' ].forEach(uri => { - expect(() => registry.handleUrl(uri)).toThrow() + expect(() => registry.handleURI(uri)).toThrow() }) }) }) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 5efd62fe4..55c27eb61 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -233,13 +233,13 @@ class ApplicationDelegate new Disposable -> ipcRenderer.removeListener('context-command', outerCallback) - onURLMessage: (callback) -> + onURIMessage: (callback) -> outerCallback = (event, args...) -> callback(args...) - ipcRenderer.on('url-message', outerCallback) + ipcRenderer.on('uri-message', outerCallback) new Disposable -> - ipcRenderer.removeListener('url-message', outerCallback) + ipcRenderer.removeListener('uri-message', outerCallback) onDidRequestUnload: (callback) -> outerCallback = (event, message) -> diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7bbbdd78e..a32c4424b 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -22,7 +22,7 @@ Config = require './config' KeymapManager = require './keymap-extensions' TooltipManager = require './tooltip-manager' CommandRegistry = require './command-registry' -UrlHandlerRegistry = require './url-handler-registry' +URIHandlerRegistry = require './uri-handler-registry' GrammarRegistry = require './grammar-registry' {HistoryManager, HistoryProject} = require './history-manager' ReopenProjectMenuManager = require './reopen-project-menu-manager' @@ -149,14 +149,14 @@ class AtomEnvironment extends Model @keymaps = new KeymapManager({notificationManager: @notifications}) @tooltips = new TooltipManager(keymapManager: @keymaps, viewRegistry: @views) @commands = new CommandRegistry - @urlHandlerRegistry = new UrlHandlerRegistry + @uriHandlerRegistry = new URIHandlerRegistry @grammars = new GrammarRegistry({@config}) @styles = new StyleManager() @packages = new PackageManager({ @config, styleManager: @styles, commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications, grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views, - urlHandlerRegistry: @urlHandlerRegistry + uriHandlerRegistry: @uriHandlerRegistry }) @themes = new ThemeManager({ packageManager: @packages, @config, styleManager: @styles, @@ -356,7 +356,7 @@ class AtomEnvironment extends Model @stylesElement.remove() @config.unobserveUserConfig() @autoUpdater.destroy() - @urlHandlerRegistry.destroy() + @uriHandlerRegistry.destroy() @uninstallWindowEventHandler() @@ -697,7 +697,7 @@ class AtomEnvironment extends Model @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this))) @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this))) @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this))) - @disposables.add(@applicationDelegate.onURLMessage(@dispatchUrlMessage.bind(this))) + @disposables.add(@applicationDelegate.onURIMessage(@dispatchURIMessage.bind(this))) @disposables.add @applicationDelegate.onDidRequestUnload => @saveState({isUnloading: true}) .catch(console.error) @@ -1100,13 +1100,13 @@ class AtomEnvironment extends Model dispatchContextMenuCommand: (command, args...) -> @commands.dispatch(@contextMenu.activeElement, command, args) - dispatchUrlMessage: (uri) -> + dispatchURIMessage: (uri) -> if @packages.hasLoadedInitialPackages() - @urlHandlerRegistry.handleUrl(uri) + @uriHandlerRegistry.handleURI(uri) else sub = @packages.onDidLoadInitialPackages -> sub.dispose() - @urlHandlerRegistry.handleUrl(uri) + @uriHandlerRegistry.handleURI(uri) openLocations: (locations) -> needsProjectPaths = @project?.getPaths().length is 0 diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index ebde3b40a..2b3a58923 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -666,12 +666,12 @@ class AtomApplication windowInitializationScript ?= require.resolve('../initialize-application-window') if @lastFocusedWindow? - @lastFocusedWindow.sendUrlMessage url + @lastFocusedWindow.sendURIMessage url else windowDimensions = @getDimensionsForNewWindow() @lastFocusedWindow = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) @lastFocusedWindow.on 'window:loaded', => - @lastFocusedWindow.sendUrlMessage url + @lastFocusedWindow.sendURIMessage url findPackageWithName: (packageName, devMode) -> _.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 574cd22b0..ca3995c05 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -232,8 +232,8 @@ class AtomWindow unless @atomApplication.sendCommandToFirstResponder(command) @sendCommandToBrowserWindow(command, args...) - sendUrlMessage: (url) -> - @browserWindow.webContents.send 'url-message', url + sendURIMessage: (uri) -> + @browserWindow.webContents.send 'uri-message', uri sendCommandToBrowserWindow: (command, args...) -> action = if args[0]?.contextCommand then 'context-command' else 'command' diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 67d238883..3b0654962 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -58,15 +58,15 @@ module.exports = function parseCommandLine (processArgs) { options.string('user-data-dir') options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.') options.boolean('enable-electron-logging').describe('enable-electron-logging', 'Enable low-level logging messages from Electron.') - options.boolean('url-handler') + options.boolean('uri-handler') let args = options.argv - // If --url-handler is set, then we parse NOTHING else - if (args.urlHandler) { + // If --uri-handler is set, then we parse NOTHING else + if (args.uriHandler) { args = { - urlHandler: true, - 'url-handler': true, + uriHandler: true, + 'uri-handler': true, _: args._.filter(str => str.startsWith('atom://')).slice(0, 1) } } diff --git a/src/package-manager.js b/src/package-manager.js index d9984e40c..80c089c33 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -32,7 +32,7 @@ module.exports = class PackageManager { ({ config: this.config, styleManager: this.styleManager, notificationManager: this.notificationManager, keymapManager: this.keymapManager, commandRegistry: this.commandRegistry, grammarRegistry: this.grammarRegistry, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry, - urlHandlerRegistry: this.urlHandlerRegistry + uriHandlerRegistry: this.uriHandlerRegistry } = params) this.emitter = new Emitter() @@ -649,7 +649,7 @@ module.exports = class PackageManager { } registerUrlHandlerForPackage (packageName, handler) { - return this.urlHandlerRegistry.registerHostHandler(packageName, handler) + return this.uriHandlerRegistry.registerHostHandler(packageName, handler) } // another type of package manager can handle other package types. diff --git a/src/package.coffee b/src/package.coffee index 19df43b3c..a7168b30c 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -323,13 +323,13 @@ class Package registerUrlHandler: -> handlerConfig = @getUrlHandler() if methodName = handlerConfig?.method - @urlHandlerSubscription = @packageManager.registerUrlHandlerForPackage @name, (args...) => - @handleUrl(methodName, args) + @uriHandlerSubscription = @packageManager.registerUrlHandlerForPackage @name, (args...) => + @handleURI(methodName, args) unregisterUrlHandler: -> - @urlHandlerSubscription?.dispose() + @uriHandlerSubscription?.dispose() - handleUrl: (methodName, args) -> + handleURI: (methodName, args) -> @activate().then => @mainModule[methodName]?.apply(@mainModule, args) unless @mainActivated @@ -695,7 +695,7 @@ class Package @activationHooks = _.uniq(@activationHooks) getUrlHandler: -> - @metadata?.urlHandler + @metadata?.uriHandler # Does the given module path contain native code? isNativeModule: (modulePath) -> diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js index b3acfe8f8..0a55bff41 100644 --- a/src/protocol-handler-installer.js +++ b/src/protocol-handler-installer.js @@ -12,13 +12,13 @@ class ProtocolHandlerInstaller { } isDefaultProtocolClient () { - return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--url-handler']) + return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--uri-handler']) } setAsDefaultProtocolClient () { // This Electron API is only available on Windows and macOS. There might be some // hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440 - return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--url-handler']) + return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--uri-handler']) } initialize (config, notifications) { diff --git a/src/url-handler-registry.js b/src/uri-handler-registry.js similarity index 67% rename from src/url-handler-registry.js rename to src/uri-handler-registry.js index f8d421833..297f916eb 100644 --- a/src/url-handler-registry.js +++ b/src/uri-handler-registry.js @@ -1,41 +1,41 @@ const url = require('url') const {Emitter, Disposable} = require('event-kit') -// Private: Associates listener functions with URLs from outside the application. +// Private: Associates listener functions with URIs from outside the application. // -// The global URL handler registry maps URLs to listener functions. URLs are mapped -// based on the hostname of the URL; the format is atom://package/command?args. -// The "core" package name is reserved for URLs handled by Atom core (it is not possible +// The global URI handler registry maps URIs to listener functions. URIs are mapped +// based on the hostname of the URI; the format is atom://package/command?args. +// The "core" package name is reserved for URIs handled by Atom core (it is not possible // to register a package with the name "core"). // -// Because URL handling can be triggered from outside the application (e.g. from +// Because URI handling can be triggered from outside the application (e.g. from // the user's browser), package authors should take great care to ensure that malicious // activities cannot be performed by an attacker. A good rule to follow is that -// **URL handlers should not take action on behalf of the user**. For example, clicking +// **URI handlers should not take action on behalf of the user**. For example, clicking // a link to open a pane item that prompts the user to install a package is okay; // automatically installing the package right away is not. // -// Packages can register their desire to handle URLs via a special key in their -// `package.json` called "urlHandler". The value of this key should be an object +// Packages can register their desire to handle URIs via a special key in their +// `package.json` called "uriHandler". The value of this key should be an object // that contains, at minimum, a key named "method". This is the name of the method -// on your package object that Atom will call when it receives a URL your package -// is responsible for handling. It will pass the parsed URL as the first argument (by using +// on your package object that Atom will call when it receives a URI your package +// is responsible for handling. It will pass the parsed URI as the first argument (by using // [Node's `url.parse(uri, true)`](https://nodejs.org/docs/latest/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost)) -// and the raw URL as the second argument. +// and the raw URI string as the second argument. // -// By default, Atom will defer activation of your package until a URL it needs to handle +// By default, Atom will defer activation of your package until a URI it needs to handle // is triggered. If you need your package to activate right away, you can add -// `"deferActivation": false` to your "urlHandler" configuration object. When activation -// is deferred, once Atom receives a request for a URL in your package's namespace, it will +// `"deferActivation": false` to your "uriHandler" configuration object. When activation +// is deferred, once Atom receives a request for a URI in your package's namespace, it will // activate your pacakge and then call `methodName` on it as before. // -// If your package specifies a deprecated `urlMain` property, you cannot register URL handlers -// via the `urlHandler` key. +// If your package specifies a deprecated `urlMain` property, you cannot register URI handlers +// via the `uriHandler` key. // // ## Example // -// Here is a sample package that will be activated and have its `handleUrl` method called -// when a URL beginning with `atom://my-package` is triggered: +// Here is a sample package that will be activated and have its `handleURI` method called +// when a URI beginning with `atom://my-package` is triggered: // // `package.json`: // @@ -43,8 +43,8 @@ const {Emitter, Disposable} = require('event-kit') // { // "name": "my-package", // "main": "./lib/my-package.js", -// "urlHandler": { -// "method": "handleUrl" +// "uriHandler": { +// "method": "handleURI" // } // } // ``` @@ -57,13 +57,13 @@ const {Emitter, Disposable} = require('event-kit') // // code to activate your package // } // -// handleUrl(url) { -// // parse and handle url +// handleURI(parsedUri, rawUri) { +// // parse and handle uri // } // } // ``` module.exports = -class UrlHandlerRegistry { +class URIHandlerRegistry { constructor (maxHistoryLength = 50) { this.registrations = new Map() this.history = [] @@ -75,11 +75,11 @@ class UrlHandlerRegistry { registerHostHandler (host, callback) { if (typeof callback !== 'function') { - throw new Error('Cannot register a URL host handler with a non-function callback') + throw new Error('Cannot register a URI host handler with a non-function callback') } if (this.registrations.has(host)) { - throw new Error(`There is already a URL host handler for the host ${host}`) + throw new Error(`There is already a URI host handler for the host ${host}`) } else { this.registrations.set(host, callback) } @@ -89,15 +89,15 @@ class UrlHandlerRegistry { }) } - handleUrl (uri) { + handleURI (uri) { const parsed = url.parse(uri, true) const {protocol, slashes, auth, port, host} = parsed if (protocol !== 'atom:' || slashes !== true || auth || port) { - throw new Error(`UrlHandlerRegistry#handleUrl asked to handle an invalid URL: ${uri}`) + throw new Error(`URIHandlerRegistry#handleURI asked to handle an invalid URI: ${uri}`) } const registration = this.registrations.get(host) - const historyEntry = {id: ++this._id, url: uri, handled: false, host} + const historyEntry = {id: ++this._id, uri: uri, handled: false, host} try { if (registration) { historyEntry.handled = true @@ -112,7 +112,7 @@ class UrlHandlerRegistry { } } - getRecentlyHandledUrls () { + getRecentlyHandledURIs () { return this.history } From 9b5f95a14d02fd2b84e06183febd7f0c7f470ec5 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 17 Oct 2017 15:26:29 -0700 Subject: [PATCH 086/301] openWithAtomUrl -> openPackageUriHandler --- src/main-process/atom-application.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 2b3a58923..0c587020e 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -655,9 +655,9 @@ class AtomApplication if pack?.urlMain @openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env) else - @openWithAtomUrl(urlToOpen, devMode, safeMode, env) + @openPackageUriHandler(urlToOpen, devMode, safeMode, env) - openWithAtomUrl: (url, devMode, safeMode, env) -> + openPackageUriHandler: (url, devMode, safeMode, env) -> resourcePath = @resourcePath if devMode try From 8b989ffc4e812a82b9e5c7c90332d220bae0d955 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 17 Oct 2017 15:29:27 -0700 Subject: [PATCH 087/301] More url -> URI --- src/package-manager.js | 2 +- src/package.coffee | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/package-manager.js b/src/package-manager.js index 80c089c33..17a5f2214 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -648,7 +648,7 @@ module.exports = class PackageManager { }) } - registerUrlHandlerForPackage (packageName, handler) { + registerURIHandlerForPackage (packageName, handler) { return this.uriHandlerRegistry.registerHostHandler(packageName, handler) } diff --git a/src/package.coffee b/src/package.coffee index a7168b30c..1d1529f23 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -84,7 +84,7 @@ class Package @loadMenus() @registerDeserializerMethods() @activateCoreStartupServices() - @registerUrlHandler() + @registerURIHandler() @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @requireMainModule() @settingsPromise = @loadSettings() @@ -115,7 +115,7 @@ class Package @loadStylesheets() @registerDeserializerMethods() @activateCoreStartupServices() - @registerUrlHandler() + @registerURIHandler() @registerTranspilerConfig() @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @settingsPromise = @loadSettings() @@ -320,13 +320,13 @@ class Package @activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) return - registerUrlHandler: -> - handlerConfig = @getUrlHandler() + registerURIHandler: -> + handlerConfig = @getURIHandler() if methodName = handlerConfig?.method - @uriHandlerSubscription = @packageManager.registerUrlHandlerForPackage @name, (args...) => + @uriHandlerSubscription = @packageManager.registerURIHandlerForPackage @name, (args...) => @handleURI(methodName, args) - unregisterUrlHandler: -> + unregisterURIHandler: -> @uriHandlerSubscription?.dispose() handleURI: (methodName, args) -> @@ -521,7 +521,7 @@ class Package @activationCommandSubscriptions?.dispose() @activationHookSubscriptions?.dispose() @configSchemaRegisteredOnActivate = false - @unregisterUrlHandler() + @unregisterURIHandler() @deactivateResources() @deactivateKeymaps() @@ -613,7 +613,7 @@ class Package @mainModulePath = fs.resolveExtension(mainModulePath, ["", CompileCache.supportedExtensions...]) activationShouldBeDeferred: -> - @hasActivationCommands() or @hasActivationHooks() or @hasDeferredUrlHandler() + @hasActivationCommands() or @hasActivationHooks() or @hasDeferredURIHandler() hasActivationHooks: -> @getActivationHooks()?.length > 0 @@ -623,8 +623,8 @@ class Package return true if commands.length > 0 false - hasDeferredUrlHandler: -> - @getUrlHandler() and @getUrlHandler().deferActivation isnt false + hasDeferredURIHandler: -> + @getURIHandler() and @getURIHandler().deferActivation isnt false subscribeToDeferredActivation: -> @subscribeToActivationCommands() @@ -694,7 +694,7 @@ class Package @activationHooks = _.uniq(@activationHooks) - getUrlHandler: -> + getURIHandler: -> @metadata?.uriHandler # Does the given module path contain native code? From 5bbf0b6ade7816a0ce8f3d008a224a1a331a8bd8 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 17 Oct 2017 15:37:49 -0700 Subject: [PATCH 088/301] :art: package activation --- src/package.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/package.coffee b/src/package.coffee index 1d1529f23..1635c75dc 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -330,10 +330,8 @@ class Package @uriHandlerSubscription?.dispose() handleURI: (methodName, args) -> - @activate().then => - @mainModule[methodName]?.apply(@mainModule, args) - unless @mainActivated - @activateNow() + @activate().then => @mainModule[methodName]?.apply(@mainModule, args) + @activateNow() unless @mainActivated registerTranspilerConfig: -> if @metadata.atomTranspilers From 85c9f2291d5ea97f78e8c6445a909b9efbe96890 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 18 Oct 2017 15:16:11 -0700 Subject: [PATCH 089/301] :arrow_up: settings-view@0.252.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9acf0492..dc454e3ab 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.10", + "settings-view": "0.252.0", "snippets": "1.1.5", "spell-check": "0.72.3", "status-bar": "1.8.13", From a11efa7376a564245c1cf51b955ebfa245d8c71d Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 18 Oct 2017 16:00:37 -0700 Subject: [PATCH 090/301] Add core URI handlers --- src/atom-environment.coffee | 2 ++ src/core-uri-handlers.js | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/core-uri-handlers.js diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index a32c4424b..7bbc513d6 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -32,6 +32,7 @@ ThemeManager = require './theme-manager' MenuManager = require './menu-manager' ContextMenuManager = require './context-menu-manager' CommandInstaller = require './command-installer' +CoreURIHandlers = require './core-uri-handlers' ProtocolHandlerInstaller = require './protocol-handler-installer' Project = require './project' TitleBar = require './title-bar' @@ -238,6 +239,7 @@ class AtomEnvironment extends Model @commandInstaller.initialize(@getVersion()) @protocolHandlerInstaller.initialize(@config, @notifications) + @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(@)) @autoUpdater.initialize() @config.load() diff --git a/src/core-uri-handlers.js b/src/core-uri-handlers.js new file mode 100644 index 000000000..c575b3f40 --- /dev/null +++ b/src/core-uri-handlers.js @@ -0,0 +1,24 @@ +function openFile (atom, {query}) { + const {filename, line, column} = query + + atom.workspace.open(filename, { + initialLine: parseInt(line || 0, 10), + initialColumn: parseInt(column || 0, 10), + searchAllPanes: true + }) +} + +const ROUTER = { + '/open/file': openFile +} + +module.exports = { + create (atomEnv) { + return function coreURIHandler (parsed) { + const handler = ROUTER[parsed.pathname] + if (handler) { + handler(atomEnv, parsed) + } + } + } +} From c632e6ca58f6c965956d1e75e9bc0c78134a8e6e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:22:02 -0400 Subject: [PATCH 091/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/project-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply results of running: ``` $ decaffeinate --keep-commonjs --prefer-const --loose-default-params --loose-for-expressions --loose-for-of --loose-includes spec/project-spec.js $ standard --fix spec/project-spec.js ``` --- spec/project-spec.coffee | 802 --------------------------------- spec/project-spec.js | 935 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 935 insertions(+), 802 deletions(-) delete mode 100644 spec/project-spec.coffee create mode 100644 spec/project-spec.js diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee deleted file mode 100644 index 1f5eb54a4..000000000 --- a/spec/project-spec.coffee +++ /dev/null @@ -1,802 +0,0 @@ -temp = require('temp').track() -TextBuffer = require('text-buffer') -Project = require '../src/project' -fs = require 'fs-plus' -path = require 'path' -{Directory} = require 'pathwatcher' -{stopAllWatchers} = require '../src/path-watcher' -GitRepository = require '../src/git-repository' - -describe "Project", -> - beforeEach -> - atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) - - # Wait for project's service consumers to be asynchronously added - waits(1) - - describe "serialization", -> - deserializedProject = null - notQuittingProject = null - quittingProject = null - - afterEach -> - deserializedProject?.destroy() - notQuittingProject?.destroy() - quittingProject?.destroy() - - it "does not deserialize paths to directories that don't exist", -> - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - state = atom.project.serialize() - state.paths.push('/directory/that/does/not/exist') - - err = null - waitsForPromise -> - deserializedProject.deserialize(state, atom.deserializers) - .catch (e) -> err = e - - runs -> - expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) - expect(err.missingProjectPaths).toEqual ['/directory/that/does/not/exist'] - - it "does not deserialize paths that are now files", -> - childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child') - fs.mkdirSync(childPath) - - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - atom.project.setPaths([childPath]) - state = atom.project.serialize() - - fs.rmdirSync(childPath) - fs.writeFileSync(childPath, 'surprise!\n') - - err = null - waitsForPromise -> - deserializedProject.deserialize(state, atom.deserializers) - .catch (e) -> err = e - - runs -> - expect(deserializedProject.getPaths()).toEqual([]) - expect(err.missingProjectPaths).toEqual [childPath] - - it "does not include unretained buffers in the serialized state", -> - waitsForPromise -> - atom.project.bufferForPath('a') - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 0 - - it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", -> - waitsForPromise -> - atom.workspace.open('a') - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 1 - deserializedProject.getBuffers()[0].destroy() - expect(deserializedProject.getBuffers().length).toBe 0 - - it "does not deserialize buffers when their path is now a directory", -> - pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') - - waitsForPromise -> - atom.workspace.open(pathToOpen) - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - fs.mkdirSync(pathToOpen) - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 0 - - it "does not deserialize buffers when their path is inaccessible", -> - return if process.platform is 'win32' # chmod not supported on win32 - pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') - fs.writeFileSync(pathToOpen, '') - - waitsForPromise -> - atom.workspace.open(pathToOpen) - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - fs.chmodSync(pathToOpen, '000') - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 0 - - it "does not deserialize buffers with their path is no longer present", -> - pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') - fs.writeFileSync(pathToOpen, '') - - waitsForPromise -> - atom.workspace.open(pathToOpen) - - runs -> - expect(atom.project.getBuffers().length).toBe 1 - fs.unlinkSync(pathToOpen) - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 0 - - it "deserializes buffers that have never been saved before", -> - pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') - - waitsForPromise -> - atom.workspace.open(pathToOpen) - - runs -> - atom.workspace.getActiveTextEditor().setText('unsaved\n') - expect(atom.project.getBuffers().length).toBe 1 - - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(deserializedProject.getBuffers().length).toBe 1 - expect(deserializedProject.getBuffers()[0].getPath()).toBe pathToOpen - expect(deserializedProject.getBuffers()[0].getText()).toBe 'unsaved\n' - - it "serializes marker layers and history only if Atom is quitting", -> - waitsForPromise -> atom.workspace.open('a') - - bufferA = null - layerA = null - markerA = null - - runs -> - bufferA = atom.project.getBuffers()[0] - layerA = bufferA.addMarkerLayer(persistent: true) - markerA = layerA.markPosition([0, 3]) - bufferA.append('!') - notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})) - - runs -> - expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) - quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - - waitsForPromise -> quittingProject.deserialize(atom.project.serialize({isUnloading: true})) - - runs -> - expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined() - expect(quittingProject.getBuffers()[0].undo()).toBe(true) - - describe "when an editor is saved and the project has no path", -> - it "sets the project's path to the saved file's parent directory", -> - tempFile = temp.openSync().path - atom.project.setPaths([]) - expect(atom.project.getPaths()[0]).toBeUndefined() - editor = null - - waitsForPromise -> - atom.workspace.open().then (o) -> editor = o - - waitsForPromise -> - editor.saveAs(tempFile) - - runs -> - expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile) - - describe "before and after saving a buffer", -> - [buffer] = [] - beforeEach -> - waitsForPromise -> - atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then (o) -> - buffer = o - buffer.retain() - - afterEach -> - buffer.release() - - it "emits save events on the main process", -> - spyOn(atom.project.applicationDelegate, 'emitDidSavePath') - spyOn(atom.project.applicationDelegate, 'emitWillSavePath') - - waitsForPromise -> buffer.save() - - runs -> - expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1) - expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath()) - expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1) - expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath()) - - describe "when a watch error is thrown from the TextBuffer", -> - editor = null - beforeEach -> - waitsForPromise -> - atom.workspace.open(require.resolve('./fixtures/dir/a')).then (o) -> editor = o - - it "creates a warning notification", -> - atom.notifications.onDidAddNotification noteSpy = jasmine.createSpy() - - error = new Error('SomeError') - error.eventType = 'resurrect' - editor.buffer.emitter.emit 'will-throw-watch-error', - handle: jasmine.createSpy() - error: error - - expect(noteSpy).toHaveBeenCalled() - - notification = noteSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getDetail()).toBe 'SomeError' - expect(notification.getMessage()).toContain '`resurrect`' - expect(notification.getMessage()).toContain path.join('fixtures', 'dir', 'a') - - describe "when a custom repository-provider service is provided", -> - [fakeRepositoryProvider, fakeRepository] = [] - - beforeEach -> - fakeRepository = {destroy: -> null} - fakeRepositoryProvider = { - repositoryForDirectory: (directory) -> Promise.resolve(fakeRepository) - repositoryForDirectorySync: (directory) -> fakeRepository - } - - it "uses it to create repositories for any directories that need one", -> - projectPath = temp.mkdirSync('atom-project') - atom.project.setPaths([projectPath]) - expect(atom.project.getRepositories()).toEqual [null] - - atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) - waitsFor -> atom.project.repositoryProviders.length > 1 - runs -> atom.project.getRepositories()[0] is fakeRepository - - it "does not create any new repositories if every directory has a repository", -> - repositories = atom.project.getRepositories() - expect(repositories.length).toEqual 1 - expect(repositories[0]).toBeTruthy() - - atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) - waitsFor -> atom.project.repositoryProviders.length > 1 - runs -> expect(atom.project.getRepositories()).toBe repositories - - it "stops using it to create repositories when the service is removed", -> - atom.project.setPaths([]) - - disposable = atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider) - waitsFor -> atom.project.repositoryProviders.length > 1 - runs -> - disposable.dispose() - atom.project.addPath(temp.mkdirSync('atom-project')) - expect(atom.project.getRepositories()).toEqual [null] - - describe "when a custom directory-provider service is provided", -> - class DummyDirectory - constructor: (@path) -> - getPath: -> @path - getFile: -> {existsSync: -> false} - getSubdirectory: -> {existsSync: -> false} - isRoot: -> true - existsSync: -> @path.endsWith('does-exist') - contains: (filePath) -> filePath.startsWith(@path) - - serviceDisposable = null - - beforeEach -> - serviceDisposable = atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", { - directoryForURISync: (uri) -> - if uri.startsWith("ssh://") - new DummyDirectory(uri) - else - null - }) - - waitsFor -> - atom.project.directoryProviders.length > 0 - - it "uses the provider's custom directories for any paths that it handles", -> - localPath = temp.mkdirSync('local-path') - remotePath = "ssh://foreign-directory:8080/does-exist" - - atom.project.setPaths([localPath, remotePath]) - - directories = atom.project.getDirectories() - expect(directories[0].getPath()).toBe localPath - expect(directories[0] instanceof Directory).toBe true - expect(directories[1].getPath()).toBe remotePath - expect(directories[1] instanceof DummyDirectory).toBe true - - # It does not add new remote paths that do not exist - nonExistentRemotePath = "ssh://another-directory:8080/does-not-exist" - atom.project.addPath(nonExistentRemotePath) - expect(atom.project.getDirectories().length).toBe 2 - - # It adds new remote paths if their directories exist. - newRemotePath = "ssh://another-directory:8080/does-exist" - atom.project.addPath(newRemotePath) - directories = atom.project.getDirectories() - expect(directories[2].getPath()).toBe newRemotePath - expect(directories[2] instanceof DummyDirectory).toBe true - - it "stops using the provider when the service is removed", -> - serviceDisposable.dispose() - atom.project.setPaths(["ssh://foreign-directory:8080/does-exist"]) - expect(atom.project.getDirectories().length).toBe(0) - - describe ".open(path)", -> - [absolutePath, newBufferHandler] = [] - - beforeEach -> - absolutePath = require.resolve('./fixtures/dir/a') - newBufferHandler = jasmine.createSpy('newBufferHandler') - atom.project.onDidAddBuffer(newBufferHandler) - - describe "when given an absolute path that isn't currently open", -> - it "returns a new edit session for the given path and emits 'buffer-created'", -> - editor = null - waitsForPromise -> - atom.workspace.open(absolutePath).then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editor.buffer - - describe "when given a relative path that isn't currently opened", -> - it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", -> - editor = null - waitsForPromise -> - atom.workspace.open(absolutePath).then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBe absolutePath - expect(newBufferHandler).toHaveBeenCalledWith editor.buffer - - describe "when passed the path to a buffer that is currently opened", -> - it "returns a new edit session containing currently opened buffer", -> - editor = null - - waitsForPromise -> - atom.workspace.open(absolutePath).then (o) -> editor = o - - runs -> - newBufferHandler.reset() - - waitsForPromise -> - atom.workspace.open(absolutePath).then ({buffer}) -> - expect(buffer).toBe editor.buffer - - waitsForPromise -> - atom.workspace.open('a').then ({buffer}) -> - expect(buffer).toBe editor.buffer - expect(newBufferHandler).not.toHaveBeenCalled() - - describe "when not passed a path", -> - it "returns a new edit session and emits 'buffer-created'", -> - editor = null - waitsForPromise -> - atom.workspace.open().then (o) -> editor = o - - runs -> - expect(editor.buffer.getPath()).toBeUndefined() - expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) - - describe ".bufferForPath(path)", -> - buffer = null - - beforeEach -> - waitsForPromise -> - atom.project.bufferForPath("a").then (o) -> - buffer = o - buffer.retain() - - afterEach -> - buffer.release() - - describe "when opening a previously opened path", -> - it "does not create a new buffer", -> - waitsForPromise -> - atom.project.bufferForPath("a").then (anotherBuffer) -> - expect(anotherBuffer).toBe buffer - - waitsForPromise -> - atom.project.bufferForPath("b").then (anotherBuffer) -> - expect(anotherBuffer).not.toBe buffer - - waitsForPromise -> - Promise.all([ - atom.project.bufferForPath('c'), - atom.project.bufferForPath('c') - ]).then ([buffer1, buffer2]) -> - expect(buffer1).toBe(buffer2) - - it "retries loading the buffer if it previously failed", -> - waitsForPromise shouldReject: true, -> - spyOn(TextBuffer, 'load').andCallFake -> - Promise.reject(new Error('Could not open file')) - atom.project.bufferForPath('b') - - waitsForPromise shouldReject: false, -> - TextBuffer.load.andCallThrough() - atom.project.bufferForPath('b') - - it "creates a new buffer if the previous buffer was destroyed", -> - buffer.release() - - waitsForPromise -> - atom.project.bufferForPath("b").then (anotherBuffer) -> - expect(anotherBuffer).not.toBe buffer - - describe ".repositoryForDirectory(directory)", -> - it "resolves to null when the directory does not have a repository", -> - waitsForPromise -> - directory = new Directory("/tmp") - atom.project.repositoryForDirectory(directory).then (result) -> - expect(result).toBeNull() - expect(atom.project.repositoryProviders.length).toBeGreaterThan 0 - expect(atom.project.repositoryPromisesByPath.size).toBe 0 - - it "resolves to a GitRepository and is cached when the given directory is a Git repo", -> - waitsForPromise -> - directory = new Directory(path.join(__dirname, '..')) - promise = atom.project.repositoryForDirectory(directory) - promise.then (result) -> - expect(result).toBeInstanceOf GitRepository - dirPath = directory.getRealPathSync() - expect(result.getPath()).toBe path.join(dirPath, '.git') - - # Verify that the result is cached. - expect(atom.project.repositoryForDirectory(directory)).toBe(promise) - - it "creates a new repository if a previous one with the same directory had been destroyed", -> - repository = null - directory = new Directory(path.join(__dirname, '..')) - - waitsForPromise -> - atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo - - runs -> - expect(repository.isDestroyed()).toBe(false) - repository.destroy() - expect(repository.isDestroyed()).toBe(true) - - waitsForPromise -> - atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo - - runs -> - expect(repository.isDestroyed()).toBe(false) - - describe ".setPaths(paths, options)", -> - describe "when path is a file", -> - it "sets its path to the file's parent directory and updates the root directory", -> - filePath = require.resolve('./fixtures/dir/a') - atom.project.setPaths([filePath]) - expect(atom.project.getPaths()[0]).toEqual path.dirname(filePath) - expect(atom.project.getDirectories()[0].path).toEqual path.dirname(filePath) - - describe "when path is a directory", -> - it "assigns the directories and repositories", -> - directory1 = temp.mkdirSync("non-git-repo") - directory2 = temp.mkdirSync("git-repo1") - directory3 = temp.mkdirSync("git-repo2") - - gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')) - fs.copySync(gitDirPath, path.join(directory2, ".git")) - fs.copySync(gitDirPath, path.join(directory3, ".git")) - - atom.project.setPaths([directory1, directory2, directory3]) - - [repo1, repo2, repo3] = atom.project.getRepositories() - expect(repo1).toBeNull() - expect(repo2.getShortHead()).toBe "master" - expect(repo2.getPath()).toBe fs.realpathSync(path.join(directory2, ".git")) - expect(repo3.getShortHead()).toBe "master" - expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git")) - - it "calls callbacks registered with ::onDidChangePaths", -> - onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') - atom.project.onDidChangePaths(onDidChangePathsSpy) - - paths = [ temp.mkdirSync("dir1"), temp.mkdirSync("dir2") ] - atom.project.setPaths(paths) - - expect(onDidChangePathsSpy.callCount).toBe 1 - expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) - - it "optionally throws an error with any paths that did not exist", -> - paths = [temp.mkdirSync("exists0"), "/doesnt-exists/0", temp.mkdirSync("exists1"), "/doesnt-exists/1"] - - try - atom.project.setPaths paths, mustExist: true - expect('no exception thrown').toBeUndefined() - catch e - expect(e.missingProjectPaths).toEqual [paths[1], paths[3]] - - expect(atom.project.getPaths()).toEqual [paths[0], paths[2]] - - describe "when no paths are given", -> - it "clears its path", -> - atom.project.setPaths([]) - expect(atom.project.getPaths()).toEqual [] - expect(atom.project.getDirectories()).toEqual [] - - it "normalizes the path to remove consecutive slashes, ., and .. segments", -> - atom.project.setPaths(["#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}.."]) - expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a')) - expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a')) - - describe ".addPath(path, options)", -> - it "calls callbacks registered with ::onDidChangePaths", -> - onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') - atom.project.onDidChangePaths(onDidChangePathsSpy) - - [oldPath] = atom.project.getPaths() - - newPath = temp.mkdirSync("dir") - atom.project.addPath(newPath) - - expect(onDidChangePathsSpy.callCount).toBe 1 - expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) - - it "doesn't add redundant paths", -> - onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') - atom.project.onDidChangePaths(onDidChangePathsSpy) - [oldPath] = atom.project.getPaths() - - # Doesn't re-add an existing root directory - atom.project.addPath(oldPath) - expect(atom.project.getPaths()).toEqual([oldPath]) - expect(onDidChangePathsSpy).not.toHaveBeenCalled() - - # Doesn't add an entry for a file-path within an existing root directory - atom.project.addPath(path.join(oldPath, 'some-file.txt')) - expect(atom.project.getPaths()).toEqual([oldPath]) - expect(onDidChangePathsSpy).not.toHaveBeenCalled() - - # Does add an entry for a directory within an existing directory - newPath = path.join(oldPath, "a-dir") - atom.project.addPath(newPath) - expect(atom.project.getPaths()).toEqual([oldPath, newPath]) - expect(onDidChangePathsSpy).toHaveBeenCalled() - - it "doesn't add non-existent directories", -> - previousPaths = atom.project.getPaths() - atom.project.addPath('/this-definitely/does-not-exist') - expect(atom.project.getPaths()).toEqual(previousPaths) - - it "optionally throws on non-existent directories", -> - expect -> - atom.project.addPath '/this-definitely/does-not-exist', mustExist: true - .toThrow() - - describe ".removePath(path)", -> - onDidChangePathsSpy = null - - beforeEach -> - onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') - atom.project.onDidChangePaths(onDidChangePathsSpy) - - it "removes the directory and repository for the path", -> - result = atom.project.removePath(atom.project.getPaths()[0]) - expect(atom.project.getDirectories()).toEqual([]) - expect(atom.project.getRepositories()).toEqual([]) - expect(atom.project.getPaths()).toEqual([]) - expect(result).toBe true - expect(onDidChangePathsSpy).toHaveBeenCalled() - - it "does nothing if the path is not one of the project's root paths", -> - originalPaths = atom.project.getPaths() - result = atom.project.removePath(originalPaths[0] + "xyz") - expect(result).toBe false - expect(atom.project.getPaths()).toEqual(originalPaths) - expect(onDidChangePathsSpy).not.toHaveBeenCalled() - - it "doesn't destroy the repository if it is shared by another root directory", -> - atom.project.setPaths([__dirname, path.join(__dirname, "..", "src")]) - atom.project.removePath(__dirname) - expect(atom.project.getPaths()).toEqual([path.join(__dirname, "..", "src")]) - expect(atom.project.getRepositories()[0].isSubmodule("src")).toBe false - - it "removes a path that is represented as a URI", -> - atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", { - directoryForURISync: (uri) -> - { - getPath: -> uri - getSubdirectory: -> {} - isRoot: -> true - existsSync: -> true - off: -> - } - }) - - ftpURI = "ftp://example.com/some/folder" - - atom.project.setPaths([ftpURI]) - expect(atom.project.getPaths()).toEqual [ftpURI] - - atom.project.removePath(ftpURI) - expect(atom.project.getPaths()).toEqual [] - - describe ".onDidChangeFiles()", -> - sub = [] - events = [] - checkCallback = -> - - beforeEach -> - sub = atom.project.onDidChangeFiles (incoming) -> - events.push incoming... - checkCallback() - - afterEach -> - sub.dispose() - - waitForEvents = (paths) -> - remaining = new Set(fs.realpathSync(p) for p in paths) - new Promise (resolve, reject) -> - checkCallback = -> - remaining.delete(event.path) for event in events - resolve() if remaining.size is 0 - - expire = -> - checkCallback = -> - console.error "Paths not seen:", Array.from(remaining) - reject(new Error('Expired before all expected events were delivered.')) - - checkCallback() - setTimeout expire, 2000 - - it "reports filesystem changes within project paths", -> - dirOne = temp.mkdirSync('atom-spec-project-one') - fileOne = path.join(dirOne, 'file-one.txt') - fileTwo = path.join(dirOne, 'file-two.txt') - dirTwo = temp.mkdirSync('atom-spec-project-two') - fileThree = path.join(dirTwo, 'file-three.txt') - - # Ensure that all preexisting watchers are stopped - waitsForPromise -> stopAllWatchers() - - runs -> atom.project.setPaths([dirOne]) - waitsForPromise -> atom.project.getWatcherPromise dirOne - - runs -> - expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual undefined - - fs.writeFileSync fileThree, "three\n" - fs.writeFileSync fileTwo, "two\n" - fs.writeFileSync fileOne, "one\n" - - waitsForPromise -> waitForEvents [fileOne, fileTwo] - - runs -> - expect(events.some (event) -> event.path is fileThree).toBeFalsy() - - describe ".onDidAddBuffer()", -> - it "invokes the callback with added text buffers", -> - buffers = [] - added = [] - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/a')) - .then (o) -> buffers.push(o) - - runs -> - expect(buffers.length).toBe 1 - atom.project.onDidAddBuffer (buffer) -> added.push(buffer) - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) - .then (o) -> buffers.push(o) - - runs -> - expect(buffers.length).toBe 2 - expect(added).toEqual [buffers[1]] - - describe ".observeBuffers()", -> - it "invokes the observer with current and future text buffers", -> - buffers = [] - observed = [] - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/a')) - .then (o) -> buffers.push(o) - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) - .then (o) -> buffers.push(o) - - runs -> - expect(buffers.length).toBe 2 - atom.project.observeBuffers (buffer) -> observed.push(buffer) - expect(observed).toEqual buffers - - waitsForPromise -> - atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) - .then (o) -> buffers.push(o) - - runs -> - expect(observed.length).toBe 3 - expect(buffers.length).toBe 3 - expect(observed).toEqual buffers - - describe ".relativize(path)", -> - it "returns the path, relative to whichever root directory it is inside of", -> - atom.project.addPath(temp.mkdirSync("another-path")) - - rootPath = atom.project.getPaths()[0] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory") - - rootPath = atom.project.getPaths()[1] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory") - - it "returns the given path if it is not in any of the root directories", -> - randomPath = path.join("some", "random", "path") - expect(atom.project.relativize(randomPath)).toBe randomPath - - describe ".relativizePath(path)", -> - it "returns the root path that contains the given path, and the path relativized to that root path", -> - atom.project.addPath(temp.mkdirSync("another-path")) - - rootPath = atom.project.getPaths()[0] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")] - - rootPath = atom.project.getPaths()[1] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")] - - describe "when the given path isn't inside of any of the project's path", -> - it "returns null for the root path, and the given path unchanged", -> - randomPath = path.join("some", "random", "path") - expect(atom.project.relativizePath(randomPath)).toEqual [null, randomPath] - - describe "when the given path is a URL", -> - it "returns null for the root path, and the given path unchanged", -> - url = "http://the-path" - expect(atom.project.relativizePath(url)).toEqual [null, url] - - describe "when the given path is inside more than one root folder", -> - it "uses the root folder that is closest to the given path", -> - atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir')) - - inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt') - - expect(atom.project.getDirectories()[0].contains(inputPath)).toBe true - expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true - expect(atom.project.relativizePath(inputPath)).toEqual [ - atom.project.getPaths()[1], - path.join('somewhere', 'something.txt') - ] - - describe ".contains(path)", -> - it "returns whether or not the given path is in one of the root directories", -> - rootPath = atom.project.getPaths()[0] - childPath = path.join(rootPath, "some", "child", "directory") - expect(atom.project.contains(childPath)).toBe true - - randomPath = path.join("some", "random", "path") - expect(atom.project.contains(randomPath)).toBe false - - describe ".resolvePath(uri)", -> - it "normalizes disk drive letter in passed path on #win32", -> - expect(atom.project.resolvePath("d:\\file.txt")).toEqual "D:\\file.txt" diff --git a/spec/project-spec.js b/spec/project-spec.js new file mode 100644 index 000000000..d54c0c9a2 --- /dev/null +++ b/spec/project-spec.js @@ -0,0 +1,935 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS201: Simplify complex destructure assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const temp = require('temp').track() +const TextBuffer = require('text-buffer') +const Project = require('../src/project') +const fs = require('fs-plus') +const path = require('path') +const {Directory} = require('pathwatcher') +const {stopAllWatchers} = require('../src/path-watcher') +const GitRepository = require('../src/git-repository') + +describe('Project', function () { + beforeEach(function () { + atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]) + + // Wait for project's service consumers to be asynchronously added + return waits(1) + }) + + describe('serialization', function () { + let deserializedProject = null + let notQuittingProject = null + let quittingProject = null + + afterEach(function () { + if (deserializedProject != null) { + deserializedProject.destroy() + } + if (notQuittingProject != null) { + notQuittingProject.destroy() + } + return (quittingProject != null ? quittingProject.destroy() : undefined) + }) + + it("does not deserialize paths to directories that don't exist", function () { + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + const state = atom.project.serialize() + state.paths.push('/directory/that/does/not/exist') + + let err = null + waitsForPromise(() => + deserializedProject.deserialize(state, atom.deserializers) + .catch(e => err = e) + ) + + return runs(function () { + expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) + return expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist']) + }) + }) + + it('does not deserialize paths that are now files', function () { + const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child') + fs.mkdirSync(childPath) + + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + atom.project.setPaths([childPath]) + const state = atom.project.serialize() + + fs.rmdirSync(childPath) + fs.writeFileSync(childPath, 'surprise!\n') + + let err = null + waitsForPromise(() => + deserializedProject.deserialize(state, atom.deserializers) + .catch(e => err = e) + ) + + return runs(function () { + expect(deserializedProject.getPaths()).toEqual([]) + return expect(err.missingProjectPaths).toEqual([childPath]) + }) + }) + + it('does not include unretained buffers in the serialized state', function () { + waitsForPromise(() => atom.project.bufferForPath('a')) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + }) + + it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', function () { + waitsForPromise(() => atom.workspace.open('a')) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(function () { + expect(deserializedProject.getBuffers().length).toBe(1) + deserializedProject.getBuffers()[0].destroy() + return expect(deserializedProject.getBuffers().length).toBe(0) + }) + }) + + it('does not deserialize buffers when their path is now a directory', function () { + const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') + + waitsForPromise(() => atom.workspace.open(pathToOpen)) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + fs.mkdirSync(pathToOpen) + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + }) + + it('does not deserialize buffers when their path is inaccessible', function () { + if (process.platform === 'win32') { return } // chmod not supported on win32 + const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') + fs.writeFileSync(pathToOpen, '') + + waitsForPromise(() => atom.workspace.open(pathToOpen)) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + fs.chmodSync(pathToOpen, '000') + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + }) + + it('does not deserialize buffers with their path is no longer present', function () { + const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') + fs.writeFileSync(pathToOpen, '') + + waitsForPromise(() => atom.workspace.open(pathToOpen)) + + runs(function () { + expect(atom.project.getBuffers().length).toBe(1) + fs.unlinkSync(pathToOpen) + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + }) + + it('deserializes buffers that have never been saved before', function () { + const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') + + waitsForPromise(() => atom.workspace.open(pathToOpen)) + + runs(function () { + atom.workspace.getActiveTextEditor().setText('unsaved\n') + expect(atom.project.getBuffers().length).toBe(1) + + return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) + + return runs(function () { + expect(deserializedProject.getBuffers().length).toBe(1) + expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen) + return expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n') + }) + }) + + return it('serializes marker layers and history only if Atom is quitting', function () { + waitsForPromise(() => atom.workspace.open('a')) + + let bufferA = null + let layerA = null + let markerA = null + + runs(function () { + bufferA = atom.project.getBuffers()[0] + layerA = bufferA.addMarkerLayer({persistent: true}) + markerA = layerA.markPosition([0, 3]) + bufferA.append('!') + return notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))) + + runs(function () { + expect(__guard__(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).toBeUndefined() + expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) + return quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + }) + + waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) + + return runs(function () { + expect(__guard__(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).not.toBeUndefined() + return expect(quittingProject.getBuffers()[0].undo()).toBe(true) + }) + }) + }) + + describe('when an editor is saved and the project has no path', () => + it("sets the project's path to the saved file's parent directory", function () { + const tempFile = temp.openSync().path + atom.project.setPaths([]) + expect(atom.project.getPaths()[0]).toBeUndefined() + let editor = null + + waitsForPromise(() => atom.workspace.open().then(o => editor = o)) + + waitsForPromise(() => editor.saveAs(tempFile)) + + return runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile))) + }) + ) + + describe('before and after saving a buffer', function () { + let [buffer] = Array.from([]) + beforeEach(() => + waitsForPromise(() => + atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then(function (o) { + buffer = o + return buffer.retain() + }) + ) + ) + + afterEach(() => buffer.release()) + + return it('emits save events on the main process', function () { + spyOn(atom.project.applicationDelegate, 'emitDidSavePath') + spyOn(atom.project.applicationDelegate, 'emitWillSavePath') + + waitsForPromise(() => buffer.save()) + + return runs(function () { + expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1) + expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath()) + expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1) + return expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath()) + }) + }) + }) + + describe('when a watch error is thrown from the TextBuffer', function () { + let editor = null + beforeEach(() => + waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => editor = o)) + ) + + return it('creates a warning notification', function () { + let noteSpy + atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy()) + + const error = new Error('SomeError') + error.eventType = 'resurrect' + editor.buffer.emitter.emit('will-throw-watch-error', { + handle: jasmine.createSpy(), + error + } + ) + + expect(noteSpy).toHaveBeenCalled() + + const notification = noteSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getDetail()).toBe('SomeError') + expect(notification.getMessage()).toContain('`resurrect`') + return expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a')) + }) + }) + + describe('when a custom repository-provider service is provided', function () { + let [fakeRepositoryProvider, fakeRepository] = Array.from([]) + + beforeEach(function () { + fakeRepository = {destroy () { return null }} + return fakeRepositoryProvider = { + repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) }, + repositoryForDirectorySync (directory) { return fakeRepository } + } + }) + + it('uses it to create repositories for any directories that need one', function () { + const projectPath = temp.mkdirSync('atom-project') + atom.project.setPaths([projectPath]) + expect(atom.project.getRepositories()).toEqual([null]) + + atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) + waitsFor(() => atom.project.repositoryProviders.length > 1) + return runs(() => atom.project.getRepositories()[0] === fakeRepository) + }) + + it('does not create any new repositories if every directory has a repository', function () { + const repositories = atom.project.getRepositories() + expect(repositories.length).toEqual(1) + expect(repositories[0]).toBeTruthy() + + atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) + waitsFor(() => atom.project.repositoryProviders.length > 1) + return runs(() => expect(atom.project.getRepositories()).toBe(repositories)) + }) + + return it('stops using it to create repositories when the service is removed', function () { + atom.project.setPaths([]) + + const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) + waitsFor(() => atom.project.repositoryProviders.length > 1) + return runs(function () { + disposable.dispose() + atom.project.addPath(temp.mkdirSync('atom-project')) + return expect(atom.project.getRepositories()).toEqual([null]) + }) + }) + }) + + describe('when a custom directory-provider service is provided', function () { + class DummyDirectory { + constructor (path1) { + this.path = path1 + } + getPath () { return this.path } + getFile () { return {existsSync () { return false }} } + getSubdirectory () { return {existsSync () { return false }} } + isRoot () { return true } + existsSync () { return this.path.endsWith('does-exist') } + contains (filePath) { return filePath.startsWith(this.path) } + } + + let serviceDisposable = null + + beforeEach(function () { + serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { + directoryForURISync (uri) { + if (uri.startsWith('ssh://')) { + return new DummyDirectory(uri) + } else { + return null + } + } + }) + + return waitsFor(() => atom.project.directoryProviders.length > 0) + }) + + it("uses the provider's custom directories for any paths that it handles", function () { + const localPath = temp.mkdirSync('local-path') + const remotePath = 'ssh://foreign-directory:8080/does-exist' + + atom.project.setPaths([localPath, remotePath]) + + let directories = atom.project.getDirectories() + expect(directories[0].getPath()).toBe(localPath) + expect(directories[0] instanceof Directory).toBe(true) + expect(directories[1].getPath()).toBe(remotePath) + expect(directories[1] instanceof DummyDirectory).toBe(true) + + // It does not add new remote paths that do not exist + const nonExistentRemotePath = 'ssh://another-directory:8080/does-not-exist' + atom.project.addPath(nonExistentRemotePath) + expect(atom.project.getDirectories().length).toBe(2) + + // It adds new remote paths if their directories exist. + const newRemotePath = 'ssh://another-directory:8080/does-exist' + atom.project.addPath(newRemotePath) + directories = atom.project.getDirectories() + expect(directories[2].getPath()).toBe(newRemotePath) + return expect(directories[2] instanceof DummyDirectory).toBe(true) + }) + + return it('stops using the provider when the service is removed', function () { + serviceDisposable.dispose() + atom.project.setPaths(['ssh://foreign-directory:8080/does-exist']) + return expect(atom.project.getDirectories().length).toBe(0) + }) + }) + + describe('.open(path)', function () { + let [absolutePath, newBufferHandler] = Array.from([]) + + beforeEach(function () { + absolutePath = require.resolve('./fixtures/dir/a') + newBufferHandler = jasmine.createSpy('newBufferHandler') + return atom.project.onDidAddBuffer(newBufferHandler) + }) + + describe("when given an absolute path that isn't currently open", () => + it("returns a new edit session for the given path and emits 'buffer-created'", function () { + let editor = null + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + + return runs(function () { + expect(editor.buffer.getPath()).toBe(absolutePath) + return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + }) + }) + ) + + describe("when given a relative path that isn't currently opened", () => + it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", function () { + let editor = null + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + + return runs(function () { + expect(editor.buffer.getPath()).toBe(absolutePath) + return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + }) + }) + ) + + describe('when passed the path to a buffer that is currently opened', () => + it('returns a new edit session containing currently opened buffer', function () { + let editor = null + + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + + runs(() => newBufferHandler.reset()) + + waitsForPromise(() => + atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer)) + ) + + return waitsForPromise(() => + atom.workspace.open('a').then(function ({buffer}) { + expect(buffer).toBe(editor.buffer) + return expect(newBufferHandler).not.toHaveBeenCalled() + }) + ) + }) + ) + + return describe('when not passed a path', () => + it("returns a new edit session and emits 'buffer-created'", function () { + let editor = null + waitsForPromise(() => atom.workspace.open().then(o => editor = o)) + + return runs(function () { + expect(editor.buffer.getPath()).toBeUndefined() + return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + }) + }) + ) + }) + + describe('.bufferForPath(path)', function () { + let buffer = null + + beforeEach(() => + waitsForPromise(() => + atom.project.bufferForPath('a').then(function (o) { + buffer = o + return buffer.retain() + }) + ) + ) + + afterEach(() => buffer.release()) + + return describe('when opening a previously opened path', function () { + it('does not create a new buffer', function () { + waitsForPromise(() => + atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer)) + ) + + waitsForPromise(() => + atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer)) + ) + + return waitsForPromise(() => + Promise.all([ + atom.project.bufferForPath('c'), + atom.project.bufferForPath('c') + ]).then(function (...args) { + const [buffer1, buffer2] = Array.from(args[0]) + return expect(buffer1).toBe(buffer2) + }) + ) + }) + + it('retries loading the buffer if it previously failed', function () { + waitsForPromise({shouldReject: true}, function () { + spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file'))) + return atom.project.bufferForPath('b') + }) + + return waitsForPromise({shouldReject: false}, function () { + TextBuffer.load.andCallThrough() + return atom.project.bufferForPath('b') + }) + }) + + return it('creates a new buffer if the previous buffer was destroyed', function () { + buffer.release() + + return waitsForPromise(() => + atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer)) + ) + }) + }) + }) + + describe('.repositoryForDirectory(directory)', function () { + it('resolves to null when the directory does not have a repository', () => + waitsForPromise(function () { + const directory = new Directory('/tmp') + return atom.project.repositoryForDirectory(directory).then(function (result) { + expect(result).toBeNull() + expect(atom.project.repositoryProviders.length).toBeGreaterThan(0) + return expect(atom.project.repositoryPromisesByPath.size).toBe(0) + }) + }) + ) + + it('resolves to a GitRepository and is cached when the given directory is a Git repo', () => + waitsForPromise(function () { + const directory = new Directory(path.join(__dirname, '..')) + const promise = atom.project.repositoryForDirectory(directory) + return promise.then(function (result) { + expect(result).toBeInstanceOf(GitRepository) + const dirPath = directory.getRealPathSync() + expect(result.getPath()).toBe(path.join(dirPath, '.git')) + + // Verify that the result is cached. + return expect(atom.project.repositoryForDirectory(directory)).toBe(promise) + }) + }) + ) + + return it('creates a new repository if a previous one with the same directory had been destroyed', function () { + let repository = null + const directory = new Directory(path.join(__dirname, '..')) + + waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) + + runs(function () { + expect(repository.isDestroyed()).toBe(false) + repository.destroy() + return expect(repository.isDestroyed()).toBe(true) + }) + + waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) + + return runs(() => expect(repository.isDestroyed()).toBe(false)) + }) + }) + + describe('.setPaths(paths, options)', function () { + describe('when path is a file', () => + it("sets its path to the file's parent directory and updates the root directory", function () { + const filePath = require.resolve('./fixtures/dir/a') + atom.project.setPaths([filePath]) + expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath)) + return expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath)) + }) + ) + + describe('when path is a directory', function () { + it('assigns the directories and repositories', function () { + const directory1 = temp.mkdirSync('non-git-repo') + const directory2 = temp.mkdirSync('git-repo1') + const directory3 = temp.mkdirSync('git-repo2') + + const gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git')) + fs.copySync(gitDirPath, path.join(directory2, '.git')) + fs.copySync(gitDirPath, path.join(directory3, '.git')) + + atom.project.setPaths([directory1, directory2, directory3]) + + const [repo1, repo2, repo3] = Array.from(atom.project.getRepositories()) + expect(repo1).toBeNull() + expect(repo2.getShortHead()).toBe('master') + expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git'))) + expect(repo3.getShortHead()).toBe('master') + return expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git'))) + }) + + it('calls callbacks registered with ::onDidChangePaths', function () { + const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + const paths = [ temp.mkdirSync('dir1'), temp.mkdirSync('dir2') ] + atom.project.setPaths(paths) + + expect(onDidChangePathsSpy.callCount).toBe(1) + return expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) + }) + + return it('optionally throws an error with any paths that did not exist', function () { + const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1'] + + try { + atom.project.setPaths(paths, {mustExist: true}) + expect('no exception thrown').toBeUndefined() + } catch (e) { + expect(e.missingProjectPaths).toEqual([paths[1], paths[3]]) + } + + return expect(atom.project.getPaths()).toEqual([paths[0], paths[2]]) + }) + }) + + describe('when no paths are given', () => + it('clears its path', function () { + atom.project.setPaths([]) + expect(atom.project.getPaths()).toEqual([]) + return expect(atom.project.getDirectories()).toEqual([]) + }) + ) + + return it('normalizes the path to remove consecutive slashes, ., and .. segments', function () { + atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`]) + expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) + return expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) + }) + }) + + describe('.addPath(path, options)', function () { + it('calls callbacks registered with ::onDidChangePaths', function () { + const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + const [oldPath] = Array.from(atom.project.getPaths()) + + const newPath = temp.mkdirSync('dir') + atom.project.addPath(newPath) + + expect(onDidChangePathsSpy.callCount).toBe(1) + return expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) + }) + + it("doesn't add redundant paths", function () { + const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + const [oldPath] = Array.from(atom.project.getPaths()) + + // Doesn't re-add an existing root directory + atom.project.addPath(oldPath) + expect(atom.project.getPaths()).toEqual([oldPath]) + expect(onDidChangePathsSpy).not.toHaveBeenCalled() + + // Doesn't add an entry for a file-path within an existing root directory + atom.project.addPath(path.join(oldPath, 'some-file.txt')) + expect(atom.project.getPaths()).toEqual([oldPath]) + expect(onDidChangePathsSpy).not.toHaveBeenCalled() + + // Does add an entry for a directory within an existing directory + const newPath = path.join(oldPath, 'a-dir') + atom.project.addPath(newPath) + expect(atom.project.getPaths()).toEqual([oldPath, newPath]) + return expect(onDidChangePathsSpy).toHaveBeenCalled() + }) + + it("doesn't add non-existent directories", function () { + const previousPaths = atom.project.getPaths() + atom.project.addPath('/this-definitely/does-not-exist') + return expect(atom.project.getPaths()).toEqual(previousPaths) + }) + + return it('optionally throws on non-existent directories', () => + expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow() + ) + }) + + describe('.removePath(path)', function () { + let onDidChangePathsSpy = null + + beforeEach(function () { + onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') + return atom.project.onDidChangePaths(onDidChangePathsSpy) + }) + + it('removes the directory and repository for the path', function () { + const result = atom.project.removePath(atom.project.getPaths()[0]) + expect(atom.project.getDirectories()).toEqual([]) + expect(atom.project.getRepositories()).toEqual([]) + expect(atom.project.getPaths()).toEqual([]) + expect(result).toBe(true) + return expect(onDidChangePathsSpy).toHaveBeenCalled() + }) + + it("does nothing if the path is not one of the project's root paths", function () { + const originalPaths = atom.project.getPaths() + const result = atom.project.removePath(originalPaths[0] + 'xyz') + expect(result).toBe(false) + expect(atom.project.getPaths()).toEqual(originalPaths) + return expect(onDidChangePathsSpy).not.toHaveBeenCalled() + }) + + it("doesn't destroy the repository if it is shared by another root directory", function () { + atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')]) + atom.project.removePath(__dirname) + expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')]) + return expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false) + }) + + return it('removes a path that is represented as a URI', function () { + atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { + directoryForURISync (uri) { + return { + getPath () { return uri }, + getSubdirectory () { return {} }, + isRoot () { return true }, + existsSync () { return true }, + off () {} + } + } + }) + + const ftpURI = 'ftp://example.com/some/folder' + + atom.project.setPaths([ftpURI]) + expect(atom.project.getPaths()).toEqual([ftpURI]) + + atom.project.removePath(ftpURI) + return expect(atom.project.getPaths()).toEqual([]) + }) + }) + + describe('.onDidChangeFiles()', function () { + let sub = [] + const events = [] + let checkCallback = function () {} + + beforeEach(() => + sub = atom.project.onDidChangeFiles(function (incoming) { + events.push(...Array.from(incoming || [])) + return checkCallback() + }) + ) + + afterEach(() => sub.dispose()) + + const waitForEvents = function (paths) { + const remaining = new Set(paths.map((p) => fs.realpathSync(p))) + return new Promise(function (resolve, reject) { + checkCallback = function () { + for (let event of events) { remaining.delete(event.path) } + if (remaining.size === 0) { return resolve() } + } + + const expire = function () { + checkCallback = function () {} + console.error('Paths not seen:', Array.from(remaining)) + return reject(new Error('Expired before all expected events were delivered.')) + } + + checkCallback() + return setTimeout(expire, 2000) + }) + } + + return it('reports filesystem changes within project paths', function () { + const dirOne = temp.mkdirSync('atom-spec-project-one') + const fileOne = path.join(dirOne, 'file-one.txt') + const fileTwo = path.join(dirOne, 'file-two.txt') + const dirTwo = temp.mkdirSync('atom-spec-project-two') + const fileThree = path.join(dirTwo, 'file-three.txt') + + // Ensure that all preexisting watchers are stopped + waitsForPromise(() => stopAllWatchers()) + + runs(() => atom.project.setPaths([dirOne])) + waitsForPromise(() => atom.project.getWatcherPromise(dirOne)) + + runs(function () { + expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined) + + fs.writeFileSync(fileThree, 'three\n') + fs.writeFileSync(fileTwo, 'two\n') + return fs.writeFileSync(fileOne, 'one\n') + }) + + waitsForPromise(() => waitForEvents([fileOne, fileTwo])) + + return runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy()) + }) + }) + + describe('.onDidAddBuffer()', () => + it('invokes the callback with added text buffers', function () { + const buffers = [] + const added = [] + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/a')) + .then(o => buffers.push(o)) + ) + + runs(function () { + expect(buffers.length).toBe(1) + return atom.project.onDidAddBuffer(buffer => added.push(buffer)) + }) + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) + .then(o => buffers.push(o)) + ) + + return runs(function () { + expect(buffers.length).toBe(2) + return expect(added).toEqual([buffers[1]]) + }) + }) +) + + describe('.observeBuffers()', () => + it('invokes the observer with current and future text buffers', function () { + const buffers = [] + const observed = [] + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/a')) + .then(o => buffers.push(o)) + ) + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) + .then(o => buffers.push(o)) + ) + + runs(function () { + expect(buffers.length).toBe(2) + atom.project.observeBuffers(buffer => observed.push(buffer)) + return expect(observed).toEqual(buffers) + }) + + waitsForPromise(() => + atom.project.buildBuffer(require.resolve('./fixtures/dir/b')) + .then(o => buffers.push(o)) + ) + + return runs(function () { + expect(observed.length).toBe(3) + expect(buffers.length).toBe(3) + return expect(observed).toEqual(buffers) + }) + }) + ) + + describe('.relativize(path)', function () { + it('returns the path, relative to whichever root directory it is inside of', function () { + atom.project.addPath(temp.mkdirSync('another-path')) + + let rootPath = atom.project.getPaths()[0] + let childPath = path.join(rootPath, 'some', 'child', 'directory') + expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) + + rootPath = atom.project.getPaths()[1] + childPath = path.join(rootPath, 'some', 'child', 'directory') + return expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) + }) + + return it('returns the given path if it is not in any of the root directories', function () { + const randomPath = path.join('some', 'random', 'path') + return expect(atom.project.relativize(randomPath)).toBe(randomPath) + }) + }) + + describe('.relativizePath(path)', function () { + it('returns the root path that contains the given path, and the path relativized to that root path', function () { + atom.project.addPath(temp.mkdirSync('another-path')) + + let rootPath = atom.project.getPaths()[0] + let childPath = path.join(rootPath, 'some', 'child', 'directory') + expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')]) + + rootPath = atom.project.getPaths()[1] + childPath = path.join(rootPath, 'some', 'child', 'directory') + return expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')]) + }) + + describe("when the given path isn't inside of any of the project's path", () => + it('returns null for the root path, and the given path unchanged', function () { + const randomPath = path.join('some', 'random', 'path') + return expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath]) + }) + ) + + describe('when the given path is a URL', () => + it('returns null for the root path, and the given path unchanged', function () { + const url = 'http://the-path' + return expect(atom.project.relativizePath(url)).toEqual([null, url]) + }) + ) + + return describe('when the given path is inside more than one root folder', () => + it('uses the root folder that is closest to the given path', function () { + atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir')) + + const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt') + + expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true) + expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true) + return expect(atom.project.relativizePath(inputPath)).toEqual([ + atom.project.getPaths()[1], + path.join('somewhere', 'something.txt') + ]) + }) + ) + }) + + describe('.contains(path)', () => + it('returns whether or not the given path is in one of the root directories', function () { + const rootPath = atom.project.getPaths()[0] + const childPath = path.join(rootPath, 'some', 'child', 'directory') + expect(atom.project.contains(childPath)).toBe(true) + + const randomPath = path.join('some', 'random', 'path') + return expect(atom.project.contains(randomPath)).toBe(false) + }) + ) + + return describe('.resolvePath(uri)', () => + it('normalizes disk drive letter in passed path on #win32', () => expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')) + ) +}) + +function __guard__ (value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined +} From 61b228d8a056a14bf79dacdafd305e736d3ac92b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:25:10 -0400 Subject: [PATCH 092/301] Remove unnecessary use of Array.from --- spec/project-spec.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index d54c0c9a2..31646b8a4 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments @@ -230,7 +229,7 @@ describe('Project', function () { ) describe('before and after saving a buffer', function () { - let [buffer] = Array.from([]) + let buffer beforeEach(() => waitsForPromise(() => atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then(function (o) { @@ -286,7 +285,7 @@ describe('Project', function () { }) describe('when a custom repository-provider service is provided', function () { - let [fakeRepositoryProvider, fakeRepository] = Array.from([]) + let fakeRepositoryProvider, fakeRepository beforeEach(function () { fakeRepository = {destroy () { return null }} @@ -391,7 +390,7 @@ describe('Project', function () { }) describe('.open(path)', function () { - let [absolutePath, newBufferHandler] = Array.from([]) + let absolutePath, newBufferHandler beforeEach(function () { absolutePath = require.resolve('./fixtures/dir/a') @@ -485,8 +484,7 @@ describe('Project', function () { Promise.all([ atom.project.bufferForPath('c'), atom.project.bufferForPath('c') - ]).then(function (...args) { - const [buffer1, buffer2] = Array.from(args[0]) + ]).then(function ([buffer1, buffer2]) { return expect(buffer1).toBe(buffer2) }) ) @@ -581,7 +579,7 @@ describe('Project', function () { atom.project.setPaths([directory1, directory2, directory3]) - const [repo1, repo2, repo3] = Array.from(atom.project.getRepositories()) + const [repo1, repo2, repo3] = atom.project.getRepositories() expect(repo1).toBeNull() expect(repo2.getShortHead()).toBe('master') expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git'))) @@ -634,7 +632,7 @@ describe('Project', function () { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) - const [oldPath] = Array.from(atom.project.getPaths()) + const [oldPath] = atom.project.getPaths() const newPath = temp.mkdirSync('dir') atom.project.addPath(newPath) @@ -646,7 +644,7 @@ describe('Project', function () { it("doesn't add redundant paths", function () { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) - const [oldPath] = Array.from(atom.project.getPaths()) + const [oldPath] = atom.project.getPaths() // Doesn't re-add an existing root directory atom.project.addPath(oldPath) @@ -738,7 +736,7 @@ describe('Project', function () { beforeEach(() => sub = atom.project.onDidChangeFiles(function (incoming) { - events.push(...Array.from(incoming || [])) + events.push(...incoming || []) return checkCallback() }) ) @@ -755,7 +753,7 @@ describe('Project', function () { const expire = function () { checkCallback = function () {} - console.error('Paths not seen:', Array.from(remaining)) + console.error('Paths not seen:', remaining) return reject(new Error('Expired before all expected events were delivered.')) } From db115d3ab843445de8b30374a1c95436ff731cc8 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:30:46 -0400 Subject: [PATCH 093/301] Remove unnecessary code created because of implicit returns --- spec/project-spec.js | 211 +++++++++++++++++++++---------------------- 1 file changed, 105 insertions(+), 106 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 31646b8a4..999b63989 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments * DS207: Consider shorter variations of null checks @@ -20,7 +19,7 @@ describe('Project', function () { atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]) // Wait for project's service consumers to be asynchronously added - return waits(1) + waits(1) }) describe('serialization', function () { @@ -35,7 +34,7 @@ describe('Project', function () { if (notQuittingProject != null) { notQuittingProject.destroy() } - return (quittingProject != null ? quittingProject.destroy() : undefined) + (quittingProject != null ? quittingProject.destroy() : undefined) }) it("does not deserialize paths to directories that don't exist", function () { @@ -49,9 +48,9 @@ describe('Project', function () { .catch(e => err = e) ) - return runs(function () { + runs(function () { expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) - return expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist']) + expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist']) }) }) @@ -72,9 +71,9 @@ describe('Project', function () { .catch(e => err = e) ) - return runs(function () { + runs(function () { expect(deserializedProject.getPaths()).toEqual([]) - return expect(err.missingProjectPaths).toEqual([childPath]) + expect(err.missingProjectPaths).toEqual([childPath]) }) }) @@ -84,12 +83,12 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', function () { @@ -97,15 +96,15 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(function () { + runs(function () { expect(deserializedProject.getBuffers().length).toBe(1) deserializedProject.getBuffers()[0].destroy() - return expect(deserializedProject.getBuffers().length).toBe(0) + expect(deserializedProject.getBuffers().length).toBe(0) }) }) @@ -117,12 +116,12 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) fs.mkdirSync(pathToOpen) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) it('does not deserialize buffers when their path is inaccessible', function () { @@ -135,12 +134,12 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) fs.chmodSync(pathToOpen, '000') - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) it('does not deserialize buffers with their path is no longer present', function () { @@ -152,12 +151,12 @@ describe('Project', function () { runs(function () { expect(atom.project.getBuffers().length).toBe(1) fs.unlinkSync(pathToOpen) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) + runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) it('deserializes buffers that have never been saved before', function () { @@ -169,19 +168,19 @@ describe('Project', function () { atom.workspace.getActiveTextEditor().setText('unsaved\n') expect(atom.project.getBuffers().length).toBe(1) - return deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - return runs(function () { + runs(function () { expect(deserializedProject.getBuffers().length).toBe(1) expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen) - return expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n') + expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n') }) }) - return it('serializes marker layers and history only if Atom is quitting', function () { + it('serializes marker layers and history only if Atom is quitting', function () { waitsForPromise(() => atom.workspace.open('a')) let bufferA = null @@ -193,7 +192,7 @@ describe('Project', function () { layerA = bufferA.addMarkerLayer({persistent: true}) markerA = layerA.markPosition([0, 3]) bufferA.append('!') - return notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))) @@ -201,14 +200,14 @@ describe('Project', function () { runs(function () { expect(__guard__(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).toBeUndefined() expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) - return quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) + quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) - return runs(function () { + runs(function () { expect(__guard__(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).not.toBeUndefined() - return expect(quittingProject.getBuffers()[0].undo()).toBe(true) + expect(quittingProject.getBuffers()[0].undo()).toBe(true) }) }) }) @@ -224,7 +223,7 @@ describe('Project', function () { waitsForPromise(() => editor.saveAs(tempFile)) - return runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile))) + runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile))) }) ) @@ -234,24 +233,24 @@ describe('Project', function () { waitsForPromise(() => atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then(function (o) { buffer = o - return buffer.retain() + buffer.retain() }) ) ) afterEach(() => buffer.release()) - return it('emits save events on the main process', function () { + it('emits save events on the main process', function () { spyOn(atom.project.applicationDelegate, 'emitDidSavePath') spyOn(atom.project.applicationDelegate, 'emitWillSavePath') waitsForPromise(() => buffer.save()) - return runs(function () { + runs(function () { expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1) expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath()) expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1) - return expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath()) + expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath()) }) }) }) @@ -262,7 +261,7 @@ describe('Project', function () { waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => editor = o)) ) - return it('creates a warning notification', function () { + it('creates a warning notification', function () { let noteSpy atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy()) @@ -280,7 +279,7 @@ describe('Project', function () { expect(notification.getType()).toBe('warning') expect(notification.getDetail()).toBe('SomeError') expect(notification.getMessage()).toContain('`resurrect`') - return expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a')) + expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a')) }) }) @@ -289,7 +288,7 @@ describe('Project', function () { beforeEach(function () { fakeRepository = {destroy () { return null }} - return fakeRepositoryProvider = { + fakeRepositoryProvider = { repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) }, repositoryForDirectorySync (directory) { return fakeRepository } } @@ -302,7 +301,7 @@ describe('Project', function () { atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) waitsFor(() => atom.project.repositoryProviders.length > 1) - return runs(() => atom.project.getRepositories()[0] === fakeRepository) + runs(() => atom.project.getRepositories()[0] === fakeRepository) }) it('does not create any new repositories if every directory has a repository', function () { @@ -312,18 +311,18 @@ describe('Project', function () { atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) waitsFor(() => atom.project.repositoryProviders.length > 1) - return runs(() => expect(atom.project.getRepositories()).toBe(repositories)) + runs(() => expect(atom.project.getRepositories()).toBe(repositories)) }) - return it('stops using it to create repositories when the service is removed', function () { + it('stops using it to create repositories when the service is removed', function () { atom.project.setPaths([]) const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) waitsFor(() => atom.project.repositoryProviders.length > 1) - return runs(function () { + runs(function () { disposable.dispose() atom.project.addPath(temp.mkdirSync('atom-project')) - return expect(atom.project.getRepositories()).toEqual([null]) + expect(atom.project.getRepositories()).toEqual([null]) }) }) }) @@ -354,7 +353,7 @@ describe('Project', function () { } }) - return waitsFor(() => atom.project.directoryProviders.length > 0) + waitsFor(() => atom.project.directoryProviders.length > 0) }) it("uses the provider's custom directories for any paths that it handles", function () { @@ -379,13 +378,13 @@ describe('Project', function () { atom.project.addPath(newRemotePath) directories = atom.project.getDirectories() expect(directories[2].getPath()).toBe(newRemotePath) - return expect(directories[2] instanceof DummyDirectory).toBe(true) + expect(directories[2] instanceof DummyDirectory).toBe(true) }) - return it('stops using the provider when the service is removed', function () { + it('stops using the provider when the service is removed', function () { serviceDisposable.dispose() atom.project.setPaths(['ssh://foreign-directory:8080/does-exist']) - return expect(atom.project.getDirectories().length).toBe(0) + expect(atom.project.getDirectories().length).toBe(0) }) }) @@ -395,7 +394,7 @@ describe('Project', function () { beforeEach(function () { absolutePath = require.resolve('./fixtures/dir/a') newBufferHandler = jasmine.createSpy('newBufferHandler') - return atom.project.onDidAddBuffer(newBufferHandler) + atom.project.onDidAddBuffer(newBufferHandler) }) describe("when given an absolute path that isn't currently open", () => @@ -403,9 +402,9 @@ describe('Project', function () { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) - return runs(function () { + runs(function () { expect(editor.buffer.getPath()).toBe(absolutePath) - return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) }) ) @@ -415,9 +414,9 @@ describe('Project', function () { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) - return runs(function () { + runs(function () { expect(editor.buffer.getPath()).toBe(absolutePath) - return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) }) ) @@ -434,23 +433,23 @@ describe('Project', function () { atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer)) ) - return waitsForPromise(() => + waitsForPromise(() => atom.workspace.open('a').then(function ({buffer}) { expect(buffer).toBe(editor.buffer) - return expect(newBufferHandler).not.toHaveBeenCalled() + expect(newBufferHandler).not.toHaveBeenCalled() }) ) }) ) - return describe('when not passed a path', () => + describe('when not passed a path', () => it("returns a new edit session and emits 'buffer-created'", function () { let editor = null waitsForPromise(() => atom.workspace.open().then(o => editor = o)) - return runs(function () { + runs(function () { expect(editor.buffer.getPath()).toBeUndefined() - return expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) }) ) @@ -463,14 +462,14 @@ describe('Project', function () { waitsForPromise(() => atom.project.bufferForPath('a').then(function (o) { buffer = o - return buffer.retain() + buffer.retain() }) ) ) afterEach(() => buffer.release()) - return describe('when opening a previously opened path', function () { + describe('when opening a previously opened path', function () { it('does not create a new buffer', function () { waitsForPromise(() => atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer)) @@ -480,12 +479,12 @@ describe('Project', function () { atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer)) ) - return waitsForPromise(() => + waitsForPromise(() => Promise.all([ atom.project.bufferForPath('c'), atom.project.bufferForPath('c') ]).then(function ([buffer1, buffer2]) { - return expect(buffer1).toBe(buffer2) + expect(buffer1).toBe(buffer2) }) ) }) @@ -496,16 +495,16 @@ describe('Project', function () { return atom.project.bufferForPath('b') }) - return waitsForPromise({shouldReject: false}, function () { + waitsForPromise({shouldReject: false}, function () { TextBuffer.load.andCallThrough() return atom.project.bufferForPath('b') }) }) - return it('creates a new buffer if the previous buffer was destroyed', function () { + it('creates a new buffer if the previous buffer was destroyed', function () { buffer.release() - return waitsForPromise(() => + waitsForPromise(() => atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer)) ) }) @@ -519,7 +518,7 @@ describe('Project', function () { return atom.project.repositoryForDirectory(directory).then(function (result) { expect(result).toBeNull() expect(atom.project.repositoryProviders.length).toBeGreaterThan(0) - return expect(atom.project.repositoryPromisesByPath.size).toBe(0) + expect(atom.project.repositoryPromisesByPath.size).toBe(0) }) }) ) @@ -534,12 +533,12 @@ describe('Project', function () { expect(result.getPath()).toBe(path.join(dirPath, '.git')) // Verify that the result is cached. - return expect(atom.project.repositoryForDirectory(directory)).toBe(promise) + expect(atom.project.repositoryForDirectory(directory)).toBe(promise) }) }) ) - return it('creates a new repository if a previous one with the same directory had been destroyed', function () { + it('creates a new repository if a previous one with the same directory had been destroyed', function () { let repository = null const directory = new Directory(path.join(__dirname, '..')) @@ -548,12 +547,12 @@ describe('Project', function () { runs(function () { expect(repository.isDestroyed()).toBe(false) repository.destroy() - return expect(repository.isDestroyed()).toBe(true) + expect(repository.isDestroyed()).toBe(true) }) waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) - return runs(() => expect(repository.isDestroyed()).toBe(false)) + runs(() => expect(repository.isDestroyed()).toBe(false)) }) }) @@ -563,7 +562,7 @@ describe('Project', function () { const filePath = require.resolve('./fixtures/dir/a') atom.project.setPaths([filePath]) expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath)) - return expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath)) + expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath)) }) ) @@ -584,7 +583,7 @@ describe('Project', function () { expect(repo2.getShortHead()).toBe('master') expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git'))) expect(repo3.getShortHead()).toBe('master') - return expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git'))) + expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git'))) }) it('calls callbacks registered with ::onDidChangePaths', function () { @@ -595,10 +594,10 @@ describe('Project', function () { atom.project.setPaths(paths) expect(onDidChangePathsSpy.callCount).toBe(1) - return expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) + expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) }) - return it('optionally throws an error with any paths that did not exist', function () { + it('optionally throws an error with any paths that did not exist', function () { const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1'] try { @@ -608,7 +607,7 @@ describe('Project', function () { expect(e.missingProjectPaths).toEqual([paths[1], paths[3]]) } - return expect(atom.project.getPaths()).toEqual([paths[0], paths[2]]) + expect(atom.project.getPaths()).toEqual([paths[0], paths[2]]) }) }) @@ -616,14 +615,14 @@ describe('Project', function () { it('clears its path', function () { atom.project.setPaths([]) expect(atom.project.getPaths()).toEqual([]) - return expect(atom.project.getDirectories()).toEqual([]) + expect(atom.project.getDirectories()).toEqual([]) }) ) - return it('normalizes the path to remove consecutive slashes, ., and .. segments', function () { + it('normalizes the path to remove consecutive slashes, ., and .. segments', function () { atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`]) expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) - return expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) + expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) }) }) @@ -638,7 +637,7 @@ describe('Project', function () { atom.project.addPath(newPath) expect(onDidChangePathsSpy.callCount).toBe(1) - return expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) + expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) }) it("doesn't add redundant paths", function () { @@ -660,16 +659,16 @@ describe('Project', function () { const newPath = path.join(oldPath, 'a-dir') atom.project.addPath(newPath) expect(atom.project.getPaths()).toEqual([oldPath, newPath]) - return expect(onDidChangePathsSpy).toHaveBeenCalled() + expect(onDidChangePathsSpy).toHaveBeenCalled() }) it("doesn't add non-existent directories", function () { const previousPaths = atom.project.getPaths() atom.project.addPath('/this-definitely/does-not-exist') - return expect(atom.project.getPaths()).toEqual(previousPaths) + expect(atom.project.getPaths()).toEqual(previousPaths) }) - return it('optionally throws on non-existent directories', () => + it('optionally throws on non-existent directories', () => expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow() ) }) @@ -679,7 +678,7 @@ describe('Project', function () { beforeEach(function () { onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') - return atom.project.onDidChangePaths(onDidChangePathsSpy) + atom.project.onDidChangePaths(onDidChangePathsSpy) }) it('removes the directory and repository for the path', function () { @@ -688,7 +687,7 @@ describe('Project', function () { expect(atom.project.getRepositories()).toEqual([]) expect(atom.project.getPaths()).toEqual([]) expect(result).toBe(true) - return expect(onDidChangePathsSpy).toHaveBeenCalled() + expect(onDidChangePathsSpy).toHaveBeenCalled() }) it("does nothing if the path is not one of the project's root paths", function () { @@ -696,17 +695,17 @@ describe('Project', function () { const result = atom.project.removePath(originalPaths[0] + 'xyz') expect(result).toBe(false) expect(atom.project.getPaths()).toEqual(originalPaths) - return expect(onDidChangePathsSpy).not.toHaveBeenCalled() + expect(onDidChangePathsSpy).not.toHaveBeenCalled() }) it("doesn't destroy the repository if it is shared by another root directory", function () { atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')]) atom.project.removePath(__dirname) expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')]) - return expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false) + expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false) }) - return it('removes a path that is represented as a URI', function () { + it('removes a path that is represented as a URI', function () { atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { directoryForURISync (uri) { return { @@ -725,7 +724,7 @@ describe('Project', function () { expect(atom.project.getPaths()).toEqual([ftpURI]) atom.project.removePath(ftpURI) - return expect(atom.project.getPaths()).toEqual([]) + expect(atom.project.getPaths()).toEqual([]) }) }) @@ -737,7 +736,7 @@ describe('Project', function () { beforeEach(() => sub = atom.project.onDidChangeFiles(function (incoming) { events.push(...incoming || []) - return checkCallback() + checkCallback() }) ) @@ -748,21 +747,21 @@ describe('Project', function () { return new Promise(function (resolve, reject) { checkCallback = function () { for (let event of events) { remaining.delete(event.path) } - if (remaining.size === 0) { return resolve() } + if (remaining.size === 0) { resolve() } } const expire = function () { checkCallback = function () {} console.error('Paths not seen:', remaining) - return reject(new Error('Expired before all expected events were delivered.')) + reject(new Error('Expired before all expected events were delivered.')) } checkCallback() - return setTimeout(expire, 2000) + setTimeout(expire, 2000) }) } - return it('reports filesystem changes within project paths', function () { + it('reports filesystem changes within project paths', function () { const dirOne = temp.mkdirSync('atom-spec-project-one') const fileOne = path.join(dirOne, 'file-one.txt') const fileTwo = path.join(dirOne, 'file-two.txt') @@ -780,12 +779,12 @@ describe('Project', function () { fs.writeFileSync(fileThree, 'three\n') fs.writeFileSync(fileTwo, 'two\n') - return fs.writeFileSync(fileOne, 'one\n') + fs.writeFileSync(fileOne, 'one\n') }) waitsForPromise(() => waitForEvents([fileOne, fileTwo])) - return runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy()) + runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy()) }) }) @@ -801,7 +800,7 @@ describe('Project', function () { runs(function () { expect(buffers.length).toBe(1) - return atom.project.onDidAddBuffer(buffer => added.push(buffer)) + atom.project.onDidAddBuffer(buffer => added.push(buffer)) }) waitsForPromise(() => @@ -809,9 +808,9 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - return runs(function () { + runs(function () { expect(buffers.length).toBe(2) - return expect(added).toEqual([buffers[1]]) + expect(added).toEqual([buffers[1]]) }) }) ) @@ -834,7 +833,7 @@ describe('Project', function () { runs(function () { expect(buffers.length).toBe(2) atom.project.observeBuffers(buffer => observed.push(buffer)) - return expect(observed).toEqual(buffers) + expect(observed).toEqual(buffers) }) waitsForPromise(() => @@ -842,10 +841,10 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - return runs(function () { + runs(function () { expect(observed.length).toBe(3) expect(buffers.length).toBe(3) - return expect(observed).toEqual(buffers) + expect(observed).toEqual(buffers) }) }) ) @@ -860,12 +859,12 @@ describe('Project', function () { rootPath = atom.project.getPaths()[1] childPath = path.join(rootPath, 'some', 'child', 'directory') - return expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) + expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) }) - return it('returns the given path if it is not in any of the root directories', function () { + it('returns the given path if it is not in any of the root directories', function () { const randomPath = path.join('some', 'random', 'path') - return expect(atom.project.relativize(randomPath)).toBe(randomPath) + expect(atom.project.relativize(randomPath)).toBe(randomPath) }) }) @@ -879,24 +878,24 @@ describe('Project', function () { rootPath = atom.project.getPaths()[1] childPath = path.join(rootPath, 'some', 'child', 'directory') - return expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')]) + expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')]) }) describe("when the given path isn't inside of any of the project's path", () => it('returns null for the root path, and the given path unchanged', function () { const randomPath = path.join('some', 'random', 'path') - return expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath]) + expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath]) }) ) describe('when the given path is a URL', () => it('returns null for the root path, and the given path unchanged', function () { const url = 'http://the-path' - return expect(atom.project.relativizePath(url)).toEqual([null, url]) + expect(atom.project.relativizePath(url)).toEqual([null, url]) }) ) - return describe('when the given path is inside more than one root folder', () => + describe('when the given path is inside more than one root folder', () => it('uses the root folder that is closest to the given path', function () { atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir')) @@ -904,7 +903,7 @@ describe('Project', function () { expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true) expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true) - return expect(atom.project.relativizePath(inputPath)).toEqual([ + expect(atom.project.relativizePath(inputPath)).toEqual([ atom.project.getPaths()[1], path.join('somewhere', 'something.txt') ]) @@ -919,11 +918,11 @@ describe('Project', function () { expect(atom.project.contains(childPath)).toBe(true) const randomPath = path.join('some', 'random', 'path') - return expect(atom.project.contains(randomPath)).toBe(false) + expect(atom.project.contains(randomPath)).toBe(false) }) ) - return describe('.resolvePath(uri)', () => + describe('.resolvePath(uri)', () => it('normalizes disk drive letter in passed path on #win32', () => expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')) ) }) From 498d7c90ebd382ca966b9f1bed32ba8228d225f1 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:45:46 -0400 Subject: [PATCH 094/301] Rewrite code to no longer use __guard__ --- spec/project-spec.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 999b63989..8c45b98b9 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS103: Rewrite code to no longer use __guard__ * DS201: Simplify complex destructure assignments * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md @@ -16,7 +15,9 @@ const GitRepository = require('../src/git-repository') describe('Project', function () { beforeEach(function () { - atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]) + const directory = atom.project.getDirectories()[0] + const paths = directory ? [directory.resolve('dir')] : [null] + atom.project.setPaths(paths) // Wait for project's service consumers to be asynchronously added waits(1) @@ -198,7 +199,7 @@ describe('Project', function () { waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))) runs(function () { - expect(__guard__(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).toBeUndefined() + expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined() expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) @@ -206,7 +207,7 @@ describe('Project', function () { waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) runs(function () { - expect(__guard__(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id))).not.toBeUndefined() + expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).not.toBeUndefined() expect(quittingProject.getBuffers()[0].undo()).toBe(true) }) }) @@ -926,7 +927,3 @@ describe('Project', function () { it('normalizes disk drive letter in passed path on #win32', () => expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')) ) }) - -function __guard__ (value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined -} From 6e78281a73764d877b7febaf9bacc6a93ceda531 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:48:37 -0400 Subject: [PATCH 095/301] :art: Prefer fat arrow function syntax --- spec/project-spec.js | 224 +++++++++++++++++++++---------------------- 1 file changed, 112 insertions(+), 112 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 8c45b98b9..8cd092126 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -13,8 +13,8 @@ const {Directory} = require('pathwatcher') const {stopAllWatchers} = require('../src/path-watcher') const GitRepository = require('../src/git-repository') -describe('Project', function () { - beforeEach(function () { +describe('Project', () => { + beforeEach(() => { const directory = atom.project.getDirectories()[0] const paths = directory ? [directory.resolve('dir')] : [null] atom.project.setPaths(paths) @@ -23,12 +23,12 @@ describe('Project', function () { waits(1) }) - describe('serialization', function () { + describe('serialization', () => { let deserializedProject = null let notQuittingProject = null let quittingProject = null - afterEach(function () { + afterEach(() => { if (deserializedProject != null) { deserializedProject.destroy() } @@ -38,7 +38,7 @@ describe('Project', function () { (quittingProject != null ? quittingProject.destroy() : undefined) }) - it("does not deserialize paths to directories that don't exist", function () { + it("does not deserialize paths to directories that don't exist", () => { deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) const state = atom.project.serialize() state.paths.push('/directory/that/does/not/exist') @@ -49,13 +49,13 @@ describe('Project', function () { .catch(e => err = e) ) - runs(function () { + runs(() => { expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist']) }) }) - it('does not deserialize paths that are now files', function () { + it('does not deserialize paths that are now files', () => { const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child') fs.mkdirSync(childPath) @@ -72,16 +72,16 @@ describe('Project', function () { .catch(e => err = e) ) - runs(function () { + runs(() => { expect(deserializedProject.getPaths()).toEqual([]) expect(err.missingProjectPaths).toEqual([childPath]) }) }) - it('does not include unretained buffers in the serialized state', function () { + it('does not include unretained buffers in the serialized state', () => { waitsForPromise(() => atom.project.bufferForPath('a')) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -92,29 +92,29 @@ describe('Project', function () { runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) - it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', function () { + it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', () => { waitsForPromise(() => atom.workspace.open('a')) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) }) waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - runs(function () { + runs(() => { expect(deserializedProject.getBuffers().length).toBe(1) deserializedProject.getBuffers()[0].destroy() expect(deserializedProject.getBuffers().length).toBe(0) }) }) - it('does not deserialize buffers when their path is now a directory', function () { + it('does not deserialize buffers when their path is now a directory', () => { const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') waitsForPromise(() => atom.workspace.open(pathToOpen)) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) fs.mkdirSync(pathToOpen) deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -125,14 +125,14 @@ describe('Project', function () { runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) - it('does not deserialize buffers when their path is inaccessible', function () { + it('does not deserialize buffers when their path is inaccessible', () => { if (process.platform === 'win32') { return } // chmod not supported on win32 const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') fs.writeFileSync(pathToOpen, '') waitsForPromise(() => atom.workspace.open(pathToOpen)) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) fs.chmodSync(pathToOpen, '000') deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -143,13 +143,13 @@ describe('Project', function () { runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) - it('does not deserialize buffers with their path is no longer present', function () { + it('does not deserialize buffers with their path is no longer present', () => { const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') fs.writeFileSync(pathToOpen, '') waitsForPromise(() => atom.workspace.open(pathToOpen)) - runs(function () { + runs(() => { expect(atom.project.getBuffers().length).toBe(1) fs.unlinkSync(pathToOpen) deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -160,12 +160,12 @@ describe('Project', function () { runs(() => expect(deserializedProject.getBuffers().length).toBe(0)) }) - it('deserializes buffers that have never been saved before', function () { + it('deserializes buffers that have never been saved before', () => { const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') waitsForPromise(() => atom.workspace.open(pathToOpen)) - runs(function () { + runs(() => { atom.workspace.getActiveTextEditor().setText('unsaved\n') expect(atom.project.getBuffers().length).toBe(1) @@ -174,21 +174,21 @@ describe('Project', function () { waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))) - runs(function () { + runs(() => { expect(deserializedProject.getBuffers().length).toBe(1) expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen) expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n') }) }) - it('serializes marker layers and history only if Atom is quitting', function () { + it('serializes marker layers and history only if Atom is quitting', () => { waitsForPromise(() => atom.workspace.open('a')) let bufferA = null let layerA = null let markerA = null - runs(function () { + runs(() => { bufferA = atom.project.getBuffers()[0] layerA = bufferA.addMarkerLayer({persistent: true}) markerA = layerA.markPosition([0, 3]) @@ -198,7 +198,7 @@ describe('Project', function () { waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))) - runs(function () { + runs(() => { expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined() expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -206,7 +206,7 @@ describe('Project', function () { waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) - runs(function () { + runs(() => { expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).not.toBeUndefined() expect(quittingProject.getBuffers()[0].undo()).toBe(true) }) @@ -214,7 +214,7 @@ describe('Project', function () { }) describe('when an editor is saved and the project has no path', () => - it("sets the project's path to the saved file's parent directory", function () { + it("sets the project's path to the saved file's parent directory", () => { const tempFile = temp.openSync().path atom.project.setPaths([]) expect(atom.project.getPaths()[0]).toBeUndefined() @@ -228,11 +228,11 @@ describe('Project', function () { }) ) - describe('before and after saving a buffer', function () { + describe('before and after saving a buffer', () => { let buffer beforeEach(() => waitsForPromise(() => - atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then(function (o) { + atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then((o) => { buffer = o buffer.retain() }) @@ -241,13 +241,13 @@ describe('Project', function () { afterEach(() => buffer.release()) - it('emits save events on the main process', function () { + it('emits save events on the main process', () => { spyOn(atom.project.applicationDelegate, 'emitDidSavePath') spyOn(atom.project.applicationDelegate, 'emitWillSavePath') waitsForPromise(() => buffer.save()) - runs(function () { + runs(() => { expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1) expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath()) expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1) @@ -256,13 +256,13 @@ describe('Project', function () { }) }) - describe('when a watch error is thrown from the TextBuffer', function () { + describe('when a watch error is thrown from the TextBuffer', () => { let editor = null beforeEach(() => waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => editor = o)) ) - it('creates a warning notification', function () { + it('creates a warning notification', () => { let noteSpy atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy()) @@ -284,10 +284,10 @@ describe('Project', function () { }) }) - describe('when a custom repository-provider service is provided', function () { + describe('when a custom repository-provider service is provided', () => { let fakeRepositoryProvider, fakeRepository - beforeEach(function () { + beforeEach(() => { fakeRepository = {destroy () { return null }} fakeRepositoryProvider = { repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) }, @@ -295,7 +295,7 @@ describe('Project', function () { } }) - it('uses it to create repositories for any directories that need one', function () { + it('uses it to create repositories for any directories that need one', () => { const projectPath = temp.mkdirSync('atom-project') atom.project.setPaths([projectPath]) expect(atom.project.getRepositories()).toEqual([null]) @@ -305,7 +305,7 @@ describe('Project', function () { runs(() => atom.project.getRepositories()[0] === fakeRepository) }) - it('does not create any new repositories if every directory has a repository', function () { + it('does not create any new repositories if every directory has a repository', () => { const repositories = atom.project.getRepositories() expect(repositories.length).toEqual(1) expect(repositories[0]).toBeTruthy() @@ -315,12 +315,12 @@ describe('Project', function () { runs(() => expect(atom.project.getRepositories()).toBe(repositories)) }) - it('stops using it to create repositories when the service is removed', function () { + it('stops using it to create repositories when the service is removed', () => { atom.project.setPaths([]) const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) waitsFor(() => atom.project.repositoryProviders.length > 1) - runs(function () { + runs(() => { disposable.dispose() atom.project.addPath(temp.mkdirSync('atom-project')) expect(atom.project.getRepositories()).toEqual([null]) @@ -328,7 +328,7 @@ describe('Project', function () { }) }) - describe('when a custom directory-provider service is provided', function () { + describe('when a custom directory-provider service is provided', () => { class DummyDirectory { constructor (path1) { this.path = path1 @@ -343,7 +343,7 @@ describe('Project', function () { let serviceDisposable = null - beforeEach(function () { + beforeEach(() => { serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { directoryForURISync (uri) { if (uri.startsWith('ssh://')) { @@ -357,7 +357,7 @@ describe('Project', function () { waitsFor(() => atom.project.directoryProviders.length > 0) }) - it("uses the provider's custom directories for any paths that it handles", function () { + it("uses the provider's custom directories for any paths that it handles", () => { const localPath = temp.mkdirSync('local-path') const remotePath = 'ssh://foreign-directory:8080/does-exist' @@ -382,28 +382,28 @@ describe('Project', function () { expect(directories[2] instanceof DummyDirectory).toBe(true) }) - it('stops using the provider when the service is removed', function () { + it('stops using the provider when the service is removed', () => { serviceDisposable.dispose() atom.project.setPaths(['ssh://foreign-directory:8080/does-exist']) expect(atom.project.getDirectories().length).toBe(0) }) }) - describe('.open(path)', function () { + describe('.open(path)', () => { let absolutePath, newBufferHandler - beforeEach(function () { + beforeEach(() => { absolutePath = require.resolve('./fixtures/dir/a') newBufferHandler = jasmine.createSpy('newBufferHandler') atom.project.onDidAddBuffer(newBufferHandler) }) describe("when given an absolute path that isn't currently open", () => - it("returns a new edit session for the given path and emits 'buffer-created'", function () { + it("returns a new edit session for the given path and emits 'buffer-created'", () => { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) - runs(function () { + runs(() => { expect(editor.buffer.getPath()).toBe(absolutePath) expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) @@ -411,11 +411,11 @@ describe('Project', function () { ) describe("when given a relative path that isn't currently opened", () => - it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", function () { + it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) - runs(function () { + runs(() => { expect(editor.buffer.getPath()).toBe(absolutePath) expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) @@ -423,7 +423,7 @@ describe('Project', function () { ) describe('when passed the path to a buffer that is currently opened', () => - it('returns a new edit session containing currently opened buffer', function () { + it('returns a new edit session containing currently opened buffer', () => { let editor = null waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) @@ -435,7 +435,7 @@ describe('Project', function () { ) waitsForPromise(() => - atom.workspace.open('a').then(function ({buffer}) { + atom.workspace.open('a').then(({buffer}) => { expect(buffer).toBe(editor.buffer) expect(newBufferHandler).not.toHaveBeenCalled() }) @@ -444,11 +444,11 @@ describe('Project', function () { ) describe('when not passed a path', () => - it("returns a new edit session and emits 'buffer-created'", function () { + it("returns a new edit session and emits 'buffer-created'", () => { let editor = null waitsForPromise(() => atom.workspace.open().then(o => editor = o)) - runs(function () { + runs(() => { expect(editor.buffer.getPath()).toBeUndefined() expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) }) @@ -456,12 +456,12 @@ describe('Project', function () { ) }) - describe('.bufferForPath(path)', function () { + describe('.bufferForPath(path)', () => { let buffer = null beforeEach(() => waitsForPromise(() => - atom.project.bufferForPath('a').then(function (o) { + atom.project.bufferForPath('a').then((o) => { buffer = o buffer.retain() }) @@ -470,8 +470,8 @@ describe('Project', function () { afterEach(() => buffer.release()) - describe('when opening a previously opened path', function () { - it('does not create a new buffer', function () { + describe('when opening a previously opened path', () => { + it('does not create a new buffer', () => { waitsForPromise(() => atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer)) ) @@ -484,25 +484,25 @@ describe('Project', function () { Promise.all([ atom.project.bufferForPath('c'), atom.project.bufferForPath('c') - ]).then(function ([buffer1, buffer2]) { + ]).then(([buffer1, buffer2]) => { expect(buffer1).toBe(buffer2) }) ) }) - it('retries loading the buffer if it previously failed', function () { - waitsForPromise({shouldReject: true}, function () { + it('retries loading the buffer if it previously failed', () => { + waitsForPromise({shouldReject: true}, () => { spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file'))) return atom.project.bufferForPath('b') }) - waitsForPromise({shouldReject: false}, function () { + waitsForPromise({shouldReject: false}, () => { TextBuffer.load.andCallThrough() return atom.project.bufferForPath('b') }) }) - it('creates a new buffer if the previous buffer was destroyed', function () { + it('creates a new buffer if the previous buffer was destroyed', () => { buffer.release() waitsForPromise(() => @@ -512,11 +512,11 @@ describe('Project', function () { }) }) - describe('.repositoryForDirectory(directory)', function () { + describe('.repositoryForDirectory(directory)', () => { it('resolves to null when the directory does not have a repository', () => - waitsForPromise(function () { + waitsForPromise(() => { const directory = new Directory('/tmp') - return atom.project.repositoryForDirectory(directory).then(function (result) { + return atom.project.repositoryForDirectory(directory).then((result) => { expect(result).toBeNull() expect(atom.project.repositoryProviders.length).toBeGreaterThan(0) expect(atom.project.repositoryPromisesByPath.size).toBe(0) @@ -525,10 +525,10 @@ describe('Project', function () { ) it('resolves to a GitRepository and is cached when the given directory is a Git repo', () => - waitsForPromise(function () { + waitsForPromise(() => { const directory = new Directory(path.join(__dirname, '..')) const promise = atom.project.repositoryForDirectory(directory) - return promise.then(function (result) { + return promise.then((result) => { expect(result).toBeInstanceOf(GitRepository) const dirPath = directory.getRealPathSync() expect(result.getPath()).toBe(path.join(dirPath, '.git')) @@ -539,13 +539,13 @@ describe('Project', function () { }) ) - it('creates a new repository if a previous one with the same directory had been destroyed', function () { + it('creates a new repository if a previous one with the same directory had been destroyed', () => { let repository = null const directory = new Directory(path.join(__dirname, '..')) waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) - runs(function () { + runs(() => { expect(repository.isDestroyed()).toBe(false) repository.destroy() expect(repository.isDestroyed()).toBe(true) @@ -557,9 +557,9 @@ describe('Project', function () { }) }) - describe('.setPaths(paths, options)', function () { + describe('.setPaths(paths, options)', () => { describe('when path is a file', () => - it("sets its path to the file's parent directory and updates the root directory", function () { + it("sets its path to the file's parent directory and updates the root directory", () => { const filePath = require.resolve('./fixtures/dir/a') atom.project.setPaths([filePath]) expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath)) @@ -567,8 +567,8 @@ describe('Project', function () { }) ) - describe('when path is a directory', function () { - it('assigns the directories and repositories', function () { + describe('when path is a directory', () => { + it('assigns the directories and repositories', () => { const directory1 = temp.mkdirSync('non-git-repo') const directory2 = temp.mkdirSync('git-repo1') const directory3 = temp.mkdirSync('git-repo2') @@ -587,7 +587,7 @@ describe('Project', function () { expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git'))) }) - it('calls callbacks registered with ::onDidChangePaths', function () { + it('calls callbacks registered with ::onDidChangePaths', () => { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) @@ -598,7 +598,7 @@ describe('Project', function () { expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) }) - it('optionally throws an error with any paths that did not exist', function () { + it('optionally throws an error with any paths that did not exist', () => { const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1'] try { @@ -613,22 +613,22 @@ describe('Project', function () { }) describe('when no paths are given', () => - it('clears its path', function () { + it('clears its path', () => { atom.project.setPaths([]) expect(atom.project.getPaths()).toEqual([]) expect(atom.project.getDirectories()).toEqual([]) }) ) - it('normalizes the path to remove consecutive slashes, ., and .. segments', function () { + it('normalizes the path to remove consecutive slashes, ., and .. segments', () => { atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`]) expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a'))) }) }) - describe('.addPath(path, options)', function () { - it('calls callbacks registered with ::onDidChangePaths', function () { + describe('.addPath(path, options)', () => { + it('calls callbacks registered with ::onDidChangePaths', () => { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) @@ -641,7 +641,7 @@ describe('Project', function () { expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath]) }) - it("doesn't add redundant paths", function () { + it("doesn't add redundant paths", () => { const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) const [oldPath] = atom.project.getPaths() @@ -663,7 +663,7 @@ describe('Project', function () { expect(onDidChangePathsSpy).toHaveBeenCalled() }) - it("doesn't add non-existent directories", function () { + it("doesn't add non-existent directories", () => { const previousPaths = atom.project.getPaths() atom.project.addPath('/this-definitely/does-not-exist') expect(atom.project.getPaths()).toEqual(previousPaths) @@ -674,15 +674,15 @@ describe('Project', function () { ) }) - describe('.removePath(path)', function () { + describe('.removePath(path)', () => { let onDidChangePathsSpy = null - beforeEach(function () { + beforeEach(() => { onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') atom.project.onDidChangePaths(onDidChangePathsSpy) }) - it('removes the directory and repository for the path', function () { + it('removes the directory and repository for the path', () => { const result = atom.project.removePath(atom.project.getPaths()[0]) expect(atom.project.getDirectories()).toEqual([]) expect(atom.project.getRepositories()).toEqual([]) @@ -691,7 +691,7 @@ describe('Project', function () { expect(onDidChangePathsSpy).toHaveBeenCalled() }) - it("does nothing if the path is not one of the project's root paths", function () { + it("does nothing if the path is not one of the project's root paths", () => { const originalPaths = atom.project.getPaths() const result = atom.project.removePath(originalPaths[0] + 'xyz') expect(result).toBe(false) @@ -699,14 +699,14 @@ describe('Project', function () { expect(onDidChangePathsSpy).not.toHaveBeenCalled() }) - it("doesn't destroy the repository if it is shared by another root directory", function () { + it("doesn't destroy the repository if it is shared by another root directory", () => { atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')]) atom.project.removePath(__dirname) expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')]) expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false) }) - it('removes a path that is represented as a URI', function () { + it('removes a path that is represented as a URI', () => { atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { directoryForURISync (uri) { return { @@ -729,13 +729,13 @@ describe('Project', function () { }) }) - describe('.onDidChangeFiles()', function () { + describe('.onDidChangeFiles()', () => { let sub = [] const events = [] - let checkCallback = function () {} + let checkCallback = () => {} beforeEach(() => - sub = atom.project.onDidChangeFiles(function (incoming) { + sub = atom.project.onDidChangeFiles((incoming) => { events.push(...incoming || []) checkCallback() }) @@ -743,16 +743,16 @@ describe('Project', function () { afterEach(() => sub.dispose()) - const waitForEvents = function (paths) { + const waitForEvents = (paths) => { const remaining = new Set(paths.map((p) => fs.realpathSync(p))) - return new Promise(function (resolve, reject) { - checkCallback = function () { + return new Promise((resolve, reject) => { + checkCallback = () => { for (let event of events) { remaining.delete(event.path) } if (remaining.size === 0) { resolve() } } - const expire = function () { - checkCallback = function () {} + const expire = () => { + checkCallback = () => {} console.error('Paths not seen:', remaining) reject(new Error('Expired before all expected events were delivered.')) } @@ -762,7 +762,7 @@ describe('Project', function () { }) } - it('reports filesystem changes within project paths', function () { + it('reports filesystem changes within project paths', () => { const dirOne = temp.mkdirSync('atom-spec-project-one') const fileOne = path.join(dirOne, 'file-one.txt') const fileTwo = path.join(dirOne, 'file-two.txt') @@ -775,7 +775,7 @@ describe('Project', function () { runs(() => atom.project.setPaths([dirOne])) waitsForPromise(() => atom.project.getWatcherPromise(dirOne)) - runs(function () { + runs(() => { expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined) fs.writeFileSync(fileThree, 'three\n') @@ -790,7 +790,7 @@ describe('Project', function () { }) describe('.onDidAddBuffer()', () => - it('invokes the callback with added text buffers', function () { + it('invokes the callback with added text buffers', () => { const buffers = [] const added = [] @@ -799,7 +799,7 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - runs(function () { + runs(() => { expect(buffers.length).toBe(1) atom.project.onDidAddBuffer(buffer => added.push(buffer)) }) @@ -809,7 +809,7 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - runs(function () { + runs(() => { expect(buffers.length).toBe(2) expect(added).toEqual([buffers[1]]) }) @@ -817,7 +817,7 @@ describe('Project', function () { ) describe('.observeBuffers()', () => - it('invokes the observer with current and future text buffers', function () { + it('invokes the observer with current and future text buffers', () => { const buffers = [] const observed = [] @@ -831,7 +831,7 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - runs(function () { + runs(() => { expect(buffers.length).toBe(2) atom.project.observeBuffers(buffer => observed.push(buffer)) expect(observed).toEqual(buffers) @@ -842,7 +842,7 @@ describe('Project', function () { .then(o => buffers.push(o)) ) - runs(function () { + runs(() => { expect(observed.length).toBe(3) expect(buffers.length).toBe(3) expect(observed).toEqual(buffers) @@ -850,8 +850,8 @@ describe('Project', function () { }) ) - describe('.relativize(path)', function () { - it('returns the path, relative to whichever root directory it is inside of', function () { + describe('.relativize(path)', () => { + it('returns the path, relative to whichever root directory it is inside of', () => { atom.project.addPath(temp.mkdirSync('another-path')) let rootPath = atom.project.getPaths()[0] @@ -863,14 +863,14 @@ describe('Project', function () { expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory')) }) - it('returns the given path if it is not in any of the root directories', function () { + it('returns the given path if it is not in any of the root directories', () => { const randomPath = path.join('some', 'random', 'path') expect(atom.project.relativize(randomPath)).toBe(randomPath) }) }) - describe('.relativizePath(path)', function () { - it('returns the root path that contains the given path, and the path relativized to that root path', function () { + describe('.relativizePath(path)', () => { + it('returns the root path that contains the given path, and the path relativized to that root path', () => { atom.project.addPath(temp.mkdirSync('another-path')) let rootPath = atom.project.getPaths()[0] @@ -883,21 +883,21 @@ describe('Project', function () { }) describe("when the given path isn't inside of any of the project's path", () => - it('returns null for the root path, and the given path unchanged', function () { + it('returns null for the root path, and the given path unchanged', () => { const randomPath = path.join('some', 'random', 'path') expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath]) }) ) describe('when the given path is a URL', () => - it('returns null for the root path, and the given path unchanged', function () { + it('returns null for the root path, and the given path unchanged', () => { const url = 'http://the-path' expect(atom.project.relativizePath(url)).toEqual([null, url]) }) ) describe('when the given path is inside more than one root folder', () => - it('uses the root folder that is closest to the given path', function () { + it('uses the root folder that is closest to the given path', () => { atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir')) const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt') @@ -913,7 +913,7 @@ describe('Project', function () { }) describe('.contains(path)', () => - it('returns whether or not the given path is in one of the root directories', function () { + it('returns whether or not the given path is in one of the root directories', () => { const rootPath = atom.project.getPaths()[0] const childPath = path.join(rootPath, 'some', 'child', 'directory') expect(atom.project.contains(childPath)).toBe(true) From 49655a97c84458e88d203d976a3c5d8b4593ae2a Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 18 Oct 2017 20:10:24 -0400 Subject: [PATCH 096/301] :art: --- spec/project-spec.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 8cd092126..747defc3f 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -1,9 +1,3 @@ -/* - * decaffeinate suggestions: - * DS201: Simplify complex destructure assignments - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const temp = require('temp').track() const TextBuffer = require('text-buffer') const Project = require('../src/project') @@ -35,7 +29,9 @@ describe('Project', () => { if (notQuittingProject != null) { notQuittingProject.destroy() } - (quittingProject != null ? quittingProject.destroy() : undefined) + if (quittingProject != null) { + quittingProject.destroy() + } }) it("does not deserialize paths to directories that don't exist", () => { @@ -330,8 +326,8 @@ describe('Project', () => { describe('when a custom directory-provider service is provided', () => { class DummyDirectory { - constructor (path1) { - this.path = path1 + constructor (aPath) { + this.path = aPath } getPath () { return this.path } getFile () { return {existsSync () { return false }} } @@ -736,7 +732,7 @@ describe('Project', () => { beforeEach(() => sub = atom.project.onDidChangeFiles((incoming) => { - events.push(...incoming || []) + events.push(...incoming) checkCallback() }) ) @@ -924,6 +920,8 @@ describe('Project', () => { ) describe('.resolvePath(uri)', () => - it('normalizes disk drive letter in passed path on #win32', () => expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')) + it('normalizes disk drive letter in passed path on #win32', () => { + expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt') + }) ) }) From 4db60e34b8220c624ce245d8f05d4f0f90ab431c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 18 Oct 2017 20:13:55 -0400 Subject: [PATCH 097/301] =?UTF-8?q?=F0=9F=91=94=20Fix=20linter=20error:=20?= =?UTF-8?q?"Arrow=20function=20should=20not=20return=20assignment."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/project-spec.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 747defc3f..63c065fa6 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -42,7 +42,7 @@ describe('Project', () => { let err = null waitsForPromise(() => deserializedProject.deserialize(state, atom.deserializers) - .catch(e => err = e) + .catch(e => { err = e }) ) runs(() => { @@ -65,7 +65,7 @@ describe('Project', () => { let err = null waitsForPromise(() => deserializedProject.deserialize(state, atom.deserializers) - .catch(e => err = e) + .catch(e => { err = e }) ) runs(() => { @@ -216,7 +216,7 @@ describe('Project', () => { expect(atom.project.getPaths()[0]).toBeUndefined() let editor = null - waitsForPromise(() => atom.workspace.open().then(o => editor = o)) + waitsForPromise(() => atom.workspace.open().then(o => { editor = o })) waitsForPromise(() => editor.saveAs(tempFile)) @@ -255,7 +255,7 @@ describe('Project', () => { describe('when a watch error is thrown from the TextBuffer', () => { let editor = null beforeEach(() => - waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => editor = o)) + waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => { editor = o })) ) it('creates a warning notification', () => { @@ -397,7 +397,7 @@ describe('Project', () => { describe("when given an absolute path that isn't currently open", () => it("returns a new edit session for the given path and emits 'buffer-created'", () => { let editor = null - waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o })) runs(() => { expect(editor.buffer.getPath()).toBe(absolutePath) @@ -409,7 +409,7 @@ describe('Project', () => { describe("when given a relative path that isn't currently opened", () => it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => { let editor = null - waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o })) runs(() => { expect(editor.buffer.getPath()).toBe(absolutePath) @@ -422,7 +422,7 @@ describe('Project', () => { it('returns a new edit session containing currently opened buffer', () => { let editor = null - waitsForPromise(() => atom.workspace.open(absolutePath).then(o => editor = o)) + waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o })) runs(() => newBufferHandler.reset()) @@ -442,7 +442,7 @@ describe('Project', () => { describe('when not passed a path', () => it("returns a new edit session and emits 'buffer-created'", () => { let editor = null - waitsForPromise(() => atom.workspace.open().then(o => editor = o)) + waitsForPromise(() => atom.workspace.open().then(o => { editor = o })) runs(() => { expect(editor.buffer.getPath()).toBeUndefined() @@ -539,7 +539,7 @@ describe('Project', () => { let repository = null const directory = new Directory(path.join(__dirname, '..')) - waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) + waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo })) runs(() => { expect(repository.isDestroyed()).toBe(false) @@ -547,7 +547,7 @@ describe('Project', () => { expect(repository.isDestroyed()).toBe(true) }) - waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => repository = repo)) + waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo })) runs(() => expect(repository.isDestroyed()).toBe(false)) }) @@ -730,12 +730,12 @@ describe('Project', () => { const events = [] let checkCallback = () => {} - beforeEach(() => + beforeEach(() => { sub = atom.project.onDidChangeFiles((incoming) => { events.push(...incoming) checkCallback() }) - ) + }) afterEach(() => sub.dispose()) From 2901a484c2c796664abf10f75186b87ad5eeef4b Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 18 Oct 2017 17:34:24 -0700 Subject: [PATCH 098/301] :shirt: --- src/atom-environment.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7bbc513d6..b9c6306ab 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -239,7 +239,7 @@ class AtomEnvironment extends Model @commandInstaller.initialize(@getVersion()) @protocolHandlerInstaller.initialize(@config, @notifications) - @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(@)) + @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) @autoUpdater.initialize() @config.load() From d79e6c4b6352e708d97b1d23b29856c9af06c857 Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Wed, 18 Oct 2017 17:52:03 -0700 Subject: [PATCH 099/301] :arrow_up: tabs@0.108.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc454e3ab..b7e5ddd12 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.13", "styleguide": "0.49.7", "symbols-view": "0.118.1", - "tabs": "0.107.4", + "tabs": "0.108.0", "timecop": "0.36.0", "tree-view": "0.219.0", "update-package-dependencies": "0.12.0", From 50243c71f5fec064176c2616290f180f05cd0d18 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Wed, 18 Oct 2017 21:23:08 -0600 Subject: [PATCH 100/301] :arrow_up: autocomplete-snippets@1.11.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7e5ddd12..55b5dfd04 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", "autocomplete-plus": "2.36.7", - "autocomplete-snippets": "1.11.1", + "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", "background-tips": "0.27.1", From 53203e7f1767035e1431000c02e555d6e690b6d8 Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Wed, 18 Oct 2017 21:27:11 -0700 Subject: [PATCH 101/301] :arrow_up: tree-view@0.220.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55b5dfd04..31c2c917e 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.108.0", "timecop": "0.36.0", - "tree-view": "0.219.0", + "tree-view": "0.220.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.4", From 2289e2b8286fa28404469976599db6db26c07054 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 19 Oct 2017 08:42:20 -0400 Subject: [PATCH 102/301] Decaffeinate src/window-event-handler.coffee --- src/window-event-handler.coffee | 189 ------------------------ src/window-event-handler.js | 253 ++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 189 deletions(-) delete mode 100644 src/window-event-handler.coffee create mode 100644 src/window-event-handler.js diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee deleted file mode 100644 index 6a277b612..000000000 --- a/src/window-event-handler.coffee +++ /dev/null @@ -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) diff --git a/src/window-event-handler.js b/src/window-event-handler.js new file mode 100644 index 000000000..6d380819b --- /dev/null +++ b/src/window-event-handler.js @@ -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) + } +} From f3dc52c0bd610a7c54971b4a515bed906f0d5d38 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Thu, 19 Oct 2017 17:07:29 +0200 Subject: [PATCH 103/301] :arrow_up: language-perl@0.38.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31c2c917e..64ee0d497 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", - "language-perl": "0.37.0", + "language-perl": "0.38.0", "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", From fc83739e28f5f6b1044215050d4de70f106ae8fd Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 19 Oct 2017 17:54:22 +0200 Subject: [PATCH 104/301] Revert "Merge pull request #15939 from atom/fk_update_perl" This reverts commit cee38a41d5105b1c34b72338db5f2a49ab2e930c, reversing changes made to 53203e7f1767035e1431000c02e555d6e690b6d8. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64ee0d497..31c2c917e 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", - "language-perl": "0.38.0", + "language-perl": "0.37.0", "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", From 02b13384437b680d150d3a8a911ba45070acb9ad Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 19 Oct 2017 12:37:12 -0600 Subject: [PATCH 105/301] :arrow_up: autocomplete-plus@2.36.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31c2c917e..e5541ff0e 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.7", + "autocomplete-plus": "2.36.8", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From 158622ce48f02fbace7a7617acef619ebe9a0c24 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 19 Oct 2017 14:19:24 -0700 Subject: [PATCH 106/301] Convert array of windows in AtomApplication to a WindowStack --- spec/main-process/atom-application.test.js | 10 +-- src/main-process/atom-application.coffee | 75 +++++++++++++++------- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 62fae82b3..6434710ce 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -137,7 +137,7 @@ describe('AtomApplication', function () { // Does not change the project paths when doing so. const reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath])) assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.windows, [window1]) + assert.deepEqual(atomApplication.getAllWindows(), [window1]) activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) { sendBackToMainProcess(textEditor.getPath()) @@ -177,7 +177,7 @@ describe('AtomApplication', function () { // parent directory to the project let reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add'])) assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.windows, [window1]) + assert.deepEqual(atomApplication.getAllWindows(), [window1]) activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) { sendBackToMainProcess(textEditor.getPath()) @@ -191,7 +191,7 @@ describe('AtomApplication', function () { // the directory to the project reusedWindow = atomApplication.launch(parseCommandLine([dirBPath, '-a'])) assert.equal(reusedWindow, window1) - assert.deepEqual(atomApplication.windows, [window1]) + assert.deepEqual(atomApplication.getAllWindows(), [window1]) await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length === 3) assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath, dirBPath]) @@ -276,7 +276,7 @@ describe('AtomApplication', function () { }) assert.equal(window2EditorTitle, 'untitled') - assert.deepEqual(atomApplication.windows, [window1, window2]) + assert.deepEqual(atomApplication.getAllWindows(), [window2, window1]) }) it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async function () { @@ -514,7 +514,7 @@ describe('AtomApplication', function () { async function focusWindow (window) { window.focus() await window.loadedPromise - await conditionPromise(() => window.atomApplication.lastFocusedWindow === window) + await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window) } function mockElectronAppQuit () { diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 0c587020e..f17aef902 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -67,7 +67,7 @@ class AtomApplication {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options @socketPath = null if options.test or options.benchmark or options.benchmarkTest @pidsToOpenWindows = {} - @windows = [] + @windows = new WindowStack() @config = new Config({enablePersistence: true}) @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} @@ -114,7 +114,7 @@ class AtomApplication @launch(options) destroy: -> - windowsClosePromises = @windows.map (window) -> + windowsClosePromises = @getAllWindows().map (window) -> window.close() window.closedPromise Promise.all(windowsClosePromises).then(=> @disposable.dispose()) @@ -162,8 +162,8 @@ class AtomApplication # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> - @windows.splice(@windows.indexOf(window), 1) - if @windows.length is 0 + @windows.removeWindow(window) + if @getAllWindows().length is 0 @applicationMenu?.enableWindowSpecificItems(false) if process.platform in ['win32', 'linux'] app.quit() @@ -172,22 +172,28 @@ class AtomApplication # Public: Adds the {AtomWindow} to the global window list. addWindow: (window) -> - @windows.push window + @windows.addWindow(window) @applicationMenu?.addWindow(window.browserWindow) window.once 'window:loaded', => @autoUpdateManager?.emitUpdateAvailableEvent(window) unless window.isSpec - focusHandler = => @lastFocusedWindow = window + focusHandler = => @windows.touch(window) blurHandler = => @saveState(false) window.browserWindow.on 'focus', focusHandler window.browserWindow.on 'blur', blurHandler window.browserWindow.once 'closed', => - @lastFocusedWindow = null if window is @lastFocusedWindow + @windows.removeWindow(window) window.browserWindow.removeListener 'focus', focusHandler window.browserWindow.removeListener 'blur', blurHandler window.browserWindow.webContents.once 'did-finish-load', => @saveState(false) + getAllWindows: () => + @windows.all().slice() + + getLastFocusedWindow: () => + @windows.getLastFocusedWindow() + # Creates server to listen for additional atom application launches. # # You can run the atom command multiple times, but after the first launch @@ -276,7 +282,7 @@ class AtomApplication else event.preventDefault() @quitting = true - windowUnloadPromises = @windows.map((window) -> window.prepareToUnload()) + windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload()) Promise.all(windowUnloadPromises).then((windowUnloadedResults) -> didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow) app.quit() if didUnloadAllWindows @@ -309,7 +315,7 @@ class AtomApplication event.sender.send('did-resolve-proxy', requestId, proxy) @disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) => - for atomWindow in @windows + for atomWindow in @getAllWindows() webContents = atomWindow.browserWindow.webContents if webContents isnt event.sender webContents.send('did-change-history-manager') @@ -483,7 +489,7 @@ class AtomApplication # Returns the {AtomWindow} for the given paths. windowForPaths: (pathsToOpen, devMode) -> - _.find @windows, (atomWindow) -> + _.find @getAllWindows(), (atomWindow) -> atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen) # Returns the {AtomWindow} for the given ipcMain event. @@ -491,11 +497,11 @@ class AtomApplication @atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender)) atomWindowForBrowserWindow: (browserWindow) -> - @windows.find((atomWindow) -> atomWindow.browserWindow is browserWindow) + @getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow) # Public: Returns the currently focused {AtomWindow} or undefined if none. focusedWindow: -> - _.find @windows, (atomWindow) -> atomWindow.isFocused() + _.find @getAllWindows(), (atomWindow) -> atomWindow.isFocused() # Get the platform-specific window offset for new windows. getWindowOffsetForCurrentPlatform: -> @@ -507,8 +513,8 @@ class AtomApplication # Get the dimensions for opening a new window by cascading as appropriate to # the platform. getDimensionsForNewWindow: -> - return if (@focusedWindow() ? @lastFocusedWindow)?.isMaximized() - dimensions = (@focusedWindow() ? @lastFocusedWindow)?.getDimensions() + return if (@focusedWindow() ? @getLastFocusedWindow())?.isMaximized() + dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.getDimensions() offset = @getWindowOffsetForCurrentPlatform() if dimensions? and offset? dimensions.x += offset @@ -554,7 +560,7 @@ class AtomApplication existingWindow = @windowForPaths(pathsToOpen, devMode) stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen) unless existingWindow? - if currentWindow = window ? @lastFocusedWindow + if currentWindow = window ? @getLastFocusedWindow() existingWindow = currentWindow if ( addToLastWindow or currentWindow.devMode is devMode and @@ -583,7 +589,7 @@ class AtomApplication windowDimensions ?= @getDimensionsForNewWindow() openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env}) openedWindow.focus() - @lastFocusedWindow = openedWindow + @windows.addWindow(openedWindow) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow @@ -617,9 +623,10 @@ class AtomApplication saveState: (allowEmpty=false) -> return if @quitting states = [] - for window in @windows + for window in @getAllWindows() unless window.isSpec states.push({initialPaths: window.representedDirectoryPaths}) + states.reverse() if states.length > 0 or allowEmpty @storageFolder.storeSync('application.json', states) @emit('application:did-save-state') @@ -665,13 +672,14 @@ class AtomApplication resourcePath = @devResourcePath windowInitializationScript ?= require.resolve('../initialize-application-window') - if @lastFocusedWindow? - @lastFocusedWindow.sendURIMessage url + if @getLastFocusedWindow()? + @getLastFocusedWindow().sendURIMessage url else windowDimensions = @getDimensionsForNewWindow() - @lastFocusedWindow = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) - @lastFocusedWindow.on 'window:loaded', => - @lastFocusedWindow.sendURIMessage url + win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) + @windows.addWindow(win) + win.on 'window:loaded', => + win.sendURIMessage url findPackageWithName: (packageName, devMode) -> _.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName @@ -867,7 +875,7 @@ class AtomApplication disableZoomOnDisplayChange: -> outerCallback = => - for window in @windows + for window in @getAllWindows() window.disableZoom() # Set the limits every time a display is added or removed, otherwise the @@ -878,3 +886,24 @@ class AtomApplication new Disposable -> screen.removeListener('display-added', outerCallback) screen.removeListener('display-removed', outerCallback) + +class WindowStack + constructor: (@windows = []) -> + + addWindow: (window) => + @removeWindow(window) + @windows.unshift(window) + + touch: (window) => + @addWindow(window) + + removeWindow: (window) => + currentIndex = @windows.indexOf(window) + @windows.splice(currentIndex, 1) if currentIndex > -1 + + getLastFocusedWindow: (predicate) => + predicate ?= (win) => true + @windows.find(predicate) + + all: () => + @windows From e1bc9b593b01e062e800d71a945ab5898f255d4e Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 19 Oct 2017 14:37:08 -0700 Subject: [PATCH 107/301] Run URI handlers in last non-spec window --- src/main-process/atom-application.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index f17aef902..76b0d2bed 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -191,8 +191,8 @@ class AtomApplication getAllWindows: () => @windows.all().slice() - getLastFocusedWindow: () => - @windows.getLastFocusedWindow() + getLastFocusedWindow: (predicate) => + @windows.getLastFocusedWindow(predicate) # Creates server to listen for additional atom application launches. # @@ -672,8 +672,10 @@ class AtomApplication resourcePath = @devResourcePath windowInitializationScript ?= require.resolve('../initialize-application-window') - if @getLastFocusedWindow()? - @getLastFocusedWindow().sendURIMessage url + lastNonSpecWindow = @getLastFocusedWindow (win) -> !win.isSpecWindow() + if lastNonSpecWindow? + lastNonSpecWindow.sendURIMessage url + lastNonSpecWindow.focus() else windowDimensions = @getDimensionsForNewWindow() win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) From 8111ba6c1ede4674ce0f52603e7f321531eca911 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 19 Oct 2017 15:47:57 -0700 Subject: [PATCH 108/301] Allow core URI handlers to determine which window to trigger the URI on --- src/core-uri-handlers.js | 22 +++++++++++++--- src/main-process/atom-application.coffee | 32 ++++++++++++++---------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/core-uri-handlers.js b/src/core-uri-handlers.js index c575b3f40..2af00f610 100644 --- a/src/core-uri-handlers.js +++ b/src/core-uri-handlers.js @@ -8,17 +8,31 @@ function openFile (atom, {query}) { }) } +function windowShouldOpenFile ({query}) { + const {filename} = query + return (win) => win.containsPath(filename) +} + const ROUTER = { - '/open/file': openFile + '/open/file': { handler: openFile, getWindowPredicate: windowShouldOpenFile } } module.exports = { create (atomEnv) { return function coreURIHandler (parsed) { - const handler = ROUTER[parsed.pathname] - if (handler) { - handler(atomEnv, parsed) + const config = ROUTER[parsed.pathname] + if (config) { + config.handler(atomEnv, parsed) } } + }, + + windowPredicate (parsed) { + const config = ROUTER[parsed.pathname] + if (config && config.getWindowPredicate) { + return config.getWindowPredicate(parsed) + } else { + return (win) => true + } } } diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 76b0d2bed..1f4d7214f 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -655,28 +655,34 @@ class AtomApplication # :devMode - Boolean to control the opened window's dev mode. # :safeMode - Boolean to control the opened window's safe mode. openUrl: ({urlToOpen, devMode, safeMode, env}) -> - parsedUrl = url.parse(urlToOpen) + parsedUrl = url.parse(urlToOpen, true) return unless parsedUrl.protocol is "atom:" pack = @findPackageWithName(parsedUrl.host, devMode) if pack?.urlMain @openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env) else - @openPackageUriHandler(urlToOpen, devMode, safeMode, env) + @openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env) - openPackageUriHandler: (url, devMode, safeMode, env) -> - resourcePath = @resourcePath - if devMode - try - windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) - resourcePath = @devResourcePath + openPackageUriHandler: (url, parsedUrl, devMode, safeMode, env) -> + bestWindow = null + if parsedUrl.host is 'core' + predicate = require('../core-uri-handlers').windowPredicate(parsedUrl) + bestWindow = @getLastFocusedWindow (win) -> + !win.isSpecWindow() && predicate(win) - windowInitializationScript ?= require.resolve('../initialize-application-window') - lastNonSpecWindow = @getLastFocusedWindow (win) -> !win.isSpecWindow() - if lastNonSpecWindow? - lastNonSpecWindow.sendURIMessage url - lastNonSpecWindow.focus() + bestWindow ?= @getLastFocusedWindow (win) -> !win.isSpecWindow() + if bestWindow? + bestWindow.sendURIMessage url + bestWindow.focus() else + resourcePath = @resourcePath + if devMode + try + windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window')) + resourcePath = @devResourcePath + + windowInitializationScript ?= require.resolve('../initialize-application-window') windowDimensions = @getDimensionsForNewWindow() win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) @windows.addWindow(win) From 662a3978604ebcac0f7693a6422ec87a250a36c0 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 19 Oct 2017 16:19:44 -0700 Subject: [PATCH 109/301] :shirt: --- src/main-process/atom-application.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 1f4d7214f..fc2058dd4 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -188,7 +188,7 @@ class AtomApplication window.browserWindow.removeListener 'blur', blurHandler window.browserWindow.webContents.once 'did-finish-load', => @saveState(false) - getAllWindows: () => + getAllWindows: => @windows.all().slice() getLastFocusedWindow: (predicate) => @@ -669,9 +669,9 @@ class AtomApplication if parsedUrl.host is 'core' predicate = require('../core-uri-handlers').windowPredicate(parsedUrl) bestWindow = @getLastFocusedWindow (win) -> - !win.isSpecWindow() && predicate(win) + not win.isSpecWindow() and predicate(win) - bestWindow ?= @getLastFocusedWindow (win) -> !win.isSpecWindow() + bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow() if bestWindow? bestWindow.sendURIMessage url bestWindow.focus() @@ -686,7 +686,7 @@ class AtomApplication windowDimensions = @getDimensionsForNewWindow() win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) @windows.addWindow(win) - win.on 'window:loaded', => + win.on 'window:loaded', -> win.sendURIMessage url findPackageWithName: (packageName, devMode) -> @@ -910,8 +910,8 @@ class WindowStack @windows.splice(currentIndex, 1) if currentIndex > -1 getLastFocusedWindow: (predicate) => - predicate ?= (win) => true + predicate ?= (win) -> true @windows.find(predicate) - all: () => + all: => @windows From 9fcc6a9bce21c2404bdce269e02a81f0b7a51682 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 20 Oct 2017 01:27:15 +0200 Subject: [PATCH 110/301] Use endsWith to match modules to exclude from the snapshot --- script/lib/generate-startup-snapshot.js | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 2905bca1b..333acdc0a 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -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) => { From 31cc7251383e50d31d291a0882394eafd783a94f Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Fri, 14 Apr 2017 16:01:12 +0300 Subject: [PATCH 111/301] :arrow_up: coffee-script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5541ff0e..18dff266f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "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", "devtron": "1.3.0", From 0f89211d55cbcc5f4fdbfa6f76ed6cb709c98783 Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Fri, 20 Oct 2017 13:34:15 +0300 Subject: [PATCH 112/301] Prioritize first line matches over bundled/non bundled cirteria --- .../packages/package-with-rb-filetype/grammars/rb.cson | 1 + spec/grammars-spec.coffee | 2 ++ src/grammar-registry.js | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson b/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson index 8b4d85412..37aac3d4d 100644 --- a/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson +++ b/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson @@ -1,5 +1,6 @@ 'name': 'Test Ruby' 'scopeName': 'test.rb' +'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)' 'fileTypes': [ 'rb' ] diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index 7b70797ba..db716528d 100644 --- a/spec/grammars-spec.coffee +++ b/spec/grammars-spec.coffee @@ -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", -> diff --git a/src/grammar-registry.js b/src/grammar-registry.js index b1de16ba1..f2994acf1 100644 --- a/src/grammar-registry.js +++ b/src/grammar-registry.js @@ -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 } From d23510fce97012efd1a7fa5db1a95406e8bbd4d5 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 08:20:40 -0400 Subject: [PATCH 113/301] =?UTF-8?q?=E2=98=A0=E2=98=95=EF=B8=8F=20Decaffein?= =?UTF-8?q?ate=20spec/window-event-handler-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/window-event-handler-spec.coffee | 209 ----------------------- spec/window-event-handler-spec.js | 228 ++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 209 deletions(-) delete mode 100644 spec/window-event-handler-spec.coffee create mode 100644 spec/window-event-handler-spec.js diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee deleted file mode 100644 index 9c9f4a098..000000000 --- a/spec/window-event-handler-spec.coffee +++ /dev/null @@ -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 = """ -
- - -
- """ - 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 = """ -
- - - - - - - -
- """ - 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() diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js new file mode 100644 index 000000000..a03e168fa --- /dev/null +++ b/spec/window-event-handler-spec.js @@ -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 = ` +
+ + +
+ `.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 = ` +
+ + + + + + + +
+ `.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() + }) + ) +}) From 9f73b33b086434eae24cecc09af646f328767df9 Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Fri, 20 Oct 2017 16:06:43 +0200 Subject: [PATCH 114/301] :arrow_up: language-perl@0.38.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5541ff0e..dcbeb05c1 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", - "language-perl": "0.37.0", + "language-perl": "0.38.0", "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", From d0bdbb861ba8b0234135460bd86cc968316a3e14 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 20 Oct 2017 11:30:50 -0600 Subject: [PATCH 115/301] update overlay itself instead of text editor when resize occurs --- src/text-editor-component.js | 76 ++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5ff96eec5..18f53e945 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -804,7 +804,12 @@ class TextEditorComponent { key: overlayProps.element, overlayComponents: this.overlayComponents, measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element), - didResize: () => { this.updateSync() } + didResize: (overlayComponent) => { + this.updateOverlayToRender(overlayProps) + overlayComponent.update({ + measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) + }) + } }, overlayProps )) @@ -1339,42 +1344,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) } } @@ -4202,7 +4212,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) }) } }) @@ -4217,7 +4227,7 @@ class OverlayComponent { 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) { From 089717cbd3a8743387ee897bc4edea9afafd5db9 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 20 Oct 2017 15:46:27 -0600 Subject: [PATCH 116/301] fix failing test --- spec/text-editor-component-spec.js | 7 +++++-- src/text-editor-component.js | 24 +++++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 41d770212..d46748d91 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1896,6 +1896,9 @@ describe('TextEditorComponent', () => { const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'}) await component.getNextUpdatePromise() + let overlayComponent + component.overlayComponents.forEach(c => overlayComponent = c) + const overlayWrapper = overlayElement.parentElement expect(overlayWrapper.classList.contains('a')).toBe(true) expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) @@ -1926,12 +1929,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 diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 18f53e945..641cdad02 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -806,9 +806,12 @@ class TextEditorComponent { measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element), didResize: (overlayComponent) => { this.updateOverlayToRender(overlayProps) - overlayComponent.update({ - measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) - }) + overlayComponent.update(Object.assign( + { + measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) + }, + overlayProps + )) } }, overlayProps @@ -4225,6 +4228,19 @@ 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 = Object.assign({}, oldProps, newProps) @@ -4234,6 +4250,8 @@ class OverlayComponent { 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 () { From cdf3be846be712d79ad901de5a531cc79e8a0bcc Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 20:35:40 -0400 Subject: [PATCH 117/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/view-registry.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view-registry.coffee | 201 ------------------------------- src/view-registry.js | 253 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 201 deletions(-) delete mode 100644 src/view-registry.coffee create mode 100644 src/view-registry.js diff --git a/src/view-registry.coffee b/src/view-registry.coffee deleted file mode 100644 index f300cc031..000000000 --- a/src/view-registry.coffee +++ /dev/null @@ -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?() diff --git a/src/view-registry.js b/src/view-registry.js new file mode 100644 index 000000000..d3167cdc1 --- /dev/null +++ b/src/view-registry.js @@ -0,0 +1,253 @@ +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 + if (view = this.views.get(object)) { + return view + } else { + 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] + } + + let viewConstructor + for (let provider of this.providers) { + if (provider.modelConstructor === AnyConstructor) { + if (element = provider.createView(object, this.atomEnvironment)) { + return element + } + continue + } + + if (object instanceof provider.modelConstructor) { + if (element = provider.createView && provider.createView(object, this.atomEnvironment)) { + return element + } + + if (viewConstructor = provider.viewConstructor) { + element = new viewConstructor() + if (element.initialize) { + element.initialize(object) + } else if (element.setModel) { + element.setModel(object) + } + return element + } + } + } + + if (object && object.getViewClass) { + 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 + + let writer + while ((writer = this.documentWriters.shift())) { writer() } + + let reader + this.documentReadInProgress = true + while ((reader = this.documentReaders.shift())) { reader() } + this.documentReadInProgress = false + + // process updates requested as a result of reads + while ((writer = this.documentWriters.shift())) { writer() } + + if (resolveNextUpdatePromise) { resolveNextUpdatePromise() } + } +} From a67272e6fff6094167b0d7bf474973db92bf0e4e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 21:12:53 -0400 Subject: [PATCH 118/301] =?UTF-8?q?=F0=9F=91=94=20Fix=20"Expected=20a=20co?= =?UTF-8?q?nditional=20expression=20&=20instead=20saw=20an=20assignment"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view-registry.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/view-registry.js b/src/view-registry.js index d3167cdc1..37849f999 100644 --- a/src/view-registry.js +++ b/src/view-registry.js @@ -122,14 +122,12 @@ class ViewRegistry { getView (object) { if (object == null) { return } - let view - if (view = this.views.get(object)) { - return view - } else { + let view = this.views.get(object) + if (!view) { view = this.createView(object) this.views.set(object, view) - return view } + return view } createView (object) { @@ -154,18 +152,17 @@ class ViewRegistry { let viewConstructor for (let provider of this.providers) { if (provider.modelConstructor === AnyConstructor) { - if (element = provider.createView(object, this.atomEnvironment)) { - return element - } + element = provider.createView(object, this.atomEnvironment) + if (element) { return element } continue } if (object instanceof provider.modelConstructor) { - if (element = provider.createView && provider.createView(object, this.atomEnvironment)) { - return element - } + element = provider.createView && provider.createView(object, this.atomEnvironment) + if (element) { return element } - if (viewConstructor = provider.viewConstructor) { + viewConstructor = provider.viewConstructor + if (viewConstructor) { element = new viewConstructor() if (element.initialize) { element.initialize(object) From dfd1332a016a8af542d2a4f75a14f40341e533db Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 21:16:28 -0400 Subject: [PATCH 119/301] =?UTF-8?q?=F0=9F=91=94=20Fix=20"A=20constructor?= =?UTF-8?q?=20name=20should=20not=20start=20with=20a=20lowercase=20letter"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view-registry.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/view-registry.js b/src/view-registry.js index 37849f999..dcc1624fc 100644 --- a/src/view-registry.js +++ b/src/view-registry.js @@ -149,7 +149,6 @@ class ViewRegistry { return object[0] } - let viewConstructor for (let provider of this.providers) { if (provider.modelConstructor === AnyConstructor) { element = provider.createView(object, this.atomEnvironment) @@ -161,9 +160,9 @@ class ViewRegistry { element = provider.createView && provider.createView(object, this.atomEnvironment) if (element) { return element } - viewConstructor = provider.viewConstructor - if (viewConstructor) { - element = new viewConstructor() + let ViewConstructor = provider.viewConstructor + if (ViewConstructor) { + element = new ViewConstructor() if (element.initialize) { element.initialize(object) } else if (element.setModel) { @@ -175,9 +174,9 @@ class ViewRegistry { } if (object && object.getViewClass) { - viewConstructor = object.getViewClass() - if (viewConstructor) { - const view = new viewConstructor(object) + let ViewConstructor = object.getViewClass() + if (ViewConstructor) { + const view = new ViewConstructor(object) return view[0] } } From c6d438c5092eb42eae45ca50dbba9dfb5cb950a1 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 21 Oct 2017 09:52:59 -0400 Subject: [PATCH 120/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/view-registry-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/view-registry-spec.coffee | 163 ------------------------ spec/view-registry-spec.js | 218 +++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 163 deletions(-) delete mode 100644 spec/view-registry-spec.coffee create mode 100644 spec/view-registry-spec.js diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee deleted file mode 100644 index 4bae1d811..000000000 --- a/spec/view-registry-spec.coffee +++ /dev/null @@ -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 diff --git a/spec/view-registry-spec.js b/spec/view-registry-spec.js new file mode 100644 index 000000000..984d30718 --- /dev/null +++ b/spec/view-registry-spec.js @@ -0,0 +1,218 @@ +/* + * 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', () => { + const model = {a: 'b'} + + 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(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.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) + }) + }) + ) +}) From 9a6f4b1647a6237c587bdaeb72585a204305bbe2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 21 Oct 2017 10:05:57 -0400 Subject: [PATCH 121/301] =?UTF-8?q?=F0=9F=91=94=20Fix=20"'model'=20is=20as?= =?UTF-8?q?signed=20a=20value=20but=20never=20used"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/view-registry-spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/view-registry-spec.js b/spec/view-registry-spec.js index 984d30718..4459af10c 100644 --- a/spec/view-registry-spec.js +++ b/spec/view-registry-spec.js @@ -87,8 +87,6 @@ describe('ViewRegistry', () => { describe('when a view provider is registered generically, and works with the object', () => it('constructs a view element and assigns the model on it', () => { - const model = {a: 'b'} - registry.addViewProvider((model) => { if (model.a === 'b') { const element = document.createElement('div') From 33aea760588e5126d9c5982c70569b9fc8b85f50 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 21 Oct 2017 10:07:13 -0400 Subject: [PATCH 122/301] =?UTF-8?q?=F0=9F=91=94=20Fix=20"The=20object=20li?= =?UTF-8?q?teral=20notation=20{}=20is=20preferrable"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/view-registry-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/view-registry-spec.js b/spec/view-registry-spec.js index 4459af10c..d29c627bd 100644 --- a/spec/view-registry-spec.js +++ b/spec/view-registry-spec.js @@ -104,7 +104,7 @@ describe('ViewRegistry', () => { describe("when no view provider is registered for the object's constructor", () => it('throws an exception', () => { - expect(() => registry.getView(new Object())).toThrow() + expect(() => registry.getView({})).toThrow() }) ) }) From 01e7faa988761581392f134ebc64d9c1793b321c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 21 Oct 2017 10:10:06 -0400 Subject: [PATCH 123/301] =?UTF-8?q?=F0=9F=91=94=20Fix=20"Arrow=20function?= =?UTF-8?q?=20should=20not=20return=20assignment"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/view-registry-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/view-registry-spec.js b/spec/view-registry-spec.js index d29c627bd..db8b077f1 100644 --- a/spec/view-registry-spec.js +++ b/spec/view-registry-spec.js @@ -208,8 +208,8 @@ describe('ViewRegistry', () => { done() }) - registry.updateDocument(() => updateCalled = true) - registry.readDocument(() => readCalled = true) + registry.updateDocument(() => { updateCalled = true }) + registry.readDocument(() => { readCalled = true }) }) }) ) From 0511c0ae4a5f18b81b3c64ad31f15dbfd8ba3a38 Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Mon, 23 Oct 2017 04:11:23 +0300 Subject: [PATCH 124/301] Remove unused argument --- src/text-editor-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 2cbf3093c..d891a5868 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -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) } } From 7b76ee3f2593e48ab86fca7e1c602332b4bc8cf7 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 08:47:30 -0400 Subject: [PATCH 125/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/tooltip-manager.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply results of running: $ decaffeinate --keep-commonjs --prefer-const --loose-default-params --loose-for-expressions --loose-for-of --loose-includes src/tooltip-manager.coffee src/tooltip-manager.coffee → src/tooltip-manager.js $ standard --fix src/tooltip-manager.js src/tooltip-manager.js:210:25: Unnecessary escape character: \". src/tooltip-manager.js:210:36: Unnecessary escape character: \". --- src/tooltip-manager.coffee | 176 ------------------------------ src/tooltip-manager.js | 212 +++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 176 deletions(-) delete mode 100644 src/tooltip-manager.coffee create mode 100644 src/tooltip-manager.js diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee deleted file mode 100644 index 1a9b6fe44..000000000 --- a/src/tooltip-manager.coffee +++ /dev/null @@ -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 - "#{humanizeKeystrokes(bindings[0].keystrokes)}" diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js new file mode 100644 index 000000000..c838b6dbc --- /dev/null +++ b/src/tooltip-manager.js @@ -0,0 +1,212 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TooltipManager +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 +// +// ```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 = +(TooltipManager = (function () { + TooltipManager = class TooltipManager { + static initClass () { + this.prototype.defaults = { + trigger: 'hover', + container: 'body', + html: true, + placement: 'auto top', + viewportPadding: 2 + } + + this.prototype.hoverDefaults = + {delay: {show: 1000, hide: 100}} + } + + constructor ({keymapManager, viewRegistry}) { + 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) { + let disposable + if (target.jquery) { + disposable = new CompositeDisposable() + for (let 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}) + return tooltip.hide() + } + + window.addEventListener('resize', hideTooltip) + + 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) { + return 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 [] + } + } + } + TooltipManager.initClass() + return TooltipManager +})()) + +const humanizeKeystrokes = function (keystroke) { + let keystrokes = keystroke.split(' ') + keystrokes = (keystrokes.map((stroke) => _.humanizeKeystroke(stroke))) + return keystrokes.join(' ') +} + +var getKeystroke = function (bindings) { + if (bindings != null ? bindings.length : undefined) { + return `${humanizeKeystrokes(bindings[0].keystrokes)}` + } +} From 034f003705f07e8a8b4d8354ae1f861dc851ab72 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 08:49:27 -0400 Subject: [PATCH 126/301] :shirt: Fix 'Unnecessary escape character: \"' --- src/tooltip-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index c838b6dbc..f127d3f44 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -207,6 +207,6 @@ const humanizeKeystrokes = function (keystroke) { var getKeystroke = function (bindings) { if (bindings != null ? bindings.length : undefined) { - return `${humanizeKeystrokes(bindings[0].keystrokes)}` + return `${humanizeKeystrokes(bindings[0].keystrokes)}` } } From 157c33b5471c6bf3dfb7b361decd6e64a56b8eba Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:27:53 -0400 Subject: [PATCH 127/301] :art: DS206 Rework class to avoid initClass --- src/tooltip-manager.js | 267 ++++++++++++++++++++--------------------- 1 file changed, 130 insertions(+), 137 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index f127d3f44..00e16e405 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -1,11 +1,9 @@ /* * decaffeinate suggestions: * DS102: Remove unnecessary code created because of implicit returns - * DS206: Consider reworking classes to avoid initClass * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let TooltipManager const _ = require('underscore-plus') const {Disposable, CompositeDisposable} = require('event-kit') let Tooltip = null @@ -52,152 +50,147 @@ let Tooltip = null // keyBindingTarget: @findEditor.element // ``` module.exports = -(TooltipManager = (function () { - TooltipManager = class TooltipManager { - static initClass () { - this.prototype.defaults = { - trigger: 'hover', - container: 'body', - html: true, - placement: 'auto top', - viewportPadding: 2 - } - - this.prototype.hoverDefaults = - {delay: {show: 1000, hide: 100}} +class TooltipManager { + constructor ({keymapManager, viewRegistry}) { + this.defaults = { + trigger: 'hover', + container: 'body', + html: true, + placement: 'auto top', + viewportPadding: 2 } - constructor ({keymapManager, viewRegistry}) { - this.keymapManager = keymapManager - this.viewRegistry = viewRegistry - this.tooltips = new Map() + this.hoverDefaults = { + delay: {show: 1000, hide: 100} } - // 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) { - let disposable - if (target.jquery) { - disposable = new CompositeDisposable() - for (let 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}) - return tooltip.hide() - } - - window.addEventListener('resize', hideTooltip) - - 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) { - return this.tooltips.delete(target) - } - } - }) + 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) { + let disposable + if (target.jquery) { + disposable = new CompositeDisposable() + for (let element of target) { disposable.add(this.add(element, options)) } 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 [] + 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}) + return tooltip.hide() + } + + window.addEventListener('resize', hideTooltip) + + 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) { + return this.tooltips.delete(target) + } + } + }) + + return disposable } - TooltipManager.initClass() - return TooltipManager -})()) + + // 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 [] + } + } +} const humanizeKeystrokes = function (keystroke) { let keystrokes = keystroke.split(' ') From 028d419ce778651124c504470f9515671c58a88c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:31:32 -0400 Subject: [PATCH 128/301] :art: DS102 Remove unnecessary code created because of implicit returns --- src/tooltip-manager.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index 00e16e405..89849020c 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -1,6 +1,5 @@ /* * 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 */ @@ -153,7 +152,7 @@ class TooltipManager { const hideTooltip = function () { tooltip.leave({currentTarget: target}) - return tooltip.hide() + tooltip.hide() } window.addEventListener('resize', hideTooltip) @@ -170,7 +169,7 @@ class TooltipManager { tooltipsForTarget.splice(index, 1) } if (tooltipsForTarget.length === 0) { - return this.tooltips.delete(target) + this.tooltips.delete(target) } } }) From 4179b11cb9142ef69d4b0d4464fda06f0a992641 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:32:20 -0400 Subject: [PATCH 129/301] :art: DS207 Use shorter variations of null checks --- src/tooltip-manager.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index 89849020c..a27b860b0 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -1,8 +1,3 @@ -/* - * decaffeinate suggestions: - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const _ = require('underscore-plus') const {Disposable, CompositeDisposable} = require('event-kit') let Tooltip = null @@ -198,7 +193,7 @@ const humanizeKeystrokes = function (keystroke) { } var getKeystroke = function (bindings) { - if (bindings != null ? bindings.length : undefined) { + if (bindings && bindings.length) { return `${humanizeKeystrokes(bindings[0].keystrokes)}` } } From 74137446e79d74afac2aa7ca4b9e45581180496c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:34:20 -0400 Subject: [PATCH 130/301] =?UTF-8?q?:memo:=E2=98=A0=E2=98=95=20Decaffeinate?= =?UTF-8?q?=20TooltipManager=20API=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tooltip-manager.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index a27b860b0..73a58d1d6 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -10,38 +10,39 @@ let Tooltip = null // // The essence of displaying a tooltip // -// ```coffee -// # display it -// disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) +// ```javascript +// // display it +// const disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) // -// # remove it +// // 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 +// ```javascript +// const {CompositeDisposable} = require('atom') +// const 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'}) +// 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 +// // 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 +// ```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 { From 5e587e88a982e81ac8b96421fef75078e722579f Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 23 Oct 2017 09:34:35 -0400 Subject: [PATCH 131/301] :art: --- src/tooltip-manager.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index 73a58d1d6..937f831d1 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -112,10 +112,9 @@ class TooltipManager { // Returns a {Disposable} on which `.dispose()` can be called to remove the // tooltip. add (target, options) { - let disposable if (target.jquery) { - disposable = new CompositeDisposable() - for (let element of target) { disposable.add(this.add(element, options)) } + const disposable = new CompositeDisposable() + for (const element of target) { disposable.add(this.add(element, options)) } return disposable } @@ -153,7 +152,7 @@ class TooltipManager { window.addEventListener('resize', hideTooltip) - disposable = new Disposable(() => { + const disposable = new Disposable(() => { window.removeEventListener('resize', hideTooltip) hideTooltip() tooltip.destroy() @@ -187,13 +186,13 @@ class TooltipManager { } } -const humanizeKeystrokes = function (keystroke) { +function humanizeKeystrokes (keystroke) { let keystrokes = keystroke.split(' ') keystrokes = (keystrokes.map((stroke) => _.humanizeKeystroke(stroke))) return keystrokes.join(' ') } -var getKeystroke = function (bindings) { +function getKeystroke (bindings) { if (bindings && bindings.length) { return `${humanizeKeystrokes(bindings[0].keystrokes)}` } From 8d532e77806703dae7ec4b80f84bc4e970b0b4fd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 10:20:45 -0700 Subject: [PATCH 132/301] Fix exception when trying to fold non-foldable row --- spec/text-editor-spec.js | 20 ++++++++++++++++++++ src/text-editor.coffee | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index c81df8089..b766a8ac9 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -173,6 +173,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}) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index c00508f09..6700af089 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3310,8 +3310,8 @@ 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: -> From 6ccc807aebbdbfea1a3b147d4d1bfc09a4362e9f Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 10:45:08 -0700 Subject: [PATCH 133/301] :arrow_up: season --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5541ff0e..2fba03420 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "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", From ef6b5ee07c42cf7fd31c56b83168496e6eeda8ad Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 11:14:46 -0700 Subject: [PATCH 134/301] :arrow_up: language-gfm --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2fba03420..9c9a988db 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "language-coffee-script": "0.49.1", "language-csharp": "0.14.3", "language-css": "0.42.6", - "language-gfm": "0.90.1", + "language-gfm": "0.90.2", "language-git": "0.19.1", "language-go": "0.44.2", "language-html": "0.48.1", From 8318b7207e5fcdf3f81425d61cc95cb15c974a1a Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 11:22:34 -0700 Subject: [PATCH 135/301] :arrow_up: language-less --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c9a988db..077b8d46f 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "language-java": "0.27.4", "language-javascript": "0.127.5", "language-json": "0.19.1", - "language-less": "0.33.0", + "language-less": "0.33.1", "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", From 9e21931b91e48b71d01675a5b24850ab49cf86aa Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 12:23:06 -0700 Subject: [PATCH 136/301] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 077b8d46f..af75ffd96 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.7", + "text-buffer": "13.5.8", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From ed94726fab201200be8a33098dd91b0d54f1e1aa Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Mon, 23 Oct 2017 14:32:34 -0600 Subject: [PATCH 137/301] fix overlayComponent access syntax in test --- spec/text-editor-component-spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index d46748d91..5f0a28883 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1896,8 +1896,7 @@ describe('TextEditorComponent', () => { const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'}) await component.getNextUpdatePromise() - let overlayComponent - component.overlayComponents.forEach(c => overlayComponent = c) + const overlayComponent = component.overlayComponents.values().next().value const overlayWrapper = overlayElement.parentElement expect(overlayWrapper.classList.contains('a')).toBe(true) From 5465830dbe04a43bc98eafccacef7f6824c7bb23 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 11:23:20 -0700 Subject: [PATCH 138/301] :arrow_up: snippets --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af75ffd96..2e98cc797 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.0", - "snippets": "1.1.5", + "snippets": "1.1.6", "spell-check": "0.72.3", "status-bar": "1.8.13", "styleguide": "0.49.7", From aa4796e7d614b8b58db1f11dc6cf1b162b96eeb6 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 11:24:41 -0700 Subject: [PATCH 139/301] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e98cc797..8b90346a4 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.252.0", + "settings-view": "0.252.1", "snippets": "1.1.6", "spell-check": "0.72.3", "status-bar": "1.8.13", From adcbb7ab2c12a524cb5b91be9777c7f9f7afe0a7 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 12:35:38 -0700 Subject: [PATCH 140/301] :arrow_up: first-mate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b90346a4..910f569fc 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "etch": "^0.12.6", "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.9", + "first-mate": "7.0.10", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From f31bbc58829003a85bb4c0c6bf818ef083c6e571 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 14:04:04 -0700 Subject: [PATCH 141/301] :arrow_up: atom-keymap --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 910f569fc..2790f4c8f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "@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", From d03bedd8cff531cbcf3c62e9da377e99a469467d Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 15:03:56 -0700 Subject: [PATCH 142/301] :arrow_up: styleguide --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2790f4c8f..51f6732d2 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "snippets": "1.1.6", "spell-check": "0.72.3", "status-bar": "1.8.13", - "styleguide": "0.49.7", + "styleguide": "0.49.8", "symbols-view": "0.118.1", "tabs": "0.108.0", "timecop": "0.36.0", From d1844eccec173a16f030f2f90a08350da56fb300 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 15:13:05 -0700 Subject: [PATCH 143/301] :arrow_up: markdown-preview --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 51f6732d2..8d8b98ccc 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.15", + "markdown-preview": "0.159.16", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From bbbf09ecf274d0665c70fb200b284fc425265dea Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Oct 2017 16:18:01 -0600 Subject: [PATCH 144/301] Add preserveTrailingLineIndentation option to Selection.insertText We can use this to support a new command that preserves all formatting when pasting. --- spec/selection-spec.coffee | 5 +++++ src/selection.coffee | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee index cb070310a..b0e65be30 100644 --- a/spec/selection-spec.coffee +++ b/spec/selection-spec.coffee @@ -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]]) diff --git a/src/selection.coffee b/src/selection.coffee index 4d3fe8882..6fcf8dd36 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -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) From 6701644bbd9c983f804dc0596bc880695e09971d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Oct 2017 17:02:41 -0600 Subject: [PATCH 145/301] Respect format-preserving options in TextEditor.pasteText --- spec/text-editor-spec.coffee | 13 +++++++++++++ src/text-editor.coffee | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 53011fdcc..bc74cd443 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4222,6 +4222,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", -> diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6700af089..32dd49a18 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3247,12 +3247,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 From 40ed5838a5a80f5681b2a43dbfa5d036c491a29d Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 16:07:41 -0700 Subject: [PATCH 146/301] :arrow_up: dedent --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d8b98ccc..4056b0d71 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "clear-cut": "^2.0.2", "coffee-script": "1.11.1", "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", From fd85c1bb5abec895bd780f2ed69033f5d89b3439 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 23 Oct 2017 17:14:41 -0600 Subject: [PATCH 147/301] Add `Paste without reformatting` command It is bound to cmd-shift-V on macOS and ctrl-shift-V on Windows and Linux. It is also available in the edit menu. --- keymaps/darwin.cson | 1 + keymaps/linux.cson | 1 + keymaps/win32.cson | 1 + menus/darwin.cson | 1 + menus/linux.cson | 1 + menus/win32.cson | 1 + src/register-default-commands.coffee | 5 +++++ 7 files changed, 11 insertions(+) diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index fa942d97c..7161a8478 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -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' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index d6ded1f90..9d3e4dbb1 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -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' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index 14f5a4283..8a8e92249 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -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' diff --git a/menus/darwin.cson b/menus/darwin.cson index 055cd2405..2dffda1ef 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -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' } diff --git a/menus/linux.cson b/menus/linux.cson index 2a1ca47f8..b44900398 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -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' } diff --git a/menus/win32.cson b/menus/win32.cson index 553b6017e..a921bae74 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -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' } diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index d5b741c40..7dc0d3298 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -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() From 311567ecec887c937d45287b92f667e827fcb1db Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 16:45:12 -0700 Subject: [PATCH 148/301] Simplify .toggleLineComments method to avoid using oniguruma --- spec/tokenized-buffer-spec.js | 18 ++++---- src/tokenized-buffer.js | 78 +++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index ba43f9ff3..9dc636bef 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -692,38 +692,38 @@ describe('TokenizedBuffer', () => { 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(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(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(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%;*/') + 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%;*/ ') + 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%;*/ ') + buffer.setTextInRange([[2, 0], [2, Infinity]], ' /* width: 110%; */ ') tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) expect(buffer.lineForRow(2)).toBe(' width: 110%; ') }) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index b4bc0d41c..13a1b17fa 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -165,37 +165,32 @@ class TokenizedBuffer { toggleLineCommentsForBufferRows (start, end) { const scope = this.scopeDescriptorForPosition([start, 0]) - const commentStrings = this.commentStringsForScopeDescriptor(scope) - if (!commentStrings) return - const {commentStartString, commentEndString} = commentStrings + let {commentStartString, commentEndString} = this.commentStringsForScopeDescriptor(scope) if (!commentStartString) return - - const commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') - const commentStartRegex = new OnigRegExp(`^(\\s*)(${commentStartRegexString})`) + commentStartString = commentStartString.trim() 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) { + commentEndString = commentEndString.trim() + const startDelimiterColumnRange = this.columnRangeForStartDelimiter( + this.buffer.lineForRow(start), + commentStartString + ) + if (startDelimiterColumnRange) { + const endDelimiterColumnRange = this.columnRangeForEndDelimiter( + this.buffer.lineForRow(end), + commentEndString + ) + if (endDelimiterColumnRange) { 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]], '') + 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) + this.buffer.insert([start, indentLength], commentStartString + ' ') + this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString) }) } } else { @@ -204,7 +199,7 @@ class TokenizedBuffer { for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) if (NON_WHITESPACE_REGEX.test(line)) { - if (commentStartRegex.testSync(line)) { + if (this.columnRangeForStartDelimiter(line, commentStartString)) { hasCommentedLines = true } else { hasUncommentedLines = true @@ -216,12 +211,11 @@ class TokenizedBuffer { 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]], '') - } + const columnRange = this.columnRangeForStartDelimiter( + this.buffer.lineForRow(row), + commentStartString + ) + if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]) } } else { let minIndentLevel = Infinity @@ -247,11 +241,11 @@ class TokenizedBuffer { 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) + this.buffer.insert(Point(row, indentColumn), commentStartString + ' ') } else { this.buffer.setTextInRange( new Range(new Point(row, 0), new Point(row, Infinity)), - indentString + commentStartString + indentString + commentStartString + ' ' ) } } @@ -259,6 +253,26 @@ class TokenizedBuffer { } } + 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] + } + + 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] + } + buildIterator () { return new TokenizedBufferIterator(this) } @@ -844,6 +858,8 @@ class TokenizedBuffer { commentStringsForScopeDescriptor (scopes) { if (this.scopedSettingsDelegate) { return this.scopedSettingsDelegate.getCommentStrings(scopes) + } else { + return {} } } From 7637bc32d154f6f92b750eb26481853e94129a86 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 23 Oct 2017 16:57:34 -0700 Subject: [PATCH 149/301] :arrow_up: atom-package-manager --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index 5391c9972..e759a39d2 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.8" + "atom-package-manager": "1.18.9" } } From 079f4d901cdd006263d2844992eca6a52b898a0f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 17:00:05 -0700 Subject: [PATCH 150/301] Move all .toggleLineComments tests to text-editor-spec.js --- spec/text-editor-spec.coffee | 102 ------------- spec/text-editor-spec.js | 272 ++++++++++++++++++++++++++++++++++ spec/tokenized-buffer-spec.js | 180 ---------------------- 3 files changed, 272 insertions(+), 282 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 53011fdcc..de2f9fe8d 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4363,108 +4363,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") diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index b766a8ac9..d10efa695 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -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('') + }) + + 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') diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 9dc636bef..b1574673a 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -643,186 +643,6 @@ describe('TokenizedBuffer', () => { }) }) - describe('.toggleLineCommentsForBufferRows', () => { - describe('xml', () => { - beforeEach(async () => { - await atom.packages.activatePackage('language-xml') - buffer = new TextBuffer('') - 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') From cfe5cfce766fcdec4cd342e1b4c7c72f475c4693 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 23 Oct 2017 17:44:45 -0700 Subject: [PATCH 151/301] Move .toggleLineComments method from TokenizedBuffer to TextEditor --- src/text-editor-utils.js | 139 +++++++++++++++++++++++++++++++++++++++ src/text-editor.coffee | 9 ++- src/tokenized-buffer.js | 137 ++------------------------------------ 3 files changed, 148 insertions(+), 137 deletions(-) create mode 100644 src/text-editor-utils.js diff --git a/src/text-editor-utils.js b/src/text-editor-utils.js new file mode 100644 index 000000000..ab1104144 --- /dev/null +++ b/src/text-editor-utils.js @@ -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] +} diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6700af089..f75822d77 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -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? @@ -3621,9 +3625,6 @@ class TextEditor extends Model getNonWordCharacters: (scopes) -> @scopedSettingsDelegate?.getNonWordCharacters?(scopes) ? @nonWordCharacters - getCommentStrings: (scopes) -> - @scopedSettingsDelegate?.getCommentStrings?(scopes) - ### Section: Event Handlers ### @@ -3886,8 +3887,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)) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 13a1b17fa..2a9446256 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -163,116 +163,15 @@ class TokenizedBuffer { Section - Comments */ - toggleLineCommentsForBufferRows (start, end) { - const scope = this.scopeDescriptorForPosition([start, 0]) - let {commentStartString, commentEndString} = this.commentStringsForScopeDescriptor(scope) - if (!commentStartString) return - commentStartString = commentStartString.trim() - - if (commentEndString) { - commentEndString = commentEndString.trim() - const startDelimiterColumnRange = this.columnRangeForStartDelimiter( - this.buffer.lineForRow(start), - commentStartString - ) - if (startDelimiterColumnRange) { - const endDelimiterColumnRange = this.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) - }) - } + 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 (this.columnRangeForStartDelimiter(line, commentStartString)) { - hasCommentedLines = true - } else { - hasUncommentedLines = true - } - } - } - - const shouldUncomment = hasCommentedLines && !hasUncommentedLines - - if (shouldUncomment) { - for (let row = start; row <= end; row++) { - const columnRange = this.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 = 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 {} } } - 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] - } - - 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] - } - buildIterator () { return new TokenizedBufferIterator(this) } @@ -608,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++) { @@ -855,14 +736,6 @@ class TokenizedBuffer { } } - commentStringsForScopeDescriptor (scopes) { - if (this.scopedSettingsDelegate) { - return this.scopedSettingsDelegate.getCommentStrings(scopes) - } else { - return {} - } - } - regexForPattern (pattern) { if (pattern) { if (!this.regexesByPattern[pattern]) { From f771cf9d1ae4ca47c941dee52e7266f0e2bf41ab Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:47:44 -0400 Subject: [PATCH 152/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/tooltip-manager-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply results of running: $ decaffeinate --keep-commonjs --prefer-const --loose-default-params --loose-for-expressions --loose-for-of --loose-includes spec/tooltip-manager-spec.coffee $ standard --fix spec/tooltip-manager-spec.js --- spec/tooltip-manager-spec.coffee | 213 ------------------------- spec/tooltip-manager-spec.js | 260 +++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 213 deletions(-) delete mode 100644 spec/tooltip-manager-spec.coffee create mode 100644 spec/tooltip-manager-spec.js diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee deleted file mode 100644 index 95182853e..000000000 --- a/spec/tooltip-manager-spec.coffee +++ /dev/null @@ -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() diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js new file mode 100644 index 000000000..c022db44a --- /dev/null +++ b/spec/tooltip-manager-spec.js @@ -0,0 +1,260 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {CompositeDisposable} = require('atom') +const TooltipManager = require('../src/tooltip-manager') +const Tooltip = require('../src/tooltip') +const _ = require('underscore-plus') + +describe('TooltipManager', function () { + let [manager, element] = Array.from([]) + + const ctrlX = _.humanizeKeystroke('ctrl-x') + const ctrlY = _.humanizeKeystroke('ctrl-y') + + beforeEach(function () { + manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views}) + return element = createElement('foo') + }) + + var createElement = function (className) { + const el = document.createElement('div') + el.classList.add(className) + jasmine.attachToDOM(el) + return el + } + + const mouseEnter = function (element) { + element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) + return element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) + } + + const mouseLeave = function (element) { + element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) + return element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) + } + + const hover = function (element, fn) { + mouseEnter(element) + advanceClock(manager.hoverDefaults.delay.show) + fn() + mouseLeave(element) + return advanceClock(manager.hoverDefaults.delay.hide) + } + + return describe('::add(target, options)', function () { + describe("when the trigger is 'hover' (the default)", function () { + it('creates a tooltip when hovering over the target element', function () { + manager.add(element, {title: 'Title'}) + return hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) + }) + + return it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', function () { + 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, function () {}) + 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() + + return disposables.dispose() + }) + }) + + describe("when the trigger is 'manual'", () => + it('creates a tooltip immediately and only hides it on dispose', function () { + const disposable = manager.add(element, {title: 'Title', trigger: 'manual'}) + expect(document.body.querySelector('.tooltip')).toHaveText('Title') + disposable.dispose() + return expect(document.body.querySelector('.tooltip')).toBeNull() + }) + ) + + describe("when the trigger is 'click'", () => + it('shows and hides the tooltip when the target element is clicked', function () { + const 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() + return expect(document.body.querySelector('.tooltip')).toBeNull() + }) + ) + + it('allows a custom item to be specified for the content of the tooltip', function () { + const tooltipElement = document.createElement('div') + manager.add(element, {item: {element: tooltipElement}}) + return hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull()) + }) + + it('allows a custom class to be specified for the tooltip', function () { + const tooltipElement = document.createElement('div') + manager.add(element, {title: 'Title', class: 'custom-tooltip-class'}) + return hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) + }) + + it('allows jQuery elements to be passed as the target', function () { + 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()) + return hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + }) + + describe('when a keyBindingCommand is specified', function () { + describe('when a title is specified', () => + it('appends the key binding corresponding to the command to the title', function () { + 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'}) + + return hover(element, function () { + const tooltipElement = document.body.querySelector('.tooltip') + return expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`) + }) + }) + ) + + describe('when no title is specified', () => + it('shows the key binding corresponding to the command alone', function () { + atom.keymaps.add('test', {'.foo': {'ctrl-x ctrl-y': 'test-command'}}) + + manager.add(element, {keyBindingCommand: 'test-command'}) + + return hover(element, function () { + const tooltipElement = document.body.querySelector('.tooltip') + return expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) + }) + }) + ) + + return describe('when a keyBindingTarget is specified', function () { + it('looks up the key binding relative to the target', function () { + 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}) + + return hover(element, function () { + const tooltipElement = document.body.querySelector('.tooltip') + return expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) + }) + }) + + return it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', function () { + manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element}) + + return hover(element, function () { + const tooltipElement = document.body.querySelector('.tooltip') + return expect(tooltipElement.textContent).toBe('A Title') + }) + }) + }) + }) + + describe('when .dispose() is called on the returned disposable', () => + it('no longer displays the tooltip on hover', function () { + const disposable = manager.add(element, {title: 'Title'}) + + hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) + + disposable.dispose() + + return hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + }) + ) + + describe('when the window is resized', () => + it('hides the tooltips', function () { + const disposable = manager.add(element, {title: 'Title'}) + return hover(element, function () { + expect(document.body.querySelector('.tooltip')).not.toBeNull() + window.dispatchEvent(new CustomEvent('resize')) + expect(document.body.querySelector('.tooltip')).toBeNull() + return disposable.dispose() + }) + }) + ) + + return describe('findTooltips', function () { + it('adds and remove tooltips correctly', function () { + 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() + return expect(manager.findTooltips(element).length).toBe(0) + }) + + return it('lets us hide tooltips programmatically', function () { + const disposable = manager.add(element, {title: 'Title'}) + return hover(element, function () { + expect(document.body.querySelector('.tooltip')).not.toBeNull() + manager.findTooltips(element)[0].hide() + expect(document.body.querySelector('.tooltip')).toBeNull() + return disposable.dispose() + }) + }) + }) + }) +}) From 7f75a46b97dfbae0ade622e8edf91f4afa72623c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:51:15 -0400 Subject: [PATCH 153/301] =?UTF-8?q?=F0=9F=91=95=20Fix=20"Return=20statemen?= =?UTF-8?q?t=20should=20not=20contain=20assignment"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/tooltip-manager-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index c022db44a..222b5a766 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -17,7 +17,7 @@ describe('TooltipManager', function () { beforeEach(function () { manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views}) - return element = createElement('foo') + element = createElement('foo') }) var createElement = function (className) { From f976c93d5ad52beebdc6a4e975a1ab20a226b281 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:51:40 -0400 Subject: [PATCH 154/301] :shirt: Fix "'disposable' is assigned a value but never used" --- spec/tooltip-manager-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 222b5a766..35988c24e 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -91,7 +91,7 @@ describe('TooltipManager', function () { describe("when the trigger is 'click'", () => it('shows and hides the tooltip when the target element is clicked', function () { - const disposable = manager.add(element, {title: 'Title', trigger: 'click'}) + manager.add(element, {title: 'Title', trigger: 'click'}) expect(document.body.querySelector('.tooltip')).toBeNull() element.click() expect(document.body.querySelector('.tooltip')).not.toBeNull() From 90cfb69c7cd291d5c920e739275a735edc17fc17 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:53:24 -0400 Subject: [PATCH 155/301] :shirt: Fix "'tooltipElement' is assigned a value but never used" --- spec/tooltip-manager-spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 35988c24e..2380d2dc9 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -123,7 +123,6 @@ describe('TooltipManager', function () { }) it('allows a custom class to be specified for the tooltip', function () { - const tooltipElement = document.createElement('div') manager.add(element, {title: 'Title', class: 'custom-tooltip-class'}) return hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) }) From 76eb993e7e507ffbe88d2f60046f62d8e2bd69c2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:56:20 -0400 Subject: [PATCH 156/301] :art: DS101 Remove unnecessary use of Array.from --- spec/tooltip-manager-spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 2380d2dc9..683e08e45 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ @@ -10,7 +9,7 @@ const Tooltip = require('../src/tooltip') const _ = require('underscore-plus') describe('TooltipManager', function () { - let [manager, element] = Array.from([]) + let manager, element const ctrlX = _.humanizeKeystroke('ctrl-x') const ctrlY = _.humanizeKeystroke('ctrl-y') From 706f7e3d4408285fc6efb86504d54c7543909e60 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 08:58:59 -0400 Subject: [PATCH 157/301] :art: DS102 Remove unnecessary code created because of implicit returns --- spec/tooltip-manager-spec.js | 65 +++++++++++++++++------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 683e08e45..0edcd646f 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -1,8 +1,3 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const {CompositeDisposable} = require('atom') const TooltipManager = require('../src/tooltip-manager') const Tooltip = require('../src/tooltip') @@ -28,12 +23,12 @@ describe('TooltipManager', function () { const mouseEnter = function (element) { element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) - return element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) + element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) } const mouseLeave = function (element) { element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) - return element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) + element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) } const hover = function (element, fn) { @@ -41,17 +36,17 @@ describe('TooltipManager', function () { advanceClock(manager.hoverDefaults.delay.show) fn() mouseLeave(element) - return advanceClock(manager.hoverDefaults.delay.hide) + advanceClock(manager.hoverDefaults.delay.hide) } - return describe('::add(target, options)', function () { + describe('::add(target, options)', function () { describe("when the trigger is 'hover' (the default)", function () { it('creates a tooltip when hovering over the target element', function () { manager.add(element, {title: 'Title'}) - return hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) + hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) }) - return it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', function () { + it('displays tooltips immediately when hovering over new elements once a tooltip has been displayed once', function () { const disposables = new CompositeDisposable() const element1 = createElement('foo') disposables.add(manager.add(element1, {title: 'Title'})) @@ -75,7 +70,7 @@ describe('TooltipManager', function () { advanceClock(manager.hoverDefaults.delay.show) expect(document.body.querySelector('.tooltip')).not.toBeNull() - return disposables.dispose() + disposables.dispose() }) }) @@ -84,7 +79,7 @@ describe('TooltipManager', function () { const disposable = manager.add(element, {title: 'Title', trigger: 'manual'}) expect(document.body.querySelector('.tooltip')).toHaveText('Title') disposable.dispose() - return expect(document.body.querySelector('.tooltip')).toBeNull() + expect(document.body.querySelector('.tooltip')).toBeNull() }) ) @@ -111,19 +106,19 @@ describe('TooltipManager', function () { element.click() expect(document.body.querySelector('.tooltip')).not.toBeNull() element.click() - return expect(document.body.querySelector('.tooltip')).toBeNull() + expect(document.body.querySelector('.tooltip')).toBeNull() }) ) it('allows a custom item to be specified for the content of the tooltip', function () { const tooltipElement = document.createElement('div') manager.add(element, {item: {element: tooltipElement}}) - return hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull()) + hover(element, () => expect(tooltipElement.closest('.tooltip')).not.toBeNull()) }) it('allows a custom class to be specified for the tooltip', function () { manager.add(element, {title: 'Title', class: 'custom-tooltip-class'}) - return hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) + hover(element, () => expect(document.body.querySelector('.tooltip').classList.contains('custom-tooltip-class')).toBe(true)) }) it('allows jQuery elements to be passed as the target', function () { @@ -142,7 +137,7 @@ describe('TooltipManager', function () { disposable.dispose() hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) - return hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) }) describe('when a keyBindingCommand is specified', function () { @@ -158,9 +153,9 @@ describe('TooltipManager', function () { manager.add(element, {title: 'Title', keyBindingCommand: 'test-command'}) - return hover(element, function () { + hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') - return expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`) + expect(tooltipElement).toHaveText(`Title ${ctrlX} ${ctrlY}`) }) }) ) @@ -171,14 +166,14 @@ describe('TooltipManager', function () { manager.add(element, {keyBindingCommand: 'test-command'}) - return hover(element, function () { + hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') - return expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) + expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) }) }) ) - return describe('when a keyBindingTarget is specified', function () { + describe('when a keyBindingTarget is specified', function () { it('looks up the key binding relative to the target', function () { atom.keymaps.add('test', { '.bar': { 'ctrl-x ctrl-z': 'test-command' @@ -190,18 +185,18 @@ describe('TooltipManager', function () { manager.add(element, {keyBindingCommand: 'test-command', keyBindingTarget: element}) - return hover(element, function () { + hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') - return expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) + expect(tooltipElement).toHaveText(`${ctrlX} ${ctrlY}`) }) }) - return it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', function () { + it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', function () { manager.add(element, {title: 'A Title', keyBindingCommand: 'test-command', keyBindingTarget: element}) - return hover(element, function () { + hover(element, function () { const tooltipElement = document.body.querySelector('.tooltip') - return expect(tooltipElement.textContent).toBe('A Title') + expect(tooltipElement.textContent).toBe('A Title') }) }) }) @@ -215,23 +210,23 @@ describe('TooltipManager', function () { disposable.dispose() - return hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) + hover(element, () => expect(document.body.querySelector('.tooltip')).toBeNull()) }) ) describe('when the window is resized', () => it('hides the tooltips', function () { const disposable = manager.add(element, {title: 'Title'}) - return hover(element, function () { + hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() window.dispatchEvent(new CustomEvent('resize')) expect(document.body.querySelector('.tooltip')).toBeNull() - return disposable.dispose() + disposable.dispose() }) }) ) - return describe('findTooltips', function () { + describe('findTooltips', function () { it('adds and remove tooltips correctly', function () { expect(manager.findTooltips(element).length).toBe(0) const disposable1 = manager.add(element, {title: 'elem1'}) @@ -241,16 +236,16 @@ describe('TooltipManager', function () { disposable1.dispose() expect(manager.findTooltips(element).length).toBe(1) disposable2.dispose() - return expect(manager.findTooltips(element).length).toBe(0) + expect(manager.findTooltips(element).length).toBe(0) }) - return it('lets us hide tooltips programmatically', function () { + it('lets us hide tooltips programmatically', function () { const disposable = manager.add(element, {title: 'Title'}) - return hover(element, function () { + hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() manager.findTooltips(element)[0].hide() expect(document.body.querySelector('.tooltip')).toBeNull() - return disposable.dispose() + disposable.dispose() }) }) }) From aa69409b1b576ea50eba2bbe3306a9d3d3979980 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 09:04:11 -0400 Subject: [PATCH 158/301] :art: Prefer arrow function syntax --- spec/tooltip-manager-spec.js | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 0edcd646f..2f95299f3 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -3,7 +3,7 @@ const TooltipManager = require('../src/tooltip-manager') const Tooltip = require('../src/tooltip') const _ = require('underscore-plus') -describe('TooltipManager', function () { +describe('TooltipManager', () => { let manager, element const ctrlX = _.humanizeKeystroke('ctrl-x') @@ -39,14 +39,14 @@ describe('TooltipManager', function () { advanceClock(manager.hoverDefaults.delay.hide) } - describe('::add(target, options)', function () { - describe("when the trigger is 'hover' (the default)", function () { - it('creates a tooltip when hovering over the target element', function () { + 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', function () { + 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'})) @@ -55,7 +55,7 @@ describe('TooltipManager', function () { const element3 = createElement('baz') disposables.add(manager.add(element3, {title: 'Title'})) - hover(element1, function () {}) + hover(element1, () => {}) expect(document.body.querySelector('.tooltip')).toBeNull() mouseEnter(element2) @@ -75,7 +75,7 @@ describe('TooltipManager', function () { }) describe("when the trigger is 'manual'", () => - it('creates a tooltip immediately and only hides it on dispose', function () { + 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() @@ -84,7 +84,7 @@ describe('TooltipManager', function () { ) describe("when the trigger is 'click'", () => - it('shows and hides the tooltip when the target element is clicked', function () { + 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() @@ -110,18 +110,18 @@ describe('TooltipManager', function () { }) ) - it('allows a custom item to be specified for the content of the tooltip', function () { + 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', function () { + 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', function () { + it('allows jQuery elements to be passed as the target', () => { const element2 = document.createElement('div') jasmine.attachToDOM(element2) @@ -140,9 +140,9 @@ describe('TooltipManager', function () { hover(element2, () => expect(document.body.querySelector('.tooltip')).toBeNull()) }) - describe('when a keyBindingCommand is specified', function () { + describe('when a keyBindingCommand is specified', () => { describe('when a title is specified', () => - it('appends the key binding corresponding to the command to the title', function () { + it('appends the key binding corresponding to the command to the title', () => { atom.keymaps.add('test', { '.foo': { 'ctrl-x ctrl-y': 'test-command' }, @@ -161,7 +161,7 @@ describe('TooltipManager', function () { ) describe('when no title is specified', () => - it('shows the key binding corresponding to the command alone', function () { + 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'}) @@ -173,8 +173,8 @@ describe('TooltipManager', function () { }) ) - describe('when a keyBindingTarget is specified', function () { - it('looks up the key binding relative to the target', function () { + 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' }, @@ -191,7 +191,7 @@ describe('TooltipManager', function () { }) }) - it('does not display the keybinding if there is nothing mapped to the specified keyBindingCommand', function () { + 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 () { @@ -203,7 +203,7 @@ describe('TooltipManager', function () { }) describe('when .dispose() is called on the returned disposable', () => - it('no longer displays the tooltip on hover', function () { + it('no longer displays the tooltip on hover', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) @@ -215,7 +215,7 @@ describe('TooltipManager', function () { ) describe('when the window is resized', () => - it('hides the tooltips', function () { + it('hides the tooltips', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() @@ -226,8 +226,8 @@ describe('TooltipManager', function () { }) ) - describe('findTooltips', function () { - it('adds and remove tooltips correctly', function () { + 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) @@ -239,7 +239,7 @@ describe('TooltipManager', function () { expect(manager.findTooltips(element).length).toBe(0) }) - it('lets us hide tooltips programmatically', function () { + it('lets us hide tooltips programmatically', () => { const disposable = manager.add(element, {title: 'Title'}) hover(element, function () { expect(document.body.querySelector('.tooltip')).not.toBeNull() From fc620b9e80d67ca99f962431461b8fc4d085d9df Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Tue, 24 Oct 2017 09:06:50 -0400 Subject: [PATCH 159/301] :art: Move helper functions outside of `describe` block --- spec/tooltip-manager-spec.js | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 2f95299f3..65587839f 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -9,28 +9,6 @@ describe('TooltipManager', () => { const ctrlX = _.humanizeKeystroke('ctrl-x') const ctrlY = _.humanizeKeystroke('ctrl-y') - beforeEach(function () { - manager = new TooltipManager({keymapManager: atom.keymaps, viewRegistry: atom.views}) - element = createElement('foo') - }) - - var createElement = function (className) { - const el = document.createElement('div') - el.classList.add(className) - jasmine.attachToDOM(el) - return el - } - - const mouseEnter = function (element) { - element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) - element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) - } - - const mouseLeave = function (element) { - element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) - element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) - } - const hover = function (element, fn) { mouseEnter(element) advanceClock(manager.hoverDefaults.delay.show) @@ -39,6 +17,11 @@ describe('TooltipManager', () => { 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', () => { @@ -251,3 +234,20 @@ describe('TooltipManager', () => { }) }) }) + +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})) +} From e42435208f933d8f119a3e3d6abd97dd944a89a5 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 00:01:42 +0200 Subject: [PATCH 160/301] :arrow_up: apm@1.18.10 --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index e759a39d2..336544d3e 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.9" + "atom-package-manager": "1.18.10" } } From 54a67b60cbcad3eb001c4e7dc6f4235cb7cfeaca Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:49:04 +0200 Subject: [PATCH 161/301] :arrow_up: language-ruby@0.71.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4056b0d71..783076c91 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", - "language-ruby": "0.71.3", + "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", "language-sass": "0.61.1", "language-shellscript": "0.25.3", From 51349a79fb44c450f14f6067e415cbe6cbef59fc Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:49:54 +0200 Subject: [PATCH 162/301] :arrow_up: language-hyperlink@0.16.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 783076c91..25f4e0c5a 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "language-git": "0.19.1", "language-go": "0.44.2", "language-html": "0.48.1", - "language-hyperlink": "0.16.2", + "language-hyperlink": "0.16.3", "language-java": "0.27.4", "language-javascript": "0.127.5", "language-json": "0.19.1", From c1ec73602f55871ffb87edf653f246223d2677e8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:50:26 +0200 Subject: [PATCH 163/301] :arrow_up: language-todo@0.29.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 25f4e0c5a..a865c288d 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "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", From 287d98b321db215033eeaea47b479d4a356ba416 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:51:08 +0200 Subject: [PATCH 164/301] :arrow_up: language-javascript@0.127.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a865c288d..3fe5b5235 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "language-html": "0.48.1", "language-hyperlink": "0.16.3", "language-java": "0.27.4", - "language-javascript": "0.127.5", + "language-javascript": "0.127.6", "language-json": "0.19.1", "language-less": "0.33.1", "language-make": "0.22.3", From fff1c06c50bbf8964ed9e1df4a3a74d2318e1b27 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:51:39 +0200 Subject: [PATCH 165/301] :arrow_up: language-go@0.44.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3fe5b5235..3aba6812c 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "language-css": "0.42.6", "language-gfm": "0.90.2", "language-git": "0.19.1", - "language-go": "0.44.2", + "language-go": "0.44.3", "language-html": "0.48.1", "language-hyperlink": "0.16.3", "language-java": "0.27.4", From b525b7212bd6399aeb8de4cc945e05cf4e7b179b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:52:30 +0200 Subject: [PATCH 166/301] :arrow_up: language-php@0.42.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3aba6812c..498e17b2b 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "language-mustache": "0.14.3", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "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.4", From 2aca4268a5466fc7ea77e7f391b20ed778d0d840 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:52:57 +0200 Subject: [PATCH 167/301] :arrow_up: language-yaml@0.31.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 498e17b2b..384b4902f 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "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": { From 5173b8f23fd38cfba452a2e6ad3838fa0368dbfa Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:53:51 +0200 Subject: [PATCH 168/301] :arrow_up: language-css@0.42.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 384b4902f..1e6352564 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-clojure": "0.22.4", "language-coffee-script": "0.49.1", "language-csharp": "0.14.3", - "language-css": "0.42.6", + "language-css": "0.42.7", "language-gfm": "0.90.2", "language-git": "0.19.1", "language-go": "0.44.3", From 946b4be5cfd3659876f6963a55899b55f9d0ddd2 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:54:32 +0200 Subject: [PATCH 169/301] :arrow_up: language-sass@0.61.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e6352564..d82792259 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "language-python": "0.45.4", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.61.1", + "language-sass": "0.61.2", "language-shellscript": "0.25.3", "language-source": "0.9.0", "language-sql": "0.25.8", From f83a8f7e7e631b944ea336e0b7173494ccce6168 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:55:06 +0200 Subject: [PATCH 170/301] :arrow_up: language-shellscript@0.25.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d82792259..ba5140094 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", "language-sass": "0.61.2", - "language-shellscript": "0.25.3", + "language-shellscript": "0.25.4", "language-source": "0.9.0", "language-sql": "0.25.8", "language-text": "0.7.3", From d474ddcd746a8bbe84bfe5385f7fc5a1e43e155e Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:55:46 +0200 Subject: [PATCH 171/301] :arrow_up: language-coffee-script@0.49.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba5140094..629019363 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "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.7", "language-gfm": "0.90.2", From e76eee10e3d40a5f5b1653d913aaaba226715123 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:56:30 +0200 Subject: [PATCH 172/301] :arrow_up: language-python@0.45.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 629019363..a83d0b694 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "language-perl": "0.37.0", "language-php": "0.42.2", "language-property-list": "0.9.1", - "language-python": "0.45.4", + "language-python": "0.45.5", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", "language-sass": "0.61.2", From 6dd28c0b37e3d6440bffcc5f6f4fe2ee695f226f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:56:58 +0200 Subject: [PATCH 173/301] :arrow_up: language-java@0.27.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a83d0b694..ea9b9b8e6 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "language-go": "0.44.3", "language-html": "0.48.1", "language-hyperlink": "0.16.3", - "language-java": "0.27.4", + "language-java": "0.27.5", "language-javascript": "0.127.6", "language-json": "0.19.1", "language-less": "0.33.1", From 13ecc8a2280e47cdf5491962afe4d754cdf4d388 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:58:42 +0200 Subject: [PATCH 174/301] :arrow_up: language-mustache@0.14.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ea9b9b8e6..2919dcceb 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "language-json": "0.19.1", "language-less": "0.33.1", "language-make": "0.22.3", - "language-mustache": "0.14.3", + "language-mustache": "0.14.4", "language-objective-c": "0.15.1", "language-perl": "0.37.0", "language-php": "0.42.2", From dfd1e715bf6e845c955a1b4a3d616ede953b2598 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 14:59:25 +0200 Subject: [PATCH 175/301] :arrow_up: language-html@0.48.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2919dcceb..3902ffa30 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.2", "language-git": "0.19.1", "language-go": "0.44.3", - "language-html": "0.48.1", + "language-html": "0.48.2", "language-hyperlink": "0.16.3", "language-java": "0.27.5", "language-javascript": "0.127.6", From 7b7ddb9eb989afa7ed15efcbf1da0f5021eb6906 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 15:00:00 +0200 Subject: [PATCH 176/301] :arrow_up: language-less@0.34.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3902ffa30..2887ec8bf 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "language-java": "0.27.5", "language-javascript": "0.127.6", "language-json": "0.19.1", - "language-less": "0.33.1", + "language-less": "0.34.0", "language-make": "0.22.3", "language-mustache": "0.14.4", "language-objective-c": "0.15.1", From 2bf9e4b0c7b1a4a9ba45b6ce78a69a4f06023ac6 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 17:36:21 +0200 Subject: [PATCH 177/301] Use scope names rather than names Some languages are not guaranteed to have names --- spec/workspace-spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 43a04eba9..1bde0e6fe 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -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() From 5fc8563fe56c2962a1267cdc0fa786db0b74352d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 18:03:45 +0200 Subject: [PATCH 178/301] :arrow_up: grammar-selector@0.49.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2887ec8bf..acf21aca8 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", - "grammar-selector": "0.49.6", + "grammar-selector": "0.49.7", "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", From 364964ea0a1bf0a5b5ca612e2ee8be10bbdf8db2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 10:34:43 -0600 Subject: [PATCH 179/301] Always assign a project path outside of bundle for legacy package specs This prevents package specs that don't have a fixtures directory from attempting to read files out of a non-existent directory inside the ASAR bundle, which causes ENOTDIR errors in superstring. If the spec does not have a parent folder containing a fixtures directory, we now set the default project path to `os.tmpdir()`. --- spec/spec-helper.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c20bfc827..7621f9cae 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -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]) From 00242541aed5edb03cfdaa7570c6f84011b28b32 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 12:15:33 -0600 Subject: [PATCH 180/301] Don't destroy folds that are completely contained within a selection --- package.json | 2 +- spec/text-editor-spec.coffee | 5 ++++- src/selection.coffee | 2 +- src/text-editor.coffee | 7 ++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4056b0d71..4e1216846 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.8", + "text-buffer": "13.6.0-0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index bc74cd443..5bb010321 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -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]]) diff --git a/src/selection.coffee b/src/selection.coffee index 6fcf8dd36..0907888d6 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -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]) unless options.preserveFolds @modifySelection => needsFlash = options.flash delete options.flash if options.flash? diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 32dd49a18..400d48f97 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2495,8 +2495,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]) @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -3446,6 +3447,10 @@ class TextEditor extends Model destroyFoldsIntersectingBufferRange: (bufferRange) -> @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) + # Remove any {Fold}s found that intersect the given array of buffer positions. + destroyFoldsContainingBufferPositions: (bufferPositions) -> + @displayLayer.destroyFoldsContainingBufferPositions(bufferPositions) + ### Section: Gutters ### From 2189bd502c73a79d30ba8f8712213ce264b59fe6 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 25 Oct 2017 20:33:52 +0200 Subject: [PATCH 181/301] :arrow_up: grammar-selector@0.49.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index acf21aca8..f09178dff 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", - "grammar-selector": "0.49.7", + "grammar-selector": "0.49.8", "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", From 577911179969cc8fc51e36f416eda05765d03962 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 12:48:20 -0600 Subject: [PATCH 182/301] Use destroyFoldsContainingBufferPosition in more cases --- package.json | 2 +- src/selection.coffee | 2 +- src/text-editor-component.js | 2 +- src/text-editor.coffee | 13 ++++++------- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4e1216846..a447d53b5 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.6.0-0", + "text-buffer": "13.6.0-2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/src/selection.coffee b/src/selection.coffee index 0907888d6..cb45286b8 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -87,7 +87,7 @@ class Selection extends Model setBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) options.reversed ?= @isReversed() - @editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end]) unless options.preserveFolds + @editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) unless options.preserveFolds @modifySelection => needsFlash = options.flash delete options.flash if options.flash? diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 641cdad02..f19b7e31c 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1771,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 } diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 400d48f97..dd359df9e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2497,7 +2497,7 @@ class TextEditor extends Model addSelectionForBufferRange: (bufferRange, options={}) -> bufferRange = Range.fromObject(bufferRange) unless options.preserveFolds - @displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end]) + @displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -3318,8 +3318,7 @@ class TextEditor extends Model # 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. @@ -3348,7 +3347,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: -> @@ -3447,9 +3446,9 @@ class TextEditor extends Model destroyFoldsIntersectingBufferRange: (bufferRange) -> @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) - # Remove any {Fold}s found that intersect the given array of buffer positions. - destroyFoldsContainingBufferPositions: (bufferPositions) -> - @displayLayer.destroyFoldsContainingBufferPositions(bufferPositions) + # Remove any {Fold}s found that contain the given array of buffer positions. + destroyFoldsContainingBufferPositions: (bufferPositions, excludeEndpoints) -> + @displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) ### Section: Gutters From 1722273630bf8b306ac34bb7b4b38ffa40135cab Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2017 12:52:31 -0700 Subject: [PATCH 183/301] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f09178dff..6c1d675cc 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.8", + "autocomplete-plus": "2.37.0", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From ba7c3e57f51d2e290390715981a1b3648727625b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2017 13:19:13 -0700 Subject: [PATCH 184/301] :arrow_up: text-buffer for new onWillChange behavior --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c1d675cc..afba9de19 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.8", + "text-buffer": "13.6.0-will-change-event-1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 197425ffe42ac1ded431694b5d99907a8bb36ff8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2017 14:47:29 -0700 Subject: [PATCH 185/301] :arrow_up: text-buffer (prerelease) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afba9de19..78c4847b3 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.6.0-will-change-event-1", + "text-buffer": "13.6.0-will-change-event-2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From d715f3ee2743c48c830ca9b2859efea9fedc71df Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 16:35:02 -0600 Subject: [PATCH 186/301] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a447d53b5..43446f242 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.6.0-2", + "text-buffer": "13.6.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 3e9c6601a252435be2488074a2742162c6bb9a93 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 25 Oct 2017 20:30:42 -0600 Subject: [PATCH 187/301] :arrow_up: markdown-preview --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f4c4bbce..6578f7392 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.16", + "markdown-preview": "0.159.17", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From 6ad37f08d3b4f38cbb9c025b1e07be1399b02d1c Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 26 Oct 2017 10:50:59 +0200 Subject: [PATCH 188/301] :arrow_up: autocomplete-css@0.17.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6578f7392..b34cce317 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "about": "1.7.8", "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.3", - "autocomplete-css": "0.17.3", + "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.2", "autocomplete-plus": "2.37.0", "autocomplete-snippets": "1.11.2", From 1f5565fec748c2d2a24967e9cb885a5ac4ff87ad Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 26 Oct 2017 10:55:03 +0200 Subject: [PATCH 189/301] :arrow_up: autocomplete-html@0.8.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b34cce317..d5bea7833 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.4", - "autocomplete-html": "0.8.2", + "autocomplete-html": "0.8.3", "autocomplete-plus": "2.37.0", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", From 9ae36efc2fa2a5ad01aabf948ffffa8cf52f73e4 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 26 Oct 2017 10:59:05 +0200 Subject: [PATCH 190/301] :arrow_up: autocomplete-atom-api@0.10.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5bea7833..6ad2f68b8 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "solarized-light-syntax": "1.1.2", "about": "1.7.8", "archive-view": "0.63.4", - "autocomplete-atom-api": "0.10.3", + "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", "autocomplete-plus": "2.37.0", From a32f1c3684a9de77af1b6dbd9e133a8b3b30c904 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 Oct 2017 06:52:23 -0600 Subject: [PATCH 191/301] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6578f7392..eaf1951a5 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.252.1", + "settings-view": "0.252.2", "snippets": "1.1.6", "spell-check": "0.72.3", "status-bar": "1.8.13", From f21ede2da253c0f4c32bba5e8706daf459bf2336 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 09:59:11 -0700 Subject: [PATCH 192/301] :arrow_up: text-buffer for new onDidChange behavior --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78e75f11b..6cae9962e 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.6.1", + "text-buffer": "13.7.0-did-change-event-1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 32e9547558c60c283225e461d3b588a76f15a344 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 26 Oct 2017 14:36:42 -0600 Subject: [PATCH 193/301] :arrow_up: tree-view /cc @Alhadis --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 418a9da02..96e7330fd 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "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", From 590302572621abd6a84b70f6b5dc3a2d5a268e8c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 14:29:24 -0700 Subject: [PATCH 194/301] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6cae9962e..494654b87 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", - "autocomplete-plus": "2.37.0", + "autocomplete-plus": "2.37.1", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From ada645aaa16c51b49357b161d81af51903d07cc3 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 26 Oct 2017 15:31:43 -0600 Subject: [PATCH 195/301] fix optimizer bailing on performDocumentUpdate --- src/view-registry.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/view-registry.js b/src/view-registry.js index dcc1624fc..87bf8620f 100644 --- a/src/view-registry.js +++ b/src/view-registry.js @@ -233,16 +233,26 @@ class ViewRegistry { this.nextUpdatePromise = null this.resolveNextUpdatePromise = null - let writer - while ((writer = this.documentWriters.shift())) { writer() } + var writer = this.documentWriters.shift() + while (writer) { + writer() + writer = this.documentWriters.shift() + } - let reader + var reader = this.documentReaders.shift() this.documentReadInProgress = true - while ((reader = this.documentReaders.shift())) { reader() } + while (reader) { + reader() + reader = this.documentReaders.shift() + } this.documentReadInProgress = false // process updates requested as a result of reads - while ((writer = this.documentWriters.shift())) { writer() } + writer = this.documentWriters.shift() + while (writer) { + writer() + writer = this.documentWriters.shift() + } if (resolveNextUpdatePromise) { resolveNextUpdatePromise() } } From ab07a6ec63b37719f1b5454ef485643728889ee5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 14:56:17 -0700 Subject: [PATCH 196/301] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 494654b87..8ea46bba2 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.7.0-did-change-event-1", + "text-buffer": "13.7.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 2f0fb1798240c1c2468a37be87610d67e640cf2d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 15:39:35 -0700 Subject: [PATCH 197/301] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5343381f..491795d55 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", - "autocomplete-plus": "2.37.1", + "autocomplete-plus": "2.37.2", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6", From 67dc7c745ecc6ec3c09dbdb346bee05170b8a065 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 26 Oct 2017 16:53:16 -0700 Subject: [PATCH 198/301] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 491795d55..f800b1b3f 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.7.0", + "text-buffer": "13.7.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From e4044699dc18903a50cfffa45f733544fa60a165 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 27 Oct 2017 21:49:27 +0200 Subject: [PATCH 199/301] :memo: [ci skip] --- src/workspace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.js b/src/workspace.js index 80dfc47cb..dcaf06006 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -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. // From 06ca120efe7850aee43b80235a1e659b0a237257 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 27 Oct 2017 13:52:58 -0700 Subject: [PATCH 200/301] :arrow_up: status-bar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f800b1b3f..362de6ac3 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "settings-view": "0.252.2", "snippets": "1.1.6", "spell-check": "0.72.3", - "status-bar": "1.8.13", + "status-bar": "1.8.14", "styleguide": "0.49.8", "symbols-view": "0.118.1", "tabs": "0.108.0", From e695e6565fc70cc159ea206e499cb1169bb0313e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 27 Oct 2017 16:46:48 -0600 Subject: [PATCH 201/301] Switch to fork of nsfw to fix symlink loops on Linux --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 362de6ac3..ab8346925 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "license": "MIT", "electronVersion": "1.6.15", "dependencies": { + "@atom/nsfw": "^1.0.17", "@atom/source-map-support": "^0.3.4", "async": "0.2.6", "atom-keymap": "8.2.8", @@ -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", From ab79a2d2b29fe51af9d9b73cc58df91beaaffaf9 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 28 Oct 2017 00:53:01 +0200 Subject: [PATCH 202/301] :memo: [ci skip] --- src/command-registry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/command-registry.js b/src/command-registry.js index 30089b7f1..ba75918ab 100644 --- a/src/command-registry.js +++ b/src/command-registry.js @@ -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 From 02d348e56ed98f67b0ab7483d2444d6854f878ae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 27 Oct 2017 17:10:59 -0600 Subject: [PATCH 203/301] :arrow_up: @atom/nsfw to public version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ab8346925..e063e25ce 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "license": "MIT", "electronVersion": "1.6.15", "dependencies": { - "@atom/nsfw": "^1.0.17", + "@atom/nsfw": "^1.0.18", "@atom/source-map-support": "^0.3.4", "async": "0.2.6", "atom-keymap": "8.2.8", From 781b87144e79bc32959953154bf2db83970081d2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 27 Oct 2017 20:46:54 -0400 Subject: [PATCH 204/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/theme-package.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/theme-package.coffee | 37 --------------------------- src/theme-package.js | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 37 deletions(-) delete mode 100644 src/theme-package.coffee create mode 100644 src/theme-package.js diff --git a/src/theme-package.coffee b/src/theme-package.coffee deleted file mode 100644 index 053132d61..000000000 --- a/src/theme-package.coffee +++ /dev/null @@ -1,37 +0,0 @@ -path = require 'path' -Package = require './package' - -module.exports = -class ThemePackage extends Package - getType: -> 'theme' - - getStyleSheetPriority: -> 1 - - enable: -> - @config.unshiftAtKeyPath('core.themes', @name) - - disable: -> - @config.removeAtKeyPath('core.themes', @name) - - preload: -> - @loadTime = 0 - @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() - - finishLoading: -> - @path = path.join(@packageManager.resourcePath, @path) - - load: -> - @loadTime = 0 - @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() - this - - activate: -> - @activationPromise ?= new Promise (resolve, reject) => - @resolveActivationPromise = resolve - @rejectActivationPromise = reject - @measure 'activateTime', => - try - @loadStylesheets() - @activateNow() - catch error - @handleError("Failed to activate the #{@name} theme", error) diff --git a/src/theme-package.js b/src/theme-package.js new file mode 100644 index 000000000..7ac01bd97 --- /dev/null +++ b/src/theme-package.js @@ -0,0 +1,55 @@ +const path = require('path') +const Package = require('./package') + +module.exports = +class ThemePackage extends Package { + getType () { + return 'theme' + } + + getStyleSheetPriority () { + return 1 + } + + enable () { + this.config.unshiftAtKeyPath('core.themes', this.name) + } + + disable () { + this.config.removeAtKeyPath('core.themes', this.name) + } + + preload () { + this.loadTime = 0 + this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata() + } + + finishLoading () { + this.path = path.join(this.packageManager.resourcePath, this.path) + } + + load () { + this.loadTime = 0 + this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata() + return this + } + + activate () { + if (this.activationPromise == null) { + this.activationPromise = new Promise((resolve, reject) => { + this.resolveActivationPromise = resolve + this.rejectActivationPromise = reject + this.measure('activateTime', () => { + try { + this.loadStylesheets() + this.activateNow() + } catch (error) { + this.handleError(`Failed to activate the ${this.name} theme`, error) + } + }) + }) + } + + return this.activationPromise + } +} From 2f4d6ae3177c4005ef6ec1c9bd9bc9abfe1e2a13 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 28 Oct 2017 17:04:07 +0200 Subject: [PATCH 205/301] :arrow_up: snippets@1.1.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 362de6ac3..f945df5b3 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.2", - "snippets": "1.1.6", + "snippets": "1.1.7", "spell-check": "0.72.3", "status-bar": "1.8.14", "styleguide": "0.49.8", From 042c22f4321fb3a35e3d8d0c2562966c12a6fc35 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 29 Oct 2017 19:01:16 +0100 Subject: [PATCH 206/301] :arrow_up: first-mate@7.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63d729a3a..0df753e69 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "etch": "^0.12.6", "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.10", + "first-mate": "7.1.0", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From 8ef74222c43794120cdb56f307037fe42c6c6fef Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 29 Oct 2017 10:21:34 -0400 Subject: [PATCH 207/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/theme-manager.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/theme-manager.coffee | 322 ------------------------------- src/theme-manager.js | 399 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 399 insertions(+), 322 deletions(-) delete mode 100644 src/theme-manager.coffee create mode 100644 src/theme-manager.js diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee deleted file mode 100644 index d5a2cb0d1..000000000 --- a/src/theme-manager.coffee +++ /dev/null @@ -1,322 +0,0 @@ -path = require 'path' -_ = require 'underscore-plus' -{Emitter, CompositeDisposable} = require 'event-kit' -{File} = require 'pathwatcher' -fs = require 'fs-plus' -LessCompileCache = require './less-compile-cache' - -# Extended: Handles loading and activating available themes. -# -# An instance of this class is always available as the `atom.themes` global. -module.exports = -class ThemeManager - constructor: ({@packageManager, @config, @styleManager, @notificationManager, @viewRegistry}) -> - @emitter = new Emitter - @styleSheetDisposablesBySourcePath = {} - @lessCache = null - @initialLoadComplete = false - @packageManager.registerPackageActivator(this, ['theme']) - @packageManager.onDidActivateInitialPackages => - @onDidChangeActiveThemes => @packageManager.reloadActivePackageStyleSheets() - - initialize: ({@resourcePath, @configDirPath, @safeMode, devMode}) -> - @lessSourcesByRelativeFilePath = null - if devMode or typeof snapshotAuxiliaryData is 'undefined' - @lessSourcesByRelativeFilePath = {} - @importedFilePathsByRelativeImportPath = {} - else - @lessSourcesByRelativeFilePath = snapshotAuxiliaryData.lessSourcesByRelativeFilePath - @importedFilePathsByRelativeImportPath = snapshotAuxiliaryData.importedFilePathsByRelativeImportPath - - ### - Section: Event Subscription - ### - - # Essential: Invoke `callback` when style sheet changes associated with - # updating the list of active themes have completed. - # - # * `callback` {Function} - onDidChangeActiveThemes: (callback) -> - @emitter.on 'did-change-active-themes', callback - - ### - Section: Accessing Available Themes - ### - - getAvailableNames: -> - # TODO: Maybe should change to list all the available themes out there? - @getLoadedNames() - - ### - Section: Accessing Loaded Themes - ### - - # Public: Returns an {Array} of {String}s of all the loaded theme names. - getLoadedThemeNames: -> - theme.name for theme in @getLoadedThemes() - - # Public: Returns an {Array} of all the loaded themes. - getLoadedThemes: -> - pack for pack in @packageManager.getLoadedPackages() when pack.isTheme() - - ### - Section: Accessing Active Themes - ### - - # Public: Returns an {Array} of {String}s all the active theme names. - getActiveThemeNames: -> - theme.name for theme in @getActiveThemes() - - # Public: Returns an {Array} of all the active themes. - getActiveThemes: -> - pack for pack in @packageManager.getActivePackages() when pack.isTheme() - - activatePackages: -> @activateThemes() - - ### - Section: Managing Enabled Themes - ### - - warnForNonExistentThemes: -> - themeNames = @config.get('core.themes') ? [] - themeNames = [themeNames] unless _.isArray(themeNames) - for themeName in themeNames - unless themeName and typeof themeName is 'string' and @packageManager.resolvePackagePath(themeName) - console.warn("Enabled theme '#{themeName}' is not installed.") - - # Public: Get the enabled theme names from the config. - # - # Returns an array of theme names in the order that they should be activated. - getEnabledThemeNames: -> - themeNames = @config.get('core.themes') ? [] - themeNames = [themeNames] unless _.isArray(themeNames) - themeNames = themeNames.filter (themeName) => - if themeName and typeof themeName is 'string' - return true if @packageManager.resolvePackagePath(themeName) - false - - # Use a built-in syntax and UI theme any time the configured themes are not - # available. - if themeNames.length < 2 - builtInThemeNames = [ - 'atom-dark-syntax' - 'atom-dark-ui' - 'atom-light-syntax' - 'atom-light-ui' - 'base16-tomorrow-dark-theme' - 'base16-tomorrow-light-theme' - 'solarized-dark-syntax' - 'solarized-light-syntax' - ] - themeNames = _.intersection(themeNames, builtInThemeNames) - if themeNames.length is 0 - themeNames = ['atom-dark-syntax', 'atom-dark-ui'] - else if themeNames.length is 1 - if _.endsWith(themeNames[0], '-ui') - themeNames.unshift('atom-dark-syntax') - else - themeNames.push('atom-dark-ui') - - # Reverse so the first (top) theme is loaded after the others. We want - # the first/top theme to override later themes in the stack. - themeNames.reverse() - - ### - Section: Private - ### - - # Resolve and apply the stylesheet specified by the path. - # - # This supports both CSS and Less stylesheets. - # - # * `stylesheetPath` A {String} path to the stylesheet that can be an absolute - # path or a relative path that will be resolved against the load path. - # - # Returns a {Disposable} on which `.dispose()` can be called to remove the - # required stylesheet. - requireStylesheet: (stylesheetPath, priority, skipDeprecatedSelectorsTransformation) -> - if fullPath = @resolveStylesheet(stylesheetPath) - content = @loadStylesheet(fullPath) - @applyStylesheet(fullPath, content, priority, skipDeprecatedSelectorsTransformation) - else - throw new Error("Could not find a file at path '#{stylesheetPath}'") - - unwatchUserStylesheet: -> - @userStylesheetSubscriptions?.dispose() - @userStylesheetSubscriptions = null - @userStylesheetFile = null - @userStyleSheetDisposable?.dispose() - @userStyleSheetDisposable = null - - loadUserStylesheet: -> - @unwatchUserStylesheet() - - userStylesheetPath = @styleManager.getUserStyleSheetPath() - return unless fs.isFileSync(userStylesheetPath) - - try - @userStylesheetFile = new File(userStylesheetPath) - @userStylesheetSubscriptions = new CompositeDisposable() - reloadStylesheet = => @loadUserStylesheet() - @userStylesheetSubscriptions.add(@userStylesheetFile.onDidChange(reloadStylesheet)) - @userStylesheetSubscriptions.add(@userStylesheetFile.onDidRename(reloadStylesheet)) - @userStylesheetSubscriptions.add(@userStylesheetFile.onDidDelete(reloadStylesheet)) - catch error - message = """ - Unable to watch path: `#{path.basename(userStylesheetPath)}`. Make sure - you have permissions to `#{userStylesheetPath}`. - - On linux there are currently problems with watch sizes. See - [this document][watches] for more info. - [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path - """ - @notificationManager.addError(message, dismissable: true) - - try - userStylesheetContents = @loadStylesheet(userStylesheetPath, true) - catch - return - - @userStyleSheetDisposable = @styleManager.addStyleSheet(userStylesheetContents, sourcePath: userStylesheetPath, priority: 2) - - loadBaseStylesheets: -> - @reloadBaseStylesheets() - - reloadBaseStylesheets: -> - @requireStylesheet('../static/atom', -2, true) - - stylesheetElementForId: (id) -> - escapedId = id.replace(/\\/g, '\\\\') - document.head.querySelector("atom-styles style[source-path=\"#{escapedId}\"]") - - resolveStylesheet: (stylesheetPath) -> - if path.extname(stylesheetPath).length > 0 - fs.resolveOnLoadPath(stylesheetPath) - else - fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']) - - loadStylesheet: (stylesheetPath, importFallbackVariables) -> - if path.extname(stylesheetPath) is '.less' - @loadLessStylesheet(stylesheetPath, importFallbackVariables) - else - fs.readFileSync(stylesheetPath, 'utf8') - - loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) -> - @lessCache ?= new LessCompileCache({ - @resourcePath, - @lessSourcesByRelativeFilePath, - @importedFilePathsByRelativeImportPath, - importPaths: @getImportPaths() - }) - - try - if importFallbackVariables - baseVarImports = """ - @import "variables/ui-variables"; - @import "variables/syntax-variables"; - """ - relativeFilePath = path.relative(@resourcePath, lessStylesheetPath) - lessSource = @lessSourcesByRelativeFilePath[relativeFilePath] - if lessSource? - content = lessSource.content - digest = lessSource.digest - else - content = baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8') - digest = null - - @lessCache.cssForFile(lessStylesheetPath, content, digest) - else - @lessCache.read(lessStylesheetPath) - catch error - error.less = true - if error.line? - # Adjust line numbers for import fallbacks - error.line -= 2 if importFallbackVariables - - message = "Error compiling Less stylesheet: `#{lessStylesheetPath}`" - detail = """ - Line number: #{error.line} - #{error.message} - """ - else - message = "Error loading Less stylesheet: `#{lessStylesheetPath}`" - detail = error.message - - @notificationManager.addError(message, {detail, dismissable: true}) - throw error - - removeStylesheet: (stylesheetPath) -> - @styleSheetDisposablesBySourcePath[stylesheetPath]?.dispose() - - applyStylesheet: (path, text, priority, skipDeprecatedSelectorsTransformation) -> - @styleSheetDisposablesBySourcePath[path] = @styleManager.addStyleSheet( - text, - { - priority, - skipDeprecatedSelectorsTransformation, - sourcePath: path - } - ) - - activateThemes: -> - new Promise (resolve) => - # @config.observe runs the callback once, then on subsequent changes. - @config.observe 'core.themes', => - @deactivateThemes().then => - @warnForNonExistentThemes() - @refreshLessCache() # Update cache for packages in core.themes config - - promises = [] - for themeName in @getEnabledThemeNames() - if @packageManager.resolvePackagePath(themeName) - promises.push(@packageManager.activatePackage(themeName)) - else - console.warn("Failed to activate theme '#{themeName}' because it isn't installed.") - - Promise.all(promises).then => - @addActiveThemeClasses() - @refreshLessCache() # Update cache again now that @getActiveThemes() is populated - @loadUserStylesheet() - @reloadBaseStylesheets() - @initialLoadComplete = true - @emitter.emit 'did-change-active-themes' - resolve() - - deactivateThemes: -> - @removeActiveThemeClasses() - @unwatchUserStylesheet() - results = @getActiveThemes().map((pack) => @packageManager.deactivatePackage(pack.name)) - Promise.all(results.filter((r) -> typeof r?.then is 'function')) - - isInitialLoadComplete: -> @initialLoadComplete - - addActiveThemeClasses: -> - if workspaceElement = @viewRegistry.getView(@workspace) - for pack in @getActiveThemes() - workspaceElement.classList.add("theme-#{pack.name}") - return - - removeActiveThemeClasses: -> - workspaceElement = @viewRegistry.getView(@workspace) - for pack in @getActiveThemes() - workspaceElement.classList.remove("theme-#{pack.name}") - return - - refreshLessCache: -> - @lessCache?.setImportPaths(@getImportPaths()) - - getImportPaths: -> - activeThemes = @getActiveThemes() - if activeThemes.length > 0 - themePaths = (theme.getStylesheetsPath() for theme in activeThemes when theme) - else - themePaths = [] - for themeName in @getEnabledThemeNames() - if themePath = @packageManager.resolvePackagePath(themeName) - deprecatedPath = path.join(themePath, 'stylesheets') - if fs.isDirectorySync(deprecatedPath) - themePaths.push(deprecatedPath) - else - themePaths.push(path.join(themePath, 'styles')) - - themePaths.filter (themePath) -> fs.isDirectorySync(themePath) diff --git a/src/theme-manager.js b/src/theme-manager.js new file mode 100644 index 000000000..b46fb1ada --- /dev/null +++ b/src/theme-manager.js @@ -0,0 +1,399 @@ +const path = require('path') +const _ = require('underscore-plus') +const {Emitter, CompositeDisposable} = require('event-kit') +const {File} = require('pathwatcher') +const fs = require('fs-plus') +const LessCompileCache = require('./less-compile-cache') + +// Extended: Handles loading and activating available themes. +// +// An instance of this class is always available as the `atom.themes` global. +module.exports = +class ThemeManager { + constructor ({packageManager, config, styleManager, notificationManager, viewRegistry}) { + this.packageManager = packageManager + this.config = config + this.styleManager = styleManager + this.notificationManager = notificationManager + this.viewRegistry = viewRegistry + this.emitter = new Emitter() + this.styleSheetDisposablesBySourcePath = {} + this.lessCache = null + this.initialLoadComplete = false + this.packageManager.registerPackageActivator(this, ['theme']) + this.packageManager.onDidActivateInitialPackages(() => { + this.onDidChangeActiveThemes(() => this.packageManager.reloadActivePackageStyleSheets()) + }) + } + + initialize ({resourcePath, configDirPath, safeMode, devMode}) { + this.resourcePath = resourcePath + this.configDirPath = configDirPath + this.safeMode = safeMode + this.lessSourcesByRelativeFilePath = null + if (devMode || (typeof snapshotAuxiliaryData === 'undefined')) { + this.lessSourcesByRelativeFilePath = {} + this.importedFilePathsByRelativeImportPath = {} + } else { + this.lessSourcesByRelativeFilePath = snapshotAuxiliaryData.lessSourcesByRelativeFilePath + this.importedFilePathsByRelativeImportPath = snapshotAuxiliaryData.importedFilePathsByRelativeImportPath + } + } + + /* + Section: Event Subscription + */ + + // Essential: Invoke `callback` when style sheet changes associated with + // updating the list of active themes have completed. + // + // * `callback` {Function} + onDidChangeActiveThemes (callback) { + return this.emitter.on('did-change-active-themes', callback) + } + + /* + Section: Accessing Available Themes + */ + + getAvailableNames () { + // TODO: Maybe should change to list all the available themes out there? + return this.getLoadedNames() + } + + /* + Section: Accessing Loaded Themes + */ + + // Public: Returns an {Array} of {String}s of all the loaded theme names. + getLoadedThemeNames () { + return this.getLoadedThemes().map((theme) => theme.name) + } + + // Public: Returns an {Array} of all the loaded themes. + getLoadedThemes () { + return this.packageManager.getLoadedPackages().filter((pack) => pack.isTheme()) + } + + /* + Section: Accessing Active Themes + */ + + // Public: Returns an {Array} of {String}s of all the active theme names. + getActiveThemeNames () { + return this.getActiveThemes().map((theme) => theme.name) + } + + // Public: Returns an {Array} of all the active themes. + getActiveThemes () { + return this.packageManager.getActivePackages().filter((pack) => pack.isTheme()) + } + + activatePackages () { + return this.activateThemes() + } + + /* + Section: Managing Enabled Themes + */ + + warnForNonExistentThemes () { + let themeNames = this.config.get('core.themes') || [] + if (!_.isArray(themeNames)) { themeNames = [themeNames] } + for (let themeName of themeNames) { + if (!themeName || (typeof themeName !== 'string') || !this.packageManager.resolvePackagePath(themeName)) { + console.warn(`Enabled theme '${themeName}' is not installed.`) + } + } + } + + // Public: Get the enabled theme names from the config. + // + // Returns an array of theme names in the order that they should be activated. + getEnabledThemeNames () { + let themeNames = this.config.get('core.themes') || [] + if (!_.isArray(themeNames)) { themeNames = [themeNames] } + themeNames = themeNames.filter((themeName) => + (typeof themeName === 'string') && this.packageManager.resolvePackagePath(themeName) + ) + + // Use a built-in syntax and UI theme any time the configured themes are not + // available. + if (themeNames.length < 2) { + const builtInThemeNames = [ + 'atom-dark-syntax', + 'atom-dark-ui', + 'atom-light-syntax', + 'atom-light-ui', + 'base16-tomorrow-dark-theme', + 'base16-tomorrow-light-theme', + 'solarized-dark-syntax', + 'solarized-light-syntax' + ] + themeNames = _.intersection(themeNames, builtInThemeNames) + if (themeNames.length === 0) { + themeNames = ['atom-dark-syntax', 'atom-dark-ui'] + } else if (themeNames.length === 1) { + if (_.endsWith(themeNames[0], '-ui')) { + themeNames.unshift('atom-dark-syntax') + } else { + themeNames.push('atom-dark-ui') + } + } + } + + // Reverse so the first (top) theme is loaded after the others. We want + // the first/top theme to override later themes in the stack. + return themeNames.reverse() + } + + /* + Section: Private + */ + + // Resolve and apply the stylesheet specified by the path. + // + // This supports both CSS and Less stylesheets. + // + // * `stylesheetPath` A {String} path to the stylesheet that can be an absolute + // path or a relative path that will be resolved against the load path. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // required stylesheet. + requireStylesheet (stylesheetPath, priority, skipDeprecatedSelectorsTransformation) { + let fullPath = this.resolveStylesheet(stylesheetPath) + if (fullPath) { + const content = this.loadStylesheet(fullPath) + return this.applyStylesheet(fullPath, content, priority, skipDeprecatedSelectorsTransformation) + } else { + throw new Error(`Could not find a file at path '${stylesheetPath}'`) + } + } + + unwatchUserStylesheet () { + if (this.userStylesheetSubscriptions != null) this.userStylesheetSubscriptions.dispose() + this.userStylesheetSubscriptions = null + this.userStylesheetFile = null + if (this.userStyleSheetDisposable != null) this.userStyleSheetDisposable.dispose() + this.userStyleSheetDisposable = null + } + + loadUserStylesheet () { + this.unwatchUserStylesheet() + + const userStylesheetPath = this.styleManager.getUserStyleSheetPath() + if (!fs.isFileSync(userStylesheetPath)) { return } + + try { + this.userStylesheetFile = new File(userStylesheetPath) + this.userStylesheetSubscriptions = new CompositeDisposable() + const reloadStylesheet = () => this.loadUserStylesheet() + this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidChange(reloadStylesheet)) + this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidRename(reloadStylesheet)) + this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidDelete(reloadStylesheet)) + } catch (error) { + const message = `\ +Unable to watch path: \`${path.basename(userStylesheetPath)}\`. Make sure +you have permissions to \`${userStylesheetPath}\`. + +On linux there are currently problems with watch sizes. See +[this document][watches] for more info. +[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\ +` + this.notificationManager.addError(message, {dismissable: true}) + } + + let userStylesheetContents + try { + userStylesheetContents = this.loadStylesheet(userStylesheetPath, true) + } catch (error) { + return + } + + this.userStyleSheetDisposable = this.styleManager.addStyleSheet(userStylesheetContents, {sourcePath: userStylesheetPath, priority: 2}) + } + + loadBaseStylesheets () { + this.reloadBaseStylesheets() + } + + reloadBaseStylesheets () { + this.requireStylesheet('../static/atom', -2, true) + } + + stylesheetElementForId (id) { + const escapedId = id.replace(/\\/g, '\\\\') + return document.head.querySelector(`atom-styles style[source-path="${escapedId}"]`) + } + + resolveStylesheet (stylesheetPath) { + if (path.extname(stylesheetPath).length > 0) { + return fs.resolveOnLoadPath(stylesheetPath) + } else { + return fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']) + } + } + + loadStylesheet (stylesheetPath, importFallbackVariables) { + if (path.extname(stylesheetPath) === '.less') { + return this.loadLessStylesheet(stylesheetPath, importFallbackVariables) + } else { + return fs.readFileSync(stylesheetPath, 'utf8') + } + } + + loadLessStylesheet (lessStylesheetPath, importFallbackVariables = false) { + if (this.lessCache == null) { + this.lessCache = new LessCompileCache({ + resourcePath: this.resourcePath, + lessSourcesByRelativeFilePath: this.lessSourcesByRelativeFilePath, + importedFilePathsByRelativeImportPath: this.importedFilePathsByRelativeImportPath, + importPaths: this.getImportPaths() + }) + } + + try { + if (importFallbackVariables) { + const baseVarImports = `\ +@import "variables/ui-variables"; +@import "variables/syntax-variables";\ +` + const relativeFilePath = path.relative(this.resourcePath, lessStylesheetPath) + const lessSource = this.lessSourcesByRelativeFilePath[relativeFilePath] + + let content, digest + if (lessSource != null) { + ({ content } = lessSource); + ({ digest } = lessSource) + } else { + content = baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8') + digest = null + } + + return this.lessCache.cssForFile(lessStylesheetPath, content, digest) + } else { + return this.lessCache.read(lessStylesheetPath) + } + } catch (error) { + let detail, message + error.less = true + if (error.line != null) { + // Adjust line numbers for import fallbacks + if (importFallbackVariables) { error.line -= 2 } + + message = `Error compiling Less stylesheet: \`${lessStylesheetPath}\`` + detail = `Line number: ${error.line}\n${error.message}` + } else { + message = `Error loading Less stylesheet: \`${lessStylesheetPath}\`` + detail = error.message + } + + this.notificationManager.addError(message, {detail, dismissable: true}) + throw error + } + } + + removeStylesheet (stylesheetPath) { + if (this.styleSheetDisposablesBySourcePath[stylesheetPath] != null) { + this.styleSheetDisposablesBySourcePath[stylesheetPath].dispose() + } + } + + applyStylesheet (path, text, priority, skipDeprecatedSelectorsTransformation) { + this.styleSheetDisposablesBySourcePath[path] = this.styleManager.addStyleSheet( + text, + { + priority, + skipDeprecatedSelectorsTransformation, + sourcePath: path + } + ) + + return this.styleSheetDisposablesBySourcePath[path] + } + + activateThemes () { + return new Promise(resolve => { + // @config.observe runs the callback once, then on subsequent changes. + this.config.observe('core.themes', () => { + this.deactivateThemes().then(() => { + this.warnForNonExistentThemes() + this.refreshLessCache() // Update cache for packages in core.themes config + + const promises = [] + for (const themeName of this.getEnabledThemeNames()) { + if (this.packageManager.resolvePackagePath(themeName)) { + promises.push(this.packageManager.activatePackage(themeName)) + } else { + console.warn(`Failed to activate theme '${themeName}' because it isn't installed.`) + } + } + + return Promise.all(promises).then(() => { + this.addActiveThemeClasses() + this.refreshLessCache() // Update cache again now that @getActiveThemes() is populated + this.loadUserStylesheet() + this.reloadBaseStylesheets() + this.initialLoadComplete = true + this.emitter.emit('did-change-active-themes') + resolve() + }) + }) + }) + }) + } + + deactivateThemes () { + this.removeActiveThemeClasses() + this.unwatchUserStylesheet() + const results = this.getActiveThemes().map(pack => this.packageManager.deactivatePackage(pack.name)) + return Promise.all(results.filter((r) => (r != null) && (typeof r.then === 'function'))) + } + + isInitialLoadComplete () { + return this.initialLoadComplete + } + + addActiveThemeClasses () { + const workspaceElement = this.viewRegistry.getView(this.workspace) + if (workspaceElement) { + for (const pack of this.getActiveThemes()) { + workspaceElement.classList.add(`theme-${pack.name}`) + } + } + } + + removeActiveThemeClasses () { + const workspaceElement = this.viewRegistry.getView(this.workspace) + for (const pack of this.getActiveThemes()) { + workspaceElement.classList.remove(`theme-${pack.name}`) + } + } + + refreshLessCache () { + if (this.lessCache) this.lessCache.setImportPaths(this.getImportPaths()) + } + + getImportPaths () { + let themePaths + const activeThemes = this.getActiveThemes() + if (activeThemes.length > 0) { + themePaths = (activeThemes.filter((theme) => theme).map((theme) => theme.getStylesheetsPath())) + } else { + themePaths = [] + for (const themeName of this.getEnabledThemeNames()) { + const themePath = this.packageManager.resolvePackagePath(themeName) + if (themePath) { + const deprecatedPath = path.join(themePath, 'stylesheets') + if (fs.isDirectorySync(deprecatedPath)) { + themePaths.push(deprecatedPath) + } else { + themePaths.push(path.join(themePath, 'styles')) + } + } + } + } + + return themePaths.filter(themePath => fs.isDirectorySync(themePath)) + } +} From c06745f098a2c462c3bacc569b4aa53c868ed62b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 29 Oct 2017 14:59:12 -0400 Subject: [PATCH 208/301] =?UTF-8?q?=F0=9F=91=95=20Suppress=20"'snapshotAux?= =?UTF-8?q?iliaryData'=20is=20not=20defined"=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/theme-manager.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/theme-manager.js b/src/theme-manager.js index b46fb1ada..6abf0fc74 100644 --- a/src/theme-manager.js +++ b/src/theme-manager.js @@ -1,3 +1,5 @@ +/* global snapshotAuxiliaryData */ + const path = require('path') const _ = require('underscore-plus') const {Emitter, CompositeDisposable} = require('event-kit') From 4eea63c50b4f8061f633392bf581e3d3a4fb3e5b Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 30 Oct 2017 10:31:41 +0100 Subject: [PATCH 209/301] :memo: --- src/workspace.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workspace.js b/src/workspace.js index dcaf06006..defb43df0 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -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 From 9eb9cb1a4a7fd9c5270e40a2a50684d5a466ecf3 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 30 Oct 2017 15:09:53 +0100 Subject: [PATCH 210/301] :arrow_up: language-perl@0.38.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dcbeb05c1..777e96513 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-make": "0.22.3", "language-mustache": "0.14.3", "language-objective-c": "0.15.1", - "language-perl": "0.38.0", + "language-perl": "0.38.1", "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", From d035e41f378497cc2d23b9f7983adebc1bd1820e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 30 Oct 2017 13:27:47 -0600 Subject: [PATCH 211/301] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0df753e69..777a2be49 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.6.1", + "fuzzy-finder": "1.7.0", "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 8284995f5f1a56691d2581e773a9ba32b4e9d642 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 30 Oct 2017 13:35:08 -0600 Subject: [PATCH 212/301] Revert ":arrow_up: fuzzy-finder" This reverts commit d035e41f378497cc2d23b9f7983adebc1bd1820e. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 777a2be49..0df753e69 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.7.0", + "fuzzy-finder": "1.6.1", "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 058a42f7c2ff1a5ac35578b9b28ef32862f0d158 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 30 Oct 2017 14:03:18 -0600 Subject: [PATCH 213/301] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0df753e69..4ccff5107 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.6.1", + "fuzzy-finder": "1.7.1", "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 09c25baf6caa34774acbf04f1217bbcffd2d985e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 30 Oct 2017 13:34:45 -0700 Subject: [PATCH 214/301] :arrow_up: text-buffer for DisplayLayer.onDidChangeSync change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ccff5107..678ec53e3 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.7.1", + "text-buffer": "13.8.0-display-layer-change-event-1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From b30b1e36aba1adada4e036efb0c8de13c2e0ef23 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Mon, 30 Oct 2017 17:17:52 -0600 Subject: [PATCH 215/301] :arrow_up: text-buffer@13.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ccff5107..348bb2d41 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.7.1", + "text-buffer": "13.7.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 482824047c9c92a15d429577ed1eec8d8c49c16a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 30 Oct 2017 17:26:21 -0700 Subject: [PATCH 216/301] :arrow_up: text-buffer --- package.json | 2 +- src/text-editor-component.js | 8 ++++++-- src/text-editor.coffee | 21 ++++++++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 678ec53e3..8dfed5b41 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.8.0-display-layer-change-event-1", + "text-buffer": "13.8.0-display-layer-change-event-2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/src/text-editor-component.js b/src/text-editor-component.js index f19b7e31c..b67b45f83 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2456,8 +2456,12 @@ class TextEditorComponent { didChangeDisplayLayer (changes) { for (let i = 0; i < changes.length; i++) { - const {start, oldExtent, newExtent} = changes[i] - this.spliceLineTopIndex(start.row, oldExtent.row, newExtent.row) + const {oldRange, newRange} = changes[i] + this.spliceLineTopIndex( + newRange.start.row, + oldRange.end.row - oldRange.start.row, + newRange.end.row - newRange.start.row + ) } this.scheduleUpdate() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 79e00e31a..3bc5fa34e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -468,10 +468,10 @@ class TextEditor extends Model subscribeToDisplayLayer: -> @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) - @disposables.add @displayLayer.onDidChangeSync (e) => + @disposables.add @displayLayer.onDidChange (changes) => @mergeIntersectingSelections() - @component?.didChangeDisplayLayer(e) - @emitter.emit 'did-change', e + @component?.didChangeDisplayLayer(changes) + @emitter.emit 'did-change', changes.map (change) -> new ChangeEvent(change) @disposables.add @displayLayer.onDidReset => @mergeIntersectingSelections() @component?.didResetDisplayLayer() @@ -3911,3 +3911,18 @@ class TextEditor extends Model endRow++ new Range(new Point(startRow, 0), new Point(endRow, @buffer.lineLengthForRow(endRow))) + +class ChangeEvent + constructor: ({@oldRange, @newRange}) -> + + Object.defineProperty @prototype, 'start', { + get: -> @oldRange.start + } + + Object.defineProperty @prototype, 'oldExtent', { + get: -> @oldRange.getExtent() + } + + Object.defineProperty @prototype, 'newExtent', { + get: -> @newRange.getExtent() + } From 1395e69fe007efa7c11efe21a251cb8897c875fb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 Oct 2017 10:10:07 -0700 Subject: [PATCH 217/301] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cf9d08db9..80677b540 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.8.0-display-layer-change-event-2", + "text-buffer": "13.8.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From a88d453b4ad34752d453ac164dfb90d174d05b1e Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Tue, 31 Oct 2017 11:47:55 -0600 Subject: [PATCH 218/301] :arrow_up: snippets@1.1.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 80677b540..6fb7f3394 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.2", - "snippets": "1.1.7", + "snippets": "1.1.8", "spell-check": "0.72.3", "status-bar": "1.8.14", "styleguide": "0.49.8", From 5af83435abbebd4e40f20b841a8e68198bfed8b4 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Tue, 31 Oct 2017 18:48:58 +0100 Subject: [PATCH 219/301] :arrow_up: exception-reporting@0.41.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6fb7f3394..de9a49468 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", "encoding-selector": "0.23.7", - "exception-reporting": "0.41.4", + "exception-reporting": "0.41.5", "find-and-replace": "0.212.3", "fuzzy-finder": "1.7.1", "github": "0.7.0", From 080137f377d439ffa2bd23c391dad31953427325 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 09:36:29 -0600 Subject: [PATCH 220/301] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de9a49468..56619128a 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.7.1", + "fuzzy-finder": "1.7.2", "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From cffa433267111317bebd2254386fd282bfb0aae4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 12:15:14 -0600 Subject: [PATCH 221/301] :arrow_up: tabs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 56619128a..98ccece1a 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.14", "styleguide": "0.49.8", "symbols-view": "0.118.1", - "tabs": "0.108.0", + "tabs": "0.109.0", "timecop": "0.36.0", "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", From c3adde5846e44828ceb3eaa4880e58af3bde71e0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 12:46:15 -0600 Subject: [PATCH 222/301] :arrow_up: archive-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98ccece1a..de6813c2d 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.8", - "archive-view": "0.63.4", + "archive-view": "0.64.0", "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", From 70e9ebd545df3216e4152dd9989af25c3348ca82 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 12:53:10 -0600 Subject: [PATCH 223/301] Revert ":arrow_up: archive-view" This reverts commit c3adde5846e44828ceb3eaa4880e58af3bde71e0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de6813c2d..98ccece1a 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.8", - "archive-view": "0.64.0", + "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", From 25cb5c2c2779d74eca03573f8f908baffdc4edad Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 13:14:01 -0600 Subject: [PATCH 224/301] :arrow_up: archive-view This version should be able to be packaged cleanly. /cc @Alhadis --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 98ccece1a..72250311d 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.8", - "archive-view": "0.63.4", + "archive-view": "0.64.1", "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", From 252a98b231eac7b94581cc640a7e61f043729d60 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 31 Oct 2017 13:55:40 -0600 Subject: [PATCH 225/301] Prevent the browser from auto-scrolling the scroll container on spacebar --- src/text-editor-component.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index b67b45f83..4c639e532 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1616,11 +1616,23 @@ class TextEditorComponent { if (this.isInputEnabled()) { event.stopPropagation() - // WARNING: If we call preventDefault on the input of a space character, - // then the browser interprets the spacebar keypress as a page-down command, - // causing spaces to scroll elements containing editors. This is impossible - // to test. - if (event.data !== ' ') event.preventDefault() + // WARNING: If we call preventDefault on the input of a space + // character, then the browser interprets the spacebar keypress as a + // page-down command, causing spaces to scroll elements containing + // editors. This means typing space will actually change the contents + // of the hidden input, which will cause the browser to autoscroll the + // scroll container to reveal the input if it is off screen (See + // https://github.com/atom/atom/issues/16046). To correct for this + // situation, we automatically reset the scroll position to 0,0 after + // typing a space. None of this can really be tested. + if (event.data === ' ') { + window.setImmediate(() => { + this.refs.scrollContainer.scrollTop = 0 + this.refs.scrollContainer.scrollLeft = 0 + }) + } else { + event.preventDefault() + } // If the input event is fired while the accented character menu is open it // means that the user has chosen one of the accented alternatives. Thus, we From cbc2bdb20ba79787572b2cf948e4a43b802855be Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 31 Oct 2017 15:31:10 -0700 Subject: [PATCH 226/301] :arrow_up: github@0.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72250311d..5f2b8dfa1 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.41.5", "find-and-replace": "0.212.3", "fuzzy-finder": "1.7.2", - "github": "0.7.0", + "github": "0.8.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.8", From e76be3839ee0f1485768211eb92d7b47784340ea Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:22:11 +0100 Subject: [PATCH 227/301] :arrow_up: find-and-replace@0.212.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f2b8dfa1..dcbaf9c0d 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", - "find-and-replace": "0.212.3", + "find-and-replace": "0.212.4", "fuzzy-finder": "1.7.2", "github": "0.8.0", "git-diff": "1.3.6", From a66dcc41a6266634994dfe34c9df896189721091 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:23:49 +0100 Subject: [PATCH 228/301] :arrow_up: markdown-preview@0.159.18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dcbaf9c0d..17c9dc9e1 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.17", + "markdown-preview": "0.159.18", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From 60aa93846e201c1597b5695a59a6e3144f647163 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:25:01 +0100 Subject: [PATCH 229/301] :arrow_up: keybinding-resolver@0.38.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17c9dc9e1..1a9194951 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "grammar-selector": "0.49.8", "image-view": "0.62.4", "incompatible-packages": "0.27.3", - "keybinding-resolver": "0.38.0", + "keybinding-resolver": "0.38.1", "line-ending-selector": "0.7.4", "link": "0.31.3", "markdown-preview": "0.159.18", From 9a3d98cf9b2b3fd86af6a7c2a678f9fbdf0628c1 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:28:45 +0100 Subject: [PATCH 230/301] :arrow_down: language-less, language-sass --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1a9194951..1e92bd287 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "language-java": "0.27.5", "language-javascript": "0.127.6", "language-json": "0.19.1", - "language-less": "0.34.0", + "language-less": "0.33.0", "language-make": "0.22.3", "language-mustache": "0.14.4", "language-objective-c": "0.15.1", @@ -159,7 +159,7 @@ "language-python": "0.45.5", "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.61.2", + "language-sass": "0.61.1", "language-shellscript": "0.25.4", "language-source": "0.9.0", "language-sql": "0.25.8", From 449fcfbd24e3795f3bc9dba8b7aae6b54265e468 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:30:16 +0100 Subject: [PATCH 231/301] :arrow_up: language-java@0.27.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e92bd287..64b17206a 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "language-go": "0.44.3", "language-html": "0.48.2", "language-hyperlink": "0.16.3", - "language-java": "0.27.5", + "language-java": "0.27.6", "language-javascript": "0.127.6", "language-json": "0.19.1", "language-less": "0.33.0", From 138524db115233c9db051644922f324fdc12f103 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 12:31:23 +0100 Subject: [PATCH 232/301] :arrow_up: language-coffee-script@0.49.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64b17206a..1c88caa54 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", - "language-coffee-script": "0.49.2", + "language-coffee-script": "0.49.3", "language-csharp": "0.14.3", "language-css": "0.42.7", "language-gfm": "0.90.2", From 4c02e96f2ae03d35aa148079431cf2c80774d4db Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 13:59:02 +0100 Subject: [PATCH 233/301] Preserve whitespace --- static/jasmine.less | 1 + 1 file changed, 1 insertion(+) diff --git a/static/jasmine.less b/static/jasmine.less index ab2695179..dcd467c71 100644 --- a/static/jasmine.less +++ b/static/jasmine.less @@ -165,6 +165,7 @@ body { font-weight: bold; color: #d9534f; padding: 5px 0 5px 0; + white-space: pre-wrap; } .result-message.deprecation-message { From b6c804d637803d4d494e77de12aeb2fcc6114802 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 1 Nov 2017 13:59:21 +0100 Subject: [PATCH 234/301] Do not modify menus --- spec/menu-manager-spec.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec/menu-manager-spec.coffee b/spec/menu-manager-spec.coffee index 798aa3766..3bbd8b9da 100644 --- a/spec/menu-manager-spec.coffee +++ b/spec/menu-manager-spec.coffee @@ -6,6 +6,7 @@ describe "MenuManager", -> beforeEach -> menu = new MenuManager({keymapManager: atom.keymaps, packageManager: atom.packages}) + spyOn(menu, 'sendToBrowserProcess') # Do not modify Atom's actual menus menu.initialize({resourcePath: atom.getLoadSettings().resourcePath}) describe "::add(items)", -> @@ -54,7 +55,6 @@ describe "MenuManager", -> afterEach -> Object.defineProperty process, 'platform', value: originalPlatform it "sends the current menu template and associated key bindings to the browser process", -> - spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b' menu.update() @@ -66,7 +66,6 @@ describe "MenuManager", -> it "omits key bindings that are mapped to unset! in any context", -> # it would be nice to be smarter about omitting, but that would require a much # more dynamic interaction between the currently focused element and the menu - spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}] atom.keymaps.add 'test', 'atom-workspace': 'ctrl-b': 'b' atom.keymaps.add 'test', 'atom-text-editor': 'ctrl-b': 'unset!' @@ -77,7 +76,6 @@ describe "MenuManager", -> it "omits key bindings that could conflict with AltGraph characters on macOS", -> Object.defineProperty process, 'platform', value: 'darwin' - spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [ {label: "B", command: "b"}, {label: "C", command: "c"} @@ -98,7 +96,6 @@ describe "MenuManager", -> it "omits key bindings that could conflict with AltGraph characters on Windows", -> Object.defineProperty process, 'platform', value: 'win32' - spyOn(menu, 'sendToBrowserProcess') menu.add [{label: "A", submenu: [ {label: "B", command: "b"}, {label: "C", command: "c"} From 11511f27d5d8cfe9c23919c8a8bfaa9167f3a1b5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 09:21:13 -0600 Subject: [PATCH 235/301] Don't terminate selection dragging when a modifier key is pressed This preserves the ability to add selections via ctrl- or cmd-click. --- spec/text-editor-component-spec.js | 21 +++++++++++++++++---- src/text-editor-component.js | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 5f0a28883..97fdf45c7 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4428,11 +4428,14 @@ describe('TextEditorComponent', () => { const {component, editor} = buildComponent() let dragging = false - component.handleMouseDragUntilMouseUp({ - didDrag: (event) => { dragging = true }, - didStopDragging: () => { dragging = false } - }) + function startDragging () { + component.handleMouseDragUntilMouseUp({ + didDrag: (event) => { dragging = true }, + didStopDragging: () => { dragging = false } + }) + } + startDragging() window.dispatchEvent(new MouseEvent('mousemove')) await getNextAnimationFramePromise() expect(dragging).toBe(true) @@ -4448,6 +4451,16 @@ describe('TextEditorComponent', () => { window.dispatchEvent(new MouseEvent('mousemove')) await getNextAnimationFramePromise() expect(dragging).toBe(false) + + // Pressing a modifier key does not terminate dragging, (to ensure we can add new selections with the mouse) + startDragging() + window.dispatchEvent(new MouseEvent('mousemove')) + await getNextAnimationFramePromise() + expect(dragging).toBe(true) + component.didKeydown({key: 'Control'}) + component.didKeydown({key: 'Alt'}) + component.didKeydown({key: 'Meta'}) + expect(dragging).toBe(true) }) function getNextAnimationFramePromise () { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 4c639e532..bbb02bb7f 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1666,7 +1666,7 @@ class TextEditorComponent { // Stop dragging when user interacts with the keyboard. This prevents // unwanted selections in the case edits are performed while selecting text // at the same time. - if (this.stopDragging) this.stopDragging() + if (this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta') this.stopDragging() if (this.lastKeydownBeforeKeypress != null) { if (this.lastKeydownBeforeKeypress.code === event.code) { From afc341ed4674d53e86779ef49926a0c539638ec1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 09:50:45 -0600 Subject: [PATCH 236/301] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c88caa54..a0ae60d00 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", - "find-and-replace": "0.212.4", + "find-and-replace": "0.213.0", "fuzzy-finder": "1.7.2", "github": "0.8.0", "git-diff": "1.3.6", From f0f0ba2296ccf49474490d3394a898129a12c050 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 10:13:37 -0600 Subject: [PATCH 237/301] :arrow_up: event-kit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0ae60d00..2716fac9f 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.109.0", "timecop": "0.36.0", - "tree-view": "0.221.0", + "tree-view": "0.221.1", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.4", From f25570f135ef75dc518f02db1dc3619dd75d9f54 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 14:00:43 -0600 Subject: [PATCH 238/301] Exclude Shift from keydown events that terminate selection drags --- spec/text-editor-component-spec.js | 1 + src/text-editor-component.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 97fdf45c7..992785d6e 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4459,6 +4459,7 @@ describe('TextEditorComponent', () => { expect(dragging).toBe(true) component.didKeydown({key: 'Control'}) component.didKeydown({key: 'Alt'}) + component.didKeydown({key: 'Shift'}) component.didKeydown({key: 'Meta'}) expect(dragging).toBe(true) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index bbb02bb7f..2a77e30f8 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1665,8 +1665,11 @@ class TextEditorComponent { didKeydown (event) { // Stop dragging when user interacts with the keyboard. This prevents // unwanted selections in the case edits are performed while selecting text - // at the same time. - if (this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta') this.stopDragging() + // at the same time. Modifier keys are exempt to preserve the ability to + // add selections, shift-scroll horizontally while selecting. + if (this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta' && event.key !== 'Shift') { + this.stopDragging() + } if (this.lastKeydownBeforeKeypress != null) { if (this.lastKeydownBeforeKeypress.code === event.code) { From 61b4fc7d2921a7b11daee0f044091add9731d9f0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 1 Nov 2017 15:30:21 -0600 Subject: [PATCH 239/301] Actually require @atom/nsfw dependency in path-watcher.js :facepalm: --- src/path-watcher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path-watcher.js b/src/path-watcher.js index 2dfece46e..5a2d10bde 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -4,7 +4,7 @@ const fs = require('fs') const path = require('path') const {Emitter, Disposable, CompositeDisposable} = require('event-kit') -const nsfw = require('nsfw') +const nsfw = require('@atom/nsfw') const {NativeWatcherRegistry} = require('./native-watcher-registry') // Private: Associate native watcher action flags with descriptive String equivalents. From 2d20886cfa991a1db2431d1883ddd5db65914472 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 1 Nov 2017 14:36:23 -0700 Subject: [PATCH 240/301] Rename AtomApplication#windows -> #windowStack Update references across codebase --- src/main-process/application-menu.coffee | 2 +- src/main-process/atom-application.coffee | 18 +++++++++--------- src/main-process/auto-update-manager.coffee | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main-process/application-menu.coffee b/src/main-process/application-menu.coffee index 681677603..35bc7d66c 100644 --- a/src/main-process/application-menu.coffee +++ b/src/main-process/application-menu.coffee @@ -128,7 +128,7 @@ class ApplicationMenu ] focusedWindow: -> - _.find global.atomApplication.windows, (atomWindow) -> atomWindow.isFocused() + _.find global.atomApplication.getAllWindows(), (atomWindow) -> atomWindow.isFocused() # Combines a menu template with the appropriate keystroke. # diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index fc2058dd4..a5e3e6b0b 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -67,7 +67,7 @@ class AtomApplication {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options @socketPath = null if options.test or options.benchmark or options.benchmarkTest @pidsToOpenWindows = {} - @windows = new WindowStack() + @windowStack = new WindowStack() @config = new Config({enablePersistence: true}) @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} @@ -162,7 +162,7 @@ class AtomApplication # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> - @windows.removeWindow(window) + @windowStacktack.removeWindow(window) if @getAllWindows().length is 0 @applicationMenu?.enableWindowSpecificItems(false) if process.platform in ['win32', 'linux'] @@ -172,27 +172,27 @@ class AtomApplication # Public: Adds the {AtomWindow} to the global window list. addWindow: (window) -> - @windows.addWindow(window) + @windowStack.addWindow(window) @applicationMenu?.addWindow(window.browserWindow) window.once 'window:loaded', => @autoUpdateManager?.emitUpdateAvailableEvent(window) unless window.isSpec - focusHandler = => @windows.touch(window) + focusHandler = => @windowStack.touch(window) blurHandler = => @saveState(false) window.browserWindow.on 'focus', focusHandler window.browserWindow.on 'blur', blurHandler window.browserWindow.once 'closed', => - @windows.removeWindow(window) + @windowStack.removeWindow(window) window.browserWindow.removeListener 'focus', focusHandler window.browserWindow.removeListener 'blur', blurHandler window.browserWindow.webContents.once 'did-finish-load', => @saveState(false) getAllWindows: => - @windows.all().slice() + @windowStack.all().slice() getLastFocusedWindow: (predicate) => - @windows.getLastFocusedWindow(predicate) + @windowStack.getLastFocusedWindow(predicate) # Creates server to listen for additional atom application launches. # @@ -589,7 +589,7 @@ class AtomApplication windowDimensions ?= @getDimensionsForNewWindow() openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env}) openedWindow.focus() - @windows.addWindow(openedWindow) + @windowStack.addWindow(openedWindow) if pidToKillWhenClosed? @pidsToOpenWindows[pidToKillWhenClosed] = openedWindow @@ -685,7 +685,7 @@ class AtomApplication windowInitializationScript ?= require.resolve('../initialize-application-window') windowDimensions = @getDimensionsForNewWindow() win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) - @windows.addWindow(win) + @windowStack.addWindow(win) win.on 'window:loaded', -> win.sendURIMessage url diff --git a/src/main-process/auto-update-manager.coffee b/src/main-process/auto-update-manager.coffee index 2ff2852cb..0e4144c1a 100644 --- a/src/main-process/auto-update-manager.coffee +++ b/src/main-process/auto-update-manager.coffee @@ -138,4 +138,4 @@ class AutoUpdateManager detail: message getWindows: -> - global.atomApplication.windows + global.atomApplication.getAllWindows() From e9e23a2d09832f578874020ed10deada2c954eb6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 15:19:52 -0700 Subject: [PATCH 241/301] Convert text-editor.coffee to JS Signed-off-by: Nathan Sobo --- src/selection.coffee | 2 +- src/text-editor-utils.js | 139 -- src/text-editor.coffee | 3928 -------------------------------- src/text-editor.js | 4587 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 4588 insertions(+), 4068 deletions(-) delete mode 100644 src/text-editor-utils.js delete mode 100644 src/text-editor.coffee create mode 100644 src/text-editor.js diff --git a/src/selection.coffee b/src/selection.coffee index cb45286b8..e55f17e88 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -735,7 +735,7 @@ class Selection extends Model # # * `otherSelection` A {Selection} to merge with. # * `options` (optional) {Object} options matching those found in {::setBufferRange}. - merge: (otherSelection, options) -> + merge: (otherSelection, options = {}) -> myGoalScreenRange = @getGoalScreenRange() otherGoalScreenRange = otherSelection.getGoalScreenRange() diff --git a/src/text-editor-utils.js b/src/text-editor-utils.js deleted file mode 100644 index ab1104144..000000000 --- a/src/text-editor-utils.js +++ /dev/null @@ -1,139 +0,0 @@ -// 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] -} diff --git a/src/text-editor.coffee b/src/text-editor.coffee deleted file mode 100644 index 3bc5fa34e..000000000 --- a/src/text-editor.coffee +++ /dev/null @@ -1,3928 +0,0 @@ -_ = require 'underscore-plus' -path = require 'path' -fs = require 'fs-plus' -Grim = require 'grim' -{CompositeDisposable, Disposable, Emitter} = require 'event-kit' -{Point, Range} = TextBuffer = require 'text-buffer' -DecorationManager = require './decoration-manager' -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 -TextEditorElement = null -{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' - -NON_WHITESPACE_REGEXP = /\S/ -ZERO_WIDTH_NBSP = '\ufeff' - -# Essential: This class represents all essential editing state for a single -# {TextBuffer}, including cursor and selection positions, folds, and soft wraps. -# If you're manipulating the state of an editor, use this class. -# -# A single {TextBuffer} can belong to multiple editors. For example, if the -# same file is open in two different panes, Atom creates a separate editor for -# each pane. If the buffer is manipulated the changes are reflected in both -# editors, but each maintains its own cursor position, folded lines, etc. -# -# ## Accessing TextEditor Instances -# -# The easiest way to get hold of `TextEditor` objects is by registering a callback -# with `::observeTextEditors` on the `atom.workspace` global. Your callback will -# then be called with all current editor instances and also when any editor is -# created in the future. -# -# ```coffee -# atom.workspace.observeTextEditors (editor) -> -# editor.insertText('Hello World') -# ``` -# -# ## Buffer vs. Screen Coordinates -# -# Because editors support folds and soft-wrapping, the lines on screen don't -# always match the lines in the buffer. For example, a long line that soft wraps -# twice renders as three lines on screen, but only represents one line in the -# buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds -# to row 11 in the buffer. -# -# Your choice of coordinates systems will depend on what you're trying to -# achieve. For example, if you're writing a command that jumps the cursor up or -# down by 10 lines, you'll want to use screen coordinates because the user -# probably wants to skip lines *on screen*. However, if you're writing a package -# that jumps between method definitions, you'll want to work in buffer -# coordinates. -# -# **When in doubt, just default to buffer coordinates**, then experiment with -# soft wraps and folds to ensure your code interacts with them correctly. -module.exports = -class TextEditor extends Model - @setClipboard: (clipboard) -> - @clipboard = clipboard - - @setScheduler: (scheduler) -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.setScheduler(scheduler) - - @didUpdateStyles: -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.didUpdateStyles() - - @didUpdateScrollbarStyles: -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.didUpdateScrollbarStyles() - - @viewForItem: (item) -> item.element ? item - - serializationVersion: 1 - - buffer: null - cursors: null - showCursorOnSelection: null - selections: null - suppressSelectionMerging: false - selectionFlashDuration: 500 - gutterContainer: null - editorElement: null - verticalScrollMargin: 2 - horizontalScrollMargin: 6 - softWrapped: null - editorWidthInChars: null - lineHeightInPixels: null - defaultCharWidth: null - height: null - width: null - registered: false - atomicSoftTabs: true - invisibles: null - - Object.defineProperty @prototype, "element", - get: -> @getElement() - - Object.defineProperty @prototype, "editorElement", - get: -> - Grim.deprecate(""" - `TextEditor.prototype.editorElement` has always been private, but now - it is gone. Reading the `editorElement` property still returns a - reference to the editor element but this field will be removed in a - later version of Atom, so we recommend using the `element` property instead. - """) - - @getElement() - - Object.defineProperty(@prototype, 'displayBuffer', get: -> - Grim.deprecate(""" - `TextEditor.prototype.displayBuffer` has always been private, but now - it is gone. Reading the `displayBuffer` property now returns a reference - to the containing `TextEditor`, which now provides *some* of the API of - the defunct `DisplayBuffer` class. - """) - this - ) - - 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? - state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer - - try - tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) - return null unless tokenizedBuffer? - - state.tokenizedBuffer = tokenizedBuffer - state.tabLength = state.tokenizedBuffer.getTabLength() - catch error - if error.syscall is 'read' - return # Error reading the file, don't deserialize an editor for it - else - throw error - - state.buffer = state.tokenizedBuffer.buffer - state.assert = atomEnvironment.assert.bind(atomEnvironment) - editor = new this(state) - if state.registered - disposable = atomEnvironment.textEditors.add(editor) - editor.onDidDestroy -> disposable.dispose() - editor - - constructor: (params={}) -> - unless @constructor.clipboard? - throw new Error("Must call TextEditor.setClipboard at least once before creating TextEditor instances") - - super - - { - @softTabs, @initialScrollTopRow, @initialScrollLeftColumn, initialLine, initialColumn, tabLength, - @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation, - @mini, @placeholderText, lineNumberGutterVisible, @showLineNumbers, @largeFileMode, - @assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @scrollSensitivity, @editorWidthInChars, - @tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide, - @softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength, - @showCursorOnSelection, @maxScreenLineLength - } = params - - @assert ?= (condition) -> condition - @emitter = new Emitter - @disposables = new CompositeDisposable - @cursors = [] - @cursorsByMarkerId = new Map - @selections = [] - @hasTerminatedPendingState = false - - @mini ?= false - @scrollPastEnd ?= false - @scrollSensitivity ?= 40 - @showInvisibles ?= true - @softTabs ?= true - tabLength ?= 2 - @autoIndent ?= true - @autoIndentOnPaste ?= true - @showCursorOnSelection ?= true - @undoGroupingInterval ?= 300 - @nonWordCharacters ?= "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…" - @softWrapped ?= false - @softWrapAtPreferredLineLength ?= false - @preferredLineLength ?= 80 - @maxScreenLineLength ?= 500 - @showLineNumbers ?= true - - @buffer ?= new TextBuffer({ - shouldDestroyOnFileDelete: -> atom.config.get('core.closeDeletedFileTabs') - }) - @tokenizedBuffer ?= new TokenizedBuffer({ - grammar, tabLength, @buffer, @largeFileMode, @assert - }) - - unless @displayLayer? - displayLayerParams = { - invisibles: @getInvisibles(), - softWrapColumn: @getSoftWrapColumn(), - showIndentGuides: @doesShowIndentGuide(), - atomicSoftTabs: params.atomicSoftTabs ? true, - tabLength: tabLength, - ratioForCharacter: @ratioForCharacter.bind(this), - isWrapBoundary: isWrapBoundary, - foldCharacter: ZERO_WIDTH_NBSP, - softWrapHangingIndent: params.softWrapHangingIndentLength ? 0 - } - - if @displayLayer = @buffer.getDisplayLayer(params.displayLayerId) - @displayLayer.reset(displayLayerParams) - @selectionsMarkerLayer = @displayLayer.getMarkerLayer(params.selectionsMarkerLayerId) - else - @displayLayer = @buffer.addDisplayLayer(displayLayerParams) - - @backgroundWorkHandle = requestIdleCallback(@doBackgroundWork) - @disposables.add new Disposable => - cancelIdleCallback(@backgroundWorkHandle) if @backgroundWorkHandle? - - @displayLayer.setTextDecorationLayer(@tokenizedBuffer) - @defaultMarkerLayer = @displayLayer.addMarkerLayer() - @disposables.add(@defaultMarkerLayer.onDidDestroy => - @assert(false, "defaultMarkerLayer destroyed at an unexpected time") - ) - @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) - @selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true - - @decorationManager = new DecorationManager(this) - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'cursor') - @decorateCursorLine() unless @isMini() - - @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) - - for marker in @selectionsMarkerLayer.getMarkers() - @addSelection(marker) - - @subscribeToBuffer() - @subscribeToDisplayLayer() - - if @cursors.length is 0 and not suppressCursorCreation - initialLine = Math.max(parseInt(initialLine) or 0, 0) - initialColumn = Math.max(parseInt(initialColumn) or 0, 0) - @addCursorAtBufferPosition([initialLine, initialColumn]) - - @gutterContainer = new GutterContainer(this) - @lineNumberGutter = @gutterContainer.addGutter - name: 'line-number' - priority: 0 - visible: lineNumberGutterVisible - - decorateCursorLine: -> - @cursorLineDecorations = [ - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line', class: 'cursor-line', onlyEmpty: true), - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line'), - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) - ] - - doBackgroundWork: (deadline) => - previousLongestRow = @getApproximateLongestScreenRow() - if @displayLayer.doBackgroundWork(deadline) - @backgroundWorkHandle = requestIdleCallback(@doBackgroundWork) - else - @backgroundWorkHandle = null - - if @getApproximateLongestScreenRow() isnt previousLongestRow - @component?.scheduleUpdate() - - update: (params) -> - displayLayerParams = {} - - for param in Object.keys(params) - value = params[param] - - switch param - when 'autoIndent' - @autoIndent = value - - when 'autoIndentOnPaste' - @autoIndentOnPaste = value - - when 'undoGroupingInterval' - @undoGroupingInterval = value - - when 'nonWordCharacters' - @nonWordCharacters = value - - when 'scrollSensitivity' - @scrollSensitivity = value - - when 'encoding' - @buffer.setEncoding(value) - - when 'softTabs' - if value isnt @softTabs - @softTabs = value - - when 'atomicSoftTabs' - if value isnt @displayLayer.atomicSoftTabs - displayLayerParams.atomicSoftTabs = value - - when 'tabLength' - if value? and value isnt @tokenizedBuffer.getTabLength() - @tokenizedBuffer.setTabLength(value) - displayLayerParams.tabLength = value - - when 'softWrapped' - if value isnt @softWrapped - @softWrapped = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - @emitter.emit 'did-change-soft-wrapped', @isSoftWrapped() - - when 'softWrapHangingIndentLength' - if value isnt @displayLayer.softWrapHangingIndent - displayLayerParams.softWrapHangingIndent = value - - when 'softWrapAtPreferredLineLength' - if value isnt @softWrapAtPreferredLineLength - @softWrapAtPreferredLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'preferredLineLength' - if value isnt @preferredLineLength - @preferredLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'maxScreenLineLength' - if value isnt @maxScreenLineLength - @maxScreenLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'mini' - if value isnt @mini - @mini = value - @emitter.emit 'did-change-mini', value - displayLayerParams.invisibles = @getInvisibles() - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - displayLayerParams.showIndentGuides = @doesShowIndentGuide() - if @mini - decoration.destroy() for decoration in @cursorLineDecorations - @cursorLineDecorations = null - else - @decorateCursorLine() - @component?.scheduleUpdate() - - when 'placeholderText' - if value isnt @placeholderText - @placeholderText = value - @emitter.emit 'did-change-placeholder-text', value - - when 'lineNumberGutterVisible' - if value isnt @lineNumberGutterVisible - if value - @lineNumberGutter.show() - else - @lineNumberGutter.hide() - @emitter.emit 'did-change-line-number-gutter-visible', @lineNumberGutter.isVisible() - - when 'showIndentGuide' - if value isnt @showIndentGuide - @showIndentGuide = value - displayLayerParams.showIndentGuides = @doesShowIndentGuide() - - when 'showLineNumbers' - if value isnt @showLineNumbers - @showLineNumbers = value - @component?.scheduleUpdate() - - when 'showInvisibles' - if value isnt @showInvisibles - @showInvisibles = value - displayLayerParams.invisibles = @getInvisibles() - - when 'invisibles' - if not _.isEqual(value, @invisibles) - @invisibles = value - displayLayerParams.invisibles = @getInvisibles() - - when 'editorWidthInChars' - if value > 0 and value isnt @editorWidthInChars - @editorWidthInChars = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'width' - if value isnt @width - @width = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'scrollPastEnd' - if value isnt @scrollPastEnd - @scrollPastEnd = value - @component?.scheduleUpdate() - - when 'autoHeight' - if value isnt @autoHeight - @autoHeight = value - - when 'autoWidth' - if value isnt @autoWidth - @autoWidth = value - - when 'showCursorOnSelection' - if value isnt @showCursorOnSelection - @showCursorOnSelection = value - @component?.scheduleUpdate() - - else - if param isnt 'ref' and param isnt 'key' - throw new TypeError("Invalid TextEditor parameter: '#{param}'") - - @displayLayer.reset(displayLayerParams) - - if @component? - @component.getNextUpdatePromise() - else - Promise.resolve() - - scheduleComponentUpdate: -> - @component?.scheduleUpdate() - - serialize: -> - tokenizedBufferState = @tokenizedBuffer.serialize() - - { - deserializer: 'TextEditor' - version: @serializationVersion - - # TODO: Remove this forward-compatible fallback once 1.8 reaches stable. - displayBuffer: {tokenizedBuffer: tokenizedBufferState} - - tokenizedBuffer: tokenizedBufferState - displayLayerId: @displayLayer.id - selectionsMarkerLayerId: @selectionsMarkerLayer.id - - initialScrollTopRow: @getScrollTopRow() - initialScrollLeftColumn: @getScrollLeftColumn() - - atomicSoftTabs: @displayLayer.atomicSoftTabs - softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent - - @id, @softTabs, @softWrapped, @softWrapAtPreferredLineLength, - @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, @maxScreenLineLength, - @registered, @invisibles, @showInvisibles, @showIndentGuide, @autoHeight, @autoWidth - } - - subscribeToBuffer: -> - @buffer.retain() - @disposables.add @buffer.onDidChangePath => - @emitter.emit 'did-change-title', @getTitle() - @emitter.emit 'did-change-path', @getPath() - @disposables.add @buffer.onDidChangeEncoding => - @emitter.emit 'did-change-encoding', @getEncoding() - @disposables.add @buffer.onDidDestroy => @destroy() - @disposables.add @buffer.onDidChangeModified => - @terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified() - - terminatePendingState: -> - @emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState - @hasTerminatedPendingState = true - - onDidTerminatePendingState: (callback) -> - @emitter.on 'did-terminate-pending-state', callback - - subscribeToDisplayLayer: -> - @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) - @disposables.add @displayLayer.onDidChange (changes) => - @mergeIntersectingSelections() - @component?.didChangeDisplayLayer(changes) - @emitter.emit 'did-change', changes.map (change) -> new ChangeEvent(change) - @disposables.add @displayLayer.onDidReset => - @mergeIntersectingSelections() - @component?.didResetDisplayLayer() - @emitter.emit 'did-change', {} - @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) - @disposables.add @selectionsMarkerLayer.onDidUpdate => @component?.didUpdateSelections() - - destroyed: -> - @disposables.dispose() - @displayLayer.destroy() - @tokenizedBuffer.destroy() - selection.destroy() for selection in @selections.slice() - @buffer.release() - @gutterContainer.destroy() - @emitter.emit 'did-destroy' - @emitter.clear() - @component?.element.component = null - @component = null - @lineNumberGutter.element = null - - ### - Section: Event Subscription - ### - - # Essential: Calls your `callback` when the buffer's title has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeTitle: (callback) -> - @emitter.on 'did-change-title', callback - - # Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePath: (callback) -> - @emitter.on 'did-change-path', callback - - # Essential: Invoke the given callback synchronously when the content of the - # buffer changes. - # - # Because observers are invoked synchronously, it's important not to perform - # any expensive operations via this method. Consider {::onDidStopChanging} to - # delay expensive operations until after changes stop occurring. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - @emitter.on 'did-change', callback - - # Essential: Invoke `callback` when the buffer's contents change. It is - # emit asynchronously 300ms after the last buffer change. This is a good place - # to handle changes to the buffer without compromising typing performance. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidStopChanging: (callback) -> - @getBuffer().onDidStopChanging(callback) - - # Essential: Calls your `callback` when a {Cursor} is moved. If there are - # multiple cursors, your callback will be called for each cursor. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferPosition` {Point} - # * `oldScreenPosition` {Point} - # * `newBufferPosition` {Point} - # * `newScreenPosition` {Point} - # * `textChanged` {Boolean} - # * `cursor` {Cursor} that triggered the event - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeCursorPosition: (callback) -> - @emitter.on 'did-change-cursor-position', callback - - # Essential: Calls your `callback` when a selection's screen range changes. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferRange` {Range} - # * `oldScreenRange` {Range} - # * `newBufferRange` {Range} - # * `newScreenRange` {Range} - # * `selection` {Selection} that triggered the event - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeSelectionRange: (callback) -> - @emitter.on 'did-change-selection-range', callback - - # Extended: Calls your `callback` when soft wrap was enabled or disabled. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeSoftWrapped: (callback) -> - @emitter.on 'did-change-soft-wrapped', callback - - # Extended: Calls your `callback` when the buffer's encoding has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeEncoding: (callback) -> - @emitter.on 'did-change-encoding', callback - - # Extended: Calls your `callback` when the grammar that interprets and - # colorizes the text has been changed. Immediately calls your callback with - # the current grammar. - # - # * `callback` {Function} - # * `grammar` {Grammar} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeGrammar: (callback) -> - callback(@getGrammar()) - @onDidChangeGrammar(callback) - - # Extended: Calls your `callback` when the grammar that interprets and - # colorizes the text has been changed. - # - # * `callback` {Function} - # * `grammar` {Grammar} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeGrammar: (callback) -> - @emitter.on 'did-change-grammar', callback - - # Extended: Calls your `callback` when the result of {::isModified} changes. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeModified: (callback) -> - @getBuffer().onDidChangeModified(callback) - - # Extended: Calls your `callback` when the buffer's underlying file changes on - # disk at a moment when the result of {::isModified} is true. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidConflict: (callback) -> - @getBuffer().onDidConflict(callback) - - # Extended: Calls your `callback` before text has been inserted. - # - # * `callback` {Function} - # * `event` event {Object} - # * `text` {String} text to be inserted - # * `cancel` {Function} Call to prevent the text from being inserted - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillInsertText: (callback) -> - @emitter.on 'will-insert-text', callback - - # Extended: Calls your `callback` after text has been inserted. - # - # * `callback` {Function} - # * `event` event {Object} - # * `text` {String} text to be inserted - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidInsertText: (callback) -> - @emitter.on 'did-insert-text', callback - - # Essential: Invoke the given callback after the buffer is saved to disk. - # - # * `callback` {Function} to be called after the buffer is saved. - # * `event` {Object} with the following keys: - # * `path` The path to which the buffer was saved. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidSave: (callback) -> - @getBuffer().onDidSave(callback) - - # Essential: Invoke the given callback when the editor is destroyed. - # - # * `callback` {Function} to be called when the editor is destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - # Extended: Calls your `callback` when a {Cursor} is added to the editor. - # Immediately calls your callback for each existing cursor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeCursors: (callback) -> - callback(cursor) for cursor in @getCursors() - @onDidAddCursor(callback) - - # Extended: Calls your `callback` when a {Cursor} is added to the editor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddCursor: (callback) -> - @emitter.on 'did-add-cursor', callback - - # Extended: Calls your `callback` when a {Cursor} is removed from the editor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveCursor: (callback) -> - @emitter.on 'did-remove-cursor', callback - - # Extended: Calls your `callback` when a {Selection} is added to the editor. - # Immediately calls your callback for each existing selection. - # - # * `callback` {Function} - # * `selection` {Selection} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeSelections: (callback) -> - callback(selection) for selection in @getSelections() - @onDidAddSelection(callback) - - # Extended: Calls your `callback` when a {Selection} is added to the editor. - # - # * `callback` {Function} - # * `selection` {Selection} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddSelection: (callback) -> - @emitter.on 'did-add-selection', callback - - # Extended: Calls your `callback` when a {Selection} is removed from the editor. - # - # * `callback` {Function} - # * `selection` {Selection} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveSelection: (callback) -> - @emitter.on 'did-remove-selection', callback - - # Extended: Calls your `callback` with each {Decoration} added to the editor. - # Calls your `callback` immediately for any existing decorations. - # - # * `callback` {Function} - # * `decoration` {Decoration} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeDecorations: (callback) -> - @decorationManager.observeDecorations(callback) - - # Extended: Calls your `callback` when a {Decoration} is added to the editor. - # - # * `callback` {Function} - # * `decoration` {Decoration} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddDecoration: (callback) -> - @decorationManager.onDidAddDecoration(callback) - - # Extended: Calls your `callback` when a {Decoration} is removed from the editor. - # - # * `callback` {Function} - # * `decoration` {Decoration} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveDecoration: (callback) -> - @decorationManager.onDidRemoveDecoration(callback) - - # Called by DecorationManager when a decoration is added. - didAddDecoration: (decoration) -> - if decoration.isType('block') - @component?.addBlockDecoration(decoration) - - # Extended: Calls your `callback` when the placeholder text is changed. - # - # * `callback` {Function} - # * `placeholderText` {String} new text - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePlaceholderText: (callback) -> - @emitter.on 'did-change-placeholder-text', callback - - onDidChangeScrollTop: (callback) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") - - @getElement().onDidChangeScrollTop(callback) - - onDidChangeScrollLeft: (callback) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.") - - @getElement().onDidChangeScrollLeft(callback) - - onDidRequestAutoscroll: (callback) -> - @emitter.on 'did-request-autoscroll', callback - - # TODO Remove once the tabs package no longer uses .on subscriptions - onDidChangeIcon: (callback) -> - @emitter.on 'did-change-icon', callback - - onDidUpdateDecorations: (callback) -> - @decorationManager.onDidUpdateDecorations(callback) - - # Essential: Retrieves the current {TextBuffer}. - getBuffer: -> @buffer - - # Retrieves the current buffer's URI. - getURI: -> @buffer.getUri() - - # Create an {TextEditor} with its initial state based on this object - copy: -> - displayLayer = @displayLayer.copy() - selectionsMarkerLayer = displayLayer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id) - softTabs = @getSoftTabs() - new TextEditor({ - @buffer, selectionsMarkerLayer, softTabs, - suppressCursorCreation: true, - tabLength: @tokenizedBuffer.getTabLength(), - initialScrollTopRow: @getScrollTopRow(), - initialScrollLeftColumn: @getScrollLeftColumn(), - @assert, displayLayer, grammar: @getGrammar(), - @autoWidth, @autoHeight, @showCursorOnSelection - }) - - # Controls visibility based on the given {Boolean}. - setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) - - setMini: (mini) -> - @update({mini}) - @mini - - isMini: -> @mini - - onDidChangeMini: (callback) -> - @emitter.on 'did-change-mini', callback - - setLineNumberGutterVisible: (lineNumberGutterVisible) -> @update({lineNumberGutterVisible}) - - isLineNumberGutterVisible: -> @lineNumberGutter.isVisible() - - onDidChangeLineNumberGutterVisible: (callback) -> - @emitter.on 'did-change-line-number-gutter-visible', callback - - # Essential: Calls your `callback` when a {Gutter} is added to the editor. - # Immediately calls your callback for each existing gutter. - # - # * `callback` {Function} - # * `gutter` {Gutter} that currently exists/was added. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeGutters: (callback) -> - @gutterContainer.observeGutters callback - - # Essential: Calls your `callback` when a {Gutter} is added to the editor. - # - # * `callback` {Function} - # * `gutter` {Gutter} that was added. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddGutter: (callback) -> - @gutterContainer.onDidAddGutter callback - - # Essential: Calls your `callback` when a {Gutter} is removed from the editor. - # - # * `callback` {Function} - # * `name` The name of the {Gutter} that was removed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveGutter: (callback) -> - @gutterContainer.onDidRemoveGutter callback - - # Set the number of characters that can be displayed horizontally in the - # editor. - # - # * `editorWidthInChars` A {Number} representing the width of the - # {TextEditorElement} in characters. - setEditorWidthInChars: (editorWidthInChars) -> @update({editorWidthInChars}) - - # Returns the editor width in characters. - getEditorWidthInChars: -> - if @width? and @defaultCharWidth > 0 - Math.max(0, Math.floor(@width / @defaultCharWidth)) - else - @editorWidthInChars - - ### - Section: File Details - ### - - # Essential: Get the editor's title for display in other parts of the - # UI such as the tabs. - # - # If the editor's buffer is saved, its title is the file name. If it is - # unsaved, its title is "untitled". - # - # Returns a {String}. - getTitle: -> - @getFileName() ? 'untitled' - - # Essential: Get unique title for display in other parts of the UI, such as - # the window title. - # - # If the editor's buffer is unsaved, its title is "untitled" - # If the editor's buffer is saved, its unique title is formatted as one - # of the following, - # * "" when it is the only editing buffer with this file name. - # * "" when other buffers have this file name. - # - # Returns a {String} - getLongTitle: -> - if @getPath() - fileName = @getFileName() - - allPathSegments = [] - for textEditor in atom.workspace.getTextEditors() when textEditor isnt this - if textEditor.getFileName() is fileName - directoryPath = fs.tildify(textEditor.getDirectoryPath()) - allPathSegments.push(directoryPath.split(path.sep)) - - if allPathSegments.length is 0 - return fileName - - ourPathSegments = fs.tildify(@getDirectoryPath()).split(path.sep) - allPathSegments.push ourPathSegments - - loop - firstSegment = ourPathSegments[0] - - commonBase = _.all(allPathSegments, (pathSegments) -> pathSegments.length > 1 and pathSegments[0] is firstSegment) - if commonBase - pathSegments.shift() for pathSegments in allPathSegments - else - break - - "#{fileName} \u2014 #{path.join(pathSegments...)}" - else - 'untitled' - - # Essential: Returns the {String} path of this editor's text buffer. - getPath: -> @buffer.getPath() - - getFileName: -> - if fullPath = @getPath() - path.basename(fullPath) - else - null - - getDirectoryPath: -> - if fullPath = @getPath() - path.dirname(fullPath) - else - null - - # Extended: Returns the {String} character set encoding of this editor's text - # buffer. - getEncoding: -> @buffer.getEncoding() - - # Extended: Set the character set encoding to use in this editor's text - # buffer. - # - # * `encoding` The {String} character set encoding name such as 'utf8' - setEncoding: (encoding) -> @buffer.setEncoding(encoding) - - # Essential: Returns {Boolean} `true` if this editor has been modified. - isModified: -> @buffer.isModified() - - # Essential: Returns {Boolean} `true` if this editor has no content. - isEmpty: -> @buffer.isEmpty() - - ### - Section: File Operations - ### - - # Essential: Saves the editor's text buffer. - # - # See {TextBuffer::save} for more details. - save: -> @buffer.save() - - # Essential: Saves the editor's text buffer as the given path. - # - # See {TextBuffer::saveAs} for more details. - # - # * `filePath` A {String} path. - saveAs: (filePath) -> @buffer.saveAs(filePath) - - # Determine whether the user should be prompted to save before closing - # this editor. - shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) -> - if windowCloseRequested and projectHasPaths and atom.stateStore.isConnected() - @buffer.isInConflict() - else - @isModified() and not @buffer.hasMultipleEditors() - - # Returns an {Object} to configure dialog shown when this editor is saved - # via {Pane::saveItemAs}. - getSaveDialogOptions: -> {} - - ### - Section: Reading Text - ### - - # Essential: Returns a {String} representing the entire contents of the editor. - getText: -> @buffer.getText() - - # Essential: Get the text in the given {Range} in buffer coordinates. - # - # * `range` A {Range} or range-compatible {Array}. - # - # Returns a {String}. - getTextInBufferRange: (range) -> - @buffer.getTextInRange(range) - - # Essential: Returns a {Number} representing the number of lines in the buffer. - getLineCount: -> @buffer.getLineCount() - - # Essential: Returns a {Number} representing the number of screen lines in the - # editor. This accounts for folds. - getScreenLineCount: -> @displayLayer.getScreenLineCount() - - getApproximateScreenLineCount: -> @displayLayer.getApproximateScreenLineCount() - - # Essential: Returns a {Number} representing the last zero-indexed buffer row - # number of the editor. - getLastBufferRow: -> @buffer.getLastRow() - - # Essential: Returns a {Number} representing the last zero-indexed screen row - # number of the editor. - getLastScreenRow: -> @getScreenLineCount() - 1 - - # Essential: Returns a {String} representing the contents of the line at the - # given buffer row. - # - # * `bufferRow` A {Number} representing a zero-indexed buffer row. - lineTextForBufferRow: (bufferRow) -> @buffer.lineForRow(bufferRow) - - # Essential: Returns a {String} representing the contents of the line at the - # given screen row. - # - # * `screenRow` A {Number} representing a zero-indexed screen row. - lineTextForScreenRow: (screenRow) -> - @screenLineForScreenRow(screenRow)?.lineText - - logScreenLines: (start=0, end=@getLastScreenRow()) -> - for row in [start..end] - line = @lineTextForScreenRow(row) - console.log row, @bufferRowForScreenRow(row), line, line.length - return - - tokensForScreenRow: (screenRow) -> - tokens = [] - lineTextIndex = 0 - currentTokenScopes = [] - {lineText, tags} = @screenLineForScreenRow(screenRow) - for tag in tags - if @displayLayer.isOpenTag(tag) - currentTokenScopes.push(@displayLayer.classNameForTag(tag)) - else if @displayLayer.isCloseTag(tag) - currentTokenScopes.pop() - else - tokens.push({ - text: lineText.substr(lineTextIndex, tag) - scopes: currentTokenScopes.slice() - }) - lineTextIndex += tag - tokens - - screenLineForScreenRow: (screenRow) -> - @displayLayer.getScreenLine(screenRow) - - bufferRowForScreenRow: (screenRow) -> - @displayLayer.translateScreenPosition(Point(screenRow, 0)).row - - bufferRowsForScreenRows: (startScreenRow, endScreenRow) -> - @displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1) - - screenRowForBufferRow: (row) -> - @displayLayer.translateBufferPosition(Point(row, 0)).row - - getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition() - - getApproximateRightmostScreenPosition: -> @displayLayer.getApproximateRightmostScreenPosition() - - getMaxScreenLineLength: -> @getRightmostScreenPosition().column - - getLongestScreenRow: -> @getRightmostScreenPosition().row - - getApproximateLongestScreenRow: -> @getApproximateRightmostScreenPosition().row - - lineLengthForScreenRow: (screenRow) -> @displayLayer.lineLengthForScreenRow(screenRow) - - # Returns the range for the given buffer row. - # - # * `row` A row {Number}. - # * `options` (optional) An options hash with an `includeNewline` key. - # - # Returns a {Range}. - bufferRangeForBufferRow: (row, {includeNewline}={}) -> @buffer.rangeForRow(row, includeNewline) - - # Get the text in the given {Range}. - # - # Returns a {String}. - getTextInRange: (range) -> @buffer.getTextInRange(range) - - # {Delegates to: TextBuffer.isRowBlank} - isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) - - # {Delegates to: TextBuffer.nextNonBlankRow} - nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) - - # {Delegates to: TextBuffer.getEndPosition} - getEofBufferPosition: -> @buffer.getEndPosition() - - # Essential: Get the {Range} of the paragraph surrounding the most recently added - # cursor. - # - # Returns a {Range}. - getCurrentParagraphBufferRange: -> - @getLastCursor().getCurrentParagraphBufferRange() - - - ### - Section: Mutating Text - ### - - # Essential: Replaces the entire contents of the buffer with the given {String}. - # - # * `text` A {String} to replace with - setText: (text) -> @buffer.setText(text) - - # Essential: Set the text in the given {Range} in buffer coordinates. - # - # * `range` A {Range} or range-compatible {Array}. - # * `text` A {String} - # * `options` (optional) {Object} - # * `normalizeLineEndings` (optional) {Boolean} (default: true) - # * `undo` (optional) {String} 'skip' will skip the undo system - # - # Returns the {Range} of the newly-inserted text. - setTextInBufferRange: (range, text, options) -> @getBuffer().setTextInRange(range, text, options) - - # Essential: For each selection, replace the selected text with the given text. - # - # * `text` A {String} representing the text to insert. - # * `options` (optional) See {Selection::insertText}. - # - # Returns a {Range} when the text has been inserted - # Returns a {Boolean} false when the text has not been inserted - insertText: (text, options={}) -> - return false unless @emitWillInsertTextEvent(text) - - groupingInterval = if options.groupUndo - @undoGroupingInterval - else - 0 - - options.autoIndentNewline ?= @shouldAutoIndent() - options.autoDecreaseIndent ?= @shouldAutoIndent() - @mutateSelectedText( - (selection) => - range = selection.insertText(text, options) - didInsertEvent = {text, range} - @emitter.emit 'did-insert-text', didInsertEvent - range - , groupingInterval - ) - - # Essential: For each selection, replace the selected text with a newline. - insertNewline: (options) -> - @insertText('\n', options) - - # Essential: For each selection, if the selection is empty, delete the character - # following the cursor. Otherwise delete the selected text. - delete: -> - @mutateSelectedText (selection) -> selection.delete() - - # Essential: For each selection, if the selection is empty, delete the character - # preceding the cursor. Otherwise delete the selected text. - backspace: -> - @mutateSelectedText (selection) -> selection.backspace() - - # Extended: Mutate the text of all the selections in a single transaction. - # - # All the changes made inside the given {Function} can be reverted with a - # single call to {::undo}. - # - # * `fn` A {Function} that will be called once for each {Selection}. The first - # argument will be a {Selection} and the second argument will be the - # {Number} index of that selection. - mutateSelectedText: (fn, groupingInterval=0) -> - @mergeIntersectingSelections => - @transact groupingInterval, => - fn(selection, index) for selection, index in @getSelectionsOrderedByBufferPosition() - - # Move lines intersecting the most recent selection or multiple selections - # up by one row in screen coordinates. - moveLineUp: -> - selections = @getSelectedBufferRanges().sort((a, b) -> a.compare(b)) - - if selections[0].start.row is 0 - return - - if selections[selections.length - 1].start.row is @getLastBufferRow() and @buffer.getLastLine() is '' - return - - @transact => - newSelectionRanges = [] - - while selections.length > 0 - # Find selections spanning a contiguous set of lines - selection = selections.shift() - selectionsToMove = [selection] - - while selection.end.row is selections[0]?.start.row - selectionsToMove.push(selections[0]) - selection.end.row = selections[0].end.row - selections.shift() - - # Compute the buffer range spanned by all these selections, expanding it - # so that it includes any folded region that intersects them. - startRow = selection.start.row - endRow = selection.end.row - if selection.end.row > selection.start.row and selection.end.column is 0 - # Don't move the last line of a multi-line selection if the selection ends at column 0 - endRow-- - - startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow) - endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1) - linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) - - # If selected line range is preceded by a fold, one line above on screen - # could be multiple lines in the buffer. - precedingRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow - 1) - insertDelta = linesRange.start.row - precedingRow - - # Any folds in the text that is moved will need to be re-created. - # It includes the folds that were intersecting with the selection. - rangesToRefold = @displayLayer - .destroyFoldsIntersectingBufferRange(linesRange) - .map((range) -> range.translate([-insertDelta, 0])) - - # Delete lines spanned by selection and insert them on the preceding buffer row - lines = @buffer.getTextInRange(linesRange) - lines += @buffer.lineEndingForRow(linesRange.end.row - 2) unless lines[lines.length - 1] is '\n' - @buffer.delete(linesRange) - @buffer.insert([precedingRow, 0], lines) - - # Restore folds that existed before the lines were moved - for rangeToRefold in rangesToRefold - @displayLayer.foldBufferRange(rangeToRefold) - - for selection in selectionsToMove - newSelectionRanges.push(selection.translate([-insertDelta, 0])) - - @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) - @autoIndentSelectedRows() if @shouldAutoIndent() - @scrollToBufferPosition([newSelectionRanges[0].start.row, 0]) - - # Move lines intersecting the most recent selection or multiple selections - # down by one row in screen coordinates. - moveLineDown: -> - selections = @getSelectedBufferRanges() - selections.sort (a, b) -> a.compare(b) - selections = selections.reverse() - - @transact => - @consolidateSelections() - newSelectionRanges = [] - - while selections.length > 0 - # Find selections spanning a contiguous set of lines - selection = selections.shift() - selectionsToMove = [selection] - - # if the current selection start row matches the next selections' end row - make them one selection - while selection.start.row is selections[0]?.end.row - selectionsToMove.push(selections[0]) - selection.start.row = selections[0].start.row - selections.shift() - - # Compute the buffer range spanned by all these selections, expanding it - # so that it includes any folded region that intersects them. - startRow = selection.start.row - endRow = selection.end.row - if selection.end.row > selection.start.row and selection.end.column is 0 - # Don't move the last line of a multi-line selection if the selection ends at column 0 - endRow-- - - startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow) - endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1) - linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) - - # If selected line range is followed by a fold, one line below on screen - # could be multiple lines in the buffer. But at the same time, if the - # next buffer row is wrapped, one line in the buffer can represent many - # screen rows. - followingRow = Math.min(@buffer.getLineCount(), @displayLayer.findBoundaryFollowingBufferRow(endRow + 1)) - insertDelta = followingRow - linesRange.end.row - - # Any folds in the text that is moved will need to be re-created. - # It includes the folds that were intersecting with the selection. - rangesToRefold = @displayLayer - .destroyFoldsIntersectingBufferRange(linesRange) - .map((range) -> range.translate([insertDelta, 0])) - - # Delete lines spanned by selection and insert them on the following correct buffer row - lines = @buffer.getTextInRange(linesRange) - if followingRow - 1 is @buffer.getLastRow() - lines = "\n#{lines}" - - @buffer.insert([followingRow, 0], lines) - @buffer.delete(linesRange) - - # Restore folds that existed before the lines were moved - for rangeToRefold in rangesToRefold - @displayLayer.foldBufferRange(rangeToRefold) - - for selection in selectionsToMove - newSelectionRanges.push(selection.translate([insertDelta, 0])) - - @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) - @autoIndentSelectedRows() if @shouldAutoIndent() - @scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]) - - # Move any active selections one column to the left. - moveSelectionLeft: -> - selections = @getSelectedBufferRanges() - noSelectionAtStartOfLine = selections.every((selection) -> - selection.start.column isnt 0 - ) - - translationDelta = [0, -1] - translatedRanges = [] - - if noSelectionAtStartOfLine - @transact => - for selection in selections - charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start) - charTextToLeftOfSelection = @buffer.getTextInRange(charToLeftOfSelection) - - @buffer.insert(selection.end, charTextToLeftOfSelection) - @buffer.delete(charToLeftOfSelection) - translatedRanges.push(selection.translate(translationDelta)) - - @setSelectedBufferRanges(translatedRanges) - - # Move any active selections one column to the right. - moveSelectionRight: -> - selections = @getSelectedBufferRanges() - noSelectionAtEndOfLine = selections.every((selection) => - selection.end.column isnt @buffer.lineLengthForRow(selection.end.row) - ) - - translationDelta = [0, 1] - translatedRanges = [] - - if noSelectionAtEndOfLine - @transact => - for selection in selections - charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta)) - charTextToRightOfSelection = @buffer.getTextInRange(charToRightOfSelection) - - @buffer.delete(charToRightOfSelection) - @buffer.insert(selection.start, charTextToRightOfSelection) - translatedRanges.push(selection.translate(translationDelta)) - - @setSelectedBufferRanges(translatedRanges) - - duplicateLines: -> - @transact => - selections = @getSelectionsOrderedByBufferPosition() - previousSelectionRanges = [] - - i = selections.length - 1 - while i >= 0 - j = i - previousSelectionRanges[i] = selections[i].getBufferRange() - if selections[i].isEmpty() - {start} = selections[i].getScreenRange() - selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true) - [startRow, endRow] = selections[i].getBufferRowRange() - endRow++ - while i > 0 - [previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange() - if previousSelectionEndRow is startRow - startRow = previousSelectionStartRow - previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange() - i-- - else - break - - intersectingFolds = @displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]]) - textToDuplicate = @getTextInBufferRange([[startRow, 0], [endRow, 0]]) - textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow() - @buffer.insert([endRow, 0], textToDuplicate) - - insertedRowCount = endRow - startRow - - for k in [i..j] by 1 - selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0])) - - for fold in intersectingFolds - foldRange = @displayLayer.bufferRangeForFold(fold) - @displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0])) - - i-- - - replaceSelectedText: (options={}, fn) -> - {selectWordIfEmpty} = options - @mutateSelectedText (selection) -> - selection.getBufferRange() - if selectWordIfEmpty and selection.isEmpty() - selection.selectWord() - text = selection.getText() - selection.deleteSelectedText() - range = selection.insertText(fn(text)) - selection.setBufferRange(range) - - # Split multi-line selections into one selection per line. - # - # Operates on all selections. This method breaks apart all multi-line - # selections to create multiple single-line selections that cumulatively cover - # the same original area. - splitSelectionsIntoLines: -> - @mergeIntersectingSelections => - for selection in @getSelections() - range = selection.getBufferRange() - continue if range.isSingleLine() - - {start, end} = range - @addSelectionForBufferRange([start, [start.row, Infinity]]) - {row} = start - while ++row < end.row - @addSelectionForBufferRange([[row, 0], [row, Infinity]]) - @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0 - selection.destroy() - return - - # Extended: For each selection, transpose the selected text. - # - # If the selection is empty, the characters preceding and following the cursor - # are swapped. Otherwise, the selected characters are reversed. - transpose: -> - @mutateSelectedText (selection) -> - if selection.isEmpty() - selection.selectRight() - text = selection.getText() - selection.delete() - selection.cursor.moveLeft() - selection.insertText text - else - selection.insertText selection.getText().split('').reverse().join('') - - # Extended: Convert the selected text to upper case. - # - # For each selection, if the selection is empty, converts the containing word - # to upper case. Otherwise convert the selected text to upper case. - upperCase: -> - @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toUpperCase() - - # Extended: Convert the selected text to lower case. - # - # For each selection, if the selection is empty, converts the containing word - # to upper case. Otherwise convert the selected text to upper case. - lowerCase: -> - @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toLowerCase() - - # Extended: Toggle line comments for rows intersecting selections. - # - # If the current grammar doesn't support comments, does nothing. - toggleLineCommentsInSelection: -> - @mutateSelectedText (selection) -> selection.toggleLineComments() - - # Convert multiple lines to a single line. - # - # Operates on all selections. If the selection is empty, joins the current - # line with the next line. Otherwise it joins all lines that intersect the - # selection. - # - # Joining a line means that multiple lines are converted to a single line with - # the contents of each of the original non-empty lines separated by a space. - joinLines: -> - @mutateSelectedText (selection) -> selection.joinLines() - - # Extended: For each cursor, insert a newline at beginning the following line. - insertNewlineBelow: -> - @transact => - @moveToEndOfLine() - @insertNewline() - - # Extended: For each cursor, insert a newline at the end of the preceding line. - insertNewlineAbove: -> - @transact => - bufferRow = @getCursorBufferPosition().row - indentLevel = @indentationForBufferRow(bufferRow) - onFirstLine = bufferRow is 0 - - @moveToBeginningOfLine() - @moveLeft() - @insertNewline() - - if @shouldAutoIndent() and @indentationForBufferRow(bufferRow) < indentLevel - @setIndentationForBufferRow(bufferRow, indentLevel) - - if onFirstLine - @moveUp() - @moveToEndOfLine() - - # Extended: For each selection, if the selection is empty, delete all characters - # of the containing word that precede the cursor. Otherwise delete the - # selected text. - deleteToBeginningOfWord: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() - - # Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the - # previous word boundary. - deleteToPreviousWordBoundary: -> - @mutateSelectedText (selection) -> selection.deleteToPreviousWordBoundary() - - # Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the - # next word boundary. - deleteToNextWordBoundary: -> - @mutateSelectedText (selection) -> selection.deleteToNextWordBoundary() - - # Extended: For each selection, if the selection is empty, delete all characters - # of the containing subword following the cursor. Otherwise delete the selected - # text. - deleteToBeginningOfSubword: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfSubword() - - # Extended: For each selection, if the selection is empty, delete all characters - # of the containing subword following the cursor. Otherwise delete the selected - # text. - deleteToEndOfSubword: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfSubword() - - # Extended: For each selection, if the selection is empty, delete all characters - # of the containing line that precede the cursor. Otherwise delete the - # selected text. - deleteToBeginningOfLine: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfLine() - - # Extended: For each selection, if the selection is not empty, deletes the - # selection; otherwise, deletes all characters of the containing line - # following the cursor. If the cursor is already at the end of the line, - # deletes the following newline. - deleteToEndOfLine: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfLine() - - # Extended: For each selection, if the selection is empty, delete all characters - # of the containing word following the cursor. Otherwise delete the selected - # text. - deleteToEndOfWord: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfWord() - - # Extended: Delete all lines intersecting selections. - deleteLine: -> - @mergeSelectionsOnSameRows() - @mutateSelectedText (selection) -> selection.deleteLine() - - ### - Section: History - ### - - # Essential: Undo the last change. - undo: -> - @avoidMergingSelections => @buffer.undo() - @getLastSelection().autoscroll() - - # Essential: Redo the last change. - redo: -> - @avoidMergingSelections => @buffer.redo() - @getLastSelection().autoscroll() - - # Extended: Batch multiple operations as a single undo/redo step. - # - # Any group of operations that are logically grouped from the perspective of - # undoing and redoing should be performed in a transaction. If you want to - # abort the transaction, call {::abortTransaction} to terminate the function's - # execution and revert any changes performed up to the abortion. - # - # * `groupingInterval` (optional) The {Number} of milliseconds for which this - # transaction should be considered 'groupable' after it begins. If a transaction - # with a positive `groupingInterval` is committed while the previous transaction is - # still 'groupable', the two transactions are merged with respect to undo and redo. - # * `fn` A {Function} to call inside the transaction. - transact: (groupingInterval, fn) -> - @buffer.transact(groupingInterval, fn) - - # Deprecated: Start an open-ended transaction. - beginTransaction: (groupingInterval) -> - Grim.deprecate('Transactions should be performed via TextEditor::transact only') - @buffer.beginTransaction(groupingInterval) - - # Deprecated: Commit an open-ended transaction started with {::beginTransaction}. - commitTransaction: -> - Grim.deprecate('Transactions should be performed via TextEditor::transact only') - @buffer.commitTransaction() - - # Extended: Abort an open transaction, undoing any operations performed so far - # within the transaction. - abortTransaction: -> @buffer.abortTransaction() - - # Extended: Create a pointer to the current state of the buffer for use - # with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}. - # - # Returns a checkpoint value. - createCheckpoint: -> @buffer.createCheckpoint() - - # Extended: Revert the buffer to the state it was in when the given - # checkpoint was created. - # - # The redo stack will be empty following this operation, so changes since the - # checkpoint will be lost. If the given checkpoint is no longer present in the - # undo history, no changes will be made to the buffer and this method will - # return `false`. - # - # * `checkpoint` The checkpoint to revert to. - # - # Returns a {Boolean} indicating whether the operation succeeded. - revertToCheckpoint: (checkpoint) -> @buffer.revertToCheckpoint(checkpoint) - - # Extended: Group all changes since the given checkpoint into a single - # transaction for purposes of undo/redo. - # - # If the given checkpoint is no longer present in the undo history, no - # grouping will be performed and this method will return `false`. - # - # * `checkpoint` The checkpoint from which to group changes. - # - # Returns a {Boolean} indicating whether the operation succeeded. - groupChangesSinceCheckpoint: (checkpoint) -> @buffer.groupChangesSinceCheckpoint(checkpoint) - - ### - Section: TextEditor Coordinates - ### - - # Essential: Convert a position in buffer-coordinates to screen-coordinates. - # - # The position is clipped via {::clipBufferPosition} prior to the conversion. - # The position is also clipped via {::clipScreenPosition} following the - # conversion, which only makes a difference when `options` are supplied. - # - # * `bufferPosition` A {Point} or {Array} of [row, column]. - # * `options` (optional) An options hash for {::clipScreenPosition}. - # - # Returns a {Point}. - screenPositionForBufferPosition: (bufferPosition, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.translateBufferPosition(bufferPosition, options) - - # Essential: Convert a position in screen-coordinates to buffer-coordinates. - # - # The position is clipped via {::clipScreenPosition} prior to the conversion. - # - # * `bufferPosition` A {Point} or {Array} of [row, column]. - # * `options` (optional) An options hash for {::clipScreenPosition}. - # - # Returns a {Point}. - bufferPositionForScreenPosition: (screenPosition, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.translateScreenPosition(screenPosition, options) - - # Essential: Convert a range in buffer-coordinates to screen-coordinates. - # - # * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. - # - # Returns a {Range}. - screenRangeForBufferRange: (bufferRange, options) -> - bufferRange = Range.fromObject(bufferRange) - start = @screenPositionForBufferPosition(bufferRange.start, options) - end = @screenPositionForBufferPosition(bufferRange.end, options) - new Range(start, end) - - # Essential: Convert a range in screen-coordinates to buffer-coordinates. - # - # * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. - # - # Returns a {Range}. - bufferRangeForScreenRange: (screenRange) -> - screenRange = Range.fromObject(screenRange) - start = @bufferPositionForScreenPosition(screenRange.start) - end = @bufferPositionForScreenPosition(screenRange.end) - new Range(start, end) - - # Extended: Clip the given {Point} to a valid position in the buffer. - # - # If the given {Point} describes a position that is actually reachable by the - # cursor based on the current contents of the buffer, it is returned - # unchanged. If the {Point} does not describe a valid position, the closest - # valid position is returned instead. - # - # ## Examples - # - # ```coffee - # editor.clipBufferPosition([-1, -1]) # -> `[0, 0]` - # - # # When the line at buffer row 2 is 10 characters long - # editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]` - # ``` - # - # * `bufferPosition` The {Point} representing the position to clip. - # - # Returns a {Point}. - clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) - - # Extended: Clip the start and end of the given range to valid positions in the - # buffer. See {::clipBufferPosition} for more information. - # - # * `range` The {Range} to clip. - # - # Returns a {Range}. - clipBufferRange: (range) -> @buffer.clipRange(range) - - # Extended: Clip the given {Point} to a valid position on screen. - # - # If the given {Point} describes a position that is actually reachable by the - # cursor based on the current contents of the screen, it is returned - # unchanged. If the {Point} does not describe a valid position, the closest - # valid position is returned instead. - # - # ## Examples - # - # ```coffee - # editor.clipScreenPosition([-1, -1]) # -> `[0, 0]` - # - # # When the line at screen row 2 is 10 characters long - # editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]` - # ``` - # - # * `screenPosition` The {Point} representing the position to clip. - # * `options` (optional) {Object} - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. - # - # Returns a {Point}. - clipScreenPosition: (screenPosition, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.clipScreenPosition(screenPosition, options) - - # Extended: Clip the start and end of the given range to valid positions on screen. - # See {::clipScreenPosition} for more information. - # - # * `range` The {Range} to clip. - # * `options` (optional) See {::clipScreenPosition} `options`. - # - # Returns a {Range}. - clipScreenRange: (screenRange, options) -> - screenRange = Range.fromObject(screenRange) - start = @displayLayer.clipScreenPosition(screenRange.start, options) - end = @displayLayer.clipScreenPosition(screenRange.end, options) - Range(start, end) - - ### - Section: Decorations - ### - - # Essential: Add a decoration that tracks a {DisplayMarker}. When the - # marker moves, is invalidated, or is destroyed, the decoration will be - # updated to reflect the marker's state. - # - # The following are the supported decorations types: - # - # * __line__: Adds your CSS `class` to the line nodes within the range - # marked by the marker - # * __line-number__: Adds your CSS `class` to the line number nodes within the - # range marked by the marker - # * __highlight__: Adds a new highlight div to the editor surrounding the - # range marked by the marker. When the user selects text, the selection is - # visualized with a highlight decoration internally. The structure of this - # highlight will be - # ```html - #
- # - #
- #
- # ``` - # * __overlay__: Positions the view associated with the given item at the head - # or tail of the given `DisplayMarker`. - # * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter - # decorations are created by calling {Gutter::decorateMarker} on the - # desired `Gutter` instance. - # * __block__: Positions the view associated with the given item before or - # after the row of the given `TextEditorMarker`. - # - # ## Arguments - # - # * `marker` A {DisplayMarker} you want this decoration to follow. - # * `decorationParams` An {Object} representing the decoration e.g. - # `{type: 'line-number', class: 'linter-error'}` - # * `type` There are several supported decoration types. The behavior of the - # types are as follows: - # * `line` Adds the given `class` to the lines overlapping the rows - # spanned by the `DisplayMarker`. - # * `line-number` Adds the given `class` to the line numbers overlapping - # the rows spanned by the `DisplayMarker`. - # * `text` Injects spans into all text overlapping the marked range, - # then adds the given `class` or `style` properties to these spans. - # Use this to manipulate the foreground color or styling of text in - # a given range. - # * `highlight` Creates an absolutely-positioned `.highlight` div - # containing nested divs to cover the marked region. For example, this - # is used to implement selections. - # * `overlay` Positions the view associated with the given item at the - # head or tail of the given `DisplayMarker`, depending on the `position` - # property. - # * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling - # {Gutter::decorateMarker} on the desired `Gutter` instance. - # * `block` Positions the view associated with the given item before or - # after the row of the given `TextEditorMarker`, depending on the `position` - # property. - # * `cursor` Renders a cursor at the head of the given marker. If multiple - # decorations are created for the same marker, their class strings and - # style objects are combined into a single cursor. You can use this - # decoration type to style existing cursors by passing in their markers - # or render artificial cursors that don't actually exist in the model - # by passing a marker that isn't actually associated with a cursor. - # * `class` This CSS class will be applied to the decorated line number, - # line, text spans, highlight regions, cursors, or overlay. - # * `style` An {Object} containing CSS style properties to apply to the - # relevant DOM node. Currently this only works with a `type` of `cursor` - # or `text`. - # * `item` (optional) An {HTMLElement} or a model {Object} with a - # corresponding view registered. Only applicable to the `gutter`, - # `overlay` and `block` decoration types. - # * `onlyHead` (optional) If `true`, the decoration will only be applied to - # the head of the `DisplayMarker`. Only applicable to the `line` and - # `line-number` decoration types. - # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if - # the associated `DisplayMarker` is empty. Only applicable to the `gutter`, - # `line`, and `line-number` decoration types. - # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied - # if the associated `DisplayMarker` is non-empty. Only applicable to the - # `gutter`, `line`, and `line-number` decoration types. - # * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied - # to the last row of a non-empty range, even if it ends at column 0. - # Defaults to `true`. Only applicable to the `gutter`, `line`, and - # `line-number` decoration types. - # * `position` (optional) Only applicable to decorations of type `overlay` and `block`. - # Controls where the view is positioned relative to the `TextEditorMarker`. - # Values can be `'head'` (the default) or `'tail'` for overlay decorations, and - # `'before'` (the default) or `'after'` for block decorations. - # * `avoidOverflow` (optional) Only applicable to decorations of type - # `overlay`. Determines whether the decoration adjusts its horizontal or - # vertical position to remain fully visible when it would otherwise - # overflow the editor. Defaults to `true`. - # - # Returns a {Decoration} object - decorateMarker: (marker, decorationParams) -> - @decorationManager.decorateMarker(marker, decorationParams) - - # Essential: Add a decoration to every marker in the given marker layer. Can - # be used to decorate a large number of markers without having to create and - # manage many individual decorations. - # - # * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. - # * `decorationParams` The same parameters that are passed to - # {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`. - # - # Returns a {LayerDecoration}. - decorateMarkerLayer: (markerLayer, decorationParams) -> - @decorationManager.decorateMarkerLayer(markerLayer, decorationParams) - - # Deprecated: Get all the decorations within a screen row range on the default - # layer. - # - # * `startScreenRow` the {Number} beginning screen row - # * `endScreenRow` the {Number} end screen row (inclusive) - # - # Returns an {Object} of decorations in the form - # `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}` - # where the keys are {DisplayMarker} IDs, and the values are an array of decoration - # params objects attached to the marker. - # Returns an empty object when no decorations are found - decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> - @decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow) - - decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> - @decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) - - # Extended: Get all decorations. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getDecorations: (propertyFilter) -> - @decorationManager.getDecorations(propertyFilter) - - # Extended: Get all decorations of type 'line'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getLineDecorations: (propertyFilter) -> - @decorationManager.getLineDecorations(propertyFilter) - - # Extended: Get all decorations of type 'line-number'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getLineNumberDecorations: (propertyFilter) -> - @decorationManager.getLineNumberDecorations(propertyFilter) - - # Extended: Get all decorations of type 'highlight'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getHighlightDecorations: (propertyFilter) -> - @decorationManager.getHighlightDecorations(propertyFilter) - - # Extended: Get all decorations of type 'overlay'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getOverlayDecorations: (propertyFilter) -> - @decorationManager.getOverlayDecorations(propertyFilter) - - ### - Section: Markers - ### - - # Essential: Create a marker on the default marker layer with the given range - # in buffer coordinates. This marker will maintain its logical location as the - # buffer is changed, so if you mark a particular word, the marker will remain - # over that word even if the word's location in the buffer changes. - # - # * `range` A {Range} or range-compatible {Array} - # * `properties` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `maintainHistory` (optional) {Boolean} Whether to store this marker's - # range before and after each change in the undo history. This allows the - # marker's position to be restored more accurately for certain undo/redo - # operations, but uses more time and memory. (default: false) - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markBufferRange: (bufferRange, options) -> - @defaultMarkerLayer.markBufferRange(bufferRange, options) - - # Essential: Create a marker on the default marker layer with the given range - # in screen coordinates. This marker will maintain its logical location as the - # buffer is changed, so if you mark a particular word, the marker will remain - # over that word even if the word's location in the buffer changes. - # - # * `range` A {Range} or range-compatible {Array} - # * `properties` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `maintainHistory` (optional) {Boolean} Whether to store this marker's - # range before and after each change in the undo history. This allows the - # marker's position to be restored more accurately for certain undo/redo - # operations, but uses more time and memory. (default: false) - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markScreenRange: (screenRange, options) -> - @defaultMarkerLayer.markScreenRange(screenRange, options) - - # Essential: Create a marker on the default marker layer with the given buffer - # position and no tail. To group multiple markers together in their own - # private layer, see {::addMarkerLayer}. - # - # * `bufferPosition` A {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markBufferPosition: (bufferPosition, options) -> - @defaultMarkerLayer.markBufferPosition(bufferPosition, options) - - # Essential: Create a marker on the default marker layer with the given screen - # position and no tail. To group multiple markers together in their own - # private layer, see {::addMarkerLayer}. - # - # * `screenPosition` A {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. - # - # Returns a {DisplayMarker}. - markScreenPosition: (screenPosition, options) -> - @defaultMarkerLayer.markScreenPosition(screenPosition, options) - - # Essential: Find all {DisplayMarker}s on the default marker layer that - # match the given properties. - # - # This method finds markers based on the given properties. Markers can be - # associated with custom properties that will be compared with basic equality. - # In addition, there are several special properties that will be compared - # with the range of the markers rather than their properties. - # - # * `properties` An {Object} containing properties that each returned marker - # must satisfy. Markers can be associated with custom properties, which are - # compared with basic equality. In addition, several reserved properties - # can be used to filter markers based on their current range: - # * `startBufferRow` Only include markers starting at this row in buffer - # coordinates. - # * `endBufferRow` Only include markers ending at this row in buffer - # coordinates. - # * `containsBufferRange` Only include markers containing this {Range} or - # in range-compatible {Array} in buffer coordinates. - # * `containsBufferPosition` Only include markers containing this {Point} - # or {Array} of `[row, column]` in buffer coordinates. - # - # Returns an {Array} of {DisplayMarker}s - findMarkers: (params) -> - @defaultMarkerLayer.findMarkers(params) - - # Extended: Get the {DisplayMarker} on the default layer for the given - # marker id. - # - # * `id` {Number} id of the marker - getMarker: (id) -> - @defaultMarkerLayer.getMarker(id) - - # Extended: Get all {DisplayMarker}s on the default marker layer. Consider - # using {::findMarkers} - getMarkers: -> - @defaultMarkerLayer.getMarkers() - - # Extended: Get the number of markers in the default marker layer. - # - # Returns a {Number}. - getMarkerCount: -> - @defaultMarkerLayer.getMarkerCount() - - destroyMarker: (id) -> - @getMarker(id)?.destroy() - - # Essential: Create a marker layer to group related markers. - # - # * `options` An {Object} containing the following keys: - # * `maintainHistory` A {Boolean} indicating whether marker state should be - # restored on undo/redo. Defaults to `false`. - # * `persistent` A {Boolean} indicating whether or not this marker layer - # should be serialized and deserialized along with the rest of the - # buffer. Defaults to `false`. If `true`, the marker layer's id will be - # maintained across the serialization boundary, allowing you to retrieve - # it via {::getMarkerLayer}. - # - # Returns a {DisplayMarkerLayer}. - addMarkerLayer: (options) -> - @displayLayer.addMarkerLayer(options) - - # Essential: Get a {DisplayMarkerLayer} by id. - # - # * `id` The id of the marker layer to retrieve. - # - # Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the - # given id. - getMarkerLayer: (id) -> - @displayLayer.getMarkerLayer(id) - - # Essential: Get the default {DisplayMarkerLayer}. - # - # All marker APIs not tied to an explicit layer interact with this default - # layer. - # - # Returns a {DisplayMarkerLayer}. - getDefaultMarkerLayer: -> - @defaultMarkerLayer - - ### - Section: Cursors - ### - - # Essential: Get the position of the most recently added cursor in buffer - # coordinates. - # - # Returns a {Point} - getCursorBufferPosition: -> - @getLastCursor().getBufferPosition() - - # Essential: Get the position of all the cursor positions in buffer coordinates. - # - # Returns {Array} of {Point}s in the order they were added - getCursorBufferPositions: -> - cursor.getBufferPosition() for cursor in @getCursors() - - # Essential: Move the cursor to the given position in buffer coordinates. - # - # If there are multiple cursors, they will be consolidated to a single cursor. - # - # * `position` A {Point} or {Array} of `[row, column]` - # * `options` (optional) An {Object} containing the following keys: - # * `autoscroll` Determines whether the editor scrolls to the new cursor's - # position. Defaults to true. - setCursorBufferPosition: (position, options) -> - @moveCursors (cursor) -> cursor.setBufferPosition(position, options) - - # Essential: Get a {Cursor} at given screen coordinates {Point} - # - # * `position` A {Point} or {Array} of `[row, column]` - # - # Returns the first matched {Cursor} or undefined - getCursorAtScreenPosition: (position) -> - if selection = @getSelectionAtScreenPosition(position) - if selection.getHeadScreenPosition().isEqual(position) - selection.cursor - - # Essential: Get the position of the most recently added cursor in screen - # coordinates. - # - # Returns a {Point}. - getCursorScreenPosition: -> - @getLastCursor().getScreenPosition() - - # Essential: Get the position of all the cursor positions in screen coordinates. - # - # Returns {Array} of {Point}s in the order the cursors were added - getCursorScreenPositions: -> - cursor.getScreenPosition() for cursor in @getCursors() - - # Essential: Move the cursor to the given position in screen coordinates. - # - # If there are multiple cursors, they will be consolidated to a single cursor. - # - # * `position` A {Point} or {Array} of `[row, column]` - # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: - # * `autoscroll` Determines whether the editor scrolls to the new cursor's - # position. Defaults to true. - setCursorScreenPosition: (position, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @moveCursors (cursor) -> cursor.setScreenPosition(position, options) - - # Essential: Add a cursor at the given position in buffer coordinates. - # - # * `bufferPosition` A {Point} or {Array} of `[row, column]` - # - # Returns a {Cursor}. - addCursorAtBufferPosition: (bufferPosition, options) -> - @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) - @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false - @getLastSelection().cursor - - # Essential: Add a cursor at the position in screen coordinates. - # - # * `screenPosition` A {Point} or {Array} of `[row, column]` - # - # Returns a {Cursor}. - addCursorAtScreenPosition: (screenPosition, options) -> - @selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'}) - @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false - @getLastSelection().cursor - - # Essential: Returns {Boolean} indicating whether or not there are multiple cursors. - hasMultipleCursors: -> - @getCursors().length > 1 - - # Essential: Move every cursor up one row in screen coordinates. - # - # * `lineCount` (optional) {Number} number of lines to move - moveUp: (lineCount) -> - @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true) - - # Essential: Move every cursor down one row in screen coordinates. - # - # * `lineCount` (optional) {Number} number of lines to move - moveDown: (lineCount) -> - @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true) - - # Essential: Move every cursor left one column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - moveLeft: (columnCount) -> - @moveCursors (cursor) -> cursor.moveLeft(columnCount, moveToEndOfSelection: true) - - # Essential: Move every cursor right one column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - moveRight: (columnCount) -> - @moveCursors (cursor) -> cursor.moveRight(columnCount, moveToEndOfSelection: true) - - # Essential: Move every cursor to the beginning of its line in buffer coordinates. - moveToBeginningOfLine: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfLine() - - # Essential: Move every cursor to the beginning of its line in screen coordinates. - moveToBeginningOfScreenLine: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfScreenLine() - - # Essential: Move every cursor to the first non-whitespace character of its line. - moveToFirstCharacterOfLine: -> - @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() - - # Essential: Move every cursor to the end of its line in buffer coordinates. - moveToEndOfLine: -> - @moveCursors (cursor) -> cursor.moveToEndOfLine() - - # Essential: Move every cursor to the end of its line in screen coordinates. - moveToEndOfScreenLine: -> - @moveCursors (cursor) -> cursor.moveToEndOfScreenLine() - - # Essential: Move every cursor to the beginning of its surrounding word. - moveToBeginningOfWord: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfWord() - - # Essential: Move every cursor to the end of its surrounding word. - moveToEndOfWord: -> - @moveCursors (cursor) -> cursor.moveToEndOfWord() - - # Cursor Extended - - # Extended: Move every cursor to the top of the buffer. - # - # If there are multiple cursors, they will be merged into a single cursor. - moveToTop: -> - @moveCursors (cursor) -> cursor.moveToTop() - - # Extended: Move every cursor to the bottom of the buffer. - # - # If there are multiple cursors, they will be merged into a single cursor. - moveToBottom: -> - @moveCursors (cursor) -> cursor.moveToBottom() - - # Extended: Move every cursor to the beginning of the next word. - moveToBeginningOfNextWord: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() - - # Extended: Move every cursor to the previous word boundary. - moveToPreviousWordBoundary: -> - @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary() - - # Extended: Move every cursor to the next word boundary. - moveToNextWordBoundary: -> - @moveCursors (cursor) -> cursor.moveToNextWordBoundary() - - # Extended: Move every cursor to the previous subword boundary. - moveToPreviousSubwordBoundary: -> - @moveCursors (cursor) -> cursor.moveToPreviousSubwordBoundary() - - # Extended: Move every cursor to the next subword boundary. - moveToNextSubwordBoundary: -> - @moveCursors (cursor) -> cursor.moveToNextSubwordBoundary() - - # Extended: Move every cursor to the beginning of the next paragraph. - moveToBeginningOfNextParagraph: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfNextParagraph() - - # Extended: Move every cursor to the beginning of the previous paragraph. - moveToBeginningOfPreviousParagraph: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfPreviousParagraph() - - # Extended: Returns the most recently added {Cursor} - getLastCursor: -> - @createLastSelectionIfNeeded() - _.last(@cursors) - - # Extended: Returns the word surrounding the most recently added cursor. - # - # * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. - getWordUnderCursor: (options) -> - @getTextInBufferRange(@getLastCursor().getCurrentWordBufferRange(options)) - - # Extended: Get an Array of all {Cursor}s. - getCursors: -> - @createLastSelectionIfNeeded() - @cursors.slice() - - # Extended: Get all {Cursors}s, ordered by their position in the buffer - # instead of the order in which they were added. - # - # Returns an {Array} of {Selection}s. - getCursorsOrderedByBufferPosition: -> - @getCursors().sort (a, b) -> a.compare(b) - - cursorsForScreenRowRange: (startScreenRow, endScreenRow) -> - cursors = [] - for marker in @selectionsMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) - if cursor = @cursorsByMarkerId.get(marker.id) - cursors.push(cursor) - cursors - - # Add a cursor based on the given {DisplayMarker}. - addCursor: (marker) -> - cursor = new Cursor(editor: this, marker: marker, showCursorOnSelection: @showCursorOnSelection) - @cursors.push(cursor) - @cursorsByMarkerId.set(marker.id, cursor) - cursor - - moveCursors: (fn) -> - @transact => - fn(cursor) for cursor in @getCursors() - @mergeCursors() - - cursorMoved: (event) -> - @emitter.emit 'did-change-cursor-position', event - - # Merge cursors that have the same screen position - mergeCursors: -> - positions = {} - for cursor in @getCursors() - position = cursor.getBufferPosition().toString() - if positions.hasOwnProperty(position) - cursor.destroy() - else - positions[position] = true - return - - ### - Section: Selections - ### - - # Essential: Get the selected text of the most recently added selection. - # - # Returns a {String}. - getSelectedText: -> - @getLastSelection().getText() - - # Essential: Get the {Range} of the most recently added selection in buffer - # coordinates. - # - # Returns a {Range}. - getSelectedBufferRange: -> - @getLastSelection().getBufferRange() - - # Essential: Get the {Range}s of all selections in buffer coordinates. - # - # The ranges are sorted by when the selections were added. Most recent at the end. - # - # Returns an {Array} of {Range}s. - getSelectedBufferRanges: -> - selection.getBufferRange() for selection in @getSelections() - - # Essential: Set the selected range in buffer coordinates. If there are multiple - # selections, they are reduced to a single selection with the given range. - # - # * `bufferRange` A {Range} or range-compatible {Array}. - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - setSelectedBufferRange: (bufferRange, options) -> - @setSelectedBufferRanges([bufferRange], options) - - # Essential: Set the selected ranges in buffer coordinates. If there are multiple - # selections, they are replaced by new selections with the given ranges. - # - # * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s. - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - setSelectedBufferRanges: (bufferRanges, options={}) -> - throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length - - selections = @getSelections() - selection.destroy() for selection in selections[bufferRanges.length...] - - @mergeIntersectingSelections options, => - for bufferRange, i in bufferRanges - bufferRange = Range.fromObject(bufferRange) - if selections[i] - selections[i].setBufferRange(bufferRange, options) - else - @addSelectionForBufferRange(bufferRange, options) - return - - # Essential: Get the {Range} of the most recently added selection in screen - # coordinates. - # - # Returns a {Range}. - getSelectedScreenRange: -> - @getLastSelection().getScreenRange() - - # Essential: Get the {Range}s of all selections in screen coordinates. - # - # The ranges are sorted by when the selections were added. Most recent at the end. - # - # Returns an {Array} of {Range}s. - getSelectedScreenRanges: -> - selection.getScreenRange() for selection in @getSelections() - - # Essential: Set the selected range in screen coordinates. If there are multiple - # selections, they are reduced to a single selection with the given range. - # - # * `screenRange` A {Range} or range-compatible {Array}. - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - setSelectedScreenRange: (screenRange, options) -> - @setSelectedBufferRange(@bufferRangeForScreenRange(screenRange, options), options) - - # Essential: Set the selected ranges in screen coordinates. If there are multiple - # selections, they are replaced by new selections with the given ranges. - # - # * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s. - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - setSelectedScreenRanges: (screenRanges, options={}) -> - throw new Error("Passed an empty array to setSelectedScreenRanges") unless screenRanges.length - - selections = @getSelections() - selection.destroy() for selection in selections[screenRanges.length...] - - @mergeIntersectingSelections options, => - for screenRange, i in screenRanges - screenRange = Range.fromObject(screenRange) - if selections[i] - selections[i].setScreenRange(screenRange, options) - else - @addSelectionForScreenRange(screenRange, options) - return - - # Essential: Add a selection for the given range in buffer coordinates. - # - # * `bufferRange` A {Range} - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - # - # Returns the added {Selection}. - addSelectionForBufferRange: (bufferRange, options={}) -> - bufferRange = Range.fromObject(bufferRange) - unless options.preserveFolds - @displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) - @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) - @getLastSelection().autoscroll() unless options.autoscroll is false - @getLastSelection() - - # Essential: Add a selection for the given range in screen coordinates. - # - # * `screenRange` A {Range} - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - # Returns the added {Selection}. - addSelectionForScreenRange: (screenRange, options={}) -> - @addSelectionForBufferRange(@bufferRangeForScreenRange(screenRange), options) - - # Essential: Select from the current cursor position to the given position in - # buffer coordinates. - # - # This method may merge selections that end up intersecting. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToBufferPosition: (position) -> - lastSelection = @getLastSelection() - lastSelection.selectToBufferPosition(position) - @mergeIntersectingSelections(reversed: lastSelection.isReversed()) - - # Essential: Select from the current cursor position to the given position in - # screen coordinates. - # - # This method may merge selections that end up intersecting. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToScreenPosition: (position, options) -> - lastSelection = @getLastSelection() - lastSelection.selectToScreenPosition(position, options) - unless options?.suppressSelectionMerge - @mergeIntersectingSelections(reversed: lastSelection.isReversed()) - - # Essential: Move the cursor of each selection one character upward while - # preserving the selection's tail position. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectUp: (rowCount) -> - @expandSelectionsBackward (selection) -> selection.selectUp(rowCount) - - # Essential: Move the cursor of each selection one character downward while - # preserving the selection's tail position. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectDown: (rowCount) -> - @expandSelectionsForward (selection) -> selection.selectDown(rowCount) - - # Essential: Move the cursor of each selection one character leftward while - # preserving the selection's tail position. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectLeft: (columnCount) -> - @expandSelectionsBackward (selection) -> selection.selectLeft(columnCount) - - # Essential: Move the cursor of each selection one character rightward while - # preserving the selection's tail position. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectRight: (columnCount) -> - @expandSelectionsForward (selection) -> selection.selectRight(columnCount) - - # Essential: Select from the top of the buffer to the end of the last selection - # in the buffer. - # - # This method merges multiple selections into a single selection. - selectToTop: -> - @expandSelectionsBackward (selection) -> selection.selectToTop() - - # Essential: Selects from the top of the first selection in the buffer to the end - # of the buffer. - # - # This method merges multiple selections into a single selection. - selectToBottom: -> - @expandSelectionsForward (selection) -> selection.selectToBottom() - - # Essential: Select all text in the buffer. - # - # This method merges multiple selections into a single selection. - selectAll: -> - @expandSelectionsForward (selection) -> selection.selectAll() - - # Essential: Move the cursor of each selection to the beginning of its line - # while preserving the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToBeginningOfLine: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfLine() - - # Essential: Move the cursor of each selection to the first non-whitespace - # character of its line while preserving the selection's tail position. If the - # cursor is already on the first character of the line, move it to the - # beginning of the line. - # - # This method may merge selections that end up intersecting. - selectToFirstCharacterOfLine: -> - @expandSelectionsBackward (selection) -> selection.selectToFirstCharacterOfLine() - - # Essential: Move the cursor of each selection to the end of its line while - # preserving the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToEndOfLine: -> - @expandSelectionsForward (selection) -> selection.selectToEndOfLine() - - # Essential: Expand selections to the beginning of their containing word. - # - # Operates on all selections. Moves the cursor to the beginning of the - # containing word while preserving the selection's tail position. - selectToBeginningOfWord: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfWord() - - # Essential: Expand selections to the end of their containing word. - # - # Operates on all selections. Moves the cursor to the end of the containing - # word while preserving the selection's tail position. - selectToEndOfWord: -> - @expandSelectionsForward (selection) -> selection.selectToEndOfWord() - - # Extended: For each selection, move its cursor to the preceding subword - # boundary while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToPreviousSubwordBoundary: -> - @expandSelectionsBackward (selection) -> selection.selectToPreviousSubwordBoundary() - - # Extended: For each selection, move its cursor to the next subword boundary - # while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToNextSubwordBoundary: -> - @expandSelectionsForward (selection) -> selection.selectToNextSubwordBoundary() - - # Essential: For each cursor, select the containing line. - # - # This method merges selections on successive lines. - selectLinesContainingCursors: -> - @expandSelectionsForward (selection) -> selection.selectLine() - - # Essential: Select the word surrounding each cursor. - selectWordsContainingCursors: -> - @expandSelectionsForward (selection) -> selection.selectWord() - - # Selection Extended - - # Extended: For each selection, move its cursor to the preceding word boundary - # while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToPreviousWordBoundary: -> - @expandSelectionsBackward (selection) -> selection.selectToPreviousWordBoundary() - - # Extended: For each selection, move its cursor to the next word boundary while - # maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToNextWordBoundary: -> - @expandSelectionsForward (selection) -> selection.selectToNextWordBoundary() - - # Extended: Expand selections to the beginning of the next word. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # word while preserving the selection's tail position. - selectToBeginningOfNextWord: -> - @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextWord() - - # Extended: Expand selections to the beginning of the next paragraph. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # paragraph while preserving the selection's tail position. - selectToBeginningOfNextParagraph: -> - @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextParagraph() - - # Extended: Expand selections to the beginning of the next paragraph. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # paragraph while preserving the selection's tail position. - selectToBeginningOfPreviousParagraph: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfPreviousParagraph() - - # Extended: Select the range of the given marker if it is valid. - # - # * `marker` A {DisplayMarker} - # - # Returns the selected {Range} or `undefined` if the marker is invalid. - selectMarker: (marker) -> - if marker.isValid() - range = marker.getBufferRange() - @setSelectedBufferRange(range) - range - - # Extended: Get the most recently added {Selection}. - # - # Returns a {Selection}. - getLastSelection: -> - @createLastSelectionIfNeeded() - _.last(@selections) - - getSelectionAtScreenPosition: (position) -> - markers = @selectionsMarkerLayer.findMarkers(containsScreenPosition: position) - if markers.length > 0 - @cursorsByMarkerId.get(markers[0].id).selection - - # Extended: Get current {Selection}s. - # - # Returns: An {Array} of {Selection}s. - getSelections: -> - @createLastSelectionIfNeeded() - @selections.slice() - - # Extended: Get all {Selection}s, ordered by their position in the buffer - # instead of the order in which they were added. - # - # Returns an {Array} of {Selection}s. - getSelectionsOrderedByBufferPosition: -> - @getSelections().sort (a, b) -> a.compare(b) - - # Extended: Determine if a given range in buffer coordinates intersects a - # selection. - # - # * `bufferRange` A {Range} or range-compatible {Array}. - # - # Returns a {Boolean}. - selectionIntersectsBufferRange: (bufferRange) -> - _.any @getSelections(), (selection) -> - selection.intersectsBufferRange(bufferRange) - - # Selections Private - - # Add a similarly-shaped selection to the next eligible line below - # each selection. - # - # Operates on all selections. If the selection is empty, adds an empty - # selection to the next following non-empty line as close to the current - # selection's column as possible. If the selection is non-empty, adds a - # selection to the next line that is long enough for a non-empty selection - # starting at the same column as the current selection to be added to it. - addSelectionBelow: -> - @expandSelectionsForward (selection) -> selection.addSelectionBelow() - - # Add a similarly-shaped selection to the next eligible line above - # each selection. - # - # Operates on all selections. If the selection is empty, adds an empty - # selection to the next preceding non-empty line as close to the current - # selection's column as possible. If the selection is non-empty, adds a - # selection to the next line that is long enough for a non-empty selection - # starting at the same column as the current selection to be added to it. - addSelectionAbove: -> - @expandSelectionsBackward (selection) -> selection.addSelectionAbove() - - # Calls the given function with each selection, then merges selections - expandSelectionsForward: (fn) -> - @mergeIntersectingSelections => - fn(selection) for selection in @getSelections() - return - - # Calls the given function with each selection, then merges selections in the - # reversed orientation - expandSelectionsBackward: (fn) -> - @mergeIntersectingSelections reversed: true, => - fn(selection) for selection in @getSelections() - return - - finalizeSelections: -> - selection.finalize() for selection in @getSelections() - return - - selectionsForScreenRows: (startRow, endRow) -> - @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) - - # Merges intersecting selections. If passed a function, it executes - # the function with merging suppressed, then merges intersecting selections - # afterward. - mergeIntersectingSelections: (args...) -> - @mergeSelections args..., (previousSelection, currentSelection) -> - exclusive = not currentSelection.isEmpty() and not previousSelection.isEmpty() - - previousSelection.intersectsWith(currentSelection, exclusive) - - mergeSelectionsOnSameRows: (args...) -> - @mergeSelections args..., (previousSelection, currentSelection) -> - screenRange = currentSelection.getScreenRange() - - previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row) - - avoidMergingSelections: (args...) -> - @mergeSelections args..., -> false - - mergeSelections: (args...) -> - mergePredicate = args.pop() - fn = args.pop() if _.isFunction(_.last(args)) - options = args.pop() ? {} - - return fn?() if @suppressSelectionMerging - - if fn? - @suppressSelectionMerging = true - result = fn() - @suppressSelectionMerging = false - - reducer = (disjointSelections, selection) -> - adjacentSelection = _.last(disjointSelections) - if mergePredicate(adjacentSelection, selection) - adjacentSelection.merge(selection, options) - disjointSelections - else - disjointSelections.concat([selection]) - - [head, tail...] = @getSelectionsOrderedByBufferPosition() - _.reduce(tail, reducer, [head]) - return result if fn? - - # Add a {Selection} based on the given {DisplayMarker}. - # - # * `marker` The {DisplayMarker} to highlight - # * `options` (optional) An {Object} that pertains to the {Selection} constructor. - # - # Returns the new {Selection}. - addSelection: (marker, options={}) -> - cursor = @addCursor(marker) - selection = new Selection(Object.assign({editor: this, marker, cursor}, options)) - @selections.push(selection) - selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections(preserveFolds: options.preserveFolds) - - if selection.destroyed - for selection in @getSelections() - if selection.intersectsBufferRange(selectionBufferRange) - return selection - else - @emitter.emit 'did-add-cursor', cursor - @emitter.emit 'did-add-selection', selection - selection - - # Remove the given selection. - removeSelection: (selection) -> - _.remove(@cursors, selection.cursor) - _.remove(@selections, selection) - @cursorsByMarkerId.delete(selection.cursor.marker.id) - @emitter.emit 'did-remove-cursor', selection.cursor - @emitter.emit 'did-remove-selection', selection - - # Reduce one or more selections to a single empty selection based on the most - # recently added cursor. - clearSelections: (options) -> - @consolidateSelections() - @getLastSelection().clear(options) - - # Reduce multiple selections to the least recently added selection. - consolidateSelections: -> - selections = @getSelections() - if selections.length > 1 - selection.destroy() for selection in selections[1...(selections.length)] - selections[0].autoscroll(center: true) - true - else - false - - # Called by the selection - selectionRangeChanged: (event) -> - @component?.didChangeSelectionRange() - @emitter.emit 'did-change-selection-range', event - - createLastSelectionIfNeeded: -> - if @selections.length is 0 - @addSelectionForBufferRange([[0, 0], [0, 0]], autoscroll: false, preserveFolds: true) - - ### - Section: Searching and Replacing - ### - - # Essential: Scan regular expression matches in the entire buffer, calling the - # given iterator function on each match. - # - # `::scan` functions as the replace method as well via the `replace` - # - # If you're programmatically modifying the results, you may want to try - # {::backwardsScanInBufferRange} to avoid tripping over your own changes. - # - # * `regex` A {RegExp} to search for. - # * `options` (optional) {Object} - # * `leadingContextLineCount` {Number} default `0`; The number of lines - # before the matched line to include in the results object. - # * `trailingContextLineCount` {Number} default `0`; The number of lines - # after the matched line to include in the results object. - # * `iterator` A {Function} that's called on each match - # * `object` {Object} - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - scan: (regex, options={}, iterator) -> - if _.isFunction(options) - iterator = options - options = {} - - @buffer.scan(regex, options, iterator) - - # Essential: Scan regular expression matches in a given range, calling the given - # iterator function on each match. - # - # * `regex` A {RegExp} to search for. - # * `range` A {Range} in which to search. - # * `iterator` A {Function} that's called on each match with an {Object} - # containing the following keys: - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - scanInBufferRange: (regex, range, iterator) -> @buffer.scanInRange(regex, range, iterator) - - # Essential: Scan regular expression matches in a given range in reverse order, - # calling the given iterator function on each match. - # - # * `regex` A {RegExp} to search for. - # * `range` A {Range} in which to search. - # * `iterator` A {Function} that's called on each match with an {Object} - # containing the following keys: - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - backwardsScanInBufferRange: (regex, range, iterator) -> @buffer.backwardsScanInRange(regex, range, iterator) - - ### - Section: Tab Behavior - ### - - # Essential: Returns a {Boolean} indicating whether softTabs are enabled for this - # editor. - getSoftTabs: -> @softTabs - - # Essential: Enable or disable soft tabs for this editor. - # - # * `softTabs` A {Boolean} - setSoftTabs: (@softTabs) -> @update({@softTabs}) - - # Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. - hasAtomicSoftTabs: -> @displayLayer.atomicSoftTabs - - # Essential: Toggle soft tabs for this editor - toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) - - # Essential: Get the on-screen length of tab characters. - # - # Returns a {Number}. - getTabLength: -> @tokenizedBuffer.getTabLength() - - # Essential: Set the on-screen length of tab characters. Setting this to a - # {Number} This will override the `editor.tabLength` setting. - # - # * `tabLength` {Number} length of a single tab. Setting to `null` will - # fallback to using the `editor.tabLength` config setting - setTabLength: (tabLength) -> @update({tabLength}) - - # Returns an {Object} representing the current invisible character - # substitutions for this editor. See {::setInvisibles}. - getInvisibles: -> - if not @mini and @showInvisibles and @invisibles? - @invisibles - else - {} - - doesShowIndentGuide: -> @showIndentGuide and not @mini - - getSoftWrapHangingIndentLength: -> @displayLayer.softWrapHangingIndent - - # Extended: Determine if the buffer uses hard or soft tabs. - # - # Returns `true` if the first non-comment line with leading whitespace starts - # with a space character. Returns `false` if it starts with a hard tab (`\t`). - # - # Returns a {Boolean} or undefined if no non-comment lines had leading - # whitespace. - usesSoftTabs: -> - for bufferRow in [0..Math.min(1000, @buffer.getLastRow())] - continue if @tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() - - line = @buffer.lineForRow(bufferRow) - return true if line[0] is ' ' - return false if line[0] is '\t' - - undefined - - # Extended: Get the text representing a single level of indent. - # - # If soft tabs are enabled, the text is composed of N spaces, where N is the - # tab length. Otherwise the text is a tab character (`\t`). - # - # Returns a {String}. - getTabText: -> @buildIndentString(1) - - # If soft tabs are enabled, convert all hard tabs to soft tabs in the given - # {Range}. - normalizeTabsInBufferRange: (bufferRange) -> - return unless @getSoftTabs() - @scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText()) - - ### - Section: Soft Wrap Behavior - ### - - # Essential: Determine whether lines in this editor are soft-wrapped. - # - # Returns a {Boolean}. - isSoftWrapped: -> @softWrapped - - # Essential: Enable or disable soft wrapping for this editor. - # - # * `softWrapped` A {Boolean} - # - # Returns a {Boolean}. - setSoftWrapped: (softWrapped) -> - @update({softWrapped}) - @isSoftWrapped() - - getPreferredLineLength: -> @preferredLineLength - - # Essential: Toggle soft wrapping for this editor - # - # Returns a {Boolean}. - toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) - - # Essential: Gets the column at which column will soft wrap - getSoftWrapColumn: -> - if @isSoftWrapped() and not @mini - if @softWrapAtPreferredLineLength - Math.min(@getEditorWidthInChars(), @preferredLineLength) - else - @getEditorWidthInChars() - else - @maxScreenLineLength - - ### - Section: Indentation - ### - - # Essential: Get the indentation level of the given buffer row. - # - # Determines how deeply the given row is indented based on the soft tabs and - # tab length settings of this editor. Note that if soft tabs are enabled and - # the tab length is 2, a row with 4 leading spaces would have an indentation - # level of 2. - # - # * `bufferRow` A {Number} indicating the buffer row. - # - # Returns a {Number}. - indentationForBufferRow: (bufferRow) -> - @indentLevelForLine(@lineTextForBufferRow(bufferRow)) - - # Essential: Set the indentation level for the given buffer row. - # - # Inserts or removes hard tabs or spaces based on the soft tabs and tab length - # settings of this editor in order to bring it to the given indentation level. - # Note that if soft tabs are enabled and the tab length is 2, a row with 4 - # leading spaces would have an indentation level of 2. - # - # * `bufferRow` A {Number} indicating the buffer row. - # * `newLevel` A {Number} indicating the new indentation level. - # * `options` (optional) An {Object} with the following keys: - # * `preserveLeadingWhitespace` `true` to preserve any whitespace already at - # the beginning of the line (default: false). - setIndentationForBufferRow: (bufferRow, newLevel, {preserveLeadingWhitespace}={}) -> - if preserveLeadingWhitespace - endColumn = 0 - else - endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length - newIndentString = @buildIndentString(newLevel) - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) - - # Extended: Indent rows intersecting selections by one level. - indentSelectedRows: -> - @mutateSelectedText (selection) -> selection.indentSelectedRows() - - # Extended: Outdent rows intersecting selections by one level. - outdentSelectedRows: -> - @mutateSelectedText (selection) -> selection.outdentSelectedRows() - - # Extended: Get the indentation level of the given line of text. - # - # Determines how deeply the given line is indented based on the soft tabs and - # tab length settings of this editor. Note that if soft tabs are enabled and - # the tab length is 2, a row with 4 leading spaces would have an indentation - # level of 2. - # - # * `line` A {String} representing a line of text. - # - # Returns a {Number}. - indentLevelForLine: (line) -> - @tokenizedBuffer.indentLevelForLine(line) - - # Extended: Indent rows intersecting selections based on the grammar's suggested - # indent level. - autoIndentSelectedRows: -> - @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() - - # Indent all lines intersecting selections. See {Selection::indent} for more - # information. - indent: (options={}) -> - options.autoIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) -> selection.indent(options) - - # Constructs the string used for indents. - buildIndentString: (level, column=0) -> - if @getSoftTabs() - tabStopViolation = column % @getTabLength() - _.multiplyString(" ", Math.floor(level * @getTabLength()) - tabStopViolation) - else - excessWhitespace = _.multiplyString(' ', Math.round((level - Math.floor(level)) * @getTabLength())) - _.multiplyString("\t", Math.floor(level)) + excessWhitespace - - ### - Section: Grammars - ### - - # Essential: Get the current {Grammar} of this editor. - getGrammar: -> - @tokenizedBuffer.grammar - - # Essential: Set the current {Grammar} of this editor. - # - # Assigning a grammar will cause the editor to re-tokenize based on the new - # grammar. - # - # * `grammar` {Grammar} - setGrammar: (grammar) -> - @tokenizedBuffer.setGrammar(grammar) - - # Reload the grammar based on the file name. - reloadGrammar: -> - @tokenizedBuffer.reloadGrammar() - - # Experimental: Get a notification when async tokenization is completed. - onDidTokenize: (callback) -> - @tokenizedBuffer.onDidTokenize(callback) - - ### - Section: Managing Syntax Scopes - ### - - # Essential: Returns a {ScopeDescriptor} that includes this editor's language. - # e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with - # {Config::get} to get language specific config values. - getRootScopeDescriptor: -> - @tokenizedBuffer.rootScopeDescriptor - - # Essential: Get the syntactic scopeDescriptor for the given position in buffer - # coordinates. Useful with {Config::get}. - # - # For example, if called with a position inside the parameter list of an - # anonymous CoffeeScript function, the method returns the following array: - # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` - # - # * `bufferPosition` A {Point} or {Array} of [row, column]. - # - # Returns a {ScopeDescriptor}. - scopeDescriptorForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) - - # Extended: Get the range in buffer coordinates of all tokens surrounding the - # cursor that match the given scope selector. - # - # For example, if you wanted to find the string surrounding the cursor, you - # could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. - # - # * `scopeSelector` {String} selector. e.g. `'.source.ruby'` - # - # Returns a {Range}. - bufferRangeForScopeAtCursor: (scopeSelector) -> - @bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition()) - - bufferRangeForScopeAtPosition: (scopeSelector, position) -> - @tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position) - - # Extended: Determine if the given row is entirely a comment - isBufferRowCommented: (bufferRow) -> - if match = @lineTextForBufferRow(bufferRow).match(/\S/) - @commentScopeSelector ?= new TextMateScopeSelector('comment.*') - @commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) - - # Get the scope descriptor at the cursor. - getCursorScope: -> - @getLastCursor().getScopeDescriptor() - - tokenForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.tokenForPosition(bufferPosition) - - ### - Section: Clipboard Operations - ### - - # Essential: For each selection, copy the selected text. - copySelectedText: -> - maintainClipboard = false - for selection in @getSelectionsOrderedByBufferPosition() - if selection.isEmpty() - previousRange = selection.getBufferRange() - selection.selectLine() - selection.copy(maintainClipboard, true) - selection.setBufferRange(previousRange) - else - selection.copy(maintainClipboard, false) - maintainClipboard = true - return - - # Private: For each selection, only copy highlighted text. - copyOnlySelectedText: -> - maintainClipboard = false - for selection in @getSelectionsOrderedByBufferPosition() - if not selection.isEmpty() - selection.copy(maintainClipboard, false) - maintainClipboard = true - return - - # Essential: For each selection, cut the selected text. - cutSelectedText: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - if selection.isEmpty() - selection.selectLine() - selection.cut(maintainClipboard, true) - else - selection.cut(maintainClipboard, false) - maintainClipboard = true - - # Essential: For each selection, replace the selected text with the contents of - # the clipboard. - # - # If the clipboard contains the same number of selections as the current - # editor, each selection will be replaced with the content of the - # corresponding clipboard selection text. - # - # * `options` (optional) See {Selection::insertText}. - pasteText: (options) -> - options = Object.assign({}, options) - {text: clipboardText, metadata} = @constructor.clipboard.readWithMetadata() - return false unless @emitWillInsertTextEvent(clipboardText) - - metadata ?= {} - options.autoIndent ?= @shouldAutoIndentOnPaste() - - @mutateSelectedText (selection, index) => - if metadata.selections?.length is @getSelections().length - {text, indentBasis, fullLine} = metadata.selections[index] - else - {indentBasis, fullLine} = metadata - text = clipboardText - - delete options.indentBasis - {cursor} = selection - if indentBasis? - containsNewlines = text.indexOf('\n') isnt -1 - if containsNewlines or not cursor.hasPrecedingCharactersOnLine() - options.indentBasis ?= indentBasis - - range = null - if fullLine and selection.isEmpty() - oldPosition = selection.getBufferRange().start - selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]) - range = selection.insertText(text, options) - newPosition = oldPosition.translate([1, 0]) - selection.setBufferRange([newPosition, newPosition]) - else - range = selection.insertText(text, options) - - didInsertEvent = {text, range} - @emitter.emit 'did-insert-text', didInsertEvent - - # Essential: For each selection, if the selection is empty, cut all characters - # of the containing screen line following the cursor. Otherwise cut the selected - # text. - cutToEndOfLine: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfLine(maintainClipboard) - maintainClipboard = true - - # Essential: For each selection, if the selection is empty, cut all characters - # of the containing buffer line following the cursor. Otherwise cut the - # selected text. - cutToEndOfBufferLine: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfBufferLine(maintainClipboard) - maintainClipboard = true - - ### - Section: Folds - ### - - # Essential: Fold the most recent cursor's row based on its indentation level. - # - # The fold will extend from the nearest preceding line with a lower - # indentation level up to the nearest following row with a lower indentation - # level. - foldCurrentRow: -> - {row} = @getCursorBufferPosition() - if range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) - @displayLayer.foldBufferRange(range) - - # Essential: Unfold the most recent cursor's row by one level. - unfoldCurrentRow: -> - {row} = @getCursorBufferPosition() - @displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) - - # Essential: Fold the given row in buffer coordinates based on its indentation - # level. - # - # If the given row is foldable, the fold will begin there. Otherwise, it will - # begin at the first foldable row preceding the given row. - # - # * `bufferRow` A {Number}. - foldBufferRow: (bufferRow) -> - position = Point(bufferRow, Infinity) - loop - foldableRange = @tokenizedBuffer.getFoldableRangeContainingPoint(position, @getTabLength()) - if foldableRange - existingFolds = @displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) - if existingFolds.length is 0 - @displayLayer.foldBufferRange(foldableRange) - else - firstExistingFoldRange = @displayLayer.bufferRangeForFold(existingFolds[0]) - if firstExistingFoldRange.start.isLessThan(position) - position = Point(firstExistingFoldRange.start.row, 0) - continue - return - - # Essential: Unfold all folds containing the given row in buffer coordinates. - # - # * `bufferRow` A {Number} - unfoldBufferRow: (bufferRow) -> - position = Point(bufferRow, Infinity) - @displayLayer.destroyFoldsContainingBufferPositions([position]) - - # Extended: For each selection, fold the rows it intersects. - foldSelectedLines: -> - selection.fold() for selection in @getSelections() - return - - # Extended: Fold all foldable lines. - foldAll: -> - @displayLayer.destroyAllFolds() - for range in @tokenizedBuffer.getFoldableRanges(@getTabLength()) - @displayLayer.foldBufferRange(range) - return - - # Extended: Unfold all existing folds. - unfoldAll: -> - result = @displayLayer.destroyAllFolds() - @scrollToCursorPosition() - result - - # Extended: Fold all foldable lines at the given indent level. - # - # * `level` A {Number}. - foldAllAtIndentLevel: (level) -> - @displayLayer.destroyAllFolds() - for range in @tokenizedBuffer.getFoldableRangesAtIndentLevel(level, @getTabLength()) - @displayLayer.foldBufferRange(range) - return - - # Extended: Determine whether the given row in buffer coordinates is foldable. - # - # A *foldable* row is a row that *starts* a row range that can be folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldableAtBufferRow: (bufferRow) -> - @tokenizedBuffer.isFoldableAtRow(bufferRow) - - # Extended: Determine whether the given row in screen coordinates is foldable. - # - # A *foldable* row is a row that *starts* a row range that can be folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldableAtScreenRow: (screenRow) -> - @isFoldableAtBufferRow(@bufferRowForScreenRow(screenRow)) - - # Extended: Fold the given buffer row if it isn't currently folded, and unfold - # it otherwise. - toggleFoldAtBufferRow: (bufferRow) -> - if @isFoldedAtBufferRow(bufferRow) - @unfoldBufferRow(bufferRow) - else - @foldBufferRow(bufferRow) - - # Extended: Determine whether the most recently added cursor's row is folded. - # - # Returns a {Boolean}. - isFoldedAtCursorRow: -> - @isFoldedAtBufferRow(@getCursorBufferPosition().row) - - # Extended: Determine whether the given row in buffer coordinates is folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtBufferRow: (bufferRow) -> - range = Range( - Point(bufferRow, 0), - Point(bufferRow, @buffer.lineLengthForRow(bufferRow)) - ) - @displayLayer.foldsIntersectingBufferRange(range).length > 0 - - # Extended: Determine whether the given row in screen coordinates is folded. - # - # * `screenRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtScreenRow: (screenRow) -> - @isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow)) - - # Creates a new fold between two row numbers. - # - # startRow - The row {Number} to start folding at - # endRow - The row {Number} to end the fold - # - # Returns the new {Fold}. - foldBufferRowRange: (startRow, endRow) -> - @foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) - - foldBufferRange: (range) -> - @displayLayer.foldBufferRange(range) - - # Remove any {Fold}s found that intersect the given buffer range. - 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 - ### - - # Essential: Add a custom {Gutter}. - # - # * `options` An {Object} with the following fields: - # * `name` (required) A unique {String} to identify this gutter. - # * `priority` (optional) A {Number} that determines stacking order between - # gutters. Lower priority items are forced closer to the edges of the - # window. (default: -100) - # * `visible` (optional) {Boolean} specifying whether the gutter is visible - # initially after being created. (default: true) - # - # Returns the newly-created {Gutter}. - addGutter: (options) -> - @gutterContainer.addGutter(options) - - # Essential: Get this editor's gutters. - # - # Returns an {Array} of {Gutter}s. - getGutters: -> - @gutterContainer.getGutters() - - getLineNumberGutter: -> - @lineNumberGutter - - # Essential: Get the gutter with the given name. - # - # Returns a {Gutter}, or `null` if no gutter exists for the given name. - gutterWithName: (name) -> - @gutterContainer.gutterWithName(name) - - ### - Section: Scrolling the TextEditor - ### - - # Essential: Scroll the editor to reveal the most recently added cursor if it is - # off-screen. - # - # * `options` (optional) {Object} - # * `center` Center the editor around the cursor if possible. (default: true) - scrollToCursorPosition: (options) -> - @getLastCursor().autoscroll(center: options?.center ? true) - - # Essential: Scrolls the editor to the given buffer position. - # - # * `bufferPosition` An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # * `options` (optional) {Object} - # * `center` Center the editor around the position if possible. (default: false) - scrollToBufferPosition: (bufferPosition, options) -> - @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options) - - # Essential: Scrolls the editor to the given screen position. - # - # * `screenPosition` An object that represents a screen position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # * `options` (optional) {Object} - # * `center` Center the editor around the position if possible. (default: false) - scrollToScreenPosition: (screenPosition, options) -> - @scrollToScreenRange(new Range(screenPosition, screenPosition), options) - - scrollToTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - - @getElement().scrollToTop() - - scrollToBottom: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - - @getElement().scrollToBottom() - - scrollToScreenRange: (screenRange, options = {}) -> - screenRange = @clipScreenRange(screenRange) if options.clip isnt false - scrollEvent = {screenRange, options} - @component?.didRequestAutoscroll(scrollEvent) - @emitter.emit "did-request-autoscroll", scrollEvent - - getHorizontalScrollbarHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") - - @getElement().getHorizontalScrollbarHeight() - - getVerticalScrollbarWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.") - - @getElement().getVerticalScrollbarWidth() - - pageUp: -> - @moveUp(@getRowsPerPage()) - - pageDown: -> - @moveDown(@getRowsPerPage()) - - selectPageUp: -> - @selectUp(@getRowsPerPage()) - - selectPageDown: -> - @selectDown(@getRowsPerPage()) - - # Returns the number of rows per page - getRowsPerPage: -> - if @component? - clientHeight = @component.getScrollContainerClientHeight() - lineHeight = @component.getLineHeight() - Math.max(1, Math.ceil(clientHeight / lineHeight)) - else - 1 - - Object.defineProperty(@prototype, 'rowsPerPage', { - get: -> @getRowsPerPage() - }) - - ### - Section: Config - ### - - # Experimental: Supply an object that will provide the editor with settings - # for specific syntactic scopes. See the `ScopedSettingsDelegate` in - # `text-editor-registry.js` for an example implementation. - setScopedSettingsDelegate: (@scopedSettingsDelegate) -> - @tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate - - # Experimental: Retrieve the {Object} that provides the editor with settings - # for specific syntactic scopes. - getScopedSettingsDelegate: -> @scopedSettingsDelegate - - # Experimental: Is auto-indentation enabled for this editor? - # - # Returns a {Boolean}. - shouldAutoIndent: -> @autoIndent - - # Experimental: Is auto-indentation on paste enabled for this editor? - # - # Returns a {Boolean}. - shouldAutoIndentOnPaste: -> @autoIndentOnPaste - - # Experimental: Does this editor allow scrolling past the last line? - # - # Returns a {Boolean}. - getScrollPastEnd: -> - if @getAutoHeight() - false - else - @scrollPastEnd - - # Experimental: How fast does the editor scroll in response to mouse wheel - # movements? - # - # Returns a positive {Number}. - getScrollSensitivity: -> @scrollSensitivity - - # Experimental: Does this editor show cursors while there is a selection? - # - # Returns a positive {Boolean}. - getShowCursorOnSelection: -> @showCursorOnSelection - - # Experimental: Are line numbers enabled for this editor? - # - # Returns a {Boolean} - doesShowLineNumbers: -> @showLineNumbers - - # Experimental: Get the time interval within which text editing operations - # are grouped together in the editor's undo history. - # - # Returns the time interval {Number} in milliseconds. - getUndoGroupingInterval: -> @undoGroupingInterval - - # Experimental: Get the characters that are *not* considered part of words, - # for the purpose of word-based cursor movements. - # - # Returns a {String} containing the non-word characters. - getNonWordCharacters: (scopes) -> - @scopedSettingsDelegate?.getNonWordCharacters?(scopes) ? @nonWordCharacters - - ### - Section: Event Handlers - ### - - handleGrammarChange: -> - @unfoldAll() - @emitter.emit 'did-change-grammar', @getGrammar() - - ### - Section: TextEditor Rendering - ### - - # Get the Element for the editor. - getElement: -> - if @component? - @component.element - else - TextEditorComponent ?= require('./text-editor-component') - TextEditorElement ?= require('./text-editor-element') - new TextEditorComponent({ - model: this, - updatedSynchronously: TextEditorElement.prototype.updatedSynchronously, - @initialScrollTopRow, @initialScrollLeftColumn - }) - @component.element - - getAllowedLocations: -> - ['center'] - - # Essential: Retrieves the greyed out placeholder of a mini editor. - # - # Returns a {String}. - getPlaceholderText: -> @placeholderText - - # Essential: Set the greyed out placeholder of a mini editor. Placeholder text - # will be displayed when the editor has no content. - # - # * `placeholderText` {String} text that is displayed when the editor has no content. - setPlaceholderText: (placeholderText) -> @update({placeholderText}) - - pixelPositionForBufferPosition: (bufferPosition) -> - Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") - @getElement().pixelPositionForBufferPosition(bufferPosition) - - pixelPositionForScreenPosition: (screenPosition) -> - Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") - @getElement().pixelPositionForScreenPosition(screenPosition) - - getVerticalScrollMargin: -> - maxScrollMargin = Math.floor(((@height / @getLineHeightInPixels()) - 1) / 2) - Math.min(@verticalScrollMargin, maxScrollMargin) - - setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin - - getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@width / @getDefaultCharWidth()) - 1) / 2)) - setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin - - getLineHeightInPixels: -> @lineHeightInPixels - setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels - - getKoreanCharWidth: -> @koreanCharWidth - getHalfWidthCharWidth: -> @halfWidthCharWidth - getDoubleWidthCharWidth: -> @doubleWidthCharWidth - getDefaultCharWidth: -> @defaultCharWidth - - ratioForCharacter: (character) -> - if isKoreanCharacter(character) - @getKoreanCharWidth() / @getDefaultCharWidth() - else if isHalfWidthCharacter(character) - @getHalfWidthCharWidth() / @getDefaultCharWidth() - else if isDoubleWidthCharacter(character) - @getDoubleWidthCharWidth() / @getDefaultCharWidth() - else - 1 - - setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) -> - doubleWidthCharWidth ?= defaultCharWidth - halfWidthCharWidth ?= defaultCharWidth - koreanCharWidth ?= defaultCharWidth - if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth - @defaultCharWidth = defaultCharWidth - @doubleWidthCharWidth = doubleWidthCharWidth - @halfWidthCharWidth = halfWidthCharWidth - @koreanCharWidth = koreanCharWidth - if @isSoftWrapped() - @displayLayer.reset({ - softWrapColumn: @getSoftWrapColumn() - }) - defaultCharWidth - - setHeight: (height) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") - @getElement().setHeight(height) - - getHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") - @getElement().getHeight() - - getAutoHeight: -> @autoHeight ? true - - getAutoWidth: -> @autoWidth ? false - - setWidth: (width) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") - @getElement().setWidth(width) - - getWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") - @getElement().getWidth() - - # Use setScrollTopRow instead of this method - setFirstVisibleScreenRow: (screenRow) -> - @setScrollTopRow(screenRow) - - getFirstVisibleScreenRow: -> - @getElement().component.getFirstVisibleRow() - - getLastVisibleScreenRow: -> - @getElement().component.getLastVisibleRow() - - getVisibleRowRange: -> - [@getFirstVisibleScreenRow(), @getLastVisibleScreenRow()] - - # Use setScrollLeftColumn instead of this method - setFirstVisibleScreenColumn: (column) -> - @setScrollLeftColumn(column) - - getFirstVisibleScreenColumn: -> - @getElement().component.getFirstVisibleColumn() - - getScrollTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") - - @getElement().getScrollTop() - - setScrollTop: (scrollTop) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.") - - @getElement().setScrollTop(scrollTop) - - getScrollBottom: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.") - - @getElement().getScrollBottom() - - setScrollBottom: (scrollBottom) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.") - - @getElement().setScrollBottom(scrollBottom) - - getScrollLeft: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.") - - @getElement().getScrollLeft() - - setScrollLeft: (scrollLeft) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.") - - @getElement().setScrollLeft(scrollLeft) - - getScrollRight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.") - - @getElement().getScrollRight() - - setScrollRight: (scrollRight) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.") - - @getElement().setScrollRight(scrollRight) - - getScrollHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.") - - @getElement().getScrollHeight() - - getScrollWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.") - - @getElement().getScrollWidth() - - getMaxScrollTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getMaxScrollTop instead.") - - @getElement().getMaxScrollTop() - - getScrollTopRow: -> - @getElement().component.getScrollTopRow() - - setScrollTopRow: (scrollTopRow) -> - @getElement().component.setScrollTopRow(scrollTopRow) - - getScrollLeftColumn: -> - @getElement().component.getScrollLeftColumn() - - setScrollLeftColumn: (scrollLeftColumn) -> - @getElement().component.setScrollLeftColumn(scrollLeftColumn) - - intersectsVisibleRowRange: (startRow, endRow) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") - - @getElement().intersectsVisibleRowRange(startRow, endRow) - - selectionIntersectsVisibleRowRange: (selection) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.") - - @getElement().selectionIntersectsVisibleRowRange(selection) - - screenPositionForPixelPosition: (pixelPosition) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.") - - @getElement().screenPositionForPixelPosition(pixelPosition) - - pixelRectForScreenRange: (screenRange) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.") - - @getElement().pixelRectForScreenRange(screenRange) - - ### - Section: Utility - ### - - inspect: -> - "" - - emitWillInsertTextEvent: (text) -> - result = true - cancel = -> result = false - willInsertEvent = {cancel, text} - @emitter.emit 'will-insert-text', willInsertEvent - result - - ### - Section: Language Mode Delegated Methods - ### - - suggestedIndentForBufferRow: (bufferRow, options) -> @tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options) - - # Given a buffer row, indent it. - # - # * bufferRow - The row {Number}. - # * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. - autoIndentBufferRow: (bufferRow, options) -> - indentLevel = @suggestedIndentForBufferRow(bufferRow, options) - @setIndentationForBufferRow(bufferRow, indentLevel, options) - - # Indents all the rows between two buffer row numbers. - # - # * startRow - The row {Number} to start at - # * endRow - The row {Number} to end at - autoIndentBufferRows: (startRow, endRow) -> - row = startRow - while row <= endRow - @autoIndentBufferRow(row) - row++ - return - - autoDecreaseIndentForBufferRow: (bufferRow) -> - indentLevel = @tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) - @setIndentationForBufferRow(bufferRow, indentLevel) if indentLevel? - - toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) - - rowRangeForParagraphAtBufferRow: (bufferRow) -> - return unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(bufferRow)) - - isCommented = @tokenizedBuffer.isRowCommented(bufferRow) - - startRow = bufferRow - while startRow > 0 - break unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(startRow - 1)) - break if @tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented - startRow-- - - endRow = bufferRow - rowCount = @getLineCount() - while endRow < rowCount - break unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(endRow + 1)) - break if @tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented - endRow++ - - new Range(new Point(startRow, 0), new Point(endRow, @buffer.lineLengthForRow(endRow))) - -class ChangeEvent - constructor: ({@oldRange, @newRange}) -> - - Object.defineProperty @prototype, 'start', { - get: -> @oldRange.start - } - - Object.defineProperty @prototype, 'oldExtent', { - get: -> @oldRange.getExtent() - } - - Object.defineProperty @prototype, 'newExtent', { - get: -> @newRange.getExtent() - } diff --git a/src/text-editor.js b/src/text-editor.js new file mode 100644 index 000000000..4d7d94de0 --- /dev/null +++ b/src/text-editor.js @@ -0,0 +1,4587 @@ +const _ = require('underscore-plus') +const path = require('path') +const fs = require('fs-plus') +const Grim = require('grim') +const dedent = require('dedent') +const {CompositeDisposable, Disposable, Emitter} = require('event-kit') +const TextBuffer = require('text-buffer') +const {Point, Range} = TextBuffer +const DecorationManager = require('./decoration-manager') +const TokenizedBuffer = require('./tokenized-buffer') +const Cursor = require('./cursor') +const Selection = require('./selection') + +const TextMateScopeSelector = require('first-mate').ScopeSelector +const GutterContainer = require('./gutter-container') +let TextEditorComponent = null +let TextEditorElement = null +const {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require('./text-utils') + +const SERIALIZATION_VERSION = 1 +const NON_WHITESPACE_REGEXP = /\S/ +const ZERO_WIDTH_NBSP = '\ufeff' +let nextId = 0 + +// Essential: This class represents all essential editing state for a single +// {TextBuffer}, including cursor and selection positions, folds, and soft wraps. +// If you're manipulating the state of an editor, use this class. +// +// A single {TextBuffer} can belong to multiple editors. For example, if the +// same file is open in two different panes, Atom creates a separate editor for +// each pane. If the buffer is manipulated the changes are reflected in both +// editors, but each maintains its own cursor position, folded lines, etc. +// +// ## Accessing TextEditor Instances +// +// The easiest way to get hold of `TextEditor` objects is by registering a callback +// with `::observeTextEditors` on the `atom.workspace` global. Your callback will +// then be called with all current editor instances and also when any editor is +// created in the future. +// +// ```coffee +// atom.workspace.observeTextEditors (editor) -> +// editor.insertText('Hello World') +// ``` +// +// ## Buffer vs. Screen Coordinates +// +// Because editors support folds and soft-wrapping, the lines on screen don't +// always match the lines in the buffer. For example, a long line that soft wraps +// twice renders as three lines on screen, but only represents one line in the +// buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds +// to row 11 in the buffer. +// +// Your choice of coordinates systems will depend on what you're trying to +// achieve. For example, if you're writing a command that jumps the cursor up or +// down by 10 lines, you'll want to use screen coordinates because the user +// probably wants to skip lines *on screen*. However, if you're writing a package +// that jumps between method definitions, you'll want to work in buffer +// coordinates. +// +// **When in doubt, just default to buffer coordinates**, then experiment with +// soft wraps and folds to ensure your code interacts with them correctly. +module.exports = +class TextEditor { + static setClipboard (clipboard) { + this.clipboard = clipboard + } + + static setScheduler (scheduler) { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.setScheduler(scheduler) + } + + static didUpdateStyles () { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.didUpdateStyles() + } + + static didUpdateScrollbarStyles () { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.didUpdateScrollbarStyles() + } + + static viewForItem (item) { return item.element || item } + + static deserialize (state, atomEnvironment) { + if (state.version !== SERIALIZATION_VERSION) return null + + try { + const tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) + if (!tokenizedBuffer) return null + + state.tokenizedBuffer = tokenizedBuffer + state.tabLength = state.tokenizedBuffer.getTabLength() + } catch (error) { + if (error.syscall === 'read') { + return // Error reading the file, don't deserialize an editor for it + } else { + throw error + } + } + + state.buffer = state.tokenizedBuffer.buffer + state.assert = atomEnvironment.assert.bind(atomEnvironment) + const editor = new TextEditor(state) + if (state.registered) { + const disposable = atomEnvironment.textEditors.add(editor) + editor.onDidDestroy(() => disposable.dispose()) + } + return editor + } + + constructor (params = {}) { + if (this.constructor.clipboard == null) { + throw new Error('Must call TextEditor.setClipboard at least once before creating TextEditor instances') + } + + this.id = params.id != null ? params.id : nextId++ + this.initialScrollTopRow = params.initialScrollTopRow + this.initialScrollLeftColumn = params.initialScrollLeftColumn + this.decorationManager = params.decorationManager + this.selectionsMarkerLayer = params.selectionsMarkerLayer + this.mini = (params.mini != null) ? params.mini : false + this.placeholderText = params.placeholderText + this.showLineNumbers = params.showLineNumbers + this.largeFileMode = params.largeFileMode + this.assert = params.assert || (condition => condition) + this.showInvisibles = (params.showInvisibles != null) ? params.showInvisibles : true + this.autoHeight = params.autoHeight + this.autoWidth = params.autoWidth + this.scrollPastEnd = (params.scrollPastEnd != null) ? params.scrollPastEnd : false + this.scrollSensitivity = (params.scrollSensitivity != null) ? params.scrollSensitivity : 40 + this.editorWidthInChars = params.editorWidthInChars + this.invisibles = params.invisibles + this.showIndentGuide = params.showIndentGuide + this.softWrapped = params.softWrapped + this.softWrapAtPreferredLineLength = params.softWrapAtPreferredLineLength + this.preferredLineLength = params.preferredLineLength + this.showCursorOnSelection = (params.showCursorOnSelection != null) ? params.showCursorOnSelection : true + this.maxScreenLineLength = params.maxScreenLineLength + this.softTabs = (params.softTabs != null) ? params.softTabs : true + this.autoIndent = (params.autoIndent != null) ? params.autoIndent : true + this.autoIndentOnPaste = (params.autoIndentOnPaste != null) ? params.autoIndentOnPaste : true + this.undoGroupingInterval = (params.undoGroupingInterval != null) ? params.undoGroupingInterval : 300 + this.nonWordCharacters = (params.nonWordCharacters != null) ? params.nonWordCharacters : "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…" + this.softWrapped = (params.softWrapped != null) ? params.softWrapped : false + this.softWrapAtPreferredLineLength = (params.softWrapAtPreferredLineLength != null) ? params.softWrapAtPreferredLineLength : false + this.preferredLineLength = (params.preferredLineLength != null) ? params.preferredLineLength : 80 + this.maxScreenLineLength = (params.maxScreenLineLength != null) ? params.maxScreenLineLength : 500 + this.showLineNumbers = (params.showLineNumbers != null) ? params.showLineNumbers : true + const {tabLength = 2} = params + + this.alive = true + this.doBackgroundWork = this.doBackgroundWork.bind(this) + this.serializationVersion = 1 + this.suppressSelectionMerging = false + this.selectionFlashDuration = 500 + this.gutterContainer = null + this.verticalScrollMargin = 2 + this.horizontalScrollMargin = 6 + this.lineHeightInPixels = null + this.defaultCharWidth = null + this.height = null + this.width = null + this.registered = false + this.atomicSoftTabs = true + this.emitter = new Emitter() + this.disposables = new CompositeDisposable() + this.cursors = [] + this.cursorsByMarkerId = new Map() + this.selections = [] + this.hasTerminatedPendingState = false + + this.buffer = params.buffer || new TextBuffer({ + shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') } + }) + + this.tokenizedBuffer = params.tokenizedBuffer || new TokenizedBuffer({ + grammar: params.grammar, + tabLength, + buffer: this.buffer, + largeFileMode: this.largeFileMode, + assert: this.assert + }) + + if (params.displayLayer) { + this.displayLayer = params.displayLayer + } else { + const displayLayerParams = { + invisibles: this.getInvisibles(), + softWrapColumn: this.getSoftWrapColumn(), + showIndentGuides: this.doesShowIndentGuide(), + atomicSoftTabs: params.atomicSoftTabs != null ? params.atomicSoftTabs : true, + tabLength, + ratioForCharacter: this.ratioForCharacter.bind(this), + isWrapBoundary, + foldCharacter: ZERO_WIDTH_NBSP, + softWrapHangingIndent: params.softWrapHangingIndentLength != null ? params.softWrapHangingIndentLength : 0 + } + + this.displayLayer = this.buffer.getDisplayLayer(params.displayLayerId) + if (this.displayLayer) { + this.displayLayer.reset(displayLayerParams) + this.selectionsMarkerLayer = this.displayLayer.getMarkerLayer(params.selectionsMarkerLayerId) + } else { + this.displayLayer = this.buffer.addDisplayLayer(displayLayerParams) + } + } + + this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork) + this.disposables.add(new Disposable(() => { + if (this.backgroundWorkHandle != null) return cancelIdleCallback(this.backgroundWorkHandle) + })) + + this.defaultMarkerLayer = this.displayLayer.addMarkerLayer() + if (!this.selectionsMarkerLayer) { + this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true}) + } + + this.displayLayer.setTextDecorationLayer(this.tokenizedBuffer) + + this.decorationManager = new DecorationManager(this) + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'cursor'}) + if (!this.isMini()) this.decorateCursorLine() + + this.decorateMarkerLayer(this.displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) + + for (let marker of this.selectionsMarkerLayer.getMarkers()) { + this.addSelection(marker) + } + + this.subscribeToBuffer() + this.subscribeToDisplayLayer() + + if (this.cursors.length === 0 && !params.suppressCursorCreation) { + const initialLine = Math.max(parseInt(params.initialLine) || 0, 0) + const initialColumn = Math.max(parseInt(params.initialColumn) || 0, 0) + this.addCursorAtBufferPosition([initialLine, initialColumn]) + } + + this.gutterContainer = new GutterContainer(this) + this.lineNumberGutter = this.gutterContainer.addGutter({ + name: 'line-number', + priority: 0, + visible: params.lineNumberGutterVisible + }) + } + + get element () { + return this.getElement() + } + + get editorElement () { + Grim.deprecate(dedent`\ + \`TextEditor.prototype.editorElement\` has always been private, but now + it is gone. Reading the \`editorElement\` property still returns a + reference to the editor element but this field will be removed in a + later version of Atom, so we recommend using the \`element\` property instead.\ + `) + + return this.getElement() + } + + get displayBuffer () { + Grim.deprecate(dedent`\ + \`TextEditor.prototype.displayBuffer\` has always been private, but now + it is gone. Reading the \`displayBuffer\` property now returns a reference + to the containing \`TextEditor\`, which now provides *some* of the API of + the defunct \`DisplayBuffer\` class.\ + `) + return this + } + + get languageMode () { + return this.tokenizedBuffer + } + + get rowsPerPage () { + return this.getRowsPerPage() + } + + decorateCursorLine () { + this.cursorLineDecorations = [ + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line', class: 'cursor-line', onlyEmpty: true}), + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line-number', class: 'cursor-line'}), + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true}) + ] + } + + doBackgroundWork (deadline) { + const previousLongestRow = this.getApproximateLongestScreenRow() + if (this.displayLayer.doBackgroundWork(deadline)) { + this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork) + } else { + this.backgroundWorkHandle = null + } + + if (this.component && this.getApproximateLongestScreenRow() !== previousLongestRow) { + this.component.scheduleUpdate() + } + } + + update (params) { + const displayLayerParams = {} + + for (let param of Object.keys(params)) { + const value = params[param] + + switch (param) { + case 'autoIndent': + this.autoIndent = value + break + + case 'autoIndentOnPaste': + this.autoIndentOnPaste = value + break + + case 'undoGroupingInterval': + this.undoGroupingInterval = value + break + + case 'nonWordCharacters': + this.nonWordCharacters = value + break + + case 'scrollSensitivity': + this.scrollSensitivity = value + break + + case 'encoding': + this.buffer.setEncoding(value) + break + + case 'softTabs': + if (value !== this.softTabs) { + this.softTabs = value + } + break + + case 'atomicSoftTabs': + if (value !== this.displayLayer.atomicSoftTabs) { + displayLayerParams.atomicSoftTabs = value + } + break + + case 'tabLength': + if (value > 0 && value !== this.tokenizedBuffer.getTabLength()) { + this.tokenizedBuffer.setTabLength(value) + displayLayerParams.tabLength = value + } + break + + case 'softWrapped': + if (value !== this.softWrapped) { + this.softWrapped = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + this.emitter.emit('did-change-soft-wrapped', this.isSoftWrapped()) + } + break + + case 'softWrapHangingIndentLength': + if (value !== this.displayLayer.softWrapHangingIndent) { + displayLayerParams.softWrapHangingIndent = value + } + break + + case 'softWrapAtPreferredLineLength': + if (value !== this.softWrapAtPreferredLineLength) { + this.softWrapAtPreferredLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'preferredLineLength': + if (value !== this.preferredLineLength) { + this.preferredLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'maxScreenLineLength': + if (value !== this.maxScreenLineLength) { + this.maxScreenLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'mini': + if (value !== this.mini) { + this.mini = value + this.emitter.emit('did-change-mini', value) + displayLayerParams.invisibles = this.getInvisibles() + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + displayLayerParams.showIndentGuides = this.doesShowIndentGuide() + if (this.mini) { + for (let decoration of this.cursorLineDecorations) { decoration.destroy() } + this.cursorLineDecorations = null + } else { + this.decorateCursorLine() + } + if (this.component != null) { + this.component.scheduleUpdate() + } + } + break + + case 'placeholderText': + if (value !== this.placeholderText) { + this.placeholderText = value + this.emitter.emit('did-change-placeholder-text', value) + } + break + + case 'lineNumberGutterVisible': + if (value !== this.lineNumberGutterVisible) { + if (value) { + this.lineNumberGutter.show() + } else { + this.lineNumberGutter.hide() + } + this.emitter.emit('did-change-line-number-gutter-visible', this.lineNumberGutter.isVisible()) + } + break + + case 'showIndentGuide': + if (value !== this.showIndentGuide) { + this.showIndentGuide = value + displayLayerParams.showIndentGuides = this.doesShowIndentGuide() + } + break + + case 'showLineNumbers': + if (value !== this.showLineNumbers) { + this.showLineNumbers = value + if (this.component != null) { + this.component.scheduleUpdate() + } + } + break + + case 'showInvisibles': + if (value !== this.showInvisibles) { + this.showInvisibles = value + displayLayerParams.invisibles = this.getInvisibles() + } + break + + case 'invisibles': + if (!_.isEqual(value, this.invisibles)) { + this.invisibles = value + displayLayerParams.invisibles = this.getInvisibles() + } + break + + case 'editorWidthInChars': + if (value > 0 && value !== this.editorWidthInChars) { + this.editorWidthInChars = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'width': + if (value !== this.width) { + this.width = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'scrollPastEnd': + if (value !== this.scrollPastEnd) { + this.scrollPastEnd = value + if (this.component) this.component.scheduleUpdate() + } + break + + case 'autoHeight': + if (value !== this.autoHeight) { + this.autoHeight = value + } + break + + case 'autoWidth': + if (value !== this.autoWidth) { + this.autoWidth = value + } + break + + case 'showCursorOnSelection': + if (value !== this.showCursorOnSelection) { + this.showCursorOnSelection = value + if (this.component) this.component.scheduleUpdate() + } + break + + default: + if (param !== 'ref' && param !== 'key') { + throw new TypeError(`Invalid TextEditor parameter: '${param}'`) + } + } + } + + this.displayLayer.reset(displayLayerParams) + + if (this.component) { + return this.component.getNextUpdatePromise() + } else { + return Promise.resolve() + } + } + + scheduleComponentUpdate () { + if (this.component) this.component.scheduleUpdate() + } + + serialize () { + const tokenizedBufferState = this.tokenizedBuffer.serialize() + + return { + deserializer: 'TextEditor', + version: SERIALIZATION_VERSION, + + // TODO: Remove this forward-compatible fallback once 1.8 reaches stable. + displayBuffer: {tokenizedBuffer: tokenizedBufferState}, + + tokenizedBuffer: tokenizedBufferState, + displayLayerId: this.displayLayer.id, + selectionsMarkerLayerId: this.selectionsMarkerLayer.id, + + initialScrollTopRow: this.getScrollTopRow(), + initialScrollLeftColumn: this.getScrollLeftColumn(), + + atomicSoftTabs: this.displayLayer.atomicSoftTabs, + softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent, + + id: this.id, + softTabs: this.softTabs, + softWrapped: this.softWrapped, + softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength, + preferredLineLength: this.preferredLineLength, + mini: this.mini, + editorWidthInChars: this.editorWidthInChars, + width: this.width, + largeFileMode: this.largeFileMode, + maxScreenLineLength: this.maxScreenLineLength, + registered: this.registered, + invisibles: this.invisibles, + showInvisibles: this.showInvisibles, + showIndentGuide: this.showIndentGuide, + autoHeight: this.autoHeight, + autoWidth: this.autoWidth + } + } + + subscribeToBuffer () { + this.buffer.retain() + this.disposables.add(this.buffer.onDidChangePath(() => { + this.emitter.emit('did-change-title', this.getTitle()) + this.emitter.emit('did-change-path', this.getPath()) + })) + this.disposables.add(this.buffer.onDidChangeEncoding(() => { + this.emitter.emit('did-change-encoding', this.getEncoding()) + })) + this.disposables.add(this.buffer.onDidDestroy(() => this.destroy())) + this.disposables.add(this.buffer.onDidChangeModified(() => { + if (!this.hasTerminatedPendingState && this.buffer.isModified()) this.terminatePendingState() + })) + } + + terminatePendingState () { + if (!this.hasTerminatedPendingState) this.emitter.emit('did-terminate-pending-state') + this.hasTerminatedPendingState = true + } + + onDidTerminatePendingState (callback) { + return this.emitter.on('did-terminate-pending-state', callback) + } + + subscribeToDisplayLayer () { + this.disposables.add(this.tokenizedBuffer.onDidChangeGrammar(this.handleGrammarChange.bind(this))) + this.disposables.add(this.displayLayer.onDidChange(changes => { + this.mergeIntersectingSelections() + if (this.component) this.component.didChangeDisplayLayer(changes) + this.emitter.emit('did-change', changes.map(change => new ChangeEvent(change))) + })) + this.disposables.add(this.displayLayer.onDidReset(() => { + this.mergeIntersectingSelections() + if (this.component) this.component.didResetDisplayLayer() + this.emitter.emit('did-change', {}) + })) + this.disposables.add(this.selectionsMarkerLayer.onDidCreateMarker(this.addSelection.bind(this))) + return this.disposables.add(this.selectionsMarkerLayer.onDidUpdate(() => (this.component != null ? this.component.didUpdateSelections() : undefined))) + } + + destroy () { + if (!this.alive) return + this.alive = false + this.disposables.dispose() + this.displayLayer.destroy() + this.tokenizedBuffer.destroy() + for (let selection of this.selections.slice()) { + selection.destroy() + } + this.buffer.release() + this.gutterContainer.destroy() + this.emitter.emit('did-destroy') + this.emitter.clear() + if (this.component) this.component.element.component = null + this.component = null + this.lineNumberGutter.element = null + } + + isAlive () { return this.alive } + + isDestroyed () { return !this.alive } + + /* + Section: Event Subscription + */ + + // Essential: Calls your `callback` when the buffer's title has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeTitle (callback) { + return this.emitter.on('did-change-title', callback) + } + + // Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePath (callback) { + return this.emitter.on('did-change-path', callback) + } + + // Essential: Invoke the given callback synchronously when the content of the + // buffer changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider {::onDidStopChanging} to + // delay expensive operations until after changes stop occurring. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange (callback) { + return this.emitter.on('did-change', callback) + } + + // Essential: Invoke `callback` when the buffer's contents change. It is + // emit asynchronously 300ms after the last buffer change. This is a good place + // to handle changes to the buffer without compromising typing performance. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChanging (callback) { + return this.getBuffer().onDidStopChanging(callback) + } + + // Essential: Calls your `callback` when a {Cursor} is moved. If there are + // multiple cursors, your callback will be called for each cursor. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldBufferPosition` {Point} + // * `oldScreenPosition` {Point} + // * `newBufferPosition` {Point} + // * `newScreenPosition` {Point} + // * `textChanged` {Boolean} + // * `cursor` {Cursor} that triggered the event + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeCursorPosition (callback) { + return this.emitter.on('did-change-cursor-position', callback) + } + + // Essential: Calls your `callback` when a selection's screen range changes. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldBufferRange` {Range} + // * `oldScreenRange` {Range} + // * `newBufferRange` {Range} + // * `newScreenRange` {Range} + // * `selection` {Selection} that triggered the event + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeSelectionRange (callback) { + return this.emitter.on('did-change-selection-range', callback) + } + + // Extended: Calls your `callback` when soft wrap was enabled or disabled. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeSoftWrapped (callback) { + return this.emitter.on('did-change-soft-wrapped', callback) + } + + // Extended: Calls your `callback` when the buffer's encoding has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeEncoding (callback) { + return this.emitter.on('did-change-encoding', callback) + } + + // Extended: Calls your `callback` when the grammar that interprets and + // colorizes the text has been changed. Immediately calls your callback with + // the current grammar. + // + // * `callback` {Function} + // * `grammar` {Grammar} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeGrammar (callback) { + callback(this.getGrammar()) + return this.onDidChangeGrammar(callback) + } + + // Extended: Calls your `callback` when the grammar that interprets and + // colorizes the text has been changed. + // + // * `callback` {Function} + // * `grammar` {Grammar} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeGrammar (callback) { + return this.emitter.on('did-change-grammar', callback) + } + + // Extended: Calls your `callback` when the result of {::isModified} changes. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeModified (callback) { + return this.getBuffer().onDidChangeModified(callback) + } + + // Extended: Calls your `callback` when the buffer's underlying file changes on + // disk at a moment when the result of {::isModified} is true. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidConflict (callback) { + return this.getBuffer().onDidConflict(callback) + } + + // Extended: Calls your `callback` before text has been inserted. + // + // * `callback` {Function} + // * `event` event {Object} + // * `text` {String} text to be inserted + // * `cancel` {Function} Call to prevent the text from being inserted + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillInsertText (callback) { + return this.emitter.on('will-insert-text', callback) + } + + // Extended: Calls your `callback` after text has been inserted. + // + // * `callback` {Function} + // * `event` event {Object} + // * `text` {String} text to be inserted + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidInsertText (callback) { + return this.emitter.on('did-insert-text', callback) + } + + // Essential: Invoke the given callback after the buffer is saved to disk. + // + // * `callback` {Function} to be called after the buffer is saved. + // * `event` {Object} with the following keys: + // * `path` The path to which the buffer was saved. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidSave (callback) { + return this.getBuffer().onDidSave(callback) + } + + // Essential: Invoke the given callback when the editor is destroyed. + // + // * `callback` {Function} to be called when the editor is destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + // Extended: Calls your `callback` when a {Cursor} is added to the editor. + // Immediately calls your callback for each existing cursor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeCursors (callback) { + this.getCursors().forEach(callback) + return this.onDidAddCursor(callback) + } + + // Extended: Calls your `callback` when a {Cursor} is added to the editor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddCursor (callback) { + return this.emitter.on('did-add-cursor', callback) + } + + // Extended: Calls your `callback` when a {Cursor} is removed from the editor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveCursor (callback) { + return this.emitter.on('did-remove-cursor', callback) + } + + // Extended: Calls your `callback` when a {Selection} is added to the editor. + // Immediately calls your callback for each existing selection. + // + // * `callback` {Function} + // * `selection` {Selection} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeSelections (callback) { + this.getSelections().forEach(callback) + return this.onDidAddSelection(callback) + } + + // Extended: Calls your `callback` when a {Selection} is added to the editor. + // + // * `callback` {Function} + // * `selection` {Selection} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddSelection (callback) { + return this.emitter.on('did-add-selection', callback) + } + + // Extended: Calls your `callback` when a {Selection} is removed from the editor. + // + // * `callback` {Function} + // * `selection` {Selection} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveSelection (callback) { + return this.emitter.on('did-remove-selection', callback) + } + + // Extended: Calls your `callback` with each {Decoration} added to the editor. + // Calls your `callback` immediately for any existing decorations. + // + // * `callback` {Function} + // * `decoration` {Decoration} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeDecorations (callback) { + return this.decorationManager.observeDecorations(callback) + } + + // Extended: Calls your `callback` when a {Decoration} is added to the editor. + // + // * `callback` {Function} + // * `decoration` {Decoration} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddDecoration (callback) { + return this.decorationManager.onDidAddDecoration(callback) + } + + // Extended: Calls your `callback` when a {Decoration} is removed from the editor. + // + // * `callback` {Function} + // * `decoration` {Decoration} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveDecoration (callback) { + return this.decorationManager.onDidRemoveDecoration(callback) + } + + // Called by DecorationManager when a decoration is added. + didAddDecoration (decoration) { + if (this.component && decoration.isType('block')) { + this.component.addBlockDecoration(decoration) + } + } + + // Extended: Calls your `callback` when the placeholder text is changed. + // + // * `callback` {Function} + // * `placeholderText` {String} new text + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePlaceholderText (callback) { + return this.emitter.on('did-change-placeholder-text', callback) + } + + onDidChangeScrollTop (callback) { + Grim.deprecate('This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.') + return this.getElement().onDidChangeScrollTop(callback) + } + + onDidChangeScrollLeft (callback) { + Grim.deprecate('This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.') + return this.getElement().onDidChangeScrollLeft(callback) + } + + onDidRequestAutoscroll (callback) { + return this.emitter.on('did-request-autoscroll', callback) + } + + // TODO Remove once the tabs package no longer uses .on subscriptions + onDidChangeIcon (callback) { + return this.emitter.on('did-change-icon', callback) + } + + onDidUpdateDecorations (callback) { + return this.decorationManager.onDidUpdateDecorations(callback) + } + + // Essential: Retrieves the current {TextBuffer}. + getBuffer () { return this.buffer } + + // Retrieves the current buffer's URI. + getURI () { return this.buffer.getUri() } + + // Create an {TextEditor} with its initial state based on this object + copy () { + const displayLayer = this.displayLayer.copy() + const selectionsMarkerLayer = displayLayer.getMarkerLayer(this.buffer.getMarkerLayer(this.selectionsMarkerLayer.id).copy().id) + const softTabs = this.getSoftTabs() + return new TextEditor({ + buffer: this.buffer, + selectionsMarkerLayer, + softTabs, + suppressCursorCreation: true, + tabLength: this.tokenizedBuffer.getTabLength(), + initialScrollTopRow: this.getScrollTopRow(), + initialScrollLeftColumn: this.getScrollLeftColumn(), + assert: this.assert, + displayLayer, + grammar: this.getGrammar(), + autoWidth: this.autoWidth, + autoHeight: this.autoHeight, + showCursorOnSelection: this.showCursorOnSelection + }) + } + + // Controls visibility based on the given {Boolean}. + setVisible (visible) { this.tokenizedBuffer.setVisible(visible) } + + setMini (mini) { + this.update({mini}) + } + + isMini () { return this.mini } + + onDidChangeMini (callback) { + return this.emitter.on('did-change-mini', callback) + } + + setLineNumberGutterVisible (lineNumberGutterVisible) { this.update({lineNumberGutterVisible}) } + + isLineNumberGutterVisible () { return this.lineNumberGutter.isVisible() } + + onDidChangeLineNumberGutterVisible (callback) { + return this.emitter.on('did-change-line-number-gutter-visible', callback) + } + + // Essential: Calls your `callback` when a {Gutter} is added to the editor. + // Immediately calls your callback for each existing gutter. + // + // * `callback` {Function} + // * `gutter` {Gutter} that currently exists/was added. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeGutters (callback) { + return this.gutterContainer.observeGutters(callback) + } + + // Essential: Calls your `callback` when a {Gutter} is added to the editor. + // + // * `callback` {Function} + // * `gutter` {Gutter} that was added. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddGutter (callback) { + return this.gutterContainer.onDidAddGutter(callback) + } + + // Essential: Calls your `callback` when a {Gutter} is removed from the editor. + // + // * `callback` {Function} + // * `name` The name of the {Gutter} that was removed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveGutter (callback) { + return this.gutterContainer.onDidRemoveGutter(callback) + } + + // Set the number of characters that can be displayed horizontally in the + // editor. + // + // * `editorWidthInChars` A {Number} representing the width of the + // {TextEditorElement} in characters. + setEditorWidthInChars (editorWidthInChars) { this.update({editorWidthInChars}) } + + // Returns the editor width in characters. + getEditorWidthInChars () { + if (this.width != null && this.defaultCharWidth > 0) { + return Math.max(0, Math.floor(this.width / this.defaultCharWidth)) + } else { + return this.editorWidthInChars + } + } + + /* + Section: File Details + */ + + // Essential: Get the editor's title for display in other parts of the + // UI such as the tabs. + // + // If the editor's buffer is saved, its title is the file name. If it is + // unsaved, its title is "untitled". + // + // Returns a {String}. + getTitle () { + return this.getFileName() || 'untitled' + } + + // Essential: Get unique title for display in other parts of the UI, such as + // the window title. + // + // If the editor's buffer is unsaved, its title is "untitled" + // If the editor's buffer is saved, its unique title is formatted as one + // of the following, + // * "" when it is the only editing buffer with this file name. + // * "" when other buffers have this file name. + // + // Returns a {String} + getLongTitle () { + if (this.getPath()) { + const fileName = this.getFileName() + + let myPathSegments + const openEditorPathSegmentsWithSameFilename = [] + for (const textEditor of atom.workspace.getTextEditors()) { + const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) + if (textEditor.getFileName() === fileName) { + openEditorPathSegmentsWithSameFilename.push(pathSegments) + } + if (textEditor === this) myPathSegments = pathSegments + } + + if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName + + let commonPathSegmentCount + for (let i = 0, {length} = myPathSegments; i < length; i++) { + const myPathSegment = myPathSegments[i] + if (openEditorPathSegmentsWithSameFilename.some(segments => (segments.length === i + 1) || (segments[i] !== myPathSegment))) { + commonPathSegmentCount = i + break + } + } + + return `${fileName} \u2014 ${path.join(...myPathSegments.slice(commonPathSegmentCount))}` + } else { + return 'untitled' + } + } + + // Essential: Returns the {String} path of this editor's text buffer. + getPath () { + return this.buffer.getPath() + } + + getFileName () { + const fullPath = this.getPath() + if (fullPath) return path.basename(fullPath) + } + + getDirectoryPath () { + const fullPath = this.getPath() + if (fullPath) return path.dirname(fullPath) + } + + // Extended: Returns the {String} character set encoding of this editor's text + // buffer. + getEncoding () { return this.buffer.getEncoding() } + + // Extended: Set the character set encoding to use in this editor's text + // buffer. + // + // * `encoding` The {String} character set encoding name such as 'utf8' + setEncoding (encoding) { this.buffer.setEncoding(encoding) } + + // Essential: Returns {Boolean} `true` if this editor has been modified. + isModified () { return this.buffer.isModified() } + + // Essential: Returns {Boolean} `true` if this editor has no content. + isEmpty () { return this.buffer.isEmpty() } + + /* + Section: File Operations + */ + + // Essential: Saves the editor's text buffer. + // + // See {TextBuffer::save} for more details. + save () { return this.buffer.save() } + + // Essential: Saves the editor's text buffer as the given path. + // + // See {TextBuffer::saveAs} for more details. + // + // * `filePath` A {String} path. + saveAs (filePath) { return this.buffer.saveAs(filePath) } + + // Determine whether the user should be prompted to save before closing + // this editor. + shouldPromptToSave ({windowCloseRequested, projectHasPaths} = {}) { + if (windowCloseRequested && projectHasPaths && atom.stateStore.isConnected()) { + return this.buffer.isInConflict() + } else { + return this.isModified() && !this.buffer.hasMultipleEditors() + } + } + + // Returns an {Object} to configure dialog shown when this editor is saved + // via {Pane::saveItemAs}. + getSaveDialogOptions () { return {} } + + /* + Section: Reading Text + */ + + // Essential: Returns a {String} representing the entire contents of the editor. + getText () { return this.buffer.getText() } + + // Essential: Get the text in the given {Range} in buffer coordinates. + // + // * `range` A {Range} or range-compatible {Array}. + // + // Returns a {String}. + getTextInBufferRange (range) { + return this.buffer.getTextInRange(range) + } + + // Essential: Returns a {Number} representing the number of lines in the buffer. + getLineCount () { return this.buffer.getLineCount() } + + // Essential: Returns a {Number} representing the number of screen lines in the + // editor. This accounts for folds. + getScreenLineCount () { return this.displayLayer.getScreenLineCount() } + + getApproximateScreenLineCount () { return this.displayLayer.getApproximateScreenLineCount() } + + // Essential: Returns a {Number} representing the last zero-indexed buffer row + // number of the editor. + getLastBufferRow () { return this.buffer.getLastRow() } + + // Essential: Returns a {Number} representing the last zero-indexed screen row + // number of the editor. + getLastScreenRow () { return this.getScreenLineCount() - 1 } + + // Essential: Returns a {String} representing the contents of the line at the + // given buffer row. + // + // * `bufferRow` A {Number} representing a zero-indexed buffer row. + lineTextForBufferRow (bufferRow) { return this.buffer.lineForRow(bufferRow) } + + // Essential: Returns a {String} representing the contents of the line at the + // given screen row. + // + // * `screenRow` A {Number} representing a zero-indexed screen row. + lineTextForScreenRow (screenRow) { + const screenLine = this.screenLineForScreenRow(screenRow) + if (screenLine) return screenLine.lineText + } + + logScreenLines (start = 0, end = this.getLastScreenRow()) { + for (let row = start; row <= end; row++) { + const line = this.lineTextForScreenRow(row) + console.log(row, this.bufferRowForScreenRow(row), line, line.length) + } + } + + tokensForScreenRow (screenRow) { + const tokens = [] + let lineTextIndex = 0 + const currentTokenScopes = [] + const {lineText, tags} = this.screenLineForScreenRow(screenRow) + for (const tag of tags) { + if (this.displayLayer.isOpenTag(tag)) { + currentTokenScopes.push(this.displayLayer.classNameForTag(tag)) + } else if (this.displayLayer.isCloseTag(tag)) { + currentTokenScopes.pop() + } else { + tokens.push({ + text: lineText.substr(lineTextIndex, tag), + scopes: currentTokenScopes.slice() + }) + lineTextIndex += tag + } + } + return tokens + } + + screenLineForScreenRow (screenRow) { + return this.displayLayer.getScreenLine(screenRow) + } + + bufferRowForScreenRow (screenRow) { + return this.displayLayer.translateScreenPosition(Point(screenRow, 0)).row + } + + bufferRowsForScreenRows (startScreenRow, endScreenRow) { + return this.displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1) + } + + screenRowForBufferRow (row) { + return this.displayLayer.translateBufferPosition(Point(row, 0)).row + } + + getRightmostScreenPosition () { return this.displayLayer.getRightmostScreenPosition() } + + getApproximateRightmostScreenPosition () { return this.displayLayer.getApproximateRightmostScreenPosition() } + + getMaxScreenLineLength () { return this.getRightmostScreenPosition().column } + + getLongestScreenRow () { return this.getRightmostScreenPosition().row } + + getApproximateLongestScreenRow () { return this.getApproximateRightmostScreenPosition().row } + + lineLengthForScreenRow (screenRow) { return this.displayLayer.lineLengthForScreenRow(screenRow) } + + // Returns the range for the given buffer row. + // + // * `row` A row {Number}. + // * `options` (optional) An options hash with an `includeNewline` key. + // + // Returns a {Range}. + bufferRangeForBufferRow (row, options) { + return this.buffer.rangeForRow(row, options && options.includeNewline) + } + + // Get the text in the given {Range}. + // + // Returns a {String}. + getTextInRange (range) { return this.buffer.getTextInRange(range) } + + // {Delegates to: TextBuffer.isRowBlank} + isBufferRowBlank (bufferRow) { return this.buffer.isRowBlank(bufferRow) } + + // {Delegates to: TextBuffer.nextNonBlankRow} + nextNonBlankBufferRow (bufferRow) { return this.buffer.nextNonBlankRow(bufferRow) } + + // {Delegates to: TextBuffer.getEndPosition} + getEofBufferPosition () { return this.buffer.getEndPosition() } + + // Essential: Get the {Range} of the paragraph surrounding the most recently added + // cursor. + // + // Returns a {Range}. + getCurrentParagraphBufferRange () { + return this.getLastCursor().getCurrentParagraphBufferRange() + } + + /* + Section: Mutating Text + */ + + // Essential: Replaces the entire contents of the buffer with the given {String}. + // + // * `text` A {String} to replace with + setText (text) { return this.buffer.setText(text) } + + // Essential: Set the text in the given {Range} in buffer coordinates. + // + // * `range` A {Range} or range-compatible {Array}. + // * `text` A {String} + // * `options` (optional) {Object} + // * `normalizeLineEndings` (optional) {Boolean} (default: true) + // * `undo` (optional) {String} 'skip' will skip the undo system + // + // Returns the {Range} of the newly-inserted text. + setTextInBufferRange (range, text, options) { + return this.getBuffer().setTextInRange(range, text, options) + } + + // Essential: For each selection, replace the selected text with the given text. + // + // * `text` A {String} representing the text to insert. + // * `options` (optional) See {Selection::insertText}. + // + // Returns a {Range} when the text has been inserted + // Returns a {Boolean} false when the text has not been inserted + insertText (text, options = {}) { + if (!this.emitWillInsertTextEvent(text)) return false + + const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0 + if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent() + if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent() + return this.mutateSelectedText(selection => { + const range = selection.insertText(text, options) + const didInsertEvent = {text, range} + this.emitter.emit('did-insert-text', didInsertEvent) + return range + }, groupingInterval) + } + + // Essential: For each selection, replace the selected text with a newline. + insertNewline (options) { + return this.insertText('\n', options) + } + + // Essential: For each selection, if the selection is empty, delete the character + // following the cursor. Otherwise delete the selected text. + delete () { + return this.mutateSelectedText(selection => selection.delete()) + } + + // Essential: For each selection, if the selection is empty, delete the character + // preceding the cursor. Otherwise delete the selected text. + backspace () { + return this.mutateSelectedText(selection => selection.backspace()) + } + + // Extended: Mutate the text of all the selections in a single transaction. + // + // All the changes made inside the given {Function} can be reverted with a + // single call to {::undo}. + // + // * `fn` A {Function} that will be called once for each {Selection}. The first + // argument will be a {Selection} and the second argument will be the + // {Number} index of that selection. + mutateSelectedText (fn, groupingInterval = 0) { + return this.mergeIntersectingSelections(() => { + return this.transact(groupingInterval, () => { + return this.getSelectionsOrderedByBufferPosition().map((selection, index) => fn(selection, index)) + }) + }) + } + + // Move lines intersecting the most recent selection or multiple selections + // up by one row in screen coordinates. + moveLineUp () { + const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b)) + + if (selections[0].start.row === 0) return + if (selections[selections.length - 1].start.row === this.getLastBufferRow() && this.buffer.getLastLine() === '') return + + this.transact(() => { + const newSelectionRanges = [] + + while (selections.length > 0) { + // Find selections spanning a contiguous set of lines + const selection = selections.shift() + const selectionsToMove = [selection] + + while (selection.end.row === (selections[0] != null ? selections[0].start.row : undefined)) { + selectionsToMove.push(selections[0]) + selection.end.row = selections[0].end.row + selections.shift() + } + + // Compute the buffer range spanned by all these selections, expanding it + // so that it includes any folded region that intersects them. + let startRow = selection.start.row + let endRow = selection.end.row + if (selection.end.row > selection.start.row && selection.end.column === 0) { + // Don't move the last line of a multi-line selection if the selection ends at column 0 + endRow-- + } + + startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow) + endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1) + const linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) + + // If selected line range is preceded by a fold, one line above on screen + // could be multiple lines in the buffer. + const precedingRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow - 1) + const insertDelta = linesRange.start.row - precedingRow + + // Any folds in the text that is moved will need to be re-created. + // It includes the folds that were intersecting with the selection. + const rangesToRefold = this.displayLayer + .destroyFoldsIntersectingBufferRange(linesRange) + .map(range => range.translate([-insertDelta, 0])) + + // Delete lines spanned by selection and insert them on the preceding buffer row + let lines = this.buffer.getTextInRange(linesRange) + if (lines[lines.length - 1] !== '\n') { lines += this.buffer.lineEndingForRow(linesRange.end.row - 2) } + this.buffer.delete(linesRange) + this.buffer.insert([precedingRow, 0], lines) + + // Restore folds that existed before the lines were moved + for (let rangeToRefold of rangesToRefold) { + this.displayLayer.foldBufferRange(rangeToRefold) + } + + for (const selectionToMove of selectionsToMove) { + newSelectionRanges.push(selectionToMove.translate([-insertDelta, 0])) + } + } + + this.setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) + if (this.shouldAutoIndent()) this.autoIndentSelectedRows() + this.scrollToBufferPosition([newSelectionRanges[0].start.row, 0]) + }) + } + + // Move lines intersecting the most recent selection or multiple selections + // down by one row in screen coordinates. + moveLineDown () { + const selections = this.getSelectedBufferRanges() + selections.sort((a, b) => b.compare(a)) + + this.transact(() => { + this.consolidateSelections() + const newSelectionRanges = [] + + while (selections.length > 0) { + // Find selections spanning a contiguous set of lines + const selection = selections.shift() + const selectionsToMove = [selection] + + // if the current selection start row matches the next selections' end row - make them one selection + while (selection.start.row === (selections[0] != null ? selections[0].end.row : undefined)) { + selectionsToMove.push(selections[0]) + selection.start.row = selections[0].start.row + selections.shift() + } + + // Compute the buffer range spanned by all these selections, expanding it + // so that it includes any folded region that intersects them. + let startRow = selection.start.row + let endRow = selection.end.row + if (selection.end.row > selection.start.row && selection.end.column === 0) { + // Don't move the last line of a multi-line selection if the selection ends at column 0 + endRow-- + } + + startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow) + endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1) + const linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) + + // If selected line range is followed by a fold, one line below on screen + // could be multiple lines in the buffer. But at the same time, if the + // next buffer row is wrapped, one line in the buffer can represent many + // screen rows. + const followingRow = Math.min(this.buffer.getLineCount(), this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1)) + const insertDelta = followingRow - linesRange.end.row + + // Any folds in the text that is moved will need to be re-created. + // It includes the folds that were intersecting with the selection. + const rangesToRefold = this.displayLayer + .destroyFoldsIntersectingBufferRange(linesRange) + .map(range => range.translate([insertDelta, 0])) + + // Delete lines spanned by selection and insert them on the following correct buffer row + let lines = this.buffer.getTextInRange(linesRange) + if (followingRow - 1 === this.buffer.getLastRow()) { + lines = `\n${lines}` + } + + this.buffer.insert([followingRow, 0], lines) + this.buffer.delete(linesRange) + + // Restore folds that existed before the lines were moved + for (let rangeToRefold of rangesToRefold) { + this.displayLayer.foldBufferRange(rangeToRefold) + } + + for (const selectionToMove of selectionsToMove) { + newSelectionRanges.push(selectionToMove.translate([insertDelta, 0])) + } + } + + this.setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) + if (this.shouldAutoIndent()) this.autoIndentSelectedRows() + this.scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]) + }) + } + + // Move any active selections one column to the left. + moveSelectionLeft () { + const selections = this.getSelectedBufferRanges() + const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0) + + const translationDelta = [0, -1] + const translatedRanges = [] + + if (noSelectionAtStartOfLine) { + this.transact(() => { + for (let selection of selections) { + const charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start) + const charTextToLeftOfSelection = this.buffer.getTextInRange(charToLeftOfSelection) + + this.buffer.insert(selection.end, charTextToLeftOfSelection) + this.buffer.delete(charToLeftOfSelection) + translatedRanges.push(selection.translate(translationDelta)) + } + + this.setSelectedBufferRanges(translatedRanges) + }) + } + } + + // Move any active selections one column to the right. + moveSelectionRight () { + const selections = this.getSelectedBufferRanges() + const noSelectionAtEndOfLine = selections.every(selection => { + return selection.end.column !== this.buffer.lineLengthForRow(selection.end.row) + }) + + const translationDelta = [0, 1] + const translatedRanges = [] + + if (noSelectionAtEndOfLine) { + this.transact(() => { + for (let selection of selections) { + const charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta)) + const charTextToRightOfSelection = this.buffer.getTextInRange(charToRightOfSelection) + + this.buffer.delete(charToRightOfSelection) + this.buffer.insert(selection.start, charTextToRightOfSelection) + translatedRanges.push(selection.translate(translationDelta)) + } + + this.setSelectedBufferRanges(translatedRanges) + }) + } + } + + duplicateLines () { + this.transact(() => { + const selections = this.getSelectionsOrderedByBufferPosition() + const previousSelectionRanges = [] + + let i = selections.length - 1 + while (i >= 0) { + const j = i + previousSelectionRanges[i] = selections[i].getBufferRange() + if (selections[i].isEmpty()) { + const {start} = selections[i].getScreenRange() + selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], {preserveFolds: true}) + } + let [startRow, endRow] = selections[i].getBufferRowRange() + endRow++ + while (i > 0) { + const [previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange() + if (previousSelectionEndRow === startRow) { + startRow = previousSelectionStartRow + previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange() + i-- + } else { + break + } + } + + const intersectingFolds = this.displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]]) + let textToDuplicate = this.getTextInBufferRange([[startRow, 0], [endRow, 0]]) + if (endRow > this.getLastBufferRow()) textToDuplicate = `\n${textToDuplicate}` + this.buffer.insert([endRow, 0], textToDuplicate) + + const insertedRowCount = endRow - startRow + + for (let k = i; k <= j; k++) { + selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0])) + } + + for (const fold of intersectingFolds) { + const foldRange = this.displayLayer.bufferRangeForFold(fold) + this.displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0])) + } + + i-- + } + }) + } + + replaceSelectedText (options, fn) { + this.mutateSelectedText((selection) => { + selection.getBufferRange() + if (options && options.selectWordIfEmpty && selection.isEmpty()) { + selection.selectWord() + } + const text = selection.getText() + selection.deleteSelectedText() + const range = selection.insertText(fn(text)) + selection.setBufferRange(range) + }) + } + + // Split multi-line selections into one selection per line. + // + // Operates on all selections. This method breaks apart all multi-line + // selections to create multiple single-line selections that cumulatively cover + // the same original area. + splitSelectionsIntoLines () { + this.mergeIntersectingSelections(() => { + for (const selection of this.getSelections()) { + const range = selection.getBufferRange() + if (range.isSingleLine()) continue + + const {start, end} = range + this.addSelectionForBufferRange([start, [start.row, Infinity]]) + let {row} = start + while (++row < end.row) { + this.addSelectionForBufferRange([[row, 0], [row, Infinity]]) + } + if (end.column !== 0) this.addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) + selection.destroy() + } + }) + } + + // Extended: For each selection, transpose the selected text. + // + // If the selection is empty, the characters preceding and following the cursor + // are swapped. Otherwise, the selected characters are reversed. + transpose () { + this.mutateSelectedText(selection => { + if (selection.isEmpty()) { + selection.selectRight() + const text = selection.getText() + selection.delete() + selection.cursor.moveLeft() + selection.insertText(text) + } else { + selection.insertText(selection.getText().split('').reverse().join('')) + } + }) + } + + // Extended: Convert the selected text to upper case. + // + // For each selection, if the selection is empty, converts the containing word + // to upper case. Otherwise convert the selected text to upper case. + upperCase () { + this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase()) + } + + // Extended: Convert the selected text to lower case. + // + // For each selection, if the selection is empty, converts the containing word + // to upper case. Otherwise convert the selected text to upper case. + lowerCase () { + this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase()) + } + + // Extended: Toggle line comments for rows intersecting selections. + // + // If the current grammar doesn't support comments, does nothing. + toggleLineCommentsInSelection () { + this.mutateSelectedText(selection => selection.toggleLineComments()) + } + + // Convert multiple lines to a single line. + // + // Operates on all selections. If the selection is empty, joins the current + // line with the next line. Otherwise it joins all lines that intersect the + // selection. + // + // Joining a line means that multiple lines are converted to a single line with + // the contents of each of the original non-empty lines separated by a space. + joinLines () { + this.mutateSelectedText(selection => selection.joinLines()) + } + + // Extended: For each cursor, insert a newline at beginning the following line. + insertNewlineBelow () { + this.transact(() => { + this.moveToEndOfLine() + this.insertNewline() + }) + } + + // Extended: For each cursor, insert a newline at the end of the preceding line. + insertNewlineAbove () { + this.transact(() => { + const bufferRow = this.getCursorBufferPosition().row + const indentLevel = this.indentationForBufferRow(bufferRow) + const onFirstLine = bufferRow === 0 + + this.moveToBeginningOfLine() + this.moveLeft() + this.insertNewline() + + if (this.shouldAutoIndent() && (this.indentationForBufferRow(bufferRow) < indentLevel)) { + this.setIndentationForBufferRow(bufferRow, indentLevel) + } + + if (onFirstLine) { + this.moveUp() + this.moveToEndOfLine() + } + }) + } + + // Extended: For each selection, if the selection is empty, delete all characters + // of the containing word that precede the cursor. Otherwise delete the + // selected text. + deleteToBeginningOfWord () { + this.mutateSelectedText(selection => selection.deleteToBeginningOfWord()) + } + + // Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the + // previous word boundary. + deleteToPreviousWordBoundary () { + this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary()) + } + + // Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the + // next word boundary. + deleteToNextWordBoundary () { + this.mutateSelectedText(selection => selection.deleteToNextWordBoundary()) + } + + // Extended: For each selection, if the selection is empty, delete all characters + // of the containing subword following the cursor. Otherwise delete the selected + // text. + deleteToBeginningOfSubword () { + this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword()) + } + + // Extended: For each selection, if the selection is empty, delete all characters + // of the containing subword following the cursor. Otherwise delete the selected + // text. + deleteToEndOfSubword () { + this.mutateSelectedText(selection => selection.deleteToEndOfSubword()) + } + + // Extended: For each selection, if the selection is empty, delete all characters + // of the containing line that precede the cursor. Otherwise delete the + // selected text. + deleteToBeginningOfLine () { + this.mutateSelectedText(selection => selection.deleteToBeginningOfLine()) + } + + // Extended: For each selection, if the selection is not empty, deletes the + // selection; otherwise, deletes all characters of the containing line + // following the cursor. If the cursor is already at the end of the line, + // deletes the following newline. + deleteToEndOfLine () { + this.mutateSelectedText(selection => selection.deleteToEndOfLine()) + } + + // Extended: For each selection, if the selection is empty, delete all characters + // of the containing word following the cursor. Otherwise delete the selected + // text. + deleteToEndOfWord () { + this.mutateSelectedText(selection => selection.deleteToEndOfWord()) + } + + // Extended: Delete all lines intersecting selections. + deleteLine () { + this.mergeSelectionsOnSameRows() + this.mutateSelectedText(selection => selection.deleteLine()) + } + + /* + Section: History + */ + + // Essential: Undo the last change. + undo () { + this.avoidMergingSelections(() => this.buffer.undo()) + this.getLastSelection().autoscroll() + } + + // Essential: Redo the last change. + redo () { + this.avoidMergingSelections(() => this.buffer.redo()) + this.getLastSelection().autoscroll() + } + + // Extended: Batch multiple operations as a single undo/redo step. + // + // Any group of operations that are logically grouped from the perspective of + // undoing and redoing should be performed in a transaction. If you want to + // abort the transaction, call {::abortTransaction} to terminate the function's + // execution and revert any changes performed up to the abortion. + // + // * `groupingInterval` (optional) The {Number} of milliseconds for which this + // transaction should be considered 'groupable' after it begins. If a transaction + // with a positive `groupingInterval` is committed while the previous transaction is + // still 'groupable', the two transactions are merged with respect to undo and redo. + // * `fn` A {Function} to call inside the transaction. + transact (groupingInterval, fn) { + return this.buffer.transact(groupingInterval, fn) + } + + // Extended: Abort an open transaction, undoing any operations performed so far + // within the transaction. + abortTransaction () { return this.buffer.abortTransaction() } + + // Extended: Create a pointer to the current state of the buffer for use + // with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}. + // + // Returns a checkpoint value. + createCheckpoint () { return this.buffer.createCheckpoint() } + + // Extended: Revert the buffer to the state it was in when the given + // checkpoint was created. + // + // The redo stack will be empty following this operation, so changes since the + // checkpoint will be lost. If the given checkpoint is no longer present in the + // undo history, no changes will be made to the buffer and this method will + // return `false`. + // + // * `checkpoint` The checkpoint to revert to. + // + // Returns a {Boolean} indicating whether the operation succeeded. + revertToCheckpoint (checkpoint) { return this.buffer.revertToCheckpoint(checkpoint) } + + // Extended: Group all changes since the given checkpoint into a single + // transaction for purposes of undo/redo. + // + // If the given checkpoint is no longer present in the undo history, no + // grouping will be performed and this method will return `false`. + // + // * `checkpoint` The checkpoint from which to group changes. + // + // Returns a {Boolean} indicating whether the operation succeeded. + groupChangesSinceCheckpoint (checkpoint) { return this.buffer.groupChangesSinceCheckpoint(checkpoint) } + + /* + Section: TextEditor Coordinates + */ + + // Essential: Convert a position in buffer-coordinates to screen-coordinates. + // + // The position is clipped via {::clipBufferPosition} prior to the conversion. + // The position is also clipped via {::clipScreenPosition} following the + // conversion, which only makes a difference when `options` are supplied. + // + // * `bufferPosition` A {Point} or {Array} of [row, column]. + // * `options` (optional) An options hash for {::clipScreenPosition}. + // + // Returns a {Point}. + screenPositionForBufferPosition (bufferPosition, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.translateBufferPosition(bufferPosition, options) + } + + // Essential: Convert a position in screen-coordinates to buffer-coordinates. + // + // The position is clipped via {::clipScreenPosition} prior to the conversion. + // + // * `bufferPosition` A {Point} or {Array} of [row, column]. + // * `options` (optional) An options hash for {::clipScreenPosition}. + // + // Returns a {Point}. + bufferPositionForScreenPosition (screenPosition, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.translateScreenPosition(screenPosition, options) + } + + // Essential: Convert a range in buffer-coordinates to screen-coordinates. + // + // * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. + // + // Returns a {Range}. + screenRangeForBufferRange (bufferRange, options) { + bufferRange = Range.fromObject(bufferRange) + const start = this.screenPositionForBufferPosition(bufferRange.start, options) + const end = this.screenPositionForBufferPosition(bufferRange.end, options) + return new Range(start, end) + } + + // Essential: Convert a range in screen-coordinates to buffer-coordinates. + // + // * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. + // + // Returns a {Range}. + bufferRangeForScreenRange (screenRange) { + screenRange = Range.fromObject(screenRange) + const start = this.bufferPositionForScreenPosition(screenRange.start) + const end = this.bufferPositionForScreenPosition(screenRange.end) + return new Range(start, end) + } + + // Extended: Clip the given {Point} to a valid position in the buffer. + // + // If the given {Point} describes a position that is actually reachable by the + // cursor based on the current contents of the buffer, it is returned + // unchanged. If the {Point} does not describe a valid position, the closest + // valid position is returned instead. + // + // ## Examples + // + // ```coffee + // editor.clipBufferPosition([-1, -1]) # -> `[0, 0]` + // + // # When the line at buffer row 2 is 10 characters long + // editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]` + // ``` + // + // * `bufferPosition` The {Point} representing the position to clip. + // + // Returns a {Point}. + clipBufferPosition (bufferPosition) { return this.buffer.clipPosition(bufferPosition) } + + // Extended: Clip the start and end of the given range to valid positions in the + // buffer. See {::clipBufferPosition} for more information. + // + // * `range` The {Range} to clip. + // + // Returns a {Range}. + clipBufferRange (range) { return this.buffer.clipRange(range) } + + // Extended: Clip the given {Point} to a valid position on screen. + // + // If the given {Point} describes a position that is actually reachable by the + // cursor based on the current contents of the screen, it is returned + // unchanged. If the {Point} does not describe a valid position, the closest + // valid position is returned instead. + // + // ## Examples + // + // ```coffee + // editor.clipScreenPosition([-1, -1]) # -> `[0, 0]` + // + // # When the line at screen row 2 is 10 characters long + // editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]` + // ``` + // + // * `screenPosition` The {Point} representing the position to clip. + // * `options` (optional) {Object} + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. + // + // Returns a {Point}. + clipScreenPosition (screenPosition, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.clipScreenPosition(screenPosition, options) + } + + // Extended: Clip the start and end of the given range to valid positions on screen. + // See {::clipScreenPosition} for more information. + // + // * `range` The {Range} to clip. + // * `options` (optional) See {::clipScreenPosition} `options`. + // + // Returns a {Range}. + clipScreenRange (screenRange, options) { + screenRange = Range.fromObject(screenRange) + const start = this.displayLayer.clipScreenPosition(screenRange.start, options) + const end = this.displayLayer.clipScreenPosition(screenRange.end, options) + return Range(start, end) + } + + /* + Section: Decorations + */ + + // Essential: Add a decoration that tracks a {DisplayMarker}. When the + // marker moves, is invalidated, or is destroyed, the decoration will be + // updated to reflect the marker's state. + // + // The following are the supported decorations types: + // + // * __line__: Adds your CSS `class` to the line nodes within the range + // marked by the marker + // * __line-number__: Adds your CSS `class` to the line number nodes within the + // range marked by the marker + // * __highlight__: Adds a new highlight div to the editor surrounding the + // range marked by the marker. When the user selects text, the selection is + // visualized with a highlight decoration internally. The structure of this + // highlight will be + // ```html + //
+ // + //
+ //
+ // ``` + // * __overlay__: Positions the view associated with the given item at the head + // or tail of the given `DisplayMarker`. + // * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter + // decorations are created by calling {Gutter::decorateMarker} on the + // desired `Gutter` instance. + // * __block__: Positions the view associated with the given item before or + // after the row of the given `TextEditorMarker`. + // + // ## Arguments + // + // * `marker` A {DisplayMarker} you want this decoration to follow. + // * `decorationParams` An {Object} representing the decoration e.g. + // `{type: 'line-number', class: 'linter-error'}` + // * `type` There are several supported decoration types. The behavior of the + // types are as follows: + // * `line` Adds the given `class` to the lines overlapping the rows + // spanned by the `DisplayMarker`. + // * `line-number` Adds the given `class` to the line numbers overlapping + // the rows spanned by the `DisplayMarker`. + // * `text` Injects spans into all text overlapping the marked range, + // then adds the given `class` or `style` properties to these spans. + // Use this to manipulate the foreground color or styling of text in + // a given range. + // * `highlight` Creates an absolutely-positioned `.highlight` div + // containing nested divs to cover the marked region. For example, this + // is used to implement selections. + // * `overlay` Positions the view associated with the given item at the + // head or tail of the given `DisplayMarker`, depending on the `position` + // property. + // * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling + // {Gutter::decorateMarker} on the desired `Gutter` instance. + // * `block` Positions the view associated with the given item before or + // after the row of the given `TextEditorMarker`, depending on the `position` + // property. + // * `cursor` Renders a cursor at the head of the given marker. If multiple + // decorations are created for the same marker, their class strings and + // style objects are combined into a single cursor. You can use this + // decoration type to style existing cursors by passing in their markers + // or render artificial cursors that don't actually exist in the model + // by passing a marker that isn't actually associated with a cursor. + // * `class` This CSS class will be applied to the decorated line number, + // line, text spans, highlight regions, cursors, or overlay. + // * `style` An {Object} containing CSS style properties to apply to the + // relevant DOM node. Currently this only works with a `type` of `cursor` + // or `text`. + // * `item` (optional) An {HTMLElement} or a model {Object} with a + // corresponding view registered. Only applicable to the `gutter`, + // `overlay` and `block` decoration types. + // * `onlyHead` (optional) If `true`, the decoration will only be applied to + // the head of the `DisplayMarker`. Only applicable to the `line` and + // `line-number` decoration types. + // * `onlyEmpty` (optional) If `true`, the decoration will only be applied if + // the associated `DisplayMarker` is empty. Only applicable to the `gutter`, + // `line`, and `line-number` decoration types. + // * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied + // if the associated `DisplayMarker` is non-empty. Only applicable to the + // `gutter`, `line`, and `line-number` decoration types. + // * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied + // to the last row of a non-empty range, even if it ends at column 0. + // Defaults to `true`. Only applicable to the `gutter`, `line`, and + // `line-number` decoration types. + // * `position` (optional) Only applicable to decorations of type `overlay` and `block`. + // Controls where the view is positioned relative to the `TextEditorMarker`. + // Values can be `'head'` (the default) or `'tail'` for overlay decorations, and + // `'before'` (the default) or `'after'` for block decorations. + // * `avoidOverflow` (optional) Only applicable to decorations of type + // `overlay`. Determines whether the decoration adjusts its horizontal or + // vertical position to remain fully visible when it would otherwise + // overflow the editor. Defaults to `true`. + // + // Returns a {Decoration} object + decorateMarker (marker, decorationParams) { + return this.decorationManager.decorateMarker(marker, decorationParams) + } + + // Essential: Add a decoration to every marker in the given marker layer. Can + // be used to decorate a large number of markers without having to create and + // manage many individual decorations. + // + // * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. + // * `decorationParams` The same parameters that are passed to + // {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`. + // + // Returns a {LayerDecoration}. + decorateMarkerLayer (markerLayer, decorationParams) { + return this.decorationManager.decorateMarkerLayer(markerLayer, decorationParams) + } + + // Deprecated: Get all the decorations within a screen row range on the default + // layer. + // + // * `startScreenRow` the {Number} beginning screen row + // * `endScreenRow` the {Number} end screen row (inclusive) + // + // Returns an {Object} of decorations in the form + // `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}` + // where the keys are {DisplayMarker} IDs, and the values are an array of decoration + // params objects attached to the marker. + // Returns an empty object when no decorations are found + decorationsForScreenRowRange (startScreenRow, endScreenRow) { + return this.decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow) + } + + decorationsStateForScreenRowRange (startScreenRow, endScreenRow) { + return this.decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) + } + + // Extended: Get all decorations. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getDecorations (propertyFilter) { + return this.decorationManager.getDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'line'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getLineDecorations (propertyFilter) { + return this.decorationManager.getLineDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'line-number'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getLineNumberDecorations (propertyFilter) { + return this.decorationManager.getLineNumberDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'highlight'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getHighlightDecorations (propertyFilter) { + return this.decorationManager.getHighlightDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'overlay'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getOverlayDecorations (propertyFilter) { + return this.decorationManager.getOverlayDecorations(propertyFilter) + } + + /* + Section: Markers + */ + + // Essential: Create a marker on the default marker layer with the given range + // in buffer coordinates. This marker will maintain its logical location as the + // buffer is changed, so if you mark a particular word, the marker will remain + // over that word even if the word's location in the buffer changes. + // + // * `range` A {Range} or range-compatible {Array} + // * `properties` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `maintainHistory` (optional) {Boolean} Whether to store this marker's + // range before and after each change in the undo history. This allows the + // marker's position to be restored more accurately for certain undo/redo + // operations, but uses more time and memory. (default: false) + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markBufferRange (bufferRange, options) { + return this.defaultMarkerLayer.markBufferRange(bufferRange, options) + } + + // Essential: Create a marker on the default marker layer with the given range + // in screen coordinates. This marker will maintain its logical location as the + // buffer is changed, so if you mark a particular word, the marker will remain + // over that word even if the word's location in the buffer changes. + // + // * `range` A {Range} or range-compatible {Array} + // * `properties` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `maintainHistory` (optional) {Boolean} Whether to store this marker's + // range before and after each change in the undo history. This allows the + // marker's position to be restored more accurately for certain undo/redo + // operations, but uses more time and memory. (default: false) + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markScreenRange (screenRange, options) { + return this.defaultMarkerLayer.markScreenRange(screenRange, options) + } + + // Essential: Create a marker on the default marker layer with the given buffer + // position and no tail. To group multiple markers together in their own + // private layer, see {::addMarkerLayer}. + // + // * `bufferPosition` A {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markBufferPosition (bufferPosition, options) { + return this.defaultMarkerLayer.markBufferPosition(bufferPosition, options) + } + + // Essential: Create a marker on the default marker layer with the given screen + // position and no tail. To group multiple markers together in their own + // private layer, see {::addMarkerLayer}. + // + // * `screenPosition` A {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. + // + // Returns a {DisplayMarker}. + markScreenPosition (screenPosition, options) { + return this.defaultMarkerLayer.markScreenPosition(screenPosition, options) + } + + // Essential: Find all {DisplayMarker}s on the default marker layer that + // match the given properties. + // + // This method finds markers based on the given properties. Markers can be + // associated with custom properties that will be compared with basic equality. + // In addition, there are several special properties that will be compared + // with the range of the markers rather than their properties. + // + // * `properties` An {Object} containing properties that each returned marker + // must satisfy. Markers can be associated with custom properties, which are + // compared with basic equality. In addition, several reserved properties + // can be used to filter markers based on their current range: + // * `startBufferRow` Only include markers starting at this row in buffer + // coordinates. + // * `endBufferRow` Only include markers ending at this row in buffer + // coordinates. + // * `containsBufferRange` Only include markers containing this {Range} or + // in range-compatible {Array} in buffer coordinates. + // * `containsBufferPosition` Only include markers containing this {Point} + // or {Array} of `[row, column]` in buffer coordinates. + // + // Returns an {Array} of {DisplayMarker}s + findMarkers (params) { + return this.defaultMarkerLayer.findMarkers(params) + } + + // Extended: Get the {DisplayMarker} on the default layer for the given + // marker id. + // + // * `id` {Number} id of the marker + getMarker (id) { + return this.defaultMarkerLayer.getMarker(id) + } + + // Extended: Get all {DisplayMarker}s on the default marker layer. Consider + // using {::findMarkers} + getMarkers () { + return this.defaultMarkerLayer.getMarkers() + } + + // Extended: Get the number of markers in the default marker layer. + // + // Returns a {Number}. + getMarkerCount () { + return this.defaultMarkerLayer.getMarkerCount() + } + + destroyMarker (id) { + const marker = this.getMarker(id) + if (marker) marker.destroy() + } + + // Essential: Create a marker layer to group related markers. + // + // * `options` An {Object} containing the following keys: + // * `maintainHistory` A {Boolean} indicating whether marker state should be + // restored on undo/redo. Defaults to `false`. + // * `persistent` A {Boolean} indicating whether or not this marker layer + // should be serialized and deserialized along with the rest of the + // buffer. Defaults to `false`. If `true`, the marker layer's id will be + // maintained across the serialization boundary, allowing you to retrieve + // it via {::getMarkerLayer}. + // + // Returns a {DisplayMarkerLayer}. + addMarkerLayer (options) { + return this.displayLayer.addMarkerLayer(options) + } + + // Essential: Get a {DisplayMarkerLayer} by id. + // + // * `id` The id of the marker layer to retrieve. + // + // Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the + // given id. + getMarkerLayer (id) { + return this.displayLayer.getMarkerLayer(id) + } + + // Essential: Get the default {DisplayMarkerLayer}. + // + // All marker APIs not tied to an explicit layer interact with this default + // layer. + // + // Returns a {DisplayMarkerLayer}. + getDefaultMarkerLayer () { + return this.defaultMarkerLayer + } + + /* + Section: Cursors + */ + + // Essential: Get the position of the most recently added cursor in buffer + // coordinates. + // + // Returns a {Point} + getCursorBufferPosition () { + return this.getLastCursor().getBufferPosition() + } + + // Essential: Get the position of all the cursor positions in buffer coordinates. + // + // Returns {Array} of {Point}s in the order they were added + getCursorBufferPositions () { + return this.getCursors().map((cursor) => cursor.getBufferPosition()) + } + + // Essential: Move the cursor to the given position in buffer coordinates. + // + // If there are multiple cursors, they will be consolidated to a single cursor. + // + // * `position` A {Point} or {Array} of `[row, column]` + // * `options` (optional) An {Object} containing the following keys: + // * `autoscroll` Determines whether the editor scrolls to the new cursor's + // position. Defaults to true. + setCursorBufferPosition (position, options) { + return this.moveCursors(cursor => cursor.setBufferPosition(position, options)) + } + + // Essential: Get a {Cursor} at given screen coordinates {Point} + // + // * `position` A {Point} or {Array} of `[row, column]` + // + // Returns the first matched {Cursor} or undefined + getCursorAtScreenPosition (position) { + const selection = this.getSelectionAtScreenPosition(position) + if (selection && selection.getHeadScreenPosition().isEqual(position)) { + return selection.cursor + } + } + + // Essential: Get the position of the most recently added cursor in screen + // coordinates. + // + // Returns a {Point}. + getCursorScreenPosition () { + return this.getLastCursor().getScreenPosition() + } + + // Essential: Get the position of all the cursor positions in screen coordinates. + // + // Returns {Array} of {Point}s in the order the cursors were added + getCursorScreenPositions () { + return this.getCursors().map((cursor) => cursor.getScreenPosition()) + } + + // Essential: Move the cursor to the given position in screen coordinates. + // + // If there are multiple cursors, they will be consolidated to a single cursor. + // + // * `position` A {Point} or {Array} of `[row, column]` + // * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: + // * `autoscroll` Determines whether the editor scrolls to the new cursor's + // position. Defaults to true. + setCursorScreenPosition (position, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.moveCursors(cursor => cursor.setScreenPosition(position, options)) + } + + // Essential: Add a cursor at the given position in buffer coordinates. + // + // * `bufferPosition` A {Point} or {Array} of `[row, column]` + // + // Returns a {Cursor}. + addCursorAtBufferPosition (bufferPosition, options) { + this.selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) + if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll() + return this.getLastSelection().cursor + } + + // Essential: Add a cursor at the position in screen coordinates. + // + // * `screenPosition` A {Point} or {Array} of `[row, column]` + // + // Returns a {Cursor}. + addCursorAtScreenPosition (screenPosition, options) { + this.selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'}) + if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll() + return this.getLastSelection().cursor + } + + // Essential: Returns {Boolean} indicating whether or not there are multiple cursors. + hasMultipleCursors () { + return this.getCursors().length > 1 + } + + // Essential: Move every cursor up one row in screen coordinates. + // + // * `lineCount` (optional) {Number} number of lines to move + moveUp (lineCount) { + return this.moveCursors(cursor => cursor.moveUp(lineCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor down one row in screen coordinates. + // + // * `lineCount` (optional) {Number} number of lines to move + moveDown (lineCount) { + return this.moveCursors(cursor => cursor.moveDown(lineCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor left one column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + moveLeft (columnCount) { + return this.moveCursors(cursor => cursor.moveLeft(columnCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor right one column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + moveRight (columnCount) { + return this.moveCursors(cursor => cursor.moveRight(columnCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor to the beginning of its line in buffer coordinates. + moveToBeginningOfLine () { + return this.moveCursors(cursor => cursor.moveToBeginningOfLine()) + } + + // Essential: Move every cursor to the beginning of its line in screen coordinates. + moveToBeginningOfScreenLine () { + return this.moveCursors(cursor => cursor.moveToBeginningOfScreenLine()) + } + + // Essential: Move every cursor to the first non-whitespace character of its line. + moveToFirstCharacterOfLine () { + return this.moveCursors(cursor => cursor.moveToFirstCharacterOfLine()) + } + + // Essential: Move every cursor to the end of its line in buffer coordinates. + moveToEndOfLine () { + return this.moveCursors(cursor => cursor.moveToEndOfLine()) + } + + // Essential: Move every cursor to the end of its line in screen coordinates. + moveToEndOfScreenLine () { + return this.moveCursors(cursor => cursor.moveToEndOfScreenLine()) + } + + // Essential: Move every cursor to the beginning of its surrounding word. + moveToBeginningOfWord () { + return this.moveCursors(cursor => cursor.moveToBeginningOfWord()) + } + + // Essential: Move every cursor to the end of its surrounding word. + moveToEndOfWord () { + return this.moveCursors(cursor => cursor.moveToEndOfWord()) + } + + // Cursor Extended + + // Extended: Move every cursor to the top of the buffer. + // + // If there are multiple cursors, they will be merged into a single cursor. + moveToTop () { + return this.moveCursors(cursor => cursor.moveToTop()) + } + + // Extended: Move every cursor to the bottom of the buffer. + // + // If there are multiple cursors, they will be merged into a single cursor. + moveToBottom () { + return this.moveCursors(cursor => cursor.moveToBottom()) + } + + // Extended: Move every cursor to the beginning of the next word. + moveToBeginningOfNextWord () { + return this.moveCursors(cursor => cursor.moveToBeginningOfNextWord()) + } + + // Extended: Move every cursor to the previous word boundary. + moveToPreviousWordBoundary () { + return this.moveCursors(cursor => cursor.moveToPreviousWordBoundary()) + } + + // Extended: Move every cursor to the next word boundary. + moveToNextWordBoundary () { + return this.moveCursors(cursor => cursor.moveToNextWordBoundary()) + } + + // Extended: Move every cursor to the previous subword boundary. + moveToPreviousSubwordBoundary () { + return this.moveCursors(cursor => cursor.moveToPreviousSubwordBoundary()) + } + + // Extended: Move every cursor to the next subword boundary. + moveToNextSubwordBoundary () { + return this.moveCursors(cursor => cursor.moveToNextSubwordBoundary()) + } + + // Extended: Move every cursor to the beginning of the next paragraph. + moveToBeginningOfNextParagraph () { + return this.moveCursors(cursor => cursor.moveToBeginningOfNextParagraph()) + } + + // Extended: Move every cursor to the beginning of the previous paragraph. + moveToBeginningOfPreviousParagraph () { + return this.moveCursors(cursor => cursor.moveToBeginningOfPreviousParagraph()) + } + + // Extended: Returns the most recently added {Cursor} + getLastCursor () { + this.createLastSelectionIfNeeded() + return _.last(this.cursors) + } + + // Extended: Returns the word surrounding the most recently added cursor. + // + // * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. + getWordUnderCursor (options) { + return this.getTextInBufferRange(this.getLastCursor().getCurrentWordBufferRange(options)) + } + + // Extended: Get an Array of all {Cursor}s. + getCursors () { + this.createLastSelectionIfNeeded() + return this.cursors.slice() + } + + // Extended: Get all {Cursors}s, ordered by their position in the buffer + // instead of the order in which they were added. + // + // Returns an {Array} of {Selection}s. + getCursorsOrderedByBufferPosition () { + return this.getCursors().sort((a, b) => a.compare(b)) + } + + cursorsForScreenRowRange (startScreenRow, endScreenRow) { + const cursors = [] + for (let marker of this.selectionsMarkerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) { + const cursor = this.cursorsByMarkerId.get(marker.id) + if (cursor) cursors.push(cursor) + } + return cursors + } + + // Add a cursor based on the given {DisplayMarker}. + addCursor (marker) { + const cursor = new Cursor({editor: this, marker, showCursorOnSelection: this.showCursorOnSelection}) + this.cursors.push(cursor) + this.cursorsByMarkerId.set(marker.id, cursor) + return cursor + } + + moveCursors (fn) { + return this.transact(() => { + this.getCursors().forEach(fn) + return this.mergeCursors() + }) + } + + cursorMoved (event) { + return this.emitter.emit('did-change-cursor-position', event) + } + + // Merge cursors that have the same screen position + mergeCursors () { + const positions = {} + for (let cursor of this.getCursors()) { + const position = cursor.getBufferPosition().toString() + if (positions.hasOwnProperty(position)) { + cursor.destroy() + } else { + positions[position] = true + } + } + } + + /* + Section: Selections + */ + + // Essential: Get the selected text of the most recently added selection. + // + // Returns a {String}. + getSelectedText () { + return this.getLastSelection().getText() + } + + // Essential: Get the {Range} of the most recently added selection in buffer + // coordinates. + // + // Returns a {Range}. + getSelectedBufferRange () { + return this.getLastSelection().getBufferRange() + } + + // Essential: Get the {Range}s of all selections in buffer coordinates. + // + // The ranges are sorted by when the selections were added. Most recent at the end. + // + // Returns an {Array} of {Range}s. + getSelectedBufferRanges () { + return this.getSelections().map((selection) => selection.getBufferRange()) + } + + // Essential: Set the selected range in buffer coordinates. If there are multiple + // selections, they are reduced to a single selection with the given range. + // + // * `bufferRange` A {Range} or range-compatible {Array}. + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + setSelectedBufferRange (bufferRange, options) { + return this.setSelectedBufferRanges([bufferRange], options) + } + + // Essential: Set the selected ranges in buffer coordinates. If there are multiple + // selections, they are replaced by new selections with the given ranges. + // + // * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s. + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + setSelectedBufferRanges (bufferRanges, options = {}) { + if (!bufferRanges.length) throw new Error('Passed an empty array to setSelectedBufferRanges') + + const selections = this.getSelections() + for (let selection of selections.slice(bufferRanges.length)) { + selection.destroy() + } + + this.mergeIntersectingSelections(options, () => { + for (let i = 0; i < bufferRanges.length; i++) { + let bufferRange = bufferRanges[i] + bufferRange = Range.fromObject(bufferRange) + if (selections[i]) { + selections[i].setBufferRange(bufferRange, options) + } else { + this.addSelectionForBufferRange(bufferRange, options) + } + } + }) + } + + // Essential: Get the {Range} of the most recently added selection in screen + // coordinates. + // + // Returns a {Range}. + getSelectedScreenRange () { + return this.getLastSelection().getScreenRange() + } + + // Essential: Get the {Range}s of all selections in screen coordinates. + // + // The ranges are sorted by when the selections were added. Most recent at the end. + // + // Returns an {Array} of {Range}s. + getSelectedScreenRanges () { + return this.getSelections().map((selection) => selection.getScreenRange()) + } + + // Essential: Set the selected range in screen coordinates. If there are multiple + // selections, they are reduced to a single selection with the given range. + // + // * `screenRange` A {Range} or range-compatible {Array}. + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + setSelectedScreenRange (screenRange, options) { + return this.setSelectedBufferRange(this.bufferRangeForScreenRange(screenRange, options), options) + } + + // Essential: Set the selected ranges in screen coordinates. If there are multiple + // selections, they are replaced by new selections with the given ranges. + // + // * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s. + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + setSelectedScreenRanges (screenRanges, options = {}) { + if (!screenRanges.length) throw new Error('Passed an empty array to setSelectedScreenRanges') + + const selections = this.getSelections() + for (let selection of selections.slice(screenRanges.length)) { + selection.destroy() + } + + this.mergeIntersectingSelections(options, () => { + for (let i = 0; i < screenRanges.length; i++) { + let screenRange = screenRanges[i] + screenRange = Range.fromObject(screenRange) + if (selections[i]) { + selections[i].setScreenRange(screenRange, options) + } else { + this.addSelectionForScreenRange(screenRange, options) + } + } + }) + } + + // Essential: Add a selection for the given range in buffer coordinates. + // + // * `bufferRange` A {Range} + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + // + // Returns the added {Selection}. + addSelectionForBufferRange (bufferRange, options = {}) { + bufferRange = Range.fromObject(bufferRange) + if (!options.preserveFolds) { + this.displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) + } + this.selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed != null ? options.reversed : false}) + if (options.autoscroll !== false) this.getLastSelection().autoscroll() + return this.getLastSelection() + } + + // Essential: Add a selection for the given range in screen coordinates. + // + // * `screenRange` A {Range} + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + // Returns the added {Selection}. + addSelectionForScreenRange (screenRange, options = {}) { + return this.addSelectionForBufferRange(this.bufferRangeForScreenRange(screenRange), options) + } + + // Essential: Select from the current cursor position to the given position in + // buffer coordinates. + // + // This method may merge selections that end up intersecting. + // + // * `position` An instance of {Point}, with a given `row` and `column`. + selectToBufferPosition (position) { + const lastSelection = this.getLastSelection() + lastSelection.selectToBufferPosition(position) + return this.mergeIntersectingSelections({reversed: lastSelection.isReversed()}) + } + + // Essential: Select from the current cursor position to the given position in + // screen coordinates. + // + // This method may merge selections that end up intersecting. + // + // * `position` An instance of {Point}, with a given `row` and `column`. + selectToScreenPosition (position, options) { + const lastSelection = this.getLastSelection() + lastSelection.selectToScreenPosition(position, options) + if (!options || !options.suppressSelectionMerge) { + return this.mergeIntersectingSelections({reversed: lastSelection.isReversed()}) + } + } + + // Essential: Move the cursor of each selection one character upward while + // preserving the selection's tail position. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectUp (rowCount) { + return this.expandSelectionsBackward(selection => selection.selectUp(rowCount)) + } + + // Essential: Move the cursor of each selection one character downward while + // preserving the selection's tail position. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectDown (rowCount) { + return this.expandSelectionsForward(selection => selection.selectDown(rowCount)) + } + + // Essential: Move the cursor of each selection one character leftward while + // preserving the selection's tail position. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectLeft (columnCount) { + return this.expandSelectionsBackward(selection => selection.selectLeft(columnCount)) + } + + // Essential: Move the cursor of each selection one character rightward while + // preserving the selection's tail position. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectRight (columnCount) { + return this.expandSelectionsForward(selection => selection.selectRight(columnCount)) + } + + // Essential: Select from the top of the buffer to the end of the last selection + // in the buffer. + // + // This method merges multiple selections into a single selection. + selectToTop () { + return this.expandSelectionsBackward(selection => selection.selectToTop()) + } + + // Essential: Selects from the top of the first selection in the buffer to the end + // of the buffer. + // + // This method merges multiple selections into a single selection. + selectToBottom () { + return this.expandSelectionsForward(selection => selection.selectToBottom()) + } + + // Essential: Select all text in the buffer. + // + // This method merges multiple selections into a single selection. + selectAll () { + return this.expandSelectionsForward(selection => selection.selectAll()) + } + + // Essential: Move the cursor of each selection to the beginning of its line + // while preserving the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToBeginningOfLine () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfLine()) + } + + // Essential: Move the cursor of each selection to the first non-whitespace + // character of its line while preserving the selection's tail position. If the + // cursor is already on the first character of the line, move it to the + // beginning of the line. + // + // This method may merge selections that end up intersecting. + selectToFirstCharacterOfLine () { + return this.expandSelectionsBackward(selection => selection.selectToFirstCharacterOfLine()) + } + + // Essential: Move the cursor of each selection to the end of its line while + // preserving the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToEndOfLine () { + return this.expandSelectionsForward(selection => selection.selectToEndOfLine()) + } + + // Essential: Expand selections to the beginning of their containing word. + // + // Operates on all selections. Moves the cursor to the beginning of the + // containing word while preserving the selection's tail position. + selectToBeginningOfWord () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfWord()) + } + + // Essential: Expand selections to the end of their containing word. + // + // Operates on all selections. Moves the cursor to the end of the containing + // word while preserving the selection's tail position. + selectToEndOfWord () { + return this.expandSelectionsForward(selection => selection.selectToEndOfWord()) + } + + // Extended: For each selection, move its cursor to the preceding subword + // boundary while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToPreviousSubwordBoundary () { + return this.expandSelectionsBackward(selection => selection.selectToPreviousSubwordBoundary()) + } + + // Extended: For each selection, move its cursor to the next subword boundary + // while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToNextSubwordBoundary () { + return this.expandSelectionsForward(selection => selection.selectToNextSubwordBoundary()) + } + + // Essential: For each cursor, select the containing line. + // + // This method merges selections on successive lines. + selectLinesContainingCursors () { + return this.expandSelectionsForward(selection => selection.selectLine()) + } + + // Essential: Select the word surrounding each cursor. + selectWordsContainingCursors () { + return this.expandSelectionsForward(selection => selection.selectWord()) + } + + // Selection Extended + + // Extended: For each selection, move its cursor to the preceding word boundary + // while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToPreviousWordBoundary () { + return this.expandSelectionsBackward(selection => selection.selectToPreviousWordBoundary()) + } + + // Extended: For each selection, move its cursor to the next word boundary while + // maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToNextWordBoundary () { + return this.expandSelectionsForward(selection => selection.selectToNextWordBoundary()) + } + + // Extended: Expand selections to the beginning of the next word. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // word while preserving the selection's tail position. + selectToBeginningOfNextWord () { + return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextWord()) + } + + // Extended: Expand selections to the beginning of the next paragraph. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // paragraph while preserving the selection's tail position. + selectToBeginningOfNextParagraph () { + return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextParagraph()) + } + + // Extended: Expand selections to the beginning of the next paragraph. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // paragraph while preserving the selection's tail position. + selectToBeginningOfPreviousParagraph () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph()) + } + + // Extended: Select the range of the given marker if it is valid. + // + // * `marker` A {DisplayMarker} + // + // Returns the selected {Range} or `undefined` if the marker is invalid. + selectMarker (marker) { + if (marker.isValid()) { + const range = marker.getBufferRange() + this.setSelectedBufferRange(range) + return range + } + } + + // Extended: Get the most recently added {Selection}. + // + // Returns a {Selection}. + getLastSelection () { + this.createLastSelectionIfNeeded() + return _.last(this.selections) + } + + getSelectionAtScreenPosition (position) { + const markers = this.selectionsMarkerLayer.findMarkers({containsScreenPosition: position}) + if (markers.length > 0) return this.cursorsByMarkerId.get(markers[0].id).selection + } + + // Extended: Get current {Selection}s. + // + // Returns: An {Array} of {Selection}s. + getSelections () { + this.createLastSelectionIfNeeded() + return this.selections.slice() + } + + // Extended: Get all {Selection}s, ordered by their position in the buffer + // instead of the order in which they were added. + // + // Returns an {Array} of {Selection}s. + getSelectionsOrderedByBufferPosition () { + return this.getSelections().sort((a, b) => a.compare(b)) + } + + // Extended: Determine if a given range in buffer coordinates intersects a + // selection. + // + // * `bufferRange` A {Range} or range-compatible {Array}. + // + // Returns a {Boolean}. + selectionIntersectsBufferRange (bufferRange) { + return this.getSelections().some(selection => selection.intersectsBufferRange(bufferRange)) + } + + // Selections Private + + // Add a similarly-shaped selection to the next eligible line below + // each selection. + // + // Operates on all selections. If the selection is empty, adds an empty + // selection to the next following non-empty line as close to the current + // selection's column as possible. If the selection is non-empty, adds a + // selection to the next line that is long enough for a non-empty selection + // starting at the same column as the current selection to be added to it. + addSelectionBelow () { + return this.expandSelectionsForward(selection => selection.addSelectionBelow()) + } + + // Add a similarly-shaped selection to the next eligible line above + // each selection. + // + // Operates on all selections. If the selection is empty, adds an empty + // selection to the next preceding non-empty line as close to the current + // selection's column as possible. If the selection is non-empty, adds a + // selection to the next line that is long enough for a non-empty selection + // starting at the same column as the current selection to be added to it. + addSelectionAbove () { + return this.expandSelectionsBackward(selection => selection.addSelectionAbove()) + } + + // Calls the given function with each selection, then merges selections + expandSelectionsForward (fn) { + this.mergeIntersectingSelections(() => this.getSelections().forEach(fn)) + } + + // Calls the given function with each selection, then merges selections in the + // reversed orientation + expandSelectionsBackward (fn) { + this.mergeIntersectingSelections({reversed: true}, () => this.getSelections().forEach(fn)) + } + + finalizeSelections () { + for (let selection of this.getSelections()) { selection.finalize() } + } + + selectionsForScreenRows (startRow, endRow) { + return this.getSelections().filter(selection => selection.intersectsScreenRowRange(startRow, endRow)) + } + + // Merges intersecting selections. If passed a function, it executes + // the function with merging suppressed, then merges intersecting selections + // afterward. + mergeIntersectingSelections (...args) { + return this.mergeSelections(...args, (previousSelection, currentSelection) => { + const exclusive = !currentSelection.isEmpty() && !previousSelection.isEmpty() + return previousSelection.intersectsWith(currentSelection, exclusive) + }) + } + + mergeSelectionsOnSameRows (...args) { + return this.mergeSelections(...args, (previousSelection, currentSelection) => { + const screenRange = currentSelection.getScreenRange() + return previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row) + }) + } + + avoidMergingSelections (...args) { + return this.mergeSelections(...args, () => false) + } + + mergeSelections (...args) { + const mergePredicate = args.pop() + let fn = args.pop() + let options = args.pop() + if (typeof fn !== 'function') { + options = fn + fn = () => {} + } + + if (this.suppressSelectionMerging) return fn() + + this.suppressSelectionMerging = true + const result = fn() + this.suppressSelectionMerging = false + + const selections = this.getSelectionsOrderedByBufferPosition() + let lastSelection = selections.shift() + for (const selection of selections) { + if (mergePredicate(lastSelection, selection)) { + lastSelection.merge(selection, options) + } else { + lastSelection = selection + } + } + + return result + } + + // Add a {Selection} based on the given {DisplayMarker}. + // + // * `marker` The {DisplayMarker} to highlight + // * `options` (optional) An {Object} that pertains to the {Selection} constructor. + // + // Returns the new {Selection}. + addSelection (marker, options = {}) { + const cursor = this.addCursor(marker) + let selection = new Selection(Object.assign({editor: this, marker, cursor}, options)) + this.selections.push(selection) + const selectionBufferRange = selection.getBufferRange() + this.mergeIntersectingSelections({preserveFolds: options.preserveFolds}) + + if (selection.destroyed) { + for (selection of this.getSelections()) { + if (selection.intersectsBufferRange(selectionBufferRange)) return selection + } + } else { + this.emitter.emit('did-add-cursor', cursor) + this.emitter.emit('did-add-selection', selection) + return selection + } + } + + // Remove the given selection. + removeSelection (selection) { + _.remove(this.cursors, selection.cursor) + _.remove(this.selections, selection) + this.cursorsByMarkerId.delete(selection.cursor.marker.id) + this.emitter.emit('did-remove-cursor', selection.cursor) + return this.emitter.emit('did-remove-selection', selection) + } + + // Reduce one or more selections to a single empty selection based on the most + // recently added cursor. + clearSelections (options) { + this.consolidateSelections() + this.getLastSelection().clear(options) + } + + // Reduce multiple selections to the least recently added selection. + consolidateSelections () { + const selections = this.getSelections() + if (selections.length > 1) { + for (let selection of selections.slice(1, (selections.length))) { selection.destroy() } + selections[0].autoscroll({center: true}) + return true + } else { + return false + } + } + + // Called by the selection + selectionRangeChanged (event) { + if (this.component) this.component.didChangeSelectionRange() + this.emitter.emit('did-change-selection-range', event) + } + + createLastSelectionIfNeeded () { + if (this.selections.length === 0) { + this.addSelectionForBufferRange([[0, 0], [0, 0]], {autoscroll: false, preserveFolds: true}) + } + } + + /* + Section: Searching and Replacing + */ + + // Essential: Scan regular expression matches in the entire buffer, calling the + // given iterator function on each match. + // + // `::scan` functions as the replace method as well via the `replace` + // + // If you're programmatically modifying the results, you may want to try + // {::backwardsScanInBufferRange} to avoid tripping over your own changes. + // + // * `regex` A {RegExp} to search for. + // * `options` (optional) {Object} + // * `leadingContextLineCount` {Number} default `0`; The number of lines + // before the matched line to include in the results object. + // * `trailingContextLineCount` {Number} default `0`; The number of lines + // after the matched line to include in the results object. + // * `iterator` A {Function} that's called on each match + // * `object` {Object} + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + scan (regex, options = {}, iterator) { + if (_.isFunction(options)) { + iterator = options + options = {} + } + + return this.buffer.scan(regex, options, iterator) + } + + // Essential: Scan regular expression matches in a given range, calling the given + // iterator function on each match. + // + // * `regex` A {RegExp} to search for. + // * `range` A {Range} in which to search. + // * `iterator` A {Function} that's called on each match with an {Object} + // containing the following keys: + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + scanInBufferRange (regex, range, iterator) { return this.buffer.scanInRange(regex, range, iterator) } + + // Essential: Scan regular expression matches in a given range in reverse order, + // calling the given iterator function on each match. + // + // * `regex` A {RegExp} to search for. + // * `range` A {Range} in which to search. + // * `iterator` A {Function} that's called on each match with an {Object} + // containing the following keys: + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + backwardsScanInBufferRange (regex, range, iterator) { return this.buffer.backwardsScanInRange(regex, range, iterator) } + + /* + Section: Tab Behavior + */ + + // Essential: Returns a {Boolean} indicating whether softTabs are enabled for this + // editor. + getSoftTabs () { return this.softTabs } + + // Essential: Enable or disable soft tabs for this editor. + // + // * `softTabs` A {Boolean} + setSoftTabs (softTabs) { + this.softTabs = softTabs + this.update({softTabs: this.softTabs}) + } + + // Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. + hasAtomicSoftTabs () { return this.displayLayer.atomicSoftTabs } + + // Essential: Toggle soft tabs for this editor + toggleSoftTabs () { this.setSoftTabs(!this.getSoftTabs()) } + + // Essential: Get the on-screen length of tab characters. + // + // Returns a {Number}. + getTabLength () { return this.tokenizedBuffer.getTabLength() } + + // Essential: Set the on-screen length of tab characters. Setting this to a + // {Number} This will override the `editor.tabLength` setting. + // + // * `tabLength` {Number} length of a single tab. Setting to `null` will + // fallback to using the `editor.tabLength` config setting + setTabLength (tabLength) { this.update({tabLength}) } + + // Returns an {Object} representing the current invisible character + // substitutions for this editor. See {::setInvisibles}. + getInvisibles () { + if (!this.mini && this.showInvisibles && (this.invisibles != null)) { + return this.invisibles + } else { + return {} + } + } + + doesShowIndentGuide () { return this.showIndentGuide && !this.mini } + + getSoftWrapHangingIndentLength () { return this.displayLayer.softWrapHangingIndent } + + // Extended: Determine if the buffer uses hard or soft tabs. + // + // Returns `true` if the first non-comment line with leading whitespace starts + // with a space character. Returns `false` if it starts with a hard tab (`\t`). + // + // Returns a {Boolean} or undefined if no non-comment lines had leading + // whitespace. + usesSoftTabs () { + for (let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow()); bufferRow <= end; bufferRow++) { + const tokenizedLine = this.tokenizedBuffer.tokenizedLines[bufferRow] + if (tokenizedLine && tokenizedLine.isComment()) continue + const line = this.buffer.lineForRow(bufferRow) + if (line[0] === ' ') return true + if (line[0] === '\t') return false + } + } + + // Extended: Get the text representing a single level of indent. + // + // If soft tabs are enabled, the text is composed of N spaces, where N is the + // tab length. Otherwise the text is a tab character (`\t`). + // + // Returns a {String}. + getTabText () { return this.buildIndentString(1) } + + // If soft tabs are enabled, convert all hard tabs to soft tabs in the given + // {Range}. + normalizeTabsInBufferRange (bufferRange) { + if (!this.getSoftTabs()) { return } + return this.scanInBufferRange(/\t/g, bufferRange, ({replace}) => replace(this.getTabText())) + } + + /* + Section: Soft Wrap Behavior + */ + + // Essential: Determine whether lines in this editor are soft-wrapped. + // + // Returns a {Boolean}. + isSoftWrapped () { return this.softWrapped } + + // Essential: Enable or disable soft wrapping for this editor. + // + // * `softWrapped` A {Boolean} + // + // Returns a {Boolean}. + setSoftWrapped (softWrapped) { + this.update({softWrapped}) + return this.isSoftWrapped() + } + + getPreferredLineLength () { return this.preferredLineLength } + + // Essential: Toggle soft wrapping for this editor + // + // Returns a {Boolean}. + toggleSoftWrapped () { return this.setSoftWrapped(!this.isSoftWrapped()) } + + // Essential: Gets the column at which column will soft wrap + getSoftWrapColumn () { + if (this.isSoftWrapped() && !this.mini) { + if (this.softWrapAtPreferredLineLength) { + return Math.min(this.getEditorWidthInChars(), this.preferredLineLength) + } else { + return this.getEditorWidthInChars() + } + } else { + return this.maxScreenLineLength + } + } + + /* + Section: Indentation + */ + + // Essential: Get the indentation level of the given buffer row. + // + // Determines how deeply the given row is indented based on the soft tabs and + // tab length settings of this editor. Note that if soft tabs are enabled and + // the tab length is 2, a row with 4 leading spaces would have an indentation + // level of 2. + // + // * `bufferRow` A {Number} indicating the buffer row. + // + // Returns a {Number}. + indentationForBufferRow (bufferRow) { + return this.indentLevelForLine(this.lineTextForBufferRow(bufferRow)) + } + + // Essential: Set the indentation level for the given buffer row. + // + // Inserts or removes hard tabs or spaces based on the soft tabs and tab length + // settings of this editor in order to bring it to the given indentation level. + // Note that if soft tabs are enabled and the tab length is 2, a row with 4 + // leading spaces would have an indentation level of 2. + // + // * `bufferRow` A {Number} indicating the buffer row. + // * `newLevel` A {Number} indicating the new indentation level. + // * `options` (optional) An {Object} with the following keys: + // * `preserveLeadingWhitespace` `true` to preserve any whitespace already at + // the beginning of the line (default: false). + setIndentationForBufferRow (bufferRow, newLevel, {preserveLeadingWhitespace} = {}) { + let endColumn + if (preserveLeadingWhitespace) { + endColumn = 0 + } else { + endColumn = this.lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length + } + const newIndentString = this.buildIndentString(newLevel) + return this.buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + } + + // Extended: Indent rows intersecting selections by one level. + indentSelectedRows () { + return this.mutateSelectedText(selection => selection.indentSelectedRows()) + } + + // Extended: Outdent rows intersecting selections by one level. + outdentSelectedRows () { + return this.mutateSelectedText(selection => selection.outdentSelectedRows()) + } + + // Extended: Get the indentation level of the given line of text. + // + // Determines how deeply the given line is indented based on the soft tabs and + // tab length settings of this editor. Note that if soft tabs are enabled and + // the tab length is 2, a row with 4 leading spaces would have an indentation + // level of 2. + // + // * `line` A {String} representing a line of text. + // + // Returns a {Number}. + indentLevelForLine (line) { + return this.tokenizedBuffer.indentLevelForLine(line) + } + + // Extended: Indent rows intersecting selections based on the grammar's suggested + // indent level. + autoIndentSelectedRows () { + return this.mutateSelectedText(selection => selection.autoIndentSelectedRows()) + } + + // Indent all lines intersecting selections. See {Selection::indent} for more + // information. + indent (options = {}) { + if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent() + this.mutateSelectedText(selection => selection.indent(options)) + } + + // Constructs the string used for indents. + buildIndentString (level, column = 0) { + if (this.getSoftTabs()) { + const tabStopViolation = column % this.getTabLength() + return _.multiplyString(' ', Math.floor(level * this.getTabLength()) - tabStopViolation) + } else { + const excessWhitespace = _.multiplyString(' ', Math.round((level - Math.floor(level)) * this.getTabLength())) + return _.multiplyString('\t', Math.floor(level)) + excessWhitespace + } + } + + /* + Section: Grammars + */ + + // Essential: Get the current {Grammar} of this editor. + getGrammar () { + return this.tokenizedBuffer.grammar + } + + // Essential: Set the current {Grammar} of this editor. + // + // Assigning a grammar will cause the editor to re-tokenize based on the new + // grammar. + // + // * `grammar` {Grammar} + setGrammar (grammar) { + return this.tokenizedBuffer.setGrammar(grammar) + } + + // Reload the grammar based on the file name. + reloadGrammar () { + return this.tokenizedBuffer.reloadGrammar() + } + + // Experimental: Get a notification when async tokenization is completed. + onDidTokenize (callback) { + return this.tokenizedBuffer.onDidTokenize(callback) + } + + /* + Section: Managing Syntax Scopes + */ + + // Essential: Returns a {ScopeDescriptor} that includes this editor's language. + // e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with + // {Config::get} to get language specific config values. + getRootScopeDescriptor () { + return this.tokenizedBuffer.rootScopeDescriptor + } + + // Essential: Get the syntactic scopeDescriptor for the given position in buffer + // coordinates. Useful with {Config::get}. + // + // For example, if called with a position inside the parameter list of an + // anonymous CoffeeScript function, the method returns the following array: + // `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` + // + // * `bufferPosition` A {Point} or {Array} of [row, column]. + // + // Returns a {ScopeDescriptor}. + scopeDescriptorForBufferPosition (bufferPosition) { + return this.tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) + } + + // Extended: Get the range in buffer coordinates of all tokens surrounding the + // cursor that match the given scope selector. + // + // For example, if you wanted to find the string surrounding the cursor, you + // could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. + // + // * `scopeSelector` {String} selector. e.g. `'.source.ruby'` + // + // Returns a {Range}. + bufferRangeForScopeAtCursor (scopeSelector) { + return this.bufferRangeForScopeAtPosition(scopeSelector, this.getCursorBufferPosition()) + } + + bufferRangeForScopeAtPosition (scopeSelector, position) { + return this.tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position) + } + + // Extended: Determine if the given row is entirely a comment + isBufferRowCommented (bufferRow) { + const match = this.lineTextForBufferRow(bufferRow).match(/\S/) + if (match) { + if (!this.commentScopeSelector) this.commentScopeSelector = new TextMateScopeSelector('comment.*') + return this.commentScopeSelector.matches(this.scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) + } + } + + // Get the scope descriptor at the cursor. + getCursorScope () { + return this.getLastCursor().getScopeDescriptor() + } + + tokenForBufferPosition (bufferPosition) { + return this.tokenizedBuffer.tokenForPosition(bufferPosition) + } + + /* + Section: Clipboard Operations + */ + + // Essential: For each selection, copy the selected text. + copySelectedText () { + let maintainClipboard = false + for (let selection of this.getSelectionsOrderedByBufferPosition()) { + if (selection.isEmpty()) { + const previousRange = selection.getBufferRange() + selection.selectLine() + selection.copy(maintainClipboard, true) + selection.setBufferRange(previousRange) + } else { + selection.copy(maintainClipboard, false) + } + maintainClipboard = true + } + } + + // Private: For each selection, only copy highlighted text. + copyOnlySelectedText () { + let maintainClipboard = false + for (let selection of this.getSelectionsOrderedByBufferPosition()) { + if (!selection.isEmpty()) { + selection.copy(maintainClipboard, false) + maintainClipboard = true + } + } + } + + // Essential: For each selection, cut the selected text. + cutSelectedText () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + if (selection.isEmpty()) { + selection.selectLine() + selection.cut(maintainClipboard, true) + } else { + selection.cut(maintainClipboard, false) + } + maintainClipboard = true + }) + } + + // Essential: For each selection, replace the selected text with the contents of + // the clipboard. + // + // If the clipboard contains the same number of selections as the current + // editor, each selection will be replaced with the content of the + // corresponding clipboard selection text. + // + // * `options` (optional) See {Selection::insertText}. + pasteText (options) { + options = Object.assign({}, options) + let {text: clipboardText, metadata} = this.constructor.clipboard.readWithMetadata() + if (!this.emitWillInsertTextEvent(clipboardText)) return false + + if (!metadata) metadata = {} + if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndentOnPaste() + + this.mutateSelectedText((selection, index) => { + let fullLine, indentBasis, text + if (metadata.selections && metadata.selections.length === this.getSelections().length) { + ({text, indentBasis, fullLine} = metadata.selections[index]) + } else { + ({indentBasis, fullLine} = metadata) + text = clipboardText + } + + if (indentBasis != null && (text.includes('\n') || !selection.cursor.hasPrecedingCharactersOnLine())) { + options.indentBasis = indentBasis + } else { + options.indentBasis = null + } + + let range + if (fullLine && selection.isEmpty()) { + const oldPosition = selection.getBufferRange().start + selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]) + range = selection.insertText(text, options) + const newPosition = oldPosition.translate([1, 0]) + selection.setBufferRange([newPosition, newPosition]) + } else { + range = selection.insertText(text, options) + } + + this.emitter.emit('did-insert-text', {text, range}) + }) + } + + // Essential: For each selection, if the selection is empty, cut all characters + // of the containing screen line following the cursor. Otherwise cut the selected + // text. + cutToEndOfLine () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + selection.cutToEndOfLine(maintainClipboard) + maintainClipboard = true + }) + } + + // Essential: For each selection, if the selection is empty, cut all characters + // of the containing buffer line following the cursor. Otherwise cut the + // selected text. + cutToEndOfBufferLine () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + selection.cutToEndOfBufferLine(maintainClipboard) + maintainClipboard = true + }) + } + + /* + Section: Folds + */ + + // Essential: Fold the most recent cursor's row based on its indentation level. + // + // The fold will extend from the nearest preceding line with a lower + // indentation level up to the nearest following row with a lower indentation + // level. + foldCurrentRow () { + const {row} = this.getCursorBufferPosition() + const range = this.tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) + if (range) return this.displayLayer.foldBufferRange(range) + } + + // Essential: Unfold the most recent cursor's row by one level. + unfoldCurrentRow () { + const {row} = this.getCursorBufferPosition() + return this.displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) + } + + // Essential: Fold the given row in buffer coordinates based on its indentation + // level. + // + // If the given row is foldable, the fold will begin there. Otherwise, it will + // begin at the first foldable row preceding the given row. + // + // * `bufferRow` A {Number}. + foldBufferRow (bufferRow) { + let position = Point(bufferRow, Infinity) + while (true) { + const foldableRange = this.tokenizedBuffer.getFoldableRangeContainingPoint(position, this.getTabLength()) + if (foldableRange) { + const existingFolds = this.displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) + if (existingFolds.length === 0) { + this.displayLayer.foldBufferRange(foldableRange) + } else { + const firstExistingFoldRange = this.displayLayer.bufferRangeForFold(existingFolds[0]) + if (firstExistingFoldRange.start.isLessThan(position)) { + position = Point(firstExistingFoldRange.start.row, 0) + continue + } + } + } + break + } + } + + // Essential: Unfold all folds containing the given row in buffer coordinates. + // + // * `bufferRow` A {Number} + unfoldBufferRow (bufferRow) { + const position = Point(bufferRow, Infinity) + return this.displayLayer.destroyFoldsContainingBufferPositions([position]) + } + + // Extended: For each selection, fold the rows it intersects. + foldSelectedLines () { + for (let selection of this.selections) { + selection.fold() + } + } + + // Extended: Fold all foldable lines. + foldAll () { + this.displayLayer.destroyAllFolds() + for (let range of this.tokenizedBuffer.getFoldableRanges(this.getTabLength())) { + this.displayLayer.foldBufferRange(range) + } + } + + // Extended: Unfold all existing folds. + unfoldAll () { + const result = this.displayLayer.destroyAllFolds() + this.scrollToCursorPosition() + return result + } + + // Extended: Fold all foldable lines at the given indent level. + // + // * `level` A {Number}. + foldAllAtIndentLevel (level) { + this.displayLayer.destroyAllFolds() + for (let range of this.tokenizedBuffer.getFoldableRangesAtIndentLevel(level, this.getTabLength())) { + this.displayLayer.foldBufferRange(range) + } + } + + // Extended: Determine whether the given row in buffer coordinates is foldable. + // + // A *foldable* row is a row that *starts* a row range that can be folded. + // + // * `bufferRow` A {Number} + // + // Returns a {Boolean}. + isFoldableAtBufferRow (bufferRow) { + return this.tokenizedBuffer.isFoldableAtRow(bufferRow) + } + + // Extended: Determine whether the given row in screen coordinates is foldable. + // + // A *foldable* row is a row that *starts* a row range that can be folded. + // + // * `bufferRow` A {Number} + // + // Returns a {Boolean}. + isFoldableAtScreenRow (screenRow) { + return this.isFoldableAtBufferRow(this.bufferRowForScreenRow(screenRow)) + } + + // Extended: Fold the given buffer row if it isn't currently folded, and unfold + // it otherwise. + toggleFoldAtBufferRow (bufferRow) { + if (this.isFoldedAtBufferRow(bufferRow)) { + return this.unfoldBufferRow(bufferRow) + } else { + return this.foldBufferRow(bufferRow) + } + } + + // Extended: Determine whether the most recently added cursor's row is folded. + // + // Returns a {Boolean}. + isFoldedAtCursorRow () { + return this.isFoldedAtBufferRow(this.getCursorBufferPosition().row) + } + + // Extended: Determine whether the given row in buffer coordinates is folded. + // + // * `bufferRow` A {Number} + // + // Returns a {Boolean}. + isFoldedAtBufferRow (bufferRow) { + const range = Range( + Point(bufferRow, 0), + Point(bufferRow, this.buffer.lineLengthForRow(bufferRow)) + ) + return this.displayLayer.foldsIntersectingBufferRange(range).length > 0 + } + + // Extended: Determine whether the given row in screen coordinates is folded. + // + // * `screenRow` A {Number} + // + // Returns a {Boolean}. + isFoldedAtScreenRow (screenRow) { + return this.isFoldedAtBufferRow(this.bufferRowForScreenRow(screenRow)) + } + + // Creates a new fold between two row numbers. + // + // startRow - The row {Number} to start folding at + // endRow - The row {Number} to end the fold + // + // Returns the new {Fold}. + foldBufferRowRange (startRow, endRow) { + return this.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) + } + + foldBufferRange (range) { + return this.displayLayer.foldBufferRange(range) + } + + // Remove any {Fold}s found that intersect the given buffer range. + destroyFoldsIntersectingBufferRange (bufferRange) { + return this.displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) + } + + // Remove any {Fold}s found that contain the given array of buffer positions. + destroyFoldsContainingBufferPositions (bufferPositions, excludeEndpoints) { + return this.displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) + } + + /* + Section: Gutters + */ + + // Essential: Add a custom {Gutter}. + // + // * `options` An {Object} with the following fields: + // * `name` (required) A unique {String} to identify this gutter. + // * `priority` (optional) A {Number} that determines stacking order between + // gutters. Lower priority items are forced closer to the edges of the + // window. (default: -100) + // * `visible` (optional) {Boolean} specifying whether the gutter is visible + // initially after being created. (default: true) + // + // Returns the newly-created {Gutter}. + addGutter (options) { + return this.gutterContainer.addGutter(options) + } + + // Essential: Get this editor's gutters. + // + // Returns an {Array} of {Gutter}s. + getGutters () { + return this.gutterContainer.getGutters() + } + + getLineNumberGutter () { + return this.lineNumberGutter + } + + // Essential: Get the gutter with the given name. + // + // Returns a {Gutter}, or `null` if no gutter exists for the given name. + gutterWithName (name) { + return this.gutterContainer.gutterWithName(name) + } + + /* + Section: Scrolling the TextEditor + */ + + // Essential: Scroll the editor to reveal the most recently added cursor if it is + // off-screen. + // + // * `options` (optional) {Object} + // * `center` Center the editor around the cursor if possible. (default: true) + scrollToCursorPosition (options) { + this.getLastCursor().autoscroll({center: options && options.center !== false}) + } + + // Essential: Scrolls the editor to the given buffer position. + // + // * `bufferPosition` An object that represents a buffer position. It can be either + // an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + // * `options` (optional) {Object} + // * `center` Center the editor around the position if possible. (default: false) + scrollToBufferPosition (bufferPosition, options) { + return this.scrollToScreenPosition(this.screenPositionForBufferPosition(bufferPosition), options) + } + + // Essential: Scrolls the editor to the given screen position. + // + // * `screenPosition` An object that represents a screen position. It can be either + // an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + // * `options` (optional) {Object} + // * `center` Center the editor around the position if possible. (default: false) + scrollToScreenPosition (screenPosition, options) { + this.scrollToScreenRange(new Range(screenPosition, screenPosition), options) + } + + scrollToTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::scrollToTop instead.') + this.getElement().scrollToTop() + } + + scrollToBottom () { + Grim.deprecate('This is now a view method. Call TextEditorElement::scrollToTop instead.') + this.getElement().scrollToBottom() + } + + scrollToScreenRange (screenRange, options = {}) { + if (options.clip !== false) screenRange = this.clipScreenRange(screenRange) + const scrollEvent = {screenRange, options} + if (this.component) this.component.didRequestAutoscroll(scrollEvent) + this.emitter.emit('did-request-autoscroll', scrollEvent) + } + + getHorizontalScrollbarHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.') + return this.getElement().getHorizontalScrollbarHeight() + } + + getVerticalScrollbarWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.') + return this.getElement().getVerticalScrollbarWidth() + } + + pageUp () { + this.moveUp(this.getRowsPerPage()) + } + + pageDown () { + this.moveDown(this.getRowsPerPage()) + } + + selectPageUp () { + this.selectUp(this.getRowsPerPage()) + } + + selectPageDown () { + this.selectDown(this.getRowsPerPage()) + } + + // Returns the number of rows per page + getRowsPerPage () { + if (this.component) { + const clientHeight = this.component.getScrollContainerClientHeight() + const lineHeight = this.component.getLineHeight() + return Math.max(1, Math.ceil(clientHeight / lineHeight)) + } else { + return 1 + } + } + + /* + Section: Config + */ + + // Experimental: Supply an object that will provide the editor with settings + // for specific syntactic scopes. See the `ScopedSettingsDelegate` in + // `text-editor-registry.js` for an example implementation. + setScopedSettingsDelegate (scopedSettingsDelegate) { + this.scopedSettingsDelegate = scopedSettingsDelegate + this.tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate + } + + // Experimental: Retrieve the {Object} that provides the editor with settings + // for specific syntactic scopes. + getScopedSettingsDelegate () { return this.scopedSettingsDelegate } + + // Experimental: Is auto-indentation enabled for this editor? + // + // Returns a {Boolean}. + shouldAutoIndent () { return this.autoIndent } + + // Experimental: Is auto-indentation on paste enabled for this editor? + // + // Returns a {Boolean}. + shouldAutoIndentOnPaste () { return this.autoIndentOnPaste } + + // Experimental: Does this editor allow scrolling past the last line? + // + // Returns a {Boolean}. + getScrollPastEnd () { + if (this.getAutoHeight()) { + return false + } else { + return this.scrollPastEnd + } + } + + // Experimental: How fast does the editor scroll in response to mouse wheel + // movements? + // + // Returns a positive {Number}. + getScrollSensitivity () { return this.scrollSensitivity } + + // Experimental: Does this editor show cursors while there is a selection? + // + // Returns a positive {Boolean}. + getShowCursorOnSelection () { return this.showCursorOnSelection } + + // Experimental: Are line numbers enabled for this editor? + // + // Returns a {Boolean} + doesShowLineNumbers () { return this.showLineNumbers } + + // Experimental: Get the time interval within which text editing operations + // are grouped together in the editor's undo history. + // + // Returns the time interval {Number} in milliseconds. + getUndoGroupingInterval () { return this.undoGroupingInterval } + + // Experimental: Get the characters that are *not* considered part of words, + // for the purpose of word-based cursor movements. + // + // Returns a {String} containing the non-word characters. + getNonWordCharacters (scopes) { + if (this.scopedSettingsDelegate && this.scopedSettingsDelegate.getNonWordCharacters) { + return this.scopedSettingsDelegate.getNonWordCharacters(scopes) || this.nonWordCharacters + } else { + return this.nonWordCharacters + } + } + + /* + Section: Event Handlers + */ + + handleGrammarChange () { + this.unfoldAll() + return this.emitter.emit('did-change-grammar', this.getGrammar()) + } + + /* + Section: TextEditor Rendering + */ + + // Get the Element for the editor. + getElement () { + if (!this.component) { + if (!TextEditorComponent) TextEditorComponent = require('./text-editor-component') + if (!TextEditorElement) TextEditorElement = require('./text-editor-element') + this.component = new TextEditorComponent({ + model: this, + updatedSynchronously: TextEditorElement.prototype.updatedSynchronously, + initialScrollTopRow: this.initialScrollTopRow, + initialScrollLeftColumn: this.initialScrollLeftColumn + }) + } + return this.component.element + } + + getAllowedLocations () { + return ['center'] + } + + // Essential: Retrieves the greyed out placeholder of a mini editor. + // + // Returns a {String}. + getPlaceholderText () { return this.placeholderText } + + // Essential: Set the greyed out placeholder of a mini editor. Placeholder text + // will be displayed when the editor has no content. + // + // * `placeholderText` {String} text that is displayed when the editor has no content. + setPlaceholderText (placeholderText) { this.update({placeholderText}) } + + pixelPositionForBufferPosition (bufferPosition) { + Grim.deprecate('This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead') + return this.getElement().pixelPositionForBufferPosition(bufferPosition) + } + + pixelPositionForScreenPosition (screenPosition) { + Grim.deprecate('This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead') + return this.getElement().pixelPositionForScreenPosition(screenPosition) + } + + getVerticalScrollMargin () { + const maxScrollMargin = Math.floor(((this.height / this.getLineHeightInPixels()) - 1) / 2) + return Math.min(this.verticalScrollMargin, maxScrollMargin) + } + + setVerticalScrollMargin (verticalScrollMargin) { + this.verticalScrollMargin = verticalScrollMargin + return this.verticalScrollMargin + } + + getHorizontalScrollMargin () { + return Math.min(this.horizontalScrollMargin, Math.floor(((this.width / this.getDefaultCharWidth()) - 1) / 2)) + } + setHorizontalScrollMargin (horizontalScrollMargin) { + this.horizontalScrollMargin = horizontalScrollMargin + return this.horizontalScrollMargin + } + + getLineHeightInPixels () { return this.lineHeightInPixels } + setLineHeightInPixels (lineHeightInPixels) { + this.lineHeightInPixels = lineHeightInPixels + return this.lineHeightInPixels + } + + getKoreanCharWidth () { return this.koreanCharWidth } + getHalfWidthCharWidth () { return this.halfWidthCharWidth } + getDoubleWidthCharWidth () { return this.doubleWidthCharWidth } + getDefaultCharWidth () { return this.defaultCharWidth } + + ratioForCharacter (character) { + if (isKoreanCharacter(character)) { + return this.getKoreanCharWidth() / this.getDefaultCharWidth() + } else if (isHalfWidthCharacter(character)) { + return this.getHalfWidthCharWidth() / this.getDefaultCharWidth() + } else if (isDoubleWidthCharacter(character)) { + return this.getDoubleWidthCharWidth() / this.getDefaultCharWidth() + } else { + return 1 + } + } + + setDefaultCharWidth (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) { + if (doubleWidthCharWidth == null) { doubleWidthCharWidth = defaultCharWidth } + if (halfWidthCharWidth == null) { halfWidthCharWidth = defaultCharWidth } + if (koreanCharWidth == null) { koreanCharWidth = defaultCharWidth } + if (defaultCharWidth !== this.defaultCharWidth || + (doubleWidthCharWidth !== this.doubleWidthCharWidth && + halfWidthCharWidth !== this.halfWidthCharWidth && + koreanCharWidth !== this.koreanCharWidth)) { + this.defaultCharWidth = defaultCharWidth + this.doubleWidthCharWidth = doubleWidthCharWidth + this.halfWidthCharWidth = halfWidthCharWidth + this.koreanCharWidth = koreanCharWidth + if (this.isSoftWrapped()) { + this.displayLayer.reset({ + softWrapColumn: this.getSoftWrapColumn() + }) + } + } + return defaultCharWidth + } + + setHeight (height) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setHeight instead.') + this.getElement().setHeight(height) + } + + getHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getHeight instead.') + return this.getElement().getHeight() + } + + getAutoHeight () { return this.autoHeight != null ? this.autoHeight : true } + + getAutoWidth () { return this.autoWidth != null ? this.autoWidth : false } + + setWidth (width) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setWidth instead.') + this.getElement().setWidth(width) + } + + getWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getWidth instead.') + return this.getElement().getWidth() + } + + // Use setScrollTopRow instead of this method + setFirstVisibleScreenRow (screenRow) { + this.setScrollTopRow(screenRow) + } + + getFirstVisibleScreenRow () { + return this.getElement().component.getFirstVisibleRow() + } + + getLastVisibleScreenRow () { + return this.getElement().component.getLastVisibleRow() + } + + getVisibleRowRange () { + return [this.getFirstVisibleScreenRow(), this.getLastVisibleScreenRow()] + } + + // Use setScrollLeftColumn instead of this method + setFirstVisibleScreenColumn (column) { + return this.setScrollLeftColumn(column) + } + + getFirstVisibleScreenColumn () { + return this.getElement().component.getFirstVisibleColumn() + } + + getScrollTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollTop instead.') + return this.getElement().getScrollTop() + } + + setScrollTop (scrollTop) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollTop instead.') + this.getElement().setScrollTop(scrollTop) + } + + getScrollBottom () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollBottom instead.') + return this.getElement().getScrollBottom() + } + + setScrollBottom (scrollBottom) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollBottom instead.') + this.getElement().setScrollBottom(scrollBottom) + } + + getScrollLeft () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollLeft instead.') + return this.getElement().getScrollLeft() + } + + setScrollLeft (scrollLeft) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollLeft instead.') + this.getElement().setScrollLeft(scrollLeft) + } + + getScrollRight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollRight instead.') + return this.getElement().getScrollRight() + } + + setScrollRight (scrollRight) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollRight instead.') + this.getElement().setScrollRight(scrollRight) + } + + getScrollHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollHeight instead.') + return this.getElement().getScrollHeight() + } + + getScrollWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollWidth instead.') + return this.getElement().getScrollWidth() + } + + getMaxScrollTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getMaxScrollTop instead.') + return this.getElement().getMaxScrollTop() + } + + getScrollTopRow () { + return this.getElement().component.getScrollTopRow() + } + + setScrollTopRow (scrollTopRow) { + this.getElement().component.setScrollTopRow(scrollTopRow) + } + + getScrollLeftColumn () { + return this.getElement().component.getScrollLeftColumn() + } + + setScrollLeftColumn (scrollLeftColumn) { + this.getElement().component.setScrollLeftColumn(scrollLeftColumn) + } + + intersectsVisibleRowRange (startRow, endRow) { + Grim.deprecate('This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.') + return this.getElement().intersectsVisibleRowRange(startRow, endRow) + } + + selectionIntersectsVisibleRowRange (selection) { + Grim.deprecate('This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.') + return this.getElement().selectionIntersectsVisibleRowRange(selection) + } + + screenPositionForPixelPosition (pixelPosition) { + Grim.deprecate('This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.') + return this.getElement().screenPositionForPixelPosition(pixelPosition) + } + + pixelRectForScreenRange (screenRange) { + Grim.deprecate('This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.') + return this.getElement().pixelRectForScreenRange(screenRange) + } + + /* + Section: Utility + */ + + inspect () { + return `` + } + + emitWillInsertTextEvent (text) { + let result = true + const cancel = () => { result = false } + this.emitter.emit('will-insert-text', {cancel, text}) + return result + } + + /* + Section: Language Mode Delegated Methods + */ + + suggestedIndentForBufferRow (bufferRow, options) { + return this.tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options) + } + + // Given a buffer row, indent it. + // + // * bufferRow - The row {Number}. + // * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. + autoIndentBufferRow (bufferRow, options) { + const indentLevel = this.suggestedIndentForBufferRow(bufferRow, options) + return this.setIndentationForBufferRow(bufferRow, indentLevel, options) + } + + // Indents all the rows between two buffer row numbers. + // + // * startRow - The row {Number} to start at + // * endRow - The row {Number} to end at + autoIndentBufferRows (startRow, endRow) { + let row = startRow + while (row <= endRow) { + this.autoIndentBufferRow(row) + row++ + } + } + + autoDecreaseIndentForBufferRow (bufferRow) { + const indentLevel = this.tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) + if (indentLevel != null) this.setIndentationForBufferRow(bufferRow, indentLevel) + } + + toggleLineCommentForBufferRow (row) { this.toggleLineCommentsForBufferRows(row, row) } + + 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_REGEXP.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_REGEXP.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_REGEXP.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 + ' ' + ) + } + } + } + } + } + + rowRangeForParagraphAtBufferRow (bufferRow) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow))) return + + const isCommented = this.tokenizedBuffer.isRowCommented(bufferRow) + + let startRow = bufferRow + while (startRow > 0) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(startRow - 1))) break + if (this.tokenizedBuffer.isRowCommented(startRow - 1) !== isCommented) break + startRow-- + } + + let endRow = bufferRow + const rowCount = this.getLineCount() + while (endRow < rowCount) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break + if (this.tokenizedBuffer.isRowCommented(endRow + 1) !== isCommented) break + endRow++ + } + + return new Range(new Point(startRow, 0), new Point(endRow, this.buffer.lineLengthForRow(endRow))) + } +} + +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_REGEXP) + 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_REGEXP.test(line.slice(endColumn))) return null + if (line[startColumn - 1] === ' ') startColumn-- + return [startColumn, endColumn] +} + +class ChangeEvent { + constructor ({oldRange, newRange}) { + this.oldRange = oldRange + this.newRange = newRange + } + + get start () { + return this.newRange.start + } + + get oldExtent () { + return this.oldRange.getExtent() + } + + get newExtent () { + return this.newRange.getExtent() + } +} From 616ebe71d940e28ecdee1522347e21444155f9ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 15:51:14 -0700 Subject: [PATCH 242/301] Convert text-editor-spec.coffee to JavaScript --- spec/text-editor-spec.coffee | 5873 ------------------------------ spec/text-editor-spec.js | 6656 +++++++++++++++++++++++++++++++++- 2 files changed, 6653 insertions(+), 5876 deletions(-) delete mode 100644 spec/text-editor-spec.coffee diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee deleted file mode 100644 index b8d4bdcf9..000000000 --- a/spec/text-editor-spec.coffee +++ /dev/null @@ -1,5873 +0,0 @@ -path = require 'path' -clipboard = require '../src/safe-clipboard' -TextEditor = require '../src/text-editor' -TextBuffer = require 'text-buffer' - -describe "TextEditor", -> - [buffer, editor, lineLengths] = [] - - convertToHardTabs = (buffer) -> - buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t")) - - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', {autoIndent: false}).then (o) -> editor = o - - runs -> - buffer = editor.buffer - editor.update({autoIndent: false}) - lineLengths = buffer.getLines().map (line) -> line.length - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - describe "when the editor is deserialized", -> - it "restores selections and folds based on markers in the buffer", -> - editor.setSelectedBufferRange([[1, 2], [3, 4]]) - editor.addSelectionForBufferRange([[5, 6], [7, 5]], reversed: true) - editor.foldBufferRow(4) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - - waitsForPromise -> - TextBuffer.deserialize(editor.buffer.serialize()).then (buffer2) -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> buffer2} - }) - - expect(editor2.id).toBe editor.id - expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() - expect(editor2.getSelectedBufferRanges()).toEqual [[[1, 2], [3, 4]], [[5, 6], [7, 5]]] - expect(editor2.getSelections()[1].isReversed()).toBeTruthy() - expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() - editor2.destroy() - - it "restores the editor's layout configuration", -> - editor.update({ - softTabs: true - atomicSoftTabs: false - tabLength: 12 - softWrapped: true - softWrapAtPreferredLineLength: true - softWrapHangingIndentLength: 8 - invisibles: {space: 'S'} - showInvisibles: true - editorWidthInChars: 120 - }) - - # Force buffer and display layer to be deserialized as well, rather than - # reusing the same buffer instance - waitsForPromise -> - TextBuffer.deserialize(editor.buffer.serialize()).then (buffer2) -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> buffer2} - }) - - expect(editor2.getSoftTabs()).toBe(editor.getSoftTabs()) - expect(editor2.hasAtomicSoftTabs()).toBe(editor.hasAtomicSoftTabs()) - expect(editor2.getTabLength()).toBe(editor.getTabLength()) - expect(editor2.getSoftWrapColumn()).toBe(editor.getSoftWrapColumn()) - expect(editor2.getSoftWrapHangingIndentLength()).toBe(editor.getSoftWrapHangingIndentLength()) - expect(editor2.getInvisibles()).toEqual(editor.getInvisibles()) - expect(editor2.getEditorWidthInChars()).toBe(editor.getEditorWidthInChars()) - expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength()) - expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn()) - - it "ignores buffers with retired IDs", -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> null} - }) - - expect(editor2).toBeNull() - - describe "when the editor is constructed with the largeFileMode option set to true", -> - it "loads the editor but doesn't tokenize", -> - editor = null - - waitsForPromise -> - atom.workspace.openTextFile('sample.js', largeFileMode: true).then (o) -> editor = o - - runs -> - buffer = editor.getBuffer() - expect(editor.lineTextForScreenRow(0)).toBe buffer.lineForRow(0) - expect(editor.tokensForScreenRow(0).length).toBe 1 - expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab - expect(editor.lineTextForScreenRow(12)).toBe buffer.lineForRow(12) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - editor.insertText('hey"') - expect(editor.tokensForScreenRow(0).length).toBe 1 - expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab - - describe ".copy()", -> - it "returns a different editor with the same initial state", -> - expect(editor.getAutoHeight()).toBeFalsy() - expect(editor.getAutoWidth()).toBeFalsy() - expect(editor.getShowCursorOnSelection()).toBeTruthy() - - element = editor.getElement() - element.setHeight(100) - element.setWidth(100) - jasmine.attachToDOM(element) - - editor.update({showCursorOnSelection: false}) - editor.setSelectedBufferRange([[1, 2], [3, 4]]) - editor.addSelectionForBufferRange([[5, 6], [7, 8]], reversed: true) - editor.setScrollTopRow(3) - expect(editor.getScrollTopRow()).toBe(3) - editor.setScrollLeftColumn(4) - expect(editor.getScrollLeftColumn()).toBe(4) - editor.foldBufferRow(4) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - - editor2 = editor.copy() - element2 = editor2.getElement() - element2.setHeight(100) - element2.setWidth(100) - jasmine.attachToDOM(element2) - expect(editor2.id).not.toBe editor.id - expect(editor2.getSelectedBufferRanges()).toEqual editor.getSelectedBufferRanges() - expect(editor2.getSelections()[1].isReversed()).toBeTruthy() - expect(editor2.getScrollTopRow()).toBe(3) - expect(editor2.getScrollLeftColumn()).toBe(4) - expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor2.getAutoWidth()).toBe(false) - expect(editor2.getAutoHeight()).toBe(false) - expect(editor2.getShowCursorOnSelection()).toBeFalsy() - - # editor2 can now diverge from its origin edit session - editor2.getLastSelection().setBufferRange([[2, 1], [4, 3]]) - expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() - editor2.unfoldBufferRow(4) - expect(editor2.isFoldedAtBufferRow(4)).not.toBe editor.isFoldedAtBufferRow(4) - - describe ".update()", -> - it "updates the editor with the supplied config parameters", -> - element = editor.element # force element initialization - element.setUpdatedSynchronously(false) - editor.update({showInvisibles: true}) - editor.onDidChange(changeSpy = jasmine.createSpy('onDidChange')) - - returnedPromise = editor.update({ - tabLength: 6, softTabs: false, softWrapped: true, editorWidthInChars: 40, - showInvisibles: false, mini: false, lineNumberGutterVisible: false, scrollPastEnd: true, - autoHeight: false, maxScreenLineLength: 1000 - }) - - expect(returnedPromise).toBe(element.component.getNextUpdatePromise()) - expect(changeSpy.callCount).toBe(1) - expect(editor.getTabLength()).toBe(6) - expect(editor.getSoftTabs()).toBe(false) - expect(editor.isSoftWrapped()).toBe(true) - expect(editor.getEditorWidthInChars()).toBe(40) - expect(editor.getInvisibles()).toEqual({}) - expect(editor.isMini()).toBe(false) - expect(editor.isLineNumberGutterVisible()).toBe(false) - expect(editor.getScrollPastEnd()).toBe(true) - expect(editor.getAutoHeight()).toBe(false) - - describe "title", -> - describe ".getTitle()", -> - it "uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", -> - expect(editor.getTitle()).toBe 'sample.js' - buffer.setPath(undefined) - expect(editor.getTitle()).toBe 'untitled' - - describe ".getLongTitle()", -> - it "returns file name when there is no opened file with identical name", -> - expect(editor.getLongTitle()).toBe 'sample.js' - buffer.setPath(undefined) - expect(editor.getLongTitle()).toBe 'untitled' - - it "returns '' when opened files have identical file names", -> - editor1 = null - editor2 = null - waitsForPromise -> - atom.workspace.open(path.join('sample-theme-1', 'readme')).then (o) -> - editor1 = o - atom.workspace.open(path.join('sample-theme-2', 'readme')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1" - expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2" - - it "returns '' when opened files have identical file names in subdirectories", -> - editor1 = null - editor2 = null - path1 = path.join('sample-theme-1', 'src', 'js') - path2 = path.join('sample-theme-2', 'src', 'js') - waitsForPromise -> - atom.workspace.open(path.join(path1, 'main.js')).then (o) -> - editor1 = o - atom.workspace.open(path.join(path2, 'main.js')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "main.js \u2014 #{path1}" - expect(editor2.getLongTitle()).toBe "main.js \u2014 #{path2}" - - it "returns '' when opened files have identical file and same parent dir name", -> - editor1 = null - editor2 = null - waitsForPromise -> - atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> - editor1 = o - atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'plugin', 'main.js')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "main.js \u2014 js" - expect(editor2.getLongTitle()).toBe "main.js \u2014 " + path.join('js', 'plugin') - - it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> - observed = [] - editor.onDidChangeTitle (title) -> observed.push(title) - - buffer.setPath('/foo/bar/baz.txt') - buffer.setPath(undefined) - - expect(observed).toEqual ['baz.txt', 'untitled'] - - describe "path", -> - it "notifies ::onDidChangePath observers when the underlying buffer path changes", -> - observed = [] - editor.onDidChangePath (filePath) -> observed.push(filePath) - - buffer.setPath(__filename) - buffer.setPath(undefined) - - expect(observed).toEqual [__filename, undefined] - - describe "encoding", -> - it "notifies ::onDidChangeEncoding observers when the editor encoding changes", -> - observed = [] - editor.onDidChangeEncoding (encoding) -> observed.push(encoding) - - editor.setEncoding('utf16le') - editor.setEncoding('utf16le') - editor.setEncoding('utf16be') - editor.setEncoding() - editor.setEncoding() - - expect(observed).toEqual ['utf16le', 'utf16be', 'utf8'] - - describe "cursor", -> - describe ".getLastCursor()", -> - it "returns the most recently created cursor", -> - editor.addCursorAtScreenPosition([1, 0]) - lastCursor = editor.addCursorAtScreenPosition([2, 0]) - expect(editor.getLastCursor()).toBe lastCursor - - it "creates a new cursor at (0, 0) if the last cursor has been destroyed", -> - editor.getLastCursor().destroy() - expect(editor.getLastCursor().getBufferPosition()).toEqual([0, 0]) - - describe ".getCursors()", -> - it "creates a new cursor at (0, 0) if the last cursor has been destroyed", -> - editor.getLastCursor().destroy() - expect(editor.getCursors()[0].getBufferPosition()).toEqual([0, 0]) - - describe "when the cursor moves", -> - it "clears a goal column established by vertical movement", -> - editor.setText('b') - editor.setCursorBufferPosition([0, 0]) - editor.insertNewline() - editor.moveUp() - editor.insertText('a') - editor.moveDown() - expect(editor.getCursorBufferPosition()).toEqual [1, 1] - - it "emits an event with the old position, new position, and the cursor that moved", -> - cursorCallback = jasmine.createSpy('cursor-changed-position') - editorCallback = jasmine.createSpy('editor-changed-cursor-position') - - editor.getLastCursor().onDidChangePosition(cursorCallback) - editor.onDidChangeCursorPosition(editorCallback) - - editor.setCursorBufferPosition([2, 4]) - - expect(editorCallback).toHaveBeenCalled() - expect(cursorCallback).toHaveBeenCalled() - eventObject = editorCallback.mostRecentCall.args[0] - expect(cursorCallback.mostRecentCall.args[0]).toEqual(eventObject) - - expect(eventObject.oldBufferPosition).toEqual [0, 0] - expect(eventObject.oldScreenPosition).toEqual [0, 0] - expect(eventObject.newBufferPosition).toEqual [2, 4] - expect(eventObject.newScreenPosition).toEqual [2, 4] - expect(eventObject.cursor).toBe editor.getLastCursor() - - describe ".setCursorScreenPosition(screenPosition)", -> - it "clears a goal column established by vertical movement", -> - # set a goal column by moving down - editor.setCursorScreenPosition(row: 3, column: lineLengths[3]) - editor.moveDown() - expect(editor.getCursorScreenPosition().column).not.toBe 6 - - # clear the goal column by explicitly setting the cursor position - editor.setCursorScreenPosition([4, 6]) - expect(editor.getCursorScreenPosition().column).toBe 6 - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe 6 - - it "merges multiple cursors", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 1]) - [cursor1, cursor2] = editor.getCursors() - editor.setCursorScreenPosition([4, 7]) - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursors()).toEqual [cursor1] - expect(editor.getCursorScreenPosition()).toEqual [4, 7] - - describe "when soft-wrap is enabled and code is folded", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - editor.foldBufferRowRange(2, 3) - - it "positions the cursor at the buffer position that corresponds to the given screen position", -> - editor.setCursorScreenPosition([9, 0]) - expect(editor.getCursorBufferPosition()).toEqual [8, 11] - - describe ".moveUp()", -> - it "moves the cursor up", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual [1, 2] - - it "retains the goal column across lines of differing length", -> - expect(lineLengths[6]).toBeGreaterThan(32) - editor.setCursorScreenPosition(row: 6, column: 32) - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[5] - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[4] - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe 32 - - describe "when the cursor is on the first line", -> - it "moves the cursor to the beginning of the line, but retains the goal column", -> - editor.setCursorScreenPosition([0, 4]) - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual([0, 0]) - - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual([1, 4]) - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[4, 9], [5, 10]]) - - it "moves above the selection", -> - cursor = editor.getLastCursor() - editor.moveUp() - expect(cursor.getBufferPosition()).toEqual [3, 9] - - it "merges cursors when they overlap", -> - editor.addCursorAtScreenPosition([1, 0]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveUp() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - - describe "when the cursor was moved down from the beginning of an indented soft-wrapped line", -> - it "moves to the beginning of the previous line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - editor.setCursorScreenPosition([3, 0]) - editor.moveDown() - editor.moveDown() - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual [4, 4] - - describe ".moveDown()", -> - it "moves the cursor down", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual [3, 2] - - it "retains the goal column across lines of differing length", -> - editor.setCursorScreenPosition(row: 3, column: lineLengths[3]) - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[4] - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[5] - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[3] - - describe "when the cursor is on the last line", -> - it "moves the cursor to the end of line, but retains the goal column when moving back up", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - editor.setCursorScreenPosition(row: lastLineIndex, column: editor.getTabLength()) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual(row: lastLineIndex, column: lastLine.length) - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe editor.getTabLength() - - it "retains a goal column of 0 when moving back up", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - editor.setCursorScreenPosition(row: lastLineIndex, column: 0) - editor.moveDown() - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe 0 - - describe "when the cursor is at the beginning of an indented soft-wrapped line", -> - it "moves to the beginning of the line's continuation on the next screen row", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - editor.setCursorScreenPosition([3, 0]) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual [4, 4] - - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[4, 9], [5, 10]]) - - it "moves below the selection", -> - cursor = editor.getLastCursor() - editor.moveDown() - expect(cursor.getBufferPosition()).toEqual [6, 10] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([12, 2]) - editor.addCursorAtScreenPosition([11, 2]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveDown() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [12, 2] - - describe ".moveLeft()", -> - it "moves the cursor by one column to the left", -> - editor.setCursorScreenPosition([1, 8]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [1, 7] - - it "moves the cursor by n columns to the left", -> - editor.setCursorScreenPosition([1, 8]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [1, 4] - - it "moves the cursor by two rows up when the columnCount is longer than an entire line", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveLeft(34) - expect(editor.getCursorScreenPosition()).toEqual [0, 29] - - it "moves the cursor to the beginning columnCount is longer than the position in the buffer", -> - editor.setCursorScreenPosition([1, 0]) - editor.moveLeft(100) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - - describe "when the cursor is in the first column", -> - describe "when there is a previous line", -> - it "wraps to the end of the previous line", -> - editor.setCursorScreenPosition(row: 1, column: 0) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: buffer.lineForRow(0).length) - - it "moves the cursor by one row up and n columns to the left", -> - editor.setCursorScreenPosition([1, 0]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [0, 26] - - describe "when the next line is empty", -> - it "wraps to the beginning of the previous line", -> - editor.setCursorScreenPosition([11, 0]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [10, 0] - - describe "when line is wrapped and follow previous line indentation", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - it "wraps to the end of the previous line", -> - editor.setCursorScreenPosition([4, 4]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [3, 46] - - describe "when the cursor is on the first line", -> - it "remains in the same position (0,0)", -> - editor.setCursorScreenPosition(row: 0, column: 0) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - - it "remains in the same position (0,0) when columnCount is specified", -> - editor.setCursorScreenPosition([0, 0]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - - describe "when softTabs is enabled and the cursor is preceded by leading whitespace", -> - it "skips tabLength worth of whitespace at a time", -> - editor.setCursorBufferPosition([5, 6]) - - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [5, 4] - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[5, 22], [5, 27]]) - - it "moves to the left of the selection", -> - cursor = editor.getLastCursor() - editor.moveLeft() - expect(cursor.getBufferPosition()).toEqual [5, 22] - - editor.moveLeft() - expect(cursor.getBufferPosition()).toEqual [5, 21] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 1]) - - [cursor1, cursor2] = editor.getCursors() - editor.moveLeft() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - - describe ".moveRight()", -> - it "moves the cursor by one column to the right", -> - editor.setCursorScreenPosition([3, 3]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [3, 4] - - it "moves the cursor by n columns to the right", -> - editor.setCursorScreenPosition([3, 7]) - editor.moveRight(4) - expect(editor.getCursorScreenPosition()).toEqual [3, 11] - - it "moves the cursor by two rows down when the columnCount is longer than an entire line", -> - editor.setCursorScreenPosition([0, 29]) - editor.moveRight(34) - expect(editor.getCursorScreenPosition()).toEqual [2, 2] - - it "moves the cursor to the end of the buffer when columnCount is longer than the number of characters following the cursor position", -> - editor.setCursorScreenPosition([11, 5]) - editor.moveRight(100) - expect(editor.getCursorScreenPosition()).toEqual [12, 2] - - describe "when the cursor is on the last column of a line", -> - describe "when there is a subsequent line", -> - it "wraps to the beginning of the next line", -> - editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [1, 0] - - it "moves the cursor by one row down and n columns to the right", -> - editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) - editor.moveRight(4) - expect(editor.getCursorScreenPosition()).toEqual [1, 3] - - describe "when the next line is empty", -> - it "wraps to the beginning of the next line", -> - editor.setCursorScreenPosition([9, 4]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [10, 0] - - describe "when the cursor is on the last line", -> - it "remains in the same position", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - lastPosition = {row: lastLineIndex, column: lastLine.length} - editor.setCursorScreenPosition(lastPosition) - editor.moveRight() - - expect(editor.getCursorScreenPosition()).toEqual(lastPosition) - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[5, 22], [5, 27]]) - - it "moves to the left of the selection", -> - cursor = editor.getLastCursor() - editor.moveRight() - expect(cursor.getBufferPosition()).toEqual [5, 27] - - editor.moveRight() - expect(cursor.getBufferPosition()).toEqual [5, 28] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([12, 2]) - editor.addCursorAtScreenPosition([12, 1]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveRight() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [12, 2] - - describe ".moveToTop()", -> - it "moves the cursor to the top of the buffer", -> - editor.setCursorScreenPosition [11, 1] - editor.addCursorAtScreenPosition [12, 0] - editor.moveToTop() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".moveToBottom()", -> - it "moves the cursor to the bottom of the buffer", -> - editor.setCursorScreenPosition [0, 0] - editor.addCursorAtScreenPosition [1, 0] - editor.moveToBottom() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [12, 2] - - describe ".moveToBeginningOfScreenLine()", -> - describe "when soft wrap is on", -> - it "moves cursor to the beginning of the screen line", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToBeginningOfScreenLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [1, 0] - - describe "when soft wrap is off", -> - it "moves cursor to the beginning of the line", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - editor.moveToBeginningOfScreenLine() - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 0] - - describe ".moveToEndOfScreenLine()", -> - describe "when soft wrap is on", -> - it "moves cursor to the beginning of the screen line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToEndOfScreenLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [1, 9] - - describe "when soft wrap is off", -> - it "moves cursor to the end of line", -> - editor.setCursorScreenPosition [0, 0] - editor.addCursorAtScreenPosition [1, 0] - editor.moveToEndOfScreenLine() - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 29] - expect(cursor2.getBufferPosition()).toEqual [1, 30] - - describe ".moveToBeginningOfLine()", -> - it "moves cursor to the beginning of the buffer line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToBeginningOfLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [0, 0] - - describe ".moveToEndOfLine()", -> - it "moves cursor to the end of the buffer line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([0, 2]) - editor.moveToEndOfLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [4, 4] - - describe ".moveToFirstCharacterOfLine()", -> - describe "when soft wrap is on", -> - it "moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition [2, 5] - editor.addCursorAtScreenPosition [8, 7] - - editor.moveToFirstCharacterOfLine() - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getScreenPosition()).toEqual [2, 0] - expect(cursor2.getScreenPosition()).toEqual [8, 2] - - editor.moveToFirstCharacterOfLine() - expect(cursor1.getScreenPosition()).toEqual [2, 0] - expect(cursor2.getScreenPosition()).toEqual [8, 2] - - describe "when soft wrap is off", -> - it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - - editor.moveToFirstCharacterOfLine() - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 2] - - editor.moveToFirstCharacterOfLine() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 0] - - it "moves to the beginning of the line if it only contains whitespace ", -> - editor.setText("first\n \nthird") - editor.setCursorScreenPosition [1, 2] - editor.moveToFirstCharacterOfLine() - cursor = editor.getLastCursor() - expect(cursor.getBufferPosition()).toEqual [1, 0] - - describe "when invisible characters are enabled with soft tabs", -> - it "moves to the first character of the current line without being confused by the invisible characters", -> - editor.update({showInvisibles: true}) - editor.setCursorScreenPosition [1, 7] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - describe "when invisible characters are enabled with hard tabs", -> - it "moves to the first character of the current line without being confused by the invisible characters", -> - editor.update({showInvisibles: true}) - buffer.setTextInRange([[1, 0], [1, Infinity]], '\t\t\ta', normalizeLineEndings: false) - - editor.setCursorScreenPosition [1, 7] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 3] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - describe ".moveToBeginningOfWord()", -> - it "moves the cursor to the beginning of the word", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [1, 12] - editor.addCursorAtBufferPosition [3, 0] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToBeginningOfWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 11] - expect(cursor3.getBufferPosition()).toEqual [2, 39] - - it "does not fail at position [0, 0]", -> - editor.setCursorBufferPosition([0, 0]) - editor.moveToBeginningOfWord() - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([11, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "treats lines with only whitespace as a word (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([11, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [9, 2] - - it "works when the current line is blank (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [9, 2] - editor.buffer.setText(buffer.getText().replace(/\r\n/g, "\n")) - - describe ".moveToPreviousWordBoundary()", -> - it "moves the cursor to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 0] - editor.addCursorAtBufferPosition [2, 4] - editor.addCursorAtBufferPosition [3, 14] - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.moveToPreviousWordBoundary() - - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 30] - expect(cursor3.getBufferPosition()).toEqual [2, 0] - expect(cursor4.getBufferPosition()).toEqual [3, 13] - - describe ".moveToNextWordBoundary()", -> - it "moves the cursor to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 40] - editor.addCursorAtBufferPosition [3, 0] - editor.addCursorAtBufferPosition [3, 30] - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.moveToNextWordBoundary() - - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 0] - expect(cursor3.getBufferPosition()).toEqual [3, 4] - expect(cursor4.getBufferPosition()).toEqual [3, 31] - - describe ".moveToEndOfWord()", -> - it "moves the cursor to the end of the word", -> - editor.setCursorBufferPosition [0, 6] - editor.addCursorAtBufferPosition [1, 10] - editor.addCursorAtBufferPosition [2, 40] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToEndOfWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [1, 12] - expect(cursor3.getBufferPosition()).toEqual [3, 7] - - it "does not blow up when there is no next word", -> - editor.setCursorBufferPosition [Infinity, Infinity] - endPosition = editor.getCursorBufferPosition() - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual endPosition - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([9, 4]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "treats lines with only whitespace as a word (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([9, 4]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 8] - - it "works when the current line is blank (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([10, 0]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 8] - - describe ".moveToBeginningOfNextWord()", -> - it "moves the cursor before the first character of the next word", -> - editor.setCursorBufferPosition [0, 6] - editor.addCursorAtBufferPosition [1, 11] - editor.addCursorAtBufferPosition [2, 0] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToBeginningOfNextWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [1, 13] - expect(cursor3.getBufferPosition()).toEqual [2, 4] - - # When the cursor is on whitespace - editor.setText("ab cde- ") - editor.setCursorBufferPosition [0, 2] - cursor = editor.getLastCursor() - editor.moveToBeginningOfNextWord() - - expect(cursor.getBufferPosition()).toEqual [0, 3] - - it "does not blow up when there is no next word", -> - editor.setCursorBufferPosition [Infinity, Infinity] - endPosition = editor.getCursorBufferPosition() - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual endPosition - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([9, 4]) - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 9] - - describe ".moveToPreviousSubwordBoundary", -> - it "does not move the cursor when there is no previous subword boundary", -> - editor.setText('') - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - it "stops at word and underscore boundaries", -> - editor.setText("sub_word \n") - editor.setCursorBufferPosition([0, 9]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 8]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - editor.setText(" word\n") - editor.setCursorBufferPosition([0, 3]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "stops at camelCase boundaries", -> - editor.setText(" getPreviousWord\n") - editor.setCursorBufferPosition([0, 16]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 12]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "skips consecutive non-word characters", -> - editor.setText("e, => \n") - editor.setCursorBufferPosition([0, 6]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "skips consecutive uppercase characters", -> - editor.setText(" AAADF \n") - editor.setCursorBufferPosition([0, 7]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 6]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.setText("ALPhA\n") - editor.setCursorBufferPosition([0, 4]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - - it "skips consecutive numbers", -> - editor.setText(" 88 \n") - editor.setCursorBufferPosition([0, 4]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "works with multiple cursors", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 8]) - editor.addCursorAtBufferPosition([1, 13]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveToPreviousSubwordBoundary() - - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) - - it "works with non-English characters", -> - editor.setText("supåTøåst \n") - editor.setCursorBufferPosition([0, 9]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.setText("supaÖast \n") - editor.setCursorBufferPosition([0, 8]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe ".moveToNextSubwordBoundary", -> - it "does not move the cursor when there is no next subword boundary", -> - editor.setText('') - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - it "stops at word and underscore boundaries", -> - editor.setText(" sub_word \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 9]) - - editor.setText("word \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - it "stops at camelCase boundaries", -> - editor.setText("getPreviousWord \n") - editor.setCursorBufferPosition([0, 0]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 11]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 15]) - - it "skips consecutive non-word characters", -> - editor.setText(", => \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - it "skips consecutive uppercase characters", -> - editor.setText(" AAADF \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 6]) - - editor.setText("ALPhA\n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - - it "skips consecutive numbers", -> - editor.setText(" 88 \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - it "works with multiple cursors", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveToNextSubwordBoundary() - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) - - it "works with non-English characters", -> - editor.setText("supåTøåst \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.setText("supaÖast \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe ".moveToBeginningOfNextParagraph()", -> - it "moves the cursor before the first line of the next paragraph", -> - editor.setCursorBufferPosition [0, 6] - editor.foldBufferRow(4) - - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "moves the cursor before the first line of the next paragraph (CRLF line endings)", -> - editor.setText(editor.getText().replace(/\n/g, '\r\n')) - - editor.setCursorBufferPosition [0, 6] - editor.foldBufferRow(4) - - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".moveToBeginningOfPreviousParagraph()", -> - it "moves the cursor before the first line of the previous paragraph", -> - editor.setCursorBufferPosition [10, 0] - editor.foldBufferRow(4) - - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "moves the cursor before the first line of the previous paragraph (CRLF line endings)", -> - editor.setText(editor.getText().replace(/\n/g, '\r\n')) - - editor.setCursorBufferPosition [10, 0] - editor.foldBufferRow(4) - - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".getCurrentParagraphBufferRange()", -> - it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> - buffer.setText """ - I am the first paragraph, - bordered by the beginning of - the file - #{' '} - - I am the second paragraph - with blank lines above and below - me. - - I am the last paragraph, - bordered by the end of the file. - """ - - # in a paragraph - editor.setCursorBufferPosition([1, 7]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[0, 0], [2, 8]] - - editor.setCursorBufferPosition([7, 1]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[5, 0], [7, 3]] - - editor.setCursorBufferPosition([9, 10]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[9, 0], [10, 32]] - - # between paragraphs - editor.setCursorBufferPosition([3, 1]) - expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() - - it 'will limit paragraph range to comments', -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) - editor.setText(""" - var quicksort = function () { - /* Single line comment block */ - var sort = function(items) {}; - - /* - A multiline - comment is here - */ - var sort = function(items) {}; - - // A comment - // - // Multiple comment - // lines - var sort = function(items) {}; - // comment line after fn - - var nosort = function(items) { - item; - } - - }; - """) - - paragraphBufferRangeForRow = (row) -> - editor.setCursorBufferPosition([row, 0]) - editor.getLastCursor().getCurrentParagraphBufferRange() - - expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]]) - expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]]) - expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]]) - expect(paragraphBufferRangeForRow(3)).toBeFalsy() - expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]]) - expect(paragraphBufferRangeForRow(9)).toBeFalsy() - expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]]) - expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]]) - expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]]) - - describe "getCursorAtScreenPosition(screenPosition)", -> - it "returns the cursor at the given screenPosition", -> - cursor1 = editor.addCursorAtScreenPosition([0, 2]) - cursor2 = editor.getCursorAtScreenPosition(cursor1.getScreenPosition()) - expect(cursor2).toBe cursor1 - - describe "::getCursorScreenPositions()", -> - it "returns the cursor positions in the order they were added", -> - editor.foldBufferRow(4) - cursor1 = editor.addCursorAtBufferPosition([8, 5]) - cursor2 = editor.addCursorAtBufferPosition([3, 5]) - expect(editor.getCursorScreenPositions()).toEqual [[0, 0], [5, 5], [3, 5]] - - describe "::getCursorsOrderedByBufferPosition()", -> - it "returns all cursors ordered by buffer positions", -> - originalCursor = editor.getLastCursor() - cursor1 = editor.addCursorAtBufferPosition([8, 5]) - cursor2 = editor.addCursorAtBufferPosition([4, 5]) - expect(editor.getCursorsOrderedByBufferPosition()).toEqual [originalCursor, cursor2, cursor1] - - describe "addCursorAtScreenPosition(screenPosition)", -> - describe "when a cursor already exists at the position", -> - it "returns the existing cursor", -> - cursor1 = editor.addCursorAtScreenPosition([0, 2]) - cursor2 = editor.addCursorAtScreenPosition([0, 2]) - expect(cursor2).toBe cursor1 - - describe "addCursorAtBufferPosition(bufferPosition)", -> - describe "when a cursor already exists at the position", -> - it "returns the existing cursor", -> - cursor1 = editor.addCursorAtBufferPosition([1, 4]) - cursor2 = editor.addCursorAtBufferPosition([1, 4]) - expect(cursor2.marker).toBe cursor1.marker - - describe '.getCursorScope()', -> - it 'returns the current scope', -> - descriptor = editor.getCursorScope() - expect(descriptor.scopes).toContain('source.js') - - describe "selection", -> - selection = null - - beforeEach -> - selection = editor.getLastSelection() - - describe ".getLastSelection()", -> - it "creates a new selection at (0, 0) if the last selection has been destroyed", -> - editor.getLastSelection().destroy() - expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) - - it "doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", -> - callCount = 0 - editor.getLastSelection().destroy() - editor.onDidAddCursor (cursor) -> - callCount++ - editor.getLastSelection() - expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) - expect(callCount).toBe(1) - - describe ".getSelections()", -> - it "creates a new selection at (0, 0) if the last selection has been destroyed", -> - editor.getLastSelection().destroy() - expect(editor.getSelections()[0].getBufferRange()).toEqual([[0, 0], [0, 0]]) - - describe "when the selection range changes", -> - it "emits an event with the old range, new range, and the selection that moved", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - - editor.onDidChangeSelectionRange rangeChangedHandler = jasmine.createSpy() - editor.selectToBufferPosition([6, 2]) - - expect(rangeChangedHandler).toHaveBeenCalled() - eventObject = rangeChangedHandler.mostRecentCall.args[0] - - expect(eventObject.oldBufferRange).toEqual [[3, 0], [4, 5]] - expect(eventObject.oldScreenRange).toEqual [[3, 0], [4, 5]] - expect(eventObject.newBufferRange).toEqual [[3, 0], [6, 2]] - expect(eventObject.newScreenRange).toEqual [[3, 0], [6, 2]] - expect(eventObject.selection).toBe selection - - describe ".selectUp/Down/Left/Right()", -> - it "expands each selection to its cursor's new location", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 14]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 22]] - - editor.selectLeft() - editor.selectLeft() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - editor.selectDown() - expect(selection1.getBufferRange()).toEqual [[0, 9], [1, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [4, 20]] - - editor.selectUp() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - it "merges selections when they intersect when moving down", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]], [[2, 15], [3, 25]]]) - [selection1, selection2, selection3] = editor.getSelections() - - editor.selectDown() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]) - expect(selection1.isReversed()).toBeFalsy() - - it "merges selections when they intersect when moving up", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]]], reversed: true) - [selection1, selection2] = editor.getSelections() - - editor.selectUp() - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]) - expect(selection1.isReversed()).toBeTruthy() - - it "merges selections when they intersect when moving left", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[0, 13], [1, 20]]], reversed: true) - [selection1, selection2] = editor.getSelections() - - editor.selectLeft() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]) - expect(selection1.isReversed()).toBeTruthy() - - it "merges selections when they intersect when moving right", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 14]], [[0, 14], [1, 20]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) - expect(selection1.isReversed()).toBeFalsy() - - describe "when counts are passed into the selection functions", -> - it "expands each selection to its cursor's new location", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight(2) - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 15]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 23]] - - editor.selectLeft(3) - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - editor.selectDown(3) - expect(selection1.getBufferRange()).toEqual [[0, 9], [3, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [6, 20]] - - editor.selectUp(2) - expect(selection1.getBufferRange()).toEqual [[0, 9], [1, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [4, 20]] - - describe ".selectToBufferPosition(bufferPosition)", -> - it "expands the last selection to the given position", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtBufferPosition([5, 6]) - editor.selectToBufferPosition([6, 2]) - - selections = editor.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getBufferRange()).toEqual [[3, 0], [4, 5]] - expect(selection2.getBufferRange()).toEqual [[5, 6], [6, 2]] - - describe ".selectToScreenPosition(screenPosition)", -> - it "expands the last selection to the given position", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - selections = editor.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getScreenRange()).toEqual [[3, 0], [4, 5]] - expect(selection2.getScreenRange()).toEqual [[5, 6], [6, 2]] - - describe "when selecting with an initial screen range", -> - it "switches the direction of the selection when selecting to positions before/after the start of the initial range", -> - editor.setCursorScreenPosition([5, 10]) - editor.selectWordsContainingCursors() - editor.selectToScreenPosition([3, 0]) - expect(editor.getLastSelection().isReversed()).toBe true - editor.selectToScreenPosition([9, 0]) - expect(editor.getLastSelection().isReversed()).toBe false - - describe ".selectToBeginningOfNextParagraph()", -> - it "selects from the cursor to first line of the next paragraph", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - editor.selectToBeginningOfNextParagraph() - - selections = editor.getSelections() - expect(selections.length).toBe 1 - expect(selections[0].getScreenRange()).toEqual [[3, 0], [10, 0]] - - describe ".selectToBeginningOfPreviousParagraph()", -> - it "selects from the cursor to the first line of the previous paragraph", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - editor.selectToBeginningOfPreviousParagraph() - - selections = editor.getSelections() - expect(selections.length).toBe 1 - expect(selections[0].getScreenRange()).toEqual [[0, 0], [5, 6]] - - it "merges selections if they intersect, maintaining the directionality of the last selection", -> - editor.setCursorScreenPosition([4, 10]) - editor.selectToScreenPosition([5, 27]) - editor.addCursorAtScreenPosition([3, 10]) - editor.selectToScreenPosition([6, 27]) - - selections = editor.getSelections() - expect(selections.length).toBe 1 - [selection1] = selections - expect(selection1.getScreenRange()).toEqual [[3, 10], [6, 27]] - expect(selection1.isReversed()).toBeFalsy() - - editor.addCursorAtScreenPosition([7, 4]) - editor.selectToScreenPosition([4, 11]) - - selections = editor.getSelections() - expect(selections.length).toBe 1 - [selection1] = selections - expect(selection1.getScreenRange()).toEqual [[3, 10], [7, 4]] - expect(selection1.isReversed()).toBeTruthy() - - describe ".selectToTop()", -> - it "selects text from cursor position to the top of the buffer", -> - editor.setCursorScreenPosition [11, 2] - editor.addCursorAtScreenPosition [10, 0] - editor.selectToTop() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - expect(editor.getLastSelection().getBufferRange()).toEqual [[0, 0], [11, 2]] - expect(editor.getLastSelection().isReversed()).toBeTruthy() - - describe ".selectToBottom()", -> - it "selects text from cursor position to the bottom of the buffer", -> - editor.setCursorScreenPosition [10, 0] - editor.addCursorAtScreenPosition [9, 3] - editor.selectToBottom() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [12, 2] - expect(editor.getLastSelection().getBufferRange()).toEqual [[9, 3], [12, 2]] - expect(editor.getLastSelection().isReversed()).toBeFalsy() - - describe ".selectAll()", -> - it "selects the entire buffer", -> - editor.selectAll() - expect(editor.getLastSelection().getBufferRange()).toEqual buffer.getRange() - - describe ".selectToBeginningOfLine()", -> - it "selects text from cursor position to beginning of line", -> - editor.setCursorScreenPosition [12, 2] - editor.addCursorAtScreenPosition [11, 3] - - editor.selectToBeginningOfLine() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [12, 0] - expect(cursor2.getBufferPosition()).toEqual [11, 0] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[12, 0], [12, 2]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[11, 0], [11, 3]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".selectToEndOfLine()", -> - it "selects text from cursor position to end of line", -> - editor.setCursorScreenPosition [12, 0] - editor.addCursorAtScreenPosition [11, 3] - - editor.selectToEndOfLine() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [12, 2] - expect(cursor2.getBufferPosition()).toEqual [11, 44] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[12, 0], [12, 2]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[11, 3], [11, 44]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectLinesContainingCursors()", -> - it "selects to the entire line (including newlines) at given row", -> - editor.setCursorScreenPosition([1, 2]) - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [2, 0]] - expect(editor.getSelectedText()).toBe " var sort = function(items) {\n" - - editor.setCursorScreenPosition([12, 2]) - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[12, 0], [12, 2]] - - editor.setCursorBufferPosition([0, 2]) - editor.selectLinesContainingCursors() - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [2, 0]] - - describe "when the selection spans multiple row", -> - it "selects from the beginning of the first line to the last line", -> - selection = editor.getLastSelection() - selection.setBufferRange [[1, 10], [3, 20]] - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [4, 0]] - - describe ".selectToBeginningOfWord()", -> - it "selects text from cursor position to beginning of word", -> - editor.setCursorScreenPosition [0, 13] - editor.addCursorAtScreenPosition [3, 49] - - editor.selectToBeginningOfWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [3, 47] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 13]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[3, 47], [3, 49]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".selectToEndOfWord()", -> - it "selects text from cursor position to end of word", -> - editor.setCursorScreenPosition [0, 4] - editor.addCursorAtScreenPosition [3, 48] - - editor.selectToEndOfWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 50] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 13]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3, 48], [3, 50]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectToBeginningOfNextWord()", -> - it "selects text from cursor position to beginning of next word", -> - editor.setCursorScreenPosition [0, 4] - editor.addCursorAtScreenPosition [3, 48] - - editor.selectToBeginningOfNextWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [3, 51] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 14]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3, 48], [3, 51]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectToPreviousWordBoundary()", -> - it "select to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 0] - editor.addCursorAtBufferPosition [3, 4] - editor.addCursorAtBufferPosition [3, 14] - - editor.selectToPreviousWordBoundary() - - expect(editor.getSelections().length).toBe 4 - [selection1, selection2, selection3, selection4] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 8], [0, 4]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[2, 0], [1, 30]] - expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual [[3, 4], [3, 0]] - expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual [[3, 14], [3, 13]] - expect(selection4.isReversed()).toBeTruthy() - - describe ".selectToNextWordBoundary()", -> - it "select to the next word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 40] - editor.addCursorAtBufferPosition [4, 0] - editor.addCursorAtBufferPosition [3, 30] - - editor.selectToNextWordBoundary() - - expect(editor.getSelections().length).toBe 4 - [selection1, selection2, selection3, selection4] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 8], [0, 13]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[2, 40], [3, 0]] - expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual [[4, 0], [4, 4]] - expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual [[3, 30], [3, 31]] - expect(selection4.isReversed()).toBeFalsy() - - describe ".selectToPreviousSubwordBoundary", -> - it "selects subwords", -> - editor.setText("") - editor.insertText("_word\n") - editor.insertText(" getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 5]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 5]) - editor.addCursorAtBufferPosition([3, 3]) - [selection1, selection2, selection3, selection4] = editor.getSelections() - - editor.selectToPreviousSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) - expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) - expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) - expect(selection4.isReversed()).toBeTruthy() - - describe ".selectToNextSubwordBoundary", -> - it "selects subwords", -> - editor.setText("") - editor.insertText("word_\n") - editor.insertText("getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 1]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 2]) - editor.addCursorAtBufferPosition([3, 1]) - [selection1, selection2, selection3, selection4] = editor.getSelections() - - editor.selectToNextSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) - expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) - expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) - expect(selection4.isReversed()).toBeFalsy() - - describe ".deleteToBeginningOfSubword", -> - it "deletes subwords", -> - editor.setText("") - editor.insertText("_word\n") - editor.insertText(" getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 5]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 5]) - editor.addCursorAtBufferPosition([3, 3]) - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('_') - expect(buffer.lineForRow(1)).toBe(' getviousWord') - expect(buffer.lineForRow(2)).toBe('e, ') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 1]) - expect(cursor2.getBufferPosition()).toEqual([1, 4]) - expect(cursor3.getBufferPosition()).toEqual([2, 3]) - expect(cursor4.getBufferPosition()).toEqual([3, 1]) - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe(' viousWord') - expect(buffer.lineForRow(2)).toBe('e ') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 1]) - expect(cursor3.getBufferPosition()).toEqual([2, 1]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe('viousWord') - expect(buffer.lineForRow(2)).toBe(' ') - expect(buffer.lineForRow(3)).toBe('') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 0]) - expect(cursor4.getBufferPosition()).toEqual([2, 1]) - - describe ".deleteToEndOfSubword", -> - it "deletes subwords", -> - editor.setText("") - editor.insertText("word_\n") - editor.insertText("getPreviousWord \n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - editor.addCursorAtBufferPosition([2, 2]) - editor.addCursorAtBufferPosition([3, 0]) - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('_') - expect(buffer.lineForRow(1)).toBe('PreviousWord ') - expect(buffer.lineForRow(2)).toBe('e, ') - expect(buffer.lineForRow(3)).toBe('88 ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 2]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe('Word ') - expect(buffer.lineForRow(2)).toBe('e,') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 2]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - describe ".selectWordsContainingCursors()", -> - describe "when the cursor is inside a word", -> - it "selects the entire word", -> - editor.setCursorScreenPosition([0, 8]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'quicksort' - - describe "when the cursor is between two words", -> - it "selects the word the cursor is on", -> - editor.setCursorScreenPosition([0, 4]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.setCursorScreenPosition([0, 3]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'var' - - describe "when the cursor is inside a region of whitespace", -> - it "selects the whitespace region", -> - editor.setCursorScreenPosition([5, 2]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] - - editor.setCursorScreenPosition([5, 0]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] - - describe "when the cursor is at the end of the text", -> - it "select the previous word", -> - editor.buffer.append 'word' - editor.moveToBottom() - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]] - - it "selects words based on the non-word characters configured at the cursor's current scope", -> - editor.setText("one-one; 'two-two'; three-three") - - editor.setCursorBufferPosition([0, 1]) - editor.addCursorAtBufferPosition([0, 12]) - - scopeDescriptors = editor.getCursors().map (c) -> c.getScopeDescriptor() - expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js']) - expect(scopeDescriptors[1].getScopesArray()).toEqual(['source.js', 'string.quoted.single.js']) - - editor.setScopedSettingsDelegate({ - getNonWordCharacters: (scopes) -> - result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?' - if (scopes.some (scope) -> scope.startsWith('string')) - result - else - result + '-' - }) - - editor.selectWordsContainingCursors() - - expect(editor.getSelections()[0].getText()).toBe('one') - expect(editor.getSelections()[1].getText()).toBe('two-two') - - describe ".selectToFirstCharacterOfLine()", -> - it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - - editor.selectToFirstCharacterOfLine() - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 2] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 0], [0, 5]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[1, 2], [1, 7]] - expect(selection2.isReversed()).toBeTruthy() - - editor.selectToFirstCharacterOfLine() - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 0], [0, 5]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[1, 0], [1, 7]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".setSelectedBufferRanges(ranges)", -> - it "clears existing selections and creates selections for each of the given ranges", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[4, 4], [5, 5]]] - - editor.setSelectedBufferRanges([[[5, 5], [6, 6]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[5, 5], [6, 6]]] - - it "merges intersecting selections", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - - it "does not merge non-empty adjacent selections", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[3, 3], [5, 5]]] - - it "recycles existing selection instances", -> - selection = editor.getLastSelection() - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) - - [selection1, selection2] = editor.getSelections() - expect(selection1).toBe selection - expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]] - - describe "when the 'preserveFolds' option is false (the default)", -> - 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) - editor.foldBufferRowRange(6, 8) - editor.foldBufferRowRange(10, 11) - - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) - expect(editor.isFoldedAtScreenRow(1)).toBeFalsy() - expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() - 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]]) - editor.foldBufferRowRange(1, 4) - editor.foldBufferRowRange(6, 8) - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true) - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - - describe ".setSelectedScreenRanges(ranges)", -> - beforeEach -> - editor.foldBufferRow(4) - - it "clears existing selections and creates selections for each of the given ranges", -> - editor.setSelectedScreenRanges([[[3, 4], [3, 7]], [[5, 4], [5, 7]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[3, 4], [3, 7]], [[8, 4], [8, 7]]] - - editor.setSelectedScreenRanges([[[6, 2], [6, 4]]]) - expect(editor.getSelectedScreenRanges()).toEqual [[[6, 2], [6, 4]]] - - it "merges intersecting selections and unfolds the fold which contain them", -> - editor.foldBufferRow(0) - - # Use buffer ranges because only the first line is on screen - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - - it "recycles existing selection instances", -> - selection = editor.getLastSelection() - editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]]) - - [selection1, selection2] = editor.getSelections() - expect(selection1).toBe selection - expect(selection1.getScreenRange()).toEqual [[2, 2], [3, 4]] - - describe ".selectMarker(marker)", -> - describe "if the marker is valid", -> - it "selects the marker's range and returns the selected range", -> - marker = editor.markBufferRange([[0, 1], [3, 3]]) - expect(editor.selectMarker(marker)).toEqual [[0, 1], [3, 3]] - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 3]] - - describe "if the marker is invalid", -> - it "does not change the selection and returns a falsy value", -> - marker = editor.markBufferRange([[0, 1], [3, 3]]) - marker.destroy() - expect(editor.selectMarker(marker)).toBeFalsy() - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 0]] - - describe ".addSelectionForBufferRange(bufferRange)", -> - it "adds a selection for the specified buffer range", -> - editor.addSelectionForBufferRange([[3, 4], [5, 6]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 0]], [[3, 4], [5, 6]]] - - describe ".addSelectionBelow()", -> - describe "when the selection is non-empty", -> - it "selects the same region of the line below current selections if possible", -> - editor.setSelectedBufferRange([[3, 16], [3, 21]]) - editor.addSelectionForBufferRange([[3, 25], [3, 34]]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 16], [3, 21]] - [[3, 25], [3, 34]] - [[4, 16], [4, 21]] - [[4, 25], [4, 29]] - ] - - it "skips lines that are too short to create a non-empty selection", -> - editor.setSelectedBufferRange([[3, 31], [3, 38]]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 31], [3, 38]] - [[6, 31], [6, 38]] - ] - - it "honors the original selection's range (goal range) when adding across shorter lines", -> - editor.setSelectedBufferRange([[3, 22], [3, 38]]) - editor.addSelectionBelow() - editor.addSelectionBelow() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 38]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] - [[6, 22], [6, 38]] - ] - - it "clears selection goal ranges when the selection changes", -> - editor.setSelectedBufferRange([[3, 22], [3, 38]]) - editor.addSelectionBelow() - editor.selectLeft() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 28]] - ] - - # goal range from previous add selection is honored next time - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] # select to end of line 5 because line 4's goal range was reset by line 3 previously - [[6, 22], [6, 28]] - ] - - it "can add selections to soft-wrapped line segments", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(40) - editor.setDefaultCharWidth(1) - - editor.setSelectedScreenRange([[3, 10], [3, 15]]) - editor.addSelectionBelow() - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 10], [3, 15]] - [[4, 10], [4, 15]] - ] - - it "takes atomic tokens into account", -> - waitsForPromise -> - atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o - - runs -> - editor.setSelectedBufferRange([[2, 1], [2, 3]]) - editor.addSelectionBelow() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 1], [2, 3]] - [[3, 1], [3, 2]] - ] - - describe "when the selection is empty", -> - describe "when lines are soft-wrapped", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - it "skips soft-wrap indentation tokens", -> - editor.setCursorScreenPosition([3, 0]) - editor.addSelectionBelow() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 0], [3, 0]] - [[4, 4], [4, 4]] - ] - - it "does not skip them if they're shorter than the current column", -> - editor.setCursorScreenPosition([3, 37]) - editor.addSelectionBelow() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 37], [3, 37]] - [[4, 26], [4, 26]] - ] - - it "does not skip lines that are shorter than the current column", -> - editor.setCursorBufferPosition([3, 36]) - editor.addSelectionBelow() - editor.addSelectionBelow() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 36], [3, 36]] - [[4, 29], [4, 29]] - [[5, 30], [5, 30]] - [[6, 36], [6, 36]] - ] - - it "skips empty lines when the column is non-zero", -> - editor.setCursorBufferPosition([9, 4]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[9, 4], [9, 4]] - [[11, 4], [11, 4]] - ] - - it "does not skip empty lines when the column is zero", -> - editor.setCursorBufferPosition([9, 0]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[9, 0], [9, 0]] - [[10, 0], [10, 0]] - ] - - describe ".addSelectionAbove()", -> - describe "when the selection is non-empty", -> - it "selects the same region of the line above current selections if possible", -> - editor.setSelectedBufferRange([[3, 16], [3, 21]]) - editor.addSelectionForBufferRange([[3, 37], [3, 44]]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 16], [3, 21]] - [[3, 37], [3, 44]] - [[2, 16], [2, 21]] - [[2, 37], [2, 40]] - ] - - it "skips lines that are too short to create a non-empty selection", -> - editor.setSelectedBufferRange([[6, 31], [6, 38]]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 31], [6, 38]] - [[3, 31], [3, 38]] - ] - - it "honors the original selection's range (goal range) when adding across shorter lines", -> - editor.setSelectedBufferRange([[6, 22], [6, 38]]) - editor.addSelectionAbove() - editor.addSelectionAbove() - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 22], [6, 38]] - [[5, 22], [5, 30]] - [[4, 22], [4, 29]] - [[3, 22], [3, 38]] - ] - - it "can add selections to soft-wrapped line segments", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - editor.setSelectedScreenRange([[4, 10], [4, 15]]) - editor.addSelectionAbove() - expect(editor.getSelectedScreenRanges()).toEqual [ - [[4, 10], [4, 15]] - [[3, 10], [3, 15]] - ] - - it "takes atomic tokens into account", -> - waitsForPromise -> - atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o - - runs -> - editor.setSelectedBufferRange([[3, 1], [3, 2]]) - editor.addSelectionAbove() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 1], [3, 2]] - [[2, 1], [2, 3]] - ] - - describe "when the selection is empty", -> - describe "when lines are soft-wrapped", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - it "skips soft-wrap indentation tokens", -> - editor.setCursorScreenPosition([5, 0]) - editor.addSelectionAbove() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[5, 0], [5, 0]] - [[4, 4], [4, 4]] - ] - - it "does not skip them if they're shorter than the current column", -> - editor.setCursorScreenPosition([5, 29]) - editor.addSelectionAbove() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[5, 29], [5, 29]] - [[4, 26], [4, 26]] - ] - - it "does not skip lines that are shorter than the current column", -> - editor.setCursorBufferPosition([6, 36]) - editor.addSelectionAbove() - editor.addSelectionAbove() - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 36], [6, 36]] - [[5, 30], [5, 30]] - [[4, 29], [4, 29]] - [[3, 36], [3, 36]] - ] - - it "skips empty lines when the column is non-zero", -> - editor.setCursorBufferPosition([11, 4]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[11, 4], [11, 4]] - [[9, 4], [9, 4]] - ] - - it "does not skip empty lines when the column is zero", -> - editor.setCursorBufferPosition([10, 0]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[10, 0], [10, 0]] - [[9, 0], [9, 0]] - ] - - describe ".splitSelectionsIntoLines()", -> - it "splits all multi-line selections into one selection per line", -> - editor.setSelectedBufferRange([[0, 3], [2, 4]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 29]] - [[1, 0], [1, 30]] - [[2, 0], [2, 4]] - ] - - editor.setSelectedBufferRange([[0, 3], [1, 10]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 29]] - [[1, 0], [1, 10]] - ] - - editor.setSelectedBufferRange([[0, 0], [0, 3]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]]] - - describe "::consolidateSelections()", -> - makeMultipleSelections = -> - selection.setBufferRange [[3, 16], [3, 21]] - selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]]) - selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]]) - selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]]) - expect(editor.getSelections()).toEqual [selection, selection2, selection3, selection4] - [selection, selection2, selection3, selection4] - - it "destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed", -> - [selection1] = makeMultipleSelections() - - autoscrollEvents = [] - editor.onDidRequestAutoscroll (event) -> autoscrollEvents.push(event) - - expect(editor.consolidateSelections()).toBeTruthy() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.isEmpty()).toBeFalsy() - expect(editor.consolidateSelections()).toBeFalsy() - expect(editor.getSelections()).toEqual [selection1] - - expect(autoscrollEvents).toEqual([ - {screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}} - ]) - - describe "when the cursor is moved while there is a selection", -> - makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] - - it "clears the selection", -> - makeSelection() - editor.moveDown() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveUp() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveLeft() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveRight() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.setCursorScreenPosition([3, 3]) - expect(selection.isEmpty()).toBeTruthy() - - it "does not share selections between different edit sessions for the same buffer", -> - editor2 = null - waitsForPromise -> - atom.workspace.getActivePane().splitRight() - atom.workspace.open(editor.getPath()).then (o) -> editor2 = o - - runs -> - expect(editor2.getText()).toBe(editor.getText()) - editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) - editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) - expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() - - describe "buffer manipulation", -> - describe ".moveLineUp", -> - it "moves the line under the cursor up", -> - editor.setCursorBufferPosition([1, 0]) - editor.moveLineUp() - expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe " var sort = function(items) {" - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 0 - - it "updates the line's indentation when the the autoIndent setting is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([1, 0]) - editor.moveLineUp() - expect(editor.indentationForBufferRow(0)).toBe 0 - expect(editor.indentationForBufferRow(1)).toBe 0 - - describe "when there is a single selection", -> - describe "when the selection spans a single line", -> - describe "when there is no fold in the preceeding row", -> - it "moves the line to the preceding row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.setSelectedBufferRange([[3, 2], [3, 9]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is at the beginning of a fold", -> - it "moves the line to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true) - expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [4, 9]] - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - - describe "when the preceding row consists of folded code", -> - it "moves the line above the folded row and perseveres the correct folds", -> - expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[8, 0], [8, 4]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [4, 4]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the selection spans multiple lines", -> - it "moves the lines spanned by the selection to the preceding row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[3, 2], [4, 9]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " if (items.length <= 1) return items;" - - describe "when the selection's end intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " if (items.length <= 1) return items;" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - describe "when the selection's start intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [7, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(8)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - - describe "when the selection spans multiple lines, but ends at column 0", -> - it "does not move the last line of the selection", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[3, 2], [4, 0]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 0]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - describe "when the preceeding row is a folded row", -> - it "moves the lines spanned by the selection to the preceeding row, but preserves the folded code", -> - expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - editor.foldBufferRowRange(4, 7) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[8, 0], [9, 2]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [5, 2]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " };" - expect(editor.lineTextForBufferRow(6)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(5)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - describe "when there are multiple selections", -> - describe "when all the selections span different lines", -> - describe "when there is no folds", -> - it "moves all lines that are spanned by a selection to the preceding row", -> - editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]] - expect(editor.lineTextForBufferRow(0)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(1)).toBe "var quicksort = function () {" - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " current = items.shift();" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - describe "when one selection intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRanges([ - [[2, 2], [2, 9]], - [[4, 2], [4, 9]] - ], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 2], [1, 9]], - [[3, 2], [3, 9]] - ]) - - expect(editor.lineTextForBufferRow(1)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - describe "when there is a fold", -> - it "moves all lines that spanned by a selection to preceding row, preserving all folds", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[8, 0], [8, 3]], [[11, 0], [11, 5]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[4, 0], [4, 3]], [[10, 0], [10, 5]]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(10)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe 'when there are many folds', -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-many-folds.js', autoIndent: false).then (o) -> editor = o - - describe 'and many selections intersects folded rows', -> - it 'moves and preserves all the folds', -> - editor.foldBufferRowRange(2, 4) - editor.foldBufferRowRange(7, 9) - - editor.setSelectedBufferRanges([ - [[1, 0], [5, 4]], - [[7, 0], [7, 4]] - ], preserveFolds: true) - - editor.moveLineUp() - - expect(editor.lineTextForBufferRow(1)).toEqual "function f3() {" - expect(editor.lineTextForBufferRow(4)).toEqual "6;" - expect(editor.lineTextForBufferRow(5)).toEqual "1;" - expect(editor.lineTextForBufferRow(6)).toEqual "function f8() {" - expect(editor.lineTextForBufferRow(9)).toEqual "7;" - - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when some of the selections span the same lines", -> - it "moves lines that contain multiple selections correctly", -> - editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [2, 9]], [[2, 12], [2, 13]]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when one of the selections spans line 0", -> - it "doesn't move any lines, since line 0 can't move", -> - editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]] - expect(buffer.isModified()).toBe false - - describe "when one of the selections spans the last line, and it is empty", -> - it "doesn't move any lines, since the last line can't move", -> - buffer.append('\n') - editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]] - - describe ".moveLineDown", -> - it "moves the line under the cursor down", -> - editor.setCursorBufferPosition([0, 0]) - editor.moveLineDown() - expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe "var quicksort = function () {" - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 0 - - it "updates the line's indentation when the editor.autoIndent setting is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([0, 0]) - editor.moveLineDown() - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 2 - - describe "when there is a single selection", -> - describe "when the selection spans a single line", -> - describe "when there is no fold in the following row", -> - it "moves the line to the following row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.setSelectedBufferRange([[2, 2], [2, 9]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is at the beginning of a fold", -> - it "moves the line to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[5, 2], [5, 9]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the following row is a folded row", -> - it "moves the line below the folded row and preserves the fold", -> - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[3, 0], [3, 4]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[7, 0], [7, 4]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when the selection spans multiple lines", -> - it "moves the lines spanned by the selection to the following row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[2, 2], [3, 9]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [4, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when the selection spans multiple lines, but ends at column 0", -> - it "does not move the last line of the selection", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[2, 2], [3, 0]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [4, 0]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - describe "when the selection's end intersects a fold", -> - it "moves the lines to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [5, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the selection's start intersects a fold", -> - it "moves the lines to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[5, 2], [9, 9]] - expect(editor.lineTextForBufferRow(4)).toBe " };" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(9)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - describe "when the following row is a folded row", -> - it "moves the lines spanned by the selection to the following row, but preserves the folded code", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.foldBufferRowRange(4, 7) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[2, 0], [3, 2]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[6, 0], [7, 2]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - - describe "when the last line of selection does not end with a valid line ending", -> - it "appends line ending to last line and moves the lines spanned by the selection to the preceeding row", -> - expect(editor.lineTextForBufferRow(9)).toBe " };" - expect(editor.lineTextForBufferRow(10)).toBe "" - expect(editor.lineTextForBufferRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.lineTextForBufferRow(12)).toBe "};" - - editor.setSelectedBufferRange([[10, 0], [12, 2]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[9, 0], [11, 2]] - expect(editor.lineTextForBufferRow(9)).toBe "" - expect(editor.lineTextForBufferRow(10)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.lineTextForBufferRow(11)).toBe "};" - expect(editor.lineTextForBufferRow(12)).toBe " };" - - describe "when there are multiple selections", -> - describe "when all the selections span different lines", -> - describe "when there is no folds", -> - it "moves all lines that are spanned by a selection to the following row", -> - editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[6, 2], [6, 9]], [[4, 2], [4, 9]], [[2, 2], [2, 9]]] - expect(editor.lineTextForBufferRow(1)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(6)).toBe " current = items.shift();" - - describe 'when there are many folds', -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-many-folds.js', autoIndent: false).then (o) -> editor = o - - describe 'and many selections intersects folded rows', -> - it 'moves and preserves all the folds', -> - editor.foldBufferRowRange(2, 4) - editor.foldBufferRowRange(7, 9) - - editor.setSelectedBufferRanges([ - [[2, 0], [2, 4]], - [[6, 0], [10, 4]] - ], preserveFolds: true) - - editor.moveLineDown() - - expect(editor.lineTextForBufferRow(2)).toEqual "6;" - expect(editor.lineTextForBufferRow(3)).toEqual "function f3() {" - expect(editor.lineTextForBufferRow(6)).toEqual "12;" - expect(editor.lineTextForBufferRow(7)).toEqual "7;" - expect(editor.lineTextForBufferRow(8)).toEqual "function f8() {" - expect(editor.lineTextForBufferRow(11)).toEqual "11;" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(11)).toBeFalsy() - - describe "when there is a fold below one of the selected row", -> - it "moves all lines spanned by a selection to the following row, preserving the fold", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[1, 2], [1, 6]], [[3, 0], [3, 4]], [[8, 0], [8, 3]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[9, 0], [9, 3]], [[7, 0], [7, 4]], [[2, 2], [2, 6]]] - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(9)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", -> - it "moves all the lines below the fold, preserving the fold", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [7, 4]], [[6, 2], [6, 6]]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when one selection intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRanges([ - [[2, 2], [2, 9]], - [[4, 2], [4, 9]] - ], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual([ - [[5, 2], [5, 9]] - [[3, 2], [3, 9]], - ]) - - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when some of the selections span the same lines", -> - it "moves lines that contain multiple selections correctly", -> - editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[4, 12], [4, 13]], [[4, 2], [4, 9]]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - - describe "when the selections are above a wrapped line", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(80) - editor.setText(""" - 1 - 2 - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. - 3 - 4 - """) - - it 'moves the lines past the soft wrapped line', -> - editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]) - - editor.moveLineDown() - - expect(editor.lineTextForBufferRow(0)).not.toBe "2" - expect(editor.lineTextForBufferRow(1)).toBe "1" - expect(editor.lineTextForBufferRow(2)).toBe "2" - - describe "when the line is the last buffer row", -> - it "doesn't move it", -> - editor.setText("abc\ndef") - editor.setCursorBufferPosition([1, 0]) - editor.moveLineDown() - expect(editor.getText()).toBe("abc\ndef") - - describe ".insertText(text)", -> - describe "when there is a single selection", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 0], [1, 2]]) - - it "replaces the selection with the given text", -> - range = editor.insertText('xxx') - expect(range).toEqual [ [[1, 0], [1, 3]] ] - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - describe "when there are multiple empty selections", -> - describe "when the cursors are on the same line", -> - it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> - editor.setCursorScreenPosition([1, 2]) - editor.addCursorAtScreenPosition([1, 5]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe ' xxxvarxxx sort = function(items) {' - [cursor1, cursor2] = editor.getCursors() - - expect(cursor1.getBufferPosition()).toEqual [1, 5] - expect(cursor2.getBufferPosition()).toEqual [1, 11] - - describe "when the cursors are on different lines", -> - it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> - editor.setCursorScreenPosition([1, 2]) - editor.addCursorAtScreenPosition([2, 4]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe ' xxxvar sort = function(items) {' - expect(buffer.lineForRow(2)).toBe ' xxxif (items.length <= 1) return items;' - [cursor1, cursor2] = editor.getCursors() - - expect(cursor1.getBufferPosition()).toEqual [1, 5] - expect(cursor2.getBufferPosition()).toEqual [2, 7] - - describe "when there are multiple non-empty selections", -> - describe "when the selections are on the same line", -> - it "replaces each selection range with the inserted characters", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 22], [0, 24]]]) - editor.insertText("x") - - [cursor1, cursor2] = editor.getCursors() - [selection1, selection2] = editor.getSelections() - - expect(cursor1.getScreenPosition()).toEqual [0, 5] - expect(cursor2.getScreenPosition()).toEqual [0, 15] - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - expect(editor.lineTextForBufferRow(0)).toBe "var x = functix () {" - - describe "when the selections are on different lines", -> - it "replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", -> - editor.setSelectedBufferRanges([[[1, 0], [1, 2]], [[2, 0], [2, 4]]]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - expect(buffer.lineForRow(2)).toBe 'xxxif (items.length <= 1) return items;' - [selection1, selection2] = editor.getSelections() - - expect(selection1.isEmpty()).toBeTruthy() - expect(selection1.cursor.getBufferPosition()).toEqual [1, 3] - expect(selection2.isEmpty()).toBeTruthy() - expect(selection2.cursor.getBufferPosition()).toEqual [2, 3] - - describe "when there is a selection that ends on a folded line", -> - it "destroys the selection", -> - editor.foldBufferRowRange(2, 4) - editor.setSelectedBufferRange([[1, 0], [2, 0]]) - editor.insertText('holy cow') - expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() - - describe "when there are ::onWillInsertText and ::onDidInsertText observers", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 0], [1, 2]]) - - it "notifies the observers when inserting text", -> - willInsertSpy = jasmine.createSpy().andCallFake -> - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' - - didInsertSpy = jasmine.createSpy().andCallFake -> - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - editor.onWillInsertText(willInsertSpy) - editor.onDidInsertText(didInsertSpy) - - expect(editor.insertText('xxx')).toBeTruthy() - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - expect(willInsertSpy).toHaveBeenCalled() - expect(didInsertSpy).toHaveBeenCalled() - - options = willInsertSpy.mostRecentCall.args[0] - expect(options.text).toBe 'xxx' - expect(options.cancel).toBeDefined() - - options = didInsertSpy.mostRecentCall.args[0] - expect(options.text).toBe 'xxx' - - it "cancels text insertion when an ::onWillInsertText observer calls cancel on an event", -> - willInsertSpy = jasmine.createSpy().andCallFake ({cancel}) -> - cancel() - - didInsertSpy = jasmine.createSpy() - - editor.onWillInsertText(willInsertSpy) - editor.onDidInsertText(didInsertSpy) - - expect(editor.insertText('xxx')).toBe false - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' - - expect(willInsertSpy).toHaveBeenCalled() - expect(didInsertSpy).not.toHaveBeenCalled() - - describe "when the undo option is set to 'skip'", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 2], [1, 2]]) - - it "does not undo the skipped operation", -> - range = editor.insertText('x') - range = editor.insertText('y', undo: 'skip') - editor.undo() - expect(buffer.lineForRow(1)).toBe ' yvar sort = function(items) {' - - describe ".insertNewline()", -> - describe "when there is a single cursor", -> - describe "when the cursor is at the beginning of a line", -> - it "inserts an empty line before it", -> - editor.setCursorScreenPosition(row: 1, column: 0) - - editor.insertNewline() - - expect(buffer.lineForRow(1)).toBe '' - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when the cursor is in the middle of a line", -> - it "splits the current line to form a new line", -> - editor.setCursorScreenPosition(row: 1, column: 6) - originalLine = buffer.lineForRow(1) - lineBelowOriginalLine = buffer.lineForRow(2) - - editor.insertNewline() - - expect(buffer.lineForRow(1)).toBe originalLine[0...6] - expect(buffer.lineForRow(2)).toBe originalLine[6..] - expect(buffer.lineForRow(3)).toBe lineBelowOriginalLine - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when the cursor is on the end of a line", -> - it "inserts an empty line after it", -> - editor.setCursorScreenPosition(row: 1, column: buffer.lineForRow(1).length) - - editor.insertNewline() - - expect(buffer.lineForRow(2)).toBe '' - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when there are multiple cursors", -> - describe "when the cursors are on the same line", -> - it "breaks the line at the cursor locations", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.insertNewline() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot" - expect(editor.lineTextForBufferRow(4)).toBe " = items.shift(), current" - expect(editor.lineTextForBufferRow(5)).toBe ", left = [], right = [];" - expect(editor.lineTextForBufferRow(6)).toBe " while(items.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [4, 0] - expect(cursor2.getBufferPosition()).toEqual [5, 0] - - describe "when the cursors are on different lines", -> - it "inserts newlines at each cursor location", -> - editor.setCursorScreenPosition([3, 0]) - editor.addCursorAtScreenPosition([6, 0]) - - editor.insertText("\n") - expect(editor.lineTextForBufferRow(3)).toBe "" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(6)).toBe " current = items.shift();" - expect(editor.lineTextForBufferRow(7)).toBe "" - expect(editor.lineTextForBufferRow(8)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(9)).toBe " }" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [4, 0] - expect(cursor2.getBufferPosition()).toEqual [8, 0] - - describe ".insertNewlineBelow()", -> - describe "when the operation is undone", -> - it "places the cursor back at the previous location", -> - editor.setCursorBufferPosition([0, 2]) - editor.insertNewlineBelow() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.undo() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - - it "inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", -> - editor.update({autoIndent: true}) - editor.insertNewlineBelow() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " " - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - - describe ".insertNewlineAbove()", -> - describe "when the cursor is on first line", -> - it "inserts a newline on the first line and moves the cursor to the first line", -> - editor.setCursorBufferPosition([0]) - editor.insertNewlineAbove() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - expect(editor.lineTextForBufferRow(0)).toBe '' - expect(editor.lineTextForBufferRow(1)).toBe 'var quicksort = function () {' - expect(editor.buffer.getLineCount()).toBe 14 - - describe "when the cursor is not on the first line", -> - it "inserts a newline above the current line and moves the cursor to the inserted line", -> - editor.setCursorBufferPosition([3, 4]) - editor.insertNewlineAbove() - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - expect(editor.lineTextForBufferRow(3)).toBe '' - expect(editor.lineTextForBufferRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(editor.buffer.getLineCount()).toBe 14 - - editor.undo() - expect(editor.getCursorBufferPosition()).toEqual [3, 4] - - it "indents the new line to the correct level when editor.autoIndent is true", -> - editor.update({autoIndent: true}) - - editor.setText(' var test') - editor.setCursorBufferPosition([0, 2]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - expect(editor.lineTextForBufferRow(0)).toBe ' ' - expect(editor.lineTextForBufferRow(1)).toBe ' var test' - - editor.setText('\n var test') - editor.setCursorBufferPosition([1, 2]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - expect(editor.lineTextForBufferRow(0)).toBe '' - expect(editor.lineTextForBufferRow(1)).toBe ' ' - expect(editor.lineTextForBufferRow(2)).toBe ' var test' - - editor.setText('function() {\n}') - editor.setCursorBufferPosition([1, 1]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - expect(editor.lineTextForBufferRow(0)).toBe 'function() {' - expect(editor.lineTextForBufferRow(1)).toBe ' ' - expect(editor.lineTextForBufferRow(2)).toBe '}' - - describe ".insertNewLine()", -> - describe "when a new line is appended before a closing tag (e.g. by pressing enter before a selection)", -> - it "moves the line down and keeps the indentation level the same when editor.autoIndent is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([9, 2]) - editor.insertNewline() - expect(editor.lineTextForBufferRow(10)).toBe ' };' - - describe "when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)", -> - it "indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.js")) - editor.setText('var test = function () {\n return true;};') - editor.setCursorBufferPosition([1, 14]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - - it "indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified", -> - runs -> - editor.setGrammar(atom.grammars.selectGrammar("file")) - editor.update({autoIndent: true}) - editor.setText(' if true') - editor.setCursorBufferPosition([0, 8]) - editor.insertNewline() - expect(editor.getGrammar()).toBe atom.grammars.nullGrammar - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 1 - - it "indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language", -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.coffee")) - editor.setText('if true\n return trueelse\n return false') - editor.setCursorBufferPosition([1, 13]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - expect(editor.indentationForBufferRow(3)).toBe 1 - - describe "when a newline is appended on a line that matches the decreaseNextIndentPattern", -> - it "indents the new line to the correct level when editor.autoIndent is true", -> - waitsForPromise -> - atom.packages.activatePackage('language-go') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.go")) - editor.setText('fmt.Printf("some%s",\n "thing")') - editor.setCursorBufferPosition([1, 10]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - - describe ".backspace()", -> - describe "when there is a single cursor", -> - changeScreenRangeHandler = null - - beforeEach -> - selection = editor.getLastSelection() - changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') - selection.onDidChangeRange changeScreenRangeHandler - - describe "when the cursor is on the middle of the line", -> - it "removes the character before the cursor", -> - editor.setCursorScreenPosition(row: 1, column: 7) - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - - editor.backspace() - - line = buffer.lineForRow(1) - expect(line).toBe " var ort = function(items) {" - expect(editor.getCursorScreenPosition()).toEqual {row: 1, column: 6} - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the cursor is at the beginning of a line", -> - it "joins it with the line above", -> - originalLine0 = buffer.lineForRow(0) - expect(originalLine0).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - - editor.setCursorScreenPosition(row: 1, column: 0) - editor.backspace() - - line0 = buffer.lineForRow(0) - line1 = buffer.lineForRow(1) - expect(line0).toBe "var quicksort = function () { var sort = function(items) {" - expect(line1).toBe " if (items.length <= 1) return items;" - expect(editor.getCursorScreenPosition()).toEqual [0, originalLine0.length] - - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the cursor is at the first column of the first line", -> - it "does nothing, but doesn't raise an error", -> - editor.setCursorScreenPosition(row: 0, column: 0) - editor.backspace() - - describe "when the cursor is after a fold", -> - it "deletes the folded range", -> - editor.foldBufferRange([[4, 7], [5, 8]]) - editor.setCursorBufferPosition([5, 8]) - editor.backspace() - - expect(buffer.lineForRow(4)).toBe " whirrent = items.shift();" - expect(editor.isFoldedAtBufferRow(4)).toBe(false) - - describe "when the cursor is in the middle of a line below a fold", -> - it "backspaces as normal", -> - editor.setCursorScreenPosition([4, 0]) - editor.foldCurrentRow() - editor.setCursorScreenPosition([5, 5]) - editor.backspace() - - expect(buffer.lineForRow(7)).toBe " }" - expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));" - - describe "when the cursor is on a folded screen line", -> - it "deletes the contents of the fold before the cursor", -> - editor.setCursorBufferPosition([3, 0]) - editor.foldCurrentRow() - editor.backspace() - - expect(buffer.lineForRow(1)).toBe " var sort = function(items) var pivot = items.shift(), current, left = [], right = [];" - expect(editor.getCursorScreenPosition()).toEqual [1, 29] - - describe "when there are multiple cursors", -> - describe "when cursors are on the same line", -> - it "removes the characters preceding each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.backspace() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivo = items.shift(), curren, left = [], right = [];" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 12] - expect(cursor2.getBufferPosition()).toEqual [3, 36] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when cursors are on different lines", -> - describe "when the cursors are in the middle of their lines", -> - it "removes the characters preceding each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([4, 10]) - - editor.backspace() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivo = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " whileitems.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 12] - expect(cursor2.getBufferPosition()).toEqual [4, 9] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when the cursors are on the first column of their lines", -> - it "removes the newlines preceding each cursor", -> - editor.setCursorScreenPosition([3, 0]) - editor.addCursorAtScreenPosition([6, 0]) - - editor.backspace() - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " current = items.shift(); current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(5)).toBe " }" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [2, 40] - expect(cursor2.getBufferPosition()).toEqual [4, 30] - - describe "when there is a single selection", -> - it "deletes the selection, but not the character before it", -> - editor.setSelectedBufferRange([[0, 5], [0, 9]]) - editor.backspace() - expect(editor.buffer.lineForRow(0)).toBe 'var qsort = function () {' - - describe "when the selection ends on a folded line", -> - it "preserves the fold", -> - editor.setSelectedBufferRange([[3, 0], [4, 0]]) - editor.foldBufferRow(4) - editor.backspace() - - expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtScreenRow(3)).toBe(true) - - describe "when there are multiple selections", -> - it "removes all selected text", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - editor.backspace() - expect(editor.lineTextForBufferRow(0)).toBe 'var = () {' - - describe ".deleteToPreviousWordBoundary()", -> - describe "when no text is selected", -> - it "deletes to the previous word boundary", -> - editor.setCursorBufferPosition([0, 16]) - editor.addCursorAtBufferPosition([1, 21]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {' - expect(buffer.lineForRow(1)).toBe ' var sort = (items) {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 13] - - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort function () {' - expect(buffer.lineForRow(1)).toBe ' var sort =(items) {' - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [1, 12] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".deleteToNextWordBoundary()", -> - describe "when no text is selected", -> - it "deletes to the next word boundary", -> - editor.setCursorBufferPosition([0, 15]) - editor.addCursorAtBufferPosition([1, 24]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort = () {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =() {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it{' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".deleteToBeginningOfWord()", -> - describe "when no text is selected", -> - it "deletes all text between the cursor and the beginning of the word", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([3, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {' - expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 22] - expect(cursor2.getBufferPosition()).toEqual [3, 4] - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 21] - expect(cursor2.getBufferPosition()).toEqual [2, 39] - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = ems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 13] - expect(cursor2.getBufferPosition()).toEqual [2, 34] - - editor.setText(' var sort') - editor.setCursorBufferPosition([0, 2]) - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(0)).toBe 'var sort' - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - - describe '.deleteToEndOfLine()', -> - describe 'when no text is selected', -> - it 'deletes all text between the cursor and the end of the line', -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it' - expect(buffer.lineForRow(2)).toBe ' i' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - describe 'when at the end of the line', -> - it 'deletes the next newline', -> - editor.setCursorBufferPosition([1, 30]) - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe 'when text is selected', -> - it 'deletes only the text in the selection', -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - - describe ".deleteToBeginningOfLine()", -> - describe "when no text is selected", -> - it "deletes all text between the cursor and the beginning of the line", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe 'ems) {' - expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 0] - expect(cursor2.getBufferPosition()).toEqual [2, 0] - - describe "when at the beginning of the line", -> - it "deletes the newline", -> - editor.setCursorBufferPosition([2]) - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe "when text is selected", -> - it "still deletes all text to beginning of the line", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe 'ems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' - - describe ".delete()", -> - describe "when there is a single cursor", -> - describe "when the cursor is on the middle of a line", -> - it "deletes the character following the cursor", -> - editor.setCursorScreenPosition([1, 6]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var ort = function(items) {' - - describe "when the cursor is on the end of a line", -> - it "joins the line with the following line", -> - editor.setCursorScreenPosition([1, buffer.lineForRow(1).length]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe "when the cursor is on the last column of the last line", -> - it "does nothing, but doesn't raise an error", -> - editor.setCursorScreenPosition([12, buffer.lineForRow(12).length]) - editor.delete() - expect(buffer.lineForRow(12)).toBe '};' - - describe "when the cursor is before a fold", -> - it "only deletes the lines inside the fold", -> - editor.foldBufferRange([[3, 6], [4, 8]]) - editor.setCursorScreenPosition([3, 6]) - cursorPositionBefore = editor.getCursorScreenPosition() - - editor.delete() - - expect(buffer.lineForRow(3)).toBe " vae(items.length > 0) {" - expect(buffer.lineForRow(4)).toBe " current = items.shift();" - expect(editor.getCursorScreenPosition()).toEqual cursorPositionBefore - - describe "when the cursor is in the middle a line above a fold", -> - it "deletes as normal", -> - editor.foldBufferRow(4) - editor.setCursorScreenPosition([3, 4]) - cursorPositionBefore = editor.getCursorScreenPosition() - - editor.delete() - - expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];" - expect(editor.isFoldedAtScreenRow(4)).toBe(true) - expect(editor.getCursorScreenPosition()).toEqual [3, 4] - - describe "when the cursor is inside a fold", -> - it "removes the folded content after the cursor", -> - editor.foldBufferRange([[2, 6], [6, 21]]) - editor.setCursorBufferPosition([4, 9]) - - editor.delete() - - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(4)).toBe ' while ? left.push(current) : right.push(current);' - expect(buffer.lineForRow(5)).toBe ' }' - expect(editor.getCursorBufferPosition()).toEqual [4, 9] - - describe "when there are multiple cursors", -> - describe "when cursors are on the same line", -> - it "removes the characters following each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.delete() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot= items.shift(), current left = [], right = [];" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 37] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when cursors are on different lines", -> - describe "when the cursors are in the middle of the lines", -> - it "removes the characters following each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([4, 10]) - - editor.delete() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot= items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(tems.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 13] - expect(cursor2.getBufferPosition()).toEqual [4, 10] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when the cursors are at the end of their lines", -> - it "removes the newlines following each cursor", -> - editor.setCursorScreenPosition([0, 29]) - editor.addCursorAtScreenPosition([1, 30]) - - editor.delete() - - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 29] - expect(cursor2.getBufferPosition()).toEqual [0, 59] - - describe "when there is a single selection", -> - it "deletes the selection, but not the character following it", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - expect(editor.getLastSelection().isEmpty()).toBeTruthy() - - describe "when there are multiple selections", -> - describe "when selections are on the same line", -> - it "removes all selected text", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - editor.delete() - expect(editor.lineTextForBufferRow(0)).toBe 'var = () {' - - describe ".deleteToEndOfWord()", -> - describe "when no text is selected", -> - it "deletes to the end of the word", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe ' i (items.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' - expect(buffer.lineForRow(2)).toBe ' iitems.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".indent()", -> - describe "when the selection is empty", -> - describe "when autoIndent is disabled", -> - describe "if 'softTabs' is true (the default)", -> - it "inserts 'tabLength' spaces into the buffer", -> - tabRegex = new RegExp("^[ ]{#{editor.getTabLength()}}") - expect(buffer.lineForRow(0)).not.toMatch(tabRegex) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(tabRegex) - - it "respects the tab stops when cursor is in the middle of a tab", -> - editor.setTabLength(4) - buffer.insert([12, 2], "\n ") - editor.setCursorBufferPosition [13, 1] - editor.indent() - expect(buffer.lineForRow(13)).toMatch /^\s+$/ - expect(buffer.lineForRow(13).length).toBe 4 - expect(editor.getCursorBufferPosition()).toEqual [13, 4] - - buffer.insert([13, 0], " ") - editor.setCursorBufferPosition [13, 6] - editor.indent() - expect(buffer.lineForRow(13).length).toBe 8 - - describe "if 'softTabs' is false", -> - it "insert a \t into the buffer", -> - editor.setSoftTabs(false) - expect(buffer.lineForRow(0)).not.toMatch(/^\t/) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t/) - - describe "when autoIndent is enabled", -> - describe "when the cursor's column is less than the suggested level of indentation", -> - describe "when 'softTabs' is true (the default)", -> - it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation", -> - buffer.insert([5, 0], " \n") - editor.setCursorBufferPosition [5, 0] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(5)).toMatch /^\s+$/ - expect(buffer.lineForRow(5).length).toBe 6 - expect(editor.getCursorBufferPosition()).toEqual [5, 6] - - it "respects the tab stops when cursor is in the middle of a tab", -> - editor.setTabLength(4) - buffer.insert([12, 2], "\n ") - editor.setCursorBufferPosition [13, 1] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(13)).toMatch /^\s+$/ - expect(buffer.lineForRow(13).length).toBe 4 - expect(editor.getCursorBufferPosition()).toEqual [13, 4] - - buffer.insert([13, 0], " ") - editor.setCursorBufferPosition [13, 6] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(13).length).toBe 8 - - describe "when 'softTabs' is false", -> - it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - buffer.insert([5, 0], "\t\n") - editor.setCursorBufferPosition [5, 0] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(5)).toMatch /^\t\t\t$/ - expect(editor.getCursorBufferPosition()).toEqual [5, 3] - - describe "when the difference between the suggested level of indentation and the current level of indentation is greater than 0 but less than 1", -> - it "inserts one tab", -> - editor.setSoftTabs(false) - buffer.setText(" \ntest") - editor.setCursorBufferPosition [1, 0] - - editor.indent(autoIndent: true) - expect(buffer.lineForRow(1)).toBe '\ttest' - expect(editor.getCursorBufferPosition()).toEqual [1, 1] - - describe "when the line's indent level is greater than the suggested level of indentation", -> - describe "when 'softTabs' is true (the default)", -> - it "moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", -> - buffer.insert([7, 0], " \n") - editor.setCursorBufferPosition [7, 2] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(7)).toMatch /^\s+$/ - expect(buffer.lineForRow(7).length).toBe 8 - expect(editor.getCursorBufferPosition()).toEqual [7, 8] - - describe "when 'softTabs' is false", -> - it "moves the cursor to the end of the leading whitespace and inserts \t into the buffer", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - buffer.insert([7, 0], "\t\t\t\n") - editor.setCursorBufferPosition [7, 1] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(7)).toMatch /^\t\t\t\t$/ - expect(editor.getCursorBufferPosition()).toEqual [7, 4] - - describe "when the selection is not empty", -> - it "indents the selected lines", -> - editor.setSelectedBufferRange([[0, 0], [10, 0]]) - selection = editor.getLastSelection() - spyOn(selection, "indentSelectedRows") - editor.indent() - expect(selection.indentSelectedRows).toHaveBeenCalled() - - describe "if editor.softTabs is false", -> - it "inserts a tab character into the buffer", -> - editor.setSoftTabs(false) - expect(buffer.lineForRow(0)).not.toMatch(/^\t/) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t/) - expect(editor.getCursorBufferPosition()).toEqual [0, 1] - expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength()] - - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t\t/) - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength() * 2] - - describe "clipboard operations", -> - describe ".cutSelectedText()", -> - it "removes the selected text from the buffer and places it on the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - editor.cutSelectedText() - expect(buffer.lineForRow(0)).toBe "var = function () {" - expect(buffer.lineForRow(1)).toBe " var = function(items) {" - expect(clipboard.readText()).toBe 'quicksort\nsort' - - describe "when no text is selected", -> - beforeEach -> - editor.setSelectedBufferRanges([ - [[0, 0], [0, 0]], - [[5, 0], [5, 0]], - ]) - - it "cuts the lines on which there are cursors", -> - editor.cutSelectedText() - expect(buffer.getLineCount()).toBe(11) - expect(buffer.lineForRow(1)).toBe(" if (items.length <= 1) return items;") - expect(buffer.lineForRow(4)).toBe(" current < pivot ? left.push(current) : right.push(current);") - expect(atom.clipboard.read()).toEqual """ - var quicksort = function () { - - current = items.shift(); - - """ - - describe "when many selections get added in shuffle order", -> - it "cuts them in order", -> - editor.setSelectedBufferRanges([ - [[2, 8], [2, 13]] - [[0, 4], [0, 13]], - [[1, 6], [1, 10]], - ]) - editor.cutSelectedText() - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe ".cutToEndOfLine()", -> - describe "when soft wrap is on", -> - it "cuts up to the end of the line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(25) - editor.setCursorScreenPosition([2, 6]) - editor.cutToEndOfLine() - expect(editor.lineTextForScreenRow(2)).toBe ' var function(items) {' - - describe "when soft wrap is off", -> - describe "when nothing is selected", -> - it "cuts up to the end of the line", -> - editor.setCursorBufferPosition([2, 20]) - editor.addCursorAtBufferPosition([3, 20]) - editor.cutToEndOfLine() - expect(buffer.lineForRow(2)).toBe ' if (items.length' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' - - describe "when text is selected", -> - it "only cuts the selected text, not to the end of the line", -> - editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) - - editor.cutToEndOfLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' - - describe ".cutToEndOfBufferLine()", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - - describe "when nothing is selected", -> - it "cuts up to the end of the buffer line", -> - editor.setCursorBufferPosition([2, 20]) - editor.addCursorAtBufferPosition([3, 20]) - - editor.cutToEndOfBufferLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.length' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' - - describe "when text is selected", -> - it "only cuts the selected text, not to the end of the buffer line", -> - editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) - - editor.cutToEndOfBufferLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' - - describe ".copySelectedText()", -> - it "copies selected text onto the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) - - editor.copySelectedText() - 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;" - expect(clipboard.readText()).toBe 'quicksort\nsort\nitems' - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe "when no text is selected", -> - beforeEach -> - editor.setSelectedBufferRanges([ - [[1, 5], [1, 5]], - [[5, 8], [5, 8]] - ]) - - it "copies the lines on which there are cursors", -> - editor.copySelectedText() - expect(atom.clipboard.read()).toEqual([ - " var sort = function(items) {\n" - " current = items.shift();\n" - ].join("\n")) - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 5], [1, 5]], - [[5, 8], [5, 8]] - ]) - - describe "when many selections get added in shuffle order", -> - it "copies them in order", -> - editor.setSelectedBufferRanges([ - [[2, 8], [2, 13]] - [[0, 4], [0, 13]], - [[1, 6], [1, 10]], - ]) - editor.copySelectedText() - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe ".copyOnlySelectedText()", -> - describe "when thee are multiple selections", -> - it "copies selected text onto the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) - - editor.copyOnlySelectedText() - 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;" - expect(clipboard.readText()).toBe 'quicksort\nsort\nitems' - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe "when no text is selected", -> - it "does not copy anything", -> - editor.setCursorBufferPosition([1, 5]) - editor.copyOnlySelectedText() - expect(atom.clipboard.read()).toEqual "initial clipboard content" - - describe ".pasteText()", -> - copyText = (text, {startColumn, textEditor}={}) -> - startColumn ?= 0 - textEditor ?= editor - textEditor.setCursorBufferPosition([0, 0]) - textEditor.insertText(text) - numberOfNewlines = text.match(/\n/g)?.length - endColumn = text.match(/[^\n]*$/)[0]?.length - textEditor.getLastSelection().setBufferRange([[0, startColumn], [numberOfNewlines, endColumn]]) - textEditor.cutSelectedText() - - it "pastes text into the buffer", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - atom.clipboard.write('first') - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var first = function () {" - expect(editor.lineTextForBufferRow(1)).toBe " var first = function(items) {" - - it "notifies ::onWillInsertText observers", -> - insertedStrings = [] - editor.onWillInsertText ({text, cancel}) -> - insertedStrings.push(text) - cancel() - - atom.clipboard.write("hello") - editor.pasteText() - - expect(insertedStrings).toEqual ["hello"] - - it "notifies ::onDidInsertText observers", -> - insertedStrings = [] - editor.onDidInsertText ({text, range}) -> - insertedStrings.push(text) - - atom.clipboard.write("hello") - editor.pasteText() - - expect(insertedStrings).toEqual ["hello"] - - describe "when `autoIndentOnPaste` is true", -> - beforeEach -> - editor.update({autoIndentOnPaste: true}) - - describe "when pasting multiple lines before any non-whitespace characters", -> - it "auto-indents the lines spanned by the pasted text, based on the first pasted line", -> - atom.clipboard.write("a(x);\n b(x);\n c(x);\n", indentBasis: 0) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - # Adjust the indentation of the pasted lines while preserving - # their indentation relative to each other. Also preserve the - # indentation of the following line. - expect(editor.lineTextForBufferRow(5)).toBe " a(x);" - expect(editor.lineTextForBufferRow(6)).toBe " b(x);" - expect(editor.lineTextForBufferRow(7)).toBe " c(x);" - expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();" - - it "auto-indents lines with a mix of hard tabs and spaces without removing spaces", -> - editor.setSoftTabs(false) - expect(editor.indentationForBufferRow(5)).toBe(3) - - atom.clipboard.write("/**\n\t * testing\n\t * indent\n\t **/\n", indentBasis: 1) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - # Do not lose the alignment spaces - expect(editor.lineTextForBufferRow(5)).toBe("\t\t\t/**") - expect(editor.lineTextForBufferRow(6)).toBe("\t\t\t * testing") - expect(editor.lineTextForBufferRow(7)).toBe("\t\t\t * indent") - expect(editor.lineTextForBufferRow(8)).toBe("\t\t\t **/") - - describe "when pasting line(s) above a line that matches the decreaseIndentPattern", -> - it "auto-indents based on the pasted line(s) only", -> - atom.clipboard.write("a(x);\n b(x);\n c(x);\n", indentBasis: 0) - editor.setCursorBufferPosition([7, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(7)).toBe " a(x);" - expect(editor.lineTextForBufferRow(8)).toBe " b(x);" - expect(editor.lineTextForBufferRow(9)).toBe " c(x);" - expect(editor.lineTextForBufferRow(10)).toBe " }" - - describe "when pasting a line of text without line ending", -> - it "does not auto-indent the text", -> - atom.clipboard.write("a(x);", indentBasis: 0) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(5)).toBe "a(x); current = items.shift();" - expect(editor.lineTextForBufferRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);" - - describe "when pasting on a line after non-whitespace characters", -> - it "does not auto-indent the affected line", -> - # Before the paste, the indentation is non-standard. - editor.setText """ - if (x) { - y(); - } - """ - - atom.clipboard.write(" z();\n h();") - editor.setCursorBufferPosition([1, Infinity]) - - # The indentation of the non-standard line is unchanged. - editor.pasteText() - expect(editor.lineTextForBufferRow(1)).toBe(" y(); z();") - expect(editor.lineTextForBufferRow(2)).toBe(" h();") - - describe "when `autoIndentOnPaste` is false", -> - beforeEach -> - editor.update({autoIndentOnPaste: false}) - - describe "when the cursor is indented further than the original copied text", -> - it "increases the indentation of the copied lines to match", -> - editor.setSelectedBufferRange([[1, 2], [3, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([5, 6]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(5)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is indented less far than the original copied text", -> - it "decreases the indentation of the copied lines to match", -> - editor.setSelectedBufferRange([[6, 6], [8, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([1, 2]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(1)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(2)).toBe "}" - - describe "when the first copied line has leading whitespace", -> - it "preserves the line's leading whitespace", -> - editor.setSelectedBufferRange([[4, 0], [6, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([0, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(0)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(1)).toBe " current = items.shift();" - - describe 'when the clipboard has many selections', -> - beforeEach -> - editor.update({autoIndentOnPaste: false}) - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - editor.copySelectedText() - - it "pastes each selection in order separately into the buffer", -> - editor.setSelectedBufferRanges([ - [[1, 6], [1, 10]] - [[0, 4], [0, 13]], - ]) - - editor.moveRight() - editor.insertText("_") - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort_quicksort = function () {" - expect(editor.lineTextForBufferRow(1)).toBe " var sort_sort = function(items) {" - - describe 'and the selections count does not match', -> - beforeEach -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]]]) - - it "pastes the whole text into the buffer", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort" - expect(editor.lineTextForBufferRow(1)).toBe "sort = function () {" - - describe "when a full line was cut", -> - beforeEach -> - editor.setCursorBufferPosition([2, 13]) - editor.cutSelectedText() - editor.setCursorBufferPosition([2, 13]) - - it "pastes the line above the cursor and retains the cursor's column", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items;") - expect(editor.lineTextForBufferRow(3)).toBe(" var pivot = items.shift(), current, left = [], right = [];") - expect(editor.getCursorBufferPosition()).toEqual([3, 13]) - - describe "when a full line was copied", -> - beforeEach -> - editor.setCursorBufferPosition([2, 13]) - editor.copySelectedText() - - describe "when there is a selection", -> - it "overwrites the selection as with any copied text", -> - editor.setSelectedBufferRange([[1, 2], [1, Infinity]]) - editor.pasteText() - expect(editor.lineTextForBufferRow(1)).toBe(" if (items.length <= 1) return items;") - expect(editor.lineTextForBufferRow(2)).toBe("") - expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;") - expect(editor.getCursorBufferPosition()).toEqual([2, 0]) - - describe "when there is no selection", -> - it "pastes the line above the cursor and retains the cursor's column", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items;") - 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", -> - it "indents line and retains selection", -> - editor.setSelectedBufferRange([[0, 3], [0, 3]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 3 + editor.getTabLength()], [0, 3 + editor.getTabLength()]] - - describe "when softTabs is disabled", -> - it "indents line and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[0, 3], [0, 3]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 3 + 1], [0, 3 + 1]] - - describe "when one line is selected", -> - describe "when softTabs is enabled", -> - it "indents line and retains selection", -> - editor.setSelectedBufferRange([[0, 4], [0, 14]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "#{editor.getTabText()}var quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 4 + editor.getTabLength()], [0, 14 + editor.getTabLength()]] - - describe "when softTabs is disabled", -> - it "indents line and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[0, 4], [0, 14]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 4 + 1], [0, 14 + 1]] - - describe "when multiple lines are selected", -> - describe "when softTabs is enabled", -> - it "indents selected lines (that are not empty) and retains selection", -> - editor.setSelectedBufferRange([[9, 1], [11, 15]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe " };" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + editor.getTabLength()], [11, 15 + editor.getTabLength()]] - - it "does not indent the last row if the selection ends at column 0", -> - editor.setSelectedBufferRange([[9, 1], [11, 0]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe " };" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + editor.getTabLength()], [11, 0]] - - describe "when softTabs is disabled", -> - it "indents selected lines (that are not empty) and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[9, 1], [11, 15]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe "\t\t};" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe "\t\treturn sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + 1], [11, 15 + 1]] - - describe ".outdentSelectedRows()", -> - describe "when nothing is selected", -> - it "outdents line and retains selection", -> - editor.setSelectedBufferRange([[1, 3], [1, 3]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editor.getSelectedBufferRange()).toEqual [[1, 3 - editor.getTabLength()], [1, 3 - editor.getTabLength()]] - - it "outdents when indent is less than a tab length", -> - editor.insertText(' ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs", -> - editor.insertText('\t\t') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents when a mix of hard tabs and soft tabs are used", -> - editor.insertText('\t ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents only up to the first non-space non-tab character", -> - editor.insertText(' \tfoo\t ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tfoo\t var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "foo\t var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "foo\t var quicksort = function () {" - - describe "when one line is selected", -> - it "outdents line and retains editor", -> - editor.setSelectedBufferRange([[1, 4], [1, 14]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editor.getSelectedBufferRange()).toEqual [[1, 4 - editor.getTabLength()], [1, 14 - editor.getTabLength()]] - - describe "when multiple lines are selected", -> - it "outdents selected lines and retains editor", -> - editor.setSelectedBufferRange([[0, 1], [3, 15]]) - editor.outdentSelectedRows() - 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;" - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 15 - editor.getTabLength()]] - - it "does not outdent the last line of the selection if it ends at column 0", -> - editor.setSelectedBufferRange([[0, 1], [3, 0]]) - editor.outdentSelectedRows() - 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;" - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 0]] - - describe ".autoIndentSelectedRows", -> - it "auto-indents the selection", -> - editor.setCursorBufferPosition([2, 0]) - editor.insertText("function() {\ninside=true\n}\n i=1\n") - editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]) - editor.autoIndentSelectedRows() - - expect(editor.lineTextForBufferRow(2)).toBe " function() {" - expect(editor.lineTextForBufferRow(3)).toBe " inside=true" - expect(editor.lineTextForBufferRow(4)).toBe " }" - expect(editor.lineTextForBufferRow(5)).toBe " i=1" - - describe ".undo() and .redo()", -> - it "undoes/redoes the last change", -> - editor.insertText("foo") - editor.undo() - expect(buffer.lineForRow(0)).not.toContain "foo" - - editor.redo() - expect(buffer.lineForRow(0)).toContain "foo" - - it "batches the undo / redo of changes caused by multiple cursors", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([1, 0]) - - editor.insertText("foo") - editor.backspace() - - expect(buffer.lineForRow(0)).toContain "fovar" - expect(buffer.lineForRow(1)).toContain "fo " - - editor.undo() - - expect(buffer.lineForRow(0)).toContain "foo" - expect(buffer.lineForRow(1)).toContain "foo" - - editor.redo() - - expect(buffer.lineForRow(0)).not.toContain "foo" - expect(buffer.lineForRow(0)).toContain "fovar" - - it "restores cursors and selections to their states before and after undone and redone changes", -> - editor.setSelectedBufferRanges([ - [[0, 0], [0, 0]], - [[1, 0], [1, 3]], - ]) - editor.insertText("abc") - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 3]], - [[1, 3], [1, 3]] - ] - - editor.setCursorBufferPosition([0, 0]) - editor.setSelectedBufferRanges([ - [[2, 0], [2, 0]], - [[3, 0], [3, 0]], - [[4, 0], [4, 3]], - ]) - editor.insertText("def") - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 3], [2, 3]], - [[3, 3], [3, 3]] - [[4, 3], [4, 3]] - ] - - editor.setCursorBufferPosition([0, 0]) - editor.undo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 0], [2, 0]], - [[3, 0], [3, 0]], - [[4, 0], [4, 3]], - ] - - editor.undo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 0], [0, 0]], - [[1, 0], [1, 3]] - ] - - editor.redo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 3]], - [[1, 3], [1, 3]] - ] - - editor.redo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 3], [2, 3]], - [[3, 3], [3, 3]] - [[4, 3], [4, 3]] - ] - - it "restores the selected ranges after undo and redo", -> - editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) - editor.delete() - editor.delete() - - selections = editor.getSelections() - expect(buffer.lineForRow(1)).toBe ' var = function( {' - - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 17], [1, 17]]] - - editor.undo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - - editor.undo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 10]], [[1, 22], [1, 27]]] - - editor.redo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - - xit "restores folds after undo and redo", -> - editor.foldBufferRow(1) - editor.setSelectedBufferRange([[1, 0], [10, Infinity]], preserveFolds: true) - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - - editor.insertText """ - \ // testing - function foo() { - return 1 + 2; - } - """ - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - editor.foldBufferRow(2) - - editor.undo() - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - editor.redo() - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - - describe "::transact", -> - it "restores the selection when the transaction is undone/redone", -> - buffer.setText('1234') - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - - editor.transact -> - editor.delete() - editor.moveToEndOfLine() - editor.insertText('5') - expect(buffer.getText()).toBe '145' - - editor.undo() - expect(buffer.getText()).toBe '1234' - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] - - editor.redo() - expect(buffer.getText()).toBe '145' - expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 3]] - - describe "when the buffer is changed (via its direct api, rather than via than edit session)", -> - it "moves the cursor so it is in the same relative position of the buffer", -> - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - editor.addCursorAtScreenPosition([0, 5]) - editor.addCursorAtScreenPosition([1, 0]) - [cursor1, cursor2, cursor3] = editor.getCursors() - - buffer.insert([0, 1], 'abc') - - expect(cursor1.getScreenPosition()).toEqual [0, 0] - expect(cursor2.getScreenPosition()).toEqual [0, 8] - expect(cursor3.getScreenPosition()).toEqual [1, 0] - - it "does not destroy cursors or selections when a change encompasses them", -> - cursor = editor.getLastCursor() - cursor.setBufferPosition [3, 3] - editor.buffer.delete([[3, 1], [3, 5]]) - expect(cursor.getBufferPosition()).toEqual [3, 1] - expect(editor.getCursors().indexOf(cursor)).not.toBe -1 - - selection = editor.getLastSelection() - selection.setBufferRange [[3, 5], [3, 10]] - editor.buffer.delete [[3, 3], [3, 8]] - expect(selection.getBufferRange()).toEqual [[3, 3], [3, 5]] - expect(editor.getSelections().indexOf(selection)).not.toBe -1 - - it "merges cursors when the change causes them to overlap", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 2]) - editor.addCursorAtScreenPosition([1, 2]) - - [cursor1, cursor2, cursor3] = editor.getCursors() - expect(editor.getCursors().length).toBe 3 - - buffer.delete([[0, 0], [0, 2]]) - - expect(editor.getCursors().length).toBe 2 - expect(editor.getCursors()).toEqual [cursor1, cursor3] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor3.getBufferPosition()).toEqual [1, 2] - - describe ".moveSelectionLeft()", -> - it "moves one active selection on one line one column to the left", -> - editor.setSelectedBufferRange [[0, 4], [0, 13]] - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.moveSelectionLeft() - - expect(editor.getSelectedText()).toBe 'quicksort' - expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 12]] - - it "moves multiple active selections on one line one column to the left", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[0, 15], [0, 23]]] - - it "moves multiple active selections on multiple lines one column to the left", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[1, 5], [1, 9]]] - - describe "when a selection is at the first column of a line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe ' v' - - editor.moveSelectionLeft() - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe ' v' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[1, 0], [1, 3]]] - - describe "when multiple selections are active on one line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe 'quicksort' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe 'quicksort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[0, 4], [0, 13]]] - - describe ".moveSelectionRight()", -> - it "moves one active selection on one line one column to the right", -> - editor.setSelectedBufferRange [[0, 4], [0, 13]] - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.moveSelectionRight() - - expect(editor.getSelectedText()).toBe 'quicksort' - expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 14]] - - it "moves multiple active selections on one line one column to the right", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[0, 17], [0, 25]]] - - it "moves multiple active selections on multiple lines one column to the right", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[1, 7], [1, 11]]] - - describe "when a selection is at the last column of a line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'items;' - expect(selections[1].getText()).toBe 'shift();' - - editor.moveSelectionRight() - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'items;' - expect(selections[1].getText()).toBe 'shift();' - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 34], [2, 40]], [[5, 22], [5, 30]]] - - describe "when multiple selections are active on one line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'return' - expect(selections[1].getText()).toBe 'items;' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'return' - expect(selections[1].getText()).toBe 'items;' - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 27], [2, 33]], [[2, 34], [2, 40]]] - - describe 'reading text', -> - it '.lineTextForScreenRow(row)', -> - editor.foldBufferRow(4) - expect(editor.lineTextForScreenRow(5)).toEqual ' return sort(left).concat(pivot).concat(sort(right));' - expect(editor.lineTextForScreenRow(9)).toEqual '};' - expect(editor.lineTextForScreenRow(10)).toBeUndefined() - - describe ".deleteLine()", -> - it "deletes the first line when the cursor is there", -> - editor.getLastCursor().moveToTop() - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line1) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "deletes the last line when the cursor is there", -> - count = buffer.getLineCount() - secondToLastLine = buffer.lineForRow(count - 2) - expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine) - editor.getLastCursor().moveToBottom() - editor.deleteLine() - newCount = buffer.getLineCount() - expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine) - expect(newCount).toBe(count - 1) - - it "deletes whole lines when partial lines are selected", -> - editor.setSelectedBufferRange([[0, 2], [1, 2]]) - line2 = buffer.lineForRow(2) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line2) - expect(buffer.lineForRow(1)).not.toBe(line2) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line2) - expect(buffer.getLineCount()).toBe(count - 2) - - it "deletes a line only once when multiple selections are on the same line", -> - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - editor.setSelectedBufferRanges([ - [[0, 1], [0, 2]], - [[0, 4], [0, 5]] - ]) - expect(buffer.lineForRow(0)).not.toBe(line1) - - editor.deleteLine() - - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "only deletes first line if only newline is selected on second line", -> - editor.setSelectedBufferRange([[0, 2], [1, 0]]) - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line1) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "deletes the entire region when invoke on a folded region", -> - editor.foldBufferRow(1) - editor.getLastCursor().moveToTop() - editor.getLastCursor().moveDown() - expect(buffer.getLineCount()).toBe(13) - editor.deleteLine() - expect(buffer.getLineCount()).toBe(4) - - it "deletes the entire file from the bottom up", -> - count = buffer.getLineCount() - expect(count).toBeGreaterThan(0) - for [0...count] - editor.getLastCursor().moveToBottom() - editor.deleteLine() - expect(buffer.getLineCount()).toBe(1) - expect(buffer.getText()).toBe('') - - it "deletes the entire file from the top down", -> - count = buffer.getLineCount() - expect(count).toBeGreaterThan(0) - for [0...count] - editor.getLastCursor().moveToTop() - editor.deleteLine() - expect(buffer.getLineCount()).toBe(1) - expect(buffer.getText()).toBe('') - - describe "when soft wrap is enabled", -> - it "deletes the entire line that the cursor is on", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - editor.setCursorBufferPosition([6]) - - line7 = buffer.lineForRow(7) - count = buffer.getLineCount() - expect(buffer.lineForRow(6)).not.toBe(line7) - editor.deleteLine() - expect(buffer.lineForRow(6)).toBe(line7) - expect(buffer.getLineCount()).toBe(count - 1) - - describe "when the line being deleted precedes a fold, and the command is undone", -> - it "restores the line and preserves the fold", -> - editor.setCursorBufferPosition([4]) - editor.foldCurrentRow() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - editor.setCursorBufferPosition([3]) - editor.deleteLine() - expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() - expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' - editor.undo() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - - describe ".replaceSelectedText(options, fn)", -> - describe "when no text is selected", -> - it "inserts the text returned from the function at the cursor position", -> - editor.replaceSelectedText {}, -> '123' - expect(buffer.lineForRow(0)).toBe '123var quicksort = function () {' - - editor.setCursorBufferPosition([0]) - editor.replaceSelectedText {selectWordIfEmpty: true}, -> 'var' - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - - editor.setCursorBufferPosition([10]) - editor.replaceSelectedText null, -> '' - expect(buffer.lineForRow(10)).toBe '' - - describe "when text is selected", -> - it "replaces the selected text with the text returned from the function", -> - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - editor.replaceSelectedText {}, -> 'ia' - expect(buffer.lineForRow(0)).toBe 'via quicksort = function () {' - - it "replaces the selected text and selects the replacement text", -> - editor.setSelectedBufferRange([[0, 4], [0, 9]]) - editor.replaceSelectedText {}, -> 'whatnot' - expect(buffer.lineForRow(0)).toBe 'var whatnotsort = function () {' - expect(editor.getSelectedBufferRange()).toEqual [[0, 4], [0, 11]] - - describe ".transpose()", -> - it "swaps two characters", -> - editor.buffer.setText("abc") - editor.setCursorScreenPosition([0, 1]) - editor.transpose() - expect(editor.lineTextForBufferRow(0)).toBe 'bac' - - it "reverses a selection", -> - editor.buffer.setText("xabcz") - editor.setSelectedBufferRange([[0, 1], [0, 4]]) - editor.transpose() - expect(editor.lineTextForBufferRow(0)).toBe 'xcbaz' - - describe ".upperCase()", -> - describe "when there is no selection", -> - it "upper cases the current word", -> - editor.buffer.setText("aBc") - editor.setCursorScreenPosition([0, 1]) - editor.upperCase() - expect(editor.lineTextForBufferRow(0)).toBe 'ABC' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] - - describe "when there is a selection", -> - it "upper cases the current selection", -> - editor.buffer.setText("abc") - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - editor.upperCase() - expect(editor.lineTextForBufferRow(0)).toBe 'ABc' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - - describe ".lowerCase()", -> - describe "when there is no selection", -> - it "lower cases the current word", -> - editor.buffer.setText("aBC") - editor.setCursorScreenPosition([0, 1]) - editor.lowerCase() - expect(editor.lineTextForBufferRow(0)).toBe 'abc' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] - - describe "when there is a selection", -> - it "lower cases the current selection", -> - editor.buffer.setText("ABC") - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - editor.lowerCase() - expect(editor.lineTextForBufferRow(0)).toBe 'abC' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - - describe '.setTabLength(tabLength)', -> - it 'clips atomic soft tabs to the given tab length', -> - expect(editor.getTabLength()).toBe 2 - expect(editor.clipScreenPosition([5, 1], clipDirection: 'forward')).toEqual([5, 2]) - - editor.setTabLength(6) - expect(editor.getTabLength()).toBe 6 - expect(editor.clipScreenPosition([5, 1], clipDirection: 'forward')).toEqual([5, 6]) - - changeHandler = jasmine.createSpy('changeHandler') - editor.onDidChange(changeHandler) - editor.setTabLength(6) - expect(changeHandler).not.toHaveBeenCalled() - - it 'does not change its tab length when the given tab length is null', -> - editor.setTabLength(4) - editor.setTabLength(null) - expect(editor.getTabLength()).toBe(4) - - describe ".indentLevelForLine(line)", -> - it "returns the indent level when the line has only leading whitespace", -> - expect(editor.indentLevelForLine(" hello")).toBe(2) - expect(editor.indentLevelForLine(" hello")).toBe(1.5) - - it "returns the indent level when the line has only leading tabs", -> - expect(editor.indentLevelForLine("\t\thello")).toBe(2) - - it "returns the indent level based on the character starting the line when the leading whitespace contains both spaces and tabs", -> - expect(editor.indentLevelForLine("\t hello")).toBe(2) - expect(editor.indentLevelForLine(" \thello")).toBe(2) - expect(editor.indentLevelForLine(" \t hello")).toBe(2.5) - expect(editor.indentLevelForLine(" \t \thello")).toBe(4) - expect(editor.indentLevelForLine(" \t \thello")).toBe(4) - expect(editor.indentLevelForLine(" \t \t hello")).toBe(4.5) - - describe "when a better-matched grammar is added to syntax", -> - it "switches to the better-matched grammar and re-tokenizes the buffer", -> - editor.destroy() - - jsGrammar = atom.grammars.selectGrammar('a.js') - atom.grammars.removeGrammar(jsGrammar) - - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor = o - - runs -> - expect(editor.getGrammar()).toBe atom.grammars.nullGrammar - expect(editor.tokensForScreenRow(0).length).toBe(1) - - atom.grammars.addGrammar(jsGrammar) - expect(editor.getGrammar()).toBe jsGrammar - expect(editor.tokensForScreenRow(0).length).toBeGreaterThan 1 - - describe "editor.autoIndent", -> - describe "when editor.autoIndent is false (default)", -> - describe "when `indent` is triggered", -> - it "does not auto-indent the line", -> - editor.setCursorBufferPosition([1, 30]) - editor.insertText("\n ") - expect(editor.lineTextForBufferRow(2)).toBe " " - - editor.update({autoIndent: false}) - editor.indent() - expect(editor.lineTextForBufferRow(2)).toBe " " - - describe "when editor.autoIndent is true", -> - beforeEach -> - editor.update({autoIndent: true}) - - describe "when `indent` is triggered", -> - it "auto-indents the line", -> - editor.setCursorBufferPosition([1, 30]) - editor.insertText("\n ") - expect(editor.lineTextForBufferRow(2)).toBe " " - - editor.update({autoIndent: true}) - editor.indent() - expect(editor.lineTextForBufferRow(2)).toBe " " - - describe "when a newline is added", -> - describe "when the line preceding the newline adds a new level of indentation", -> - it "indents the newline to one additional level of indentation beyond the preceding line", -> - editor.setCursorBufferPosition([1, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - - describe "when the line preceding the newline doesn't add a level of indentation", -> - it "indents the new line to the same level as the preceding line", -> - editor.setCursorBufferPosition([5, 14]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5) - - describe "when the line preceding the newline is a comment", -> - it "maintains the indent of the commented line", -> - editor.setCursorBufferPosition([0, 0]) - editor.insertText(' //') - editor.setCursorBufferPosition([0, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(1)).toBe 2 - - describe "when the line preceding the newline contains only whitespace", -> - it "bases the new line's indentation on only the preceding line", -> - editor.setCursorBufferPosition([6, Infinity]) - editor.insertText("\n ") - expect(editor.getCursorBufferPosition()).toEqual([7, 2]) - - editor.insertNewline() - expect(editor.lineTextForBufferRow(8)).toBe(" ") - - it "does not indent the line preceding the newline", -> - editor.setCursorBufferPosition([2, 0]) - editor.insertText(' var this-line-should-be-indented-more\n') - expect(editor.indentationForBufferRow(1)).toBe 1 - - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([2, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 1 - - describe "when the cursor is before whitespace", -> - it "retains the whitespace following the cursor on the new line", -> - editor.setText(" var sort = function() {}") - editor.setCursorScreenPosition([0, 12]) - editor.insertNewline() - - expect(buffer.lineForRow(0)).toBe ' var sort =' - expect(buffer.lineForRow(1)).toBe ' function() {}' - expect(editor.getCursorScreenPosition()).toEqual [1, 2] - - describe "when inserted text matches a decrease indent pattern", -> - describe "when the preceding line matches an increase indent pattern", -> - it "decreases the indentation to match that of the preceding line", -> - editor.setCursorBufferPosition([1, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - editor.insertText('}') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) - - describe "when the preceding line doesn't match an increase indent pattern", -> - it "decreases the indentation to be one level below that of the preceding line", -> - editor.setCursorBufferPosition([3, Infinity]) - editor.insertText('\n ') - expect(editor.indentationForBufferRow(4)).toBe editor.indentationForBufferRow(3) - editor.insertText('}') - expect(editor.indentationForBufferRow(4)).toBe editor.indentationForBufferRow(3) - 1 - - it "doesn't break when decreasing the indentation on a row that has no indentation", -> - editor.setCursorBufferPosition([12, Infinity]) - editor.insertText("\n}; # too many closing brackets!") - expect(editor.lineTextForBufferRow(13)).toBe "}; # too many closing brackets!" - - describe "when inserted text does not match a decrease indent pattern", -> - it "does not decrease the indentation", -> - editor.setCursorBufferPosition([12, 0]) - editor.insertText(' ') - expect(editor.lineTextForBufferRow(12)).toBe ' };' - editor.insertText('\t\t') - expect(editor.lineTextForBufferRow(12)).toBe ' \t\t};' - - describe "when the current line does not match a decrease indent pattern", -> - it "leaves the line unchanged", -> - editor.setCursorBufferPosition([2, 4]) - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - editor.insertText('foo') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - - describe "atomic soft tabs", -> - it "skips tab-length runs of leading whitespace when moving the cursor", -> - editor.update({tabLength: 4, atomicSoftTabs: true}) - - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 4] - - editor.update({atomicSoftTabs: false}) - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 3] - - editor.update({atomicSoftTabs: true}) - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 4] - - describe ".destroy()", -> - it "destroys marker layers associated with the text editor", -> - buffer.retain() - selectionsMarkerLayerId = editor.selectionsMarkerLayer.id - foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id - editor.destroy() - expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() - expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() - buffer.release() - - it "notifies ::onDidDestroy observers when the editor is destroyed", -> - destroyObserverCalled = false - editor.onDidDestroy -> destroyObserverCalled = true - - editor.destroy() - expect(destroyObserverCalled).toBe true - - it "does not blow up when query methods are called afterward", -> - editor.destroy() - editor.getGrammar() - editor.getLastCursor() - editor.lineTextForBufferRow(0) - - it "emits the destroy event after destroying the editor's buffer", -> - events = [] - editor.getBuffer().onDidDestroy -> - expect(editor.isDestroyed()).toBe(true) - events.push('buffer-destroyed') - editor.onDidDestroy -> - expect(buffer.isDestroyed()).toBe(true) - events.push('editor-destroyed') - editor.destroy() - expect(events).toEqual(['buffer-destroyed', 'editor-destroyed']) - - describe ".joinLines()", -> - describe "when no text is selected", -> - describe "when the line below isn't empty", -> - it "joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up", -> - editor.setCursorBufferPosition([0, Infinity]) - editor.insertText(' ') - editor.setCursorBufferPosition([0]) - editor.joinLines() - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' - expect(editor.getCursorBufferPosition()).toEqual [0, 29] - - describe "when the line below is empty", -> - it "deletes the line below and moves the cursor to the end of the line", -> - editor.setCursorBufferPosition([9]) - editor.joinLines() - expect(editor.lineTextForBufferRow(9)).toBe ' };' - expect(editor.lineTextForBufferRow(10)).toBe ' return sort(Array.apply(this, arguments));' - expect(editor.getCursorBufferPosition()).toEqual [9, 4] - - describe "when the cursor is on the last row", -> - it "does nothing", -> - editor.setCursorBufferPosition([Infinity, Infinity]) - editor.joinLines() - expect(editor.lineTextForBufferRow(12)).toBe '};' - - describe "when the line is empty", -> - it "joins the line below with the current line with no added space", -> - editor.setCursorBufferPosition([10]) - editor.joinLines() - expect(editor.lineTextForBufferRow(10)).toBe 'return sort(Array.apply(this, arguments));' - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - describe "when text is selected", -> - describe "when the selection does not span multiple lines", -> - it "joins the line below with the current line separated by a space and retains the selected text", -> - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - editor.joinLines() - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] - - describe "when the selection spans multiple lines", -> - it "joins all selected lines separated by a space and retains the selected text", -> - editor.setSelectedBufferRange([[9, 3], [12, 1]]) - editor.joinLines() - expect(editor.lineTextForBufferRow(9)).toBe ' }; return sort(Array.apply(this, arguments)); };' - expect(editor.getSelectedBufferRange()).toEqual [[9, 3], [9, 49]] - - describe ".duplicateLines()", -> - it "for each selection, duplicates all buffer lines intersected by the selection", -> - editor.foldBufferRow(4) - editor.setCursorBufferPosition([2, 5]) - editor.addSelectionForBufferRange([[3, 0], [8, 0]], preserveFolds: true) - - editor.duplicateLines() - - expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe """ - \ if (items.length <= 1) return items; - if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - """ - expect(editor.getSelectedBufferRanges()).toEqual [[[3, 5], [3, 5]], [[9, 0], [14, 0]]] - - # folds are also duplicated - expect(editor.isFoldedAtScreenRow(5)).toBe(true) - expect(editor.isFoldedAtScreenRow(7)).toBe(true) - expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {" + editor.displayLayer.foldCharacter - expect(editor.lineTextForScreenRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - it "duplicates all folded lines for empty selections on lines containing folds", -> - editor.foldBufferRow(4) - editor.setCursorBufferPosition([4, 0]) - - editor.duplicateLines() - - expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe """ - \ if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - """ - expect(editor.getSelectedBufferRange()).toEqual [[8, 0], [8, 0]] - - it "can duplicate the last line of the buffer", -> - editor.setSelectedBufferRange([[11, 0], [12, 2]]) - editor.duplicateLines() - expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe """ - \ return sort(Array.apply(this, arguments)); - }; - return sort(Array.apply(this, arguments)); - }; - """ - expect(editor.getSelectedBufferRange()).toEqual [[13, 0], [14, 2]] - - it "only duplicates lines containing multiple selections once", -> - editor.setText(""" - aaaaaa - bbbbbb - cccccc - dddddd - """) - editor.setSelectedBufferRanges([ - [[0, 1], [0, 2]], - [[0, 3], [0, 4]], - [[2, 1], [2, 2]], - [[2, 3], [3, 1]], - [[3, 3], [3, 4]], - ]) - editor.duplicateLines() - expect(editor.getText()).toBe(""" - aaaaaa - aaaaaa - bbbbbb - cccccc - dddddd - cccccc - dddddd - """) - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 1], [1, 2]], - [[1, 3], [1, 4]], - [[5, 1], [5, 2]], - [[5, 3], [6, 1]], - [[6, 3], [6, 4]], - ]) - - describe "when the editor contains surrogate pair characters", -> - it "correctly backspaces over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') - editor.moveToBottom() - editor.backspace() - expect(editor.getText()).toBe '\uD835\uDF97\uD835\uDF97' - editor.backspace() - expect(editor.getText()).toBe '\uD835\uDF97' - editor.backspace() - expect(editor.getText()).toBe '' - - it "correctly deletes over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') - editor.moveToTop() - editor.delete() - expect(editor.getText()).toBe '\uD835\uDF97\uD835\uDF97' - editor.delete() - expect(editor.getText()).toBe '\uD835\uDF97' - editor.delete() - expect(editor.getText()).toBe '' - - it "correctly moves over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n') - editor.moveToTop() - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe "when the editor contains variation sequence character pairs", -> - it "correctly backspaces over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') - editor.moveToBottom() - editor.backspace() - expect(editor.getText()).toBe '\u2714\uFE0E\u2714\uFE0E' - editor.backspace() - expect(editor.getText()).toBe '\u2714\uFE0E' - editor.backspace() - expect(editor.getText()).toBe '' - - it "correctly deletes over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') - editor.moveToTop() - editor.delete() - expect(editor.getText()).toBe '\u2714\uFE0E\u2714\uFE0E' - editor.delete() - expect(editor.getText()).toBe '\u2714\uFE0E' - editor.delete() - expect(editor.getText()).toBe '' - - it "correctly moves over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E\n') - editor.moveToTop() - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".setIndentationForBufferRow", -> - describe "when the editor uses soft tabs but the row has hard tabs", -> - it "only replaces whitespace characters", -> - editor.setSoftWrapped(true) - editor.setText("\t1\n\t2") - editor.setCursorBufferPosition([0, 0]) - editor.setIndentationForBufferRow(0, 2) - expect(editor.getText()).toBe(" 1\n\t2") - - describe "when the indentation level is a non-integer", -> - it "does not throw an exception", -> - editor.setSoftWrapped(true) - editor.setText("\t1\n\t2") - editor.setCursorBufferPosition([0, 0]) - editor.setIndentationForBufferRow(0, 2.1) - expect(editor.getText()).toBe(" 1\n\t2") - - describe "when the editor's grammar has an injection selector", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-text') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - it "includes the grammar's patterns when the selector matches the current scope in other grammars", -> - waitsForPromise -> - atom.packages.activatePackage('language-hyperlink') - - runs -> - grammar = atom.grammars.selectGrammar("text.js") - {line, tags} = grammar.tokenizeLine("var i; // http://github.com") - - tokens = atom.grammars.decodeTokens(line, tags) - expect(tokens[0].value).toBe "var" - expect(tokens[0].scopes).toEqual ["source.js", "storage.type.var.js"] - - expect(tokens[6].value).toBe "http://github.com" - expect(tokens[6].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] - - describe "when the grammar is added", -> - it "retokenizes existing buffers that contain tokens that match the injection selector", -> - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - editor.setText("// http://github.com") - - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('language-hyperlink') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - {text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']} - ] - - describe "when the grammar is updated", -> - it "retokenizes existing buffers that contain tokens that match the injection selector", -> - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - editor.setText("// SELECT * FROM OCTOCATS") - - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('package-with-injection-selector') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('language-sql') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, - {text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - describe ".normalizeTabsInBufferRange()", -> - it "normalizes tabs depending on the editor's soft tab/tab length settings", -> - editor.setTabLength(1) - editor.setSoftTabs(true) - editor.setText('\t\t\t') - editor.normalizeTabsInBufferRange([[0, 0], [0, 1]]) - expect(editor.getText()).toBe ' \t\t' - - editor.setTabLength(2) - editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) - expect(editor.getText()).toBe ' ' - - editor.setSoftTabs(false) - editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) - expect(editor.getText()).toBe ' ' - - describe ".pageUp/Down()", -> - it "moves the cursor down one page length", -> - editor.update(autoHeight: false) - element = editor.getElement() - jasmine.attachToDOM(element) - element.style.height = element.component.getLineHeight() * 5 + 'px' - element.measureDimensions() - - expect(editor.getCursorBufferPosition().row).toBe 0 - - editor.pageDown() - expect(editor.getCursorBufferPosition().row).toBe 5 - - editor.pageDown() - expect(editor.getCursorBufferPosition().row).toBe 10 - - editor.pageUp() - expect(editor.getCursorBufferPosition().row).toBe 5 - - editor.pageUp() - expect(editor.getCursorBufferPosition().row).toBe 0 - - describe ".selectPageUp/Down()", -> - it "selects one screen height of text up or down", -> - editor.update(autoHeight: false) - element = editor.getElement() - jasmine.attachToDOM(element) - element.style.height = element.component.getLineHeight() * 5 + 'px' - element.measureDimensions() - - expect(editor.getCursorBufferPosition().row).toBe 0 - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [5, 0]]] - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [10, 0]]] - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]] - - editor.moveToBottom() - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [12, 2]]] - - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 0], [12, 2]]] - - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]] - - describe "::scrollToScreenPosition(position, [options])", -> - it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", -> - scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll") - editor.onDidRequestAutoscroll(scrollSpy) - - editor.scrollToScreenPosition([8, 20]) - editor.scrollToScreenPosition([8, 20], center: true) - editor.scrollToScreenPosition([8, 20], center: false, reversed: true) - - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {}) - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true}) - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}) - - describe "scroll past end", -> - it "returns false by default but can be customized", -> - expect(editor.getScrollPastEnd()).toBe(false) - editor.update({scrollPastEnd: true}) - expect(editor.getScrollPastEnd()).toBe(true) - editor.update({scrollPastEnd: false}) - expect(editor.getScrollPastEnd()).toBe(false) - - it "always returns false when autoHeight is on", -> - editor.update({autoHeight: true, scrollPastEnd: true}) - expect(editor.getScrollPastEnd()).toBe(false) - editor.update({autoHeight: false}) - expect(editor.getScrollPastEnd()).toBe(true) - - describe "auto height", -> - it "returns true by default but can be customized", -> - editor = new TextEditor - expect(editor.getAutoHeight()).toBe(true) - editor.update({autoHeight: false}) - expect(editor.getAutoHeight()).toBe(false) - editor.update({autoHeight: true}) - expect(editor.getAutoHeight()).toBe(true) - editor.destroy() - - describe "auto width", -> - it "returns false by default but can be customized", -> - expect(editor.getAutoWidth()).toBe(false) - editor.update({autoWidth: true}) - expect(editor.getAutoWidth()).toBe(true) - editor.update({autoWidth: false}) - expect(editor.getAutoWidth()).toBe(false) - - describe '.get/setPlaceholderText()', -> - it 'can be created with placeholderText', -> - newEditor = new TextEditor({ - mini: true - placeholderText: 'yep' - }) - expect(newEditor.getPlaceholderText()).toBe 'yep' - - it 'models placeholderText and emits an event when changed', -> - editor.onDidChangePlaceholderText handler = jasmine.createSpy() - - expect(editor.getPlaceholderText()).toBeUndefined() - - editor.setPlaceholderText('OK') - expect(handler).toHaveBeenCalledWith 'OK' - expect(editor.getPlaceholderText()).toBe 'OK' - - describe 'gutters', -> - describe 'the TextEditor constructor', -> - it 'creates a line-number gutter', -> - expect(editor.getGutters().length).toBe 1 - lineNumberGutter = editor.gutterWithName('line-number') - expect(lineNumberGutter.name).toBe 'line-number' - expect(lineNumberGutter.priority).toBe 0 - - describe '::addGutter', -> - it 'can add a gutter', -> - expect(editor.getGutters().length).toBe 1 # line-number gutter - options = - name: 'test-gutter' - priority: 1 - gutter = editor.addGutter options - expect(editor.getGutters().length).toBe 2 - expect(editor.getGutters()[1]).toBe gutter - - it "does not allow a custom gutter with the 'line-number' name.", -> - expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow() - - describe '::decorateMarker', -> - [marker] = [] - - beforeEach -> - marker = editor.markBufferRange([[1, 0], [1, 0]]) - - it 'reflects an added decoration when one of its custom gutters is decorated.', -> - gutter = editor.addGutter {'name': 'custom-gutter'} - decoration = gutter.decorateMarker marker, {class: 'custom-class'} - gutterDecorations = editor.getDecorations - type: 'gutter' - gutterName: 'custom-gutter' - class: 'custom-class' - expect(gutterDecorations.length).toBe 1 - expect(gutterDecorations[0]).toBe decoration - - it 'reflects an added decoration when its line-number gutter is decorated.', -> - decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'} - gutterDecorations = editor.getDecorations - type: 'line-number' - gutterName: 'line-number' - class: 'test-class' - expect(gutterDecorations.length).toBe 1 - expect(gutterDecorations[0]).toBe decoration - - describe '::observeGutters', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', -> - lineNumberGutter = editor.gutterWithName('line-number') - editor.observeGutters(callback) - expect(payloads).toEqual [lineNumberGutter] - gutter1 = editor.addGutter({name: 'test-gutter-1'}) - expect(payloads).toEqual [lineNumberGutter, gutter1] - gutter2 = editor.addGutter({name: 'test-gutter-2'}) - expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2] - - it 'does not call the callback when a gutter is removed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - editor.observeGutters(callback) - payloads = [] - gutter.destroy() - expect(payloads).toEqual [] - - it 'does not call the callback after the subscription has been disposed.', -> - subscription = editor.observeGutters(callback) - payloads = [] - subscription.dispose() - editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [] - - describe '::onDidAddGutter', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback with each newly-added gutter, but not with existing gutters.', -> - editor.onDidAddGutter(callback) - expect(payloads).toEqual [] - gutter = editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [gutter] - - it 'does not call the callback after the subscription has been disposed.', -> - subscription = editor.onDidAddGutter(callback) - payloads = [] - subscription.dispose() - editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [] - - describe '::onDidRemoveGutter', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback when a gutter is removed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - editor.onDidRemoveGutter(callback) - expect(payloads).toEqual [] - gutter.destroy() - expect(payloads).toEqual ['test-gutter'] - - it 'does not call the callback after the subscription has been disposed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - subscription = editor.onDidRemoveGutter(callback) - subscription.dispose() - gutter.destroy() - expect(payloads).toEqual [] - - describe "decorations", -> - describe "::decorateMarker", -> - it "includes the decoration in the object returned from ::decorationsStateForScreenRowRange", -> - marker = editor.markBufferRange([[2, 4], [6, 8]]) - decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') - expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual { - properties: {type: 'highlight', class: 'foo'} - screenRange: marker.getScreenRange(), - bufferRange: marker.getBufferRange(), - rangeIsReversed: false - } - - it "does not throw errors after the marker's containing layer is destroyed", -> - layer = editor.addMarkerLayer() - marker = layer.markBufferRange([[2, 4], [6, 8]]) - decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') - layer.destroy() - editor.decorationsStateForScreenRowRange(0, 5) - - describe "::decorateMarkerLayer", -> - it "based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange", -> - layer1 = editor.getBuffer().addMarkerLayer() - marker1 = layer1.markRange([[2, 4], [6, 8]]) - marker2 = layer1.markRange([[11, 0], [11, 12]]) - layer2 = editor.getBuffer().addMarkerLayer() - marker3 = layer2.markRange([[8, 0], [9, 0]]) - - layer1Decoration1 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'foo') - layer1Decoration2 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'bar') - layer2Decoration = editor.decorateMarkerLayer(layer2, type: 'highlight', class: 'baz') - - decorationState = editor.decorationsStateForScreenRowRange(0, 13) - - expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'foo'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'foo'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { - properties: {type: 'highlight', class: 'baz'}, - screenRange: marker3.getRange(), - bufferRange: marker3.getRange(), - rangeIsReversed: false - } - - layer1Decoration1.destroy() - - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toBeUndefined() - expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toBeUndefined() - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { - properties: {type: 'highlight', class: 'baz'}, - screenRange: marker3.getRange(), - bufferRange: marker3.getRange(), - rangeIsReversed: false - } - - layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'}) - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'quux'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - - layer1Decoration2.setPropertiesForMarker(marker1, null) - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - - describe "invisibles", -> - beforeEach -> - editor.update({showInvisibles: true}) - - it "substitutes invisible characters according to the given rules", -> - previousLineText = editor.lineTextForScreenRow(0) - editor.update({invisibles: {eol: '?'}}) - expect(editor.lineTextForScreenRow(0)).not.toBe(previousLineText) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) - expect(editor.getInvisibles()).toEqual(eol: '?') - - it "does not use invisibles if showInvisibles is set to false", -> - editor.update({invisibles: {eol: '?'}}) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) - - editor.update({showInvisibles: false}) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(false) - - describe "indent guides", -> - it "shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini", -> - editor.setText(" foo") - editor.setTabLength(2) - - editor.update({showIndentGuide: false}) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - editor.update({showIndentGuide: true}) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - editor.setMini(true) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - describe "when the editor is constructed with the grammar option set", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - it "sets the grammar", -> - editor = new TextEditor({grammar: atom.grammars.grammarForScopeName('source.coffee')}) - expect(editor.getGrammar().name).toBe 'CoffeeScript' - - describe "softWrapAtPreferredLineLength", -> - it "soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini", -> - editor.update({ - editorWidthInChars: 30 - softWrapped: true - softWrapAtPreferredLineLength: true - preferredLineLength: 20 - }) - - expect(editor.lineTextForScreenRow(0)).toBe 'var quicksort = ' - - editor.update({editorWidthInChars: 10}) - expect(editor.lineTextForScreenRow(0)).toBe 'var ' - - editor.update({mini: true}) - expect(editor.lineTextForScreenRow(0)).toBe 'var quicksort = function () {' - - describe "softWrapHangingIndentLength", -> - it "controls how much extra indentation is applied to soft-wrapped lines", -> - editor.setText('123456789') - editor.update({ - editorWidthInChars: 8 - softWrapped: true - softWrapHangingIndentLength: 2 - }) - expect(editor.lineTextForScreenRow(1)).toEqual ' 9' - - editor.update({softWrapHangingIndentLength: 4}) - expect(editor.lineTextForScreenRow(1)).toEqual ' 9' - - describe "::getElement", -> - it "returns an element", -> - expect(editor.getElement() instanceof HTMLElement).toBe(true) - - describe 'setMaxScreenLineLength', -> - it "sets the maximum line length in the editor before soft wrapping is forced", -> - expect(editor.getSoftWrapColumn()).toBe(500) - editor.update({ - maxScreenLineLength: 1500 - }) - expect(editor.getSoftWrapColumn()).toBe(1500) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index d10efa695..b2cc41ab7 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -1,9 +1,6655 @@ +const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers') + const fs = require('fs') +const path = require('path') 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 dedent = require('dedent') +const clipboard = require('../src/safe-clipboard') const TextEditor = require('../src/text-editor') +const TextBuffer = require('text-buffer') + +describe('TextEditor', () => { + let buffer, editor, lineLengths + + beforeEach(async () => { + editor = await atom.workspace.open('sample.js') + buffer = editor.buffer + editor.update({autoIndent: false}) + lineLengths = buffer.getLines().map(line => line.length) + await atom.packages.activatePackage('language-javascript') + }) + + describe('when the editor is deserialized', () => { + it('restores selections and folds based on markers in the buffer', async () => { + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 5]], {reversed: true}) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + const buffer2 = await TextBuffer.deserialize(editor.buffer.serialize()) + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return buffer2 }} + }) + + expect(editor2.id).toBe(editor.id) + expect(editor2.getBuffer().getPath()).toBe(editor.getBuffer().getPath()) + expect(editor2.getSelectedBufferRanges()).toEqual([[[1, 2], [3, 4]], [[5, 6], [7, 5]]]) + expect(editor2.getSelections()[1].isReversed()).toBeTruthy() + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + editor2.destroy() + }) + + it("restores the editor's layout configuration", async () => { + editor.update({ + softTabs: true, + atomicSoftTabs: false, + tabLength: 12, + softWrapped: true, + softWrapAtPreferredLineLength: true, + softWrapHangingIndentLength: 8, + invisibles: {space: 'S'}, + showInvisibles: true, + editorWidthInChars: 120 + }) + + // Force buffer and display layer to be deserialized as well, rather than + // reusing the same buffer instance + const buffer2 = await TextBuffer.deserialize(editor.buffer.serialize()) + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return buffer2 }} + }) + + expect(editor2.getSoftTabs()).toBe(editor.getSoftTabs()) + expect(editor2.hasAtomicSoftTabs()).toBe(editor.hasAtomicSoftTabs()) + expect(editor2.getTabLength()).toBe(editor.getTabLength()) + expect(editor2.getSoftWrapColumn()).toBe(editor.getSoftWrapColumn()) + expect(editor2.getSoftWrapHangingIndentLength()).toBe(editor.getSoftWrapHangingIndentLength()) + expect(editor2.getInvisibles()).toEqual(editor.getInvisibles()) + expect(editor2.getEditorWidthInChars()).toBe(editor.getEditorWidthInChars()) + expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength()) + expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn()) + }) + + it('ignores buffers with retired IDs', () => { + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return null }} + }) + + expect(editor2).toBeNull() + }) + }) + + describe('when the editor is constructed with the largeFileMode option set to true', () => { + it("loads the editor but doesn't tokenize", async () => { + editor = await atom.workspace.openTextFile('sample.js', {largeFileMode: true}) + buffer = editor.getBuffer() + expect(editor.lineTextForScreenRow(0)).toBe(buffer.lineForRow(0)) + expect(editor.tokensForScreenRow(0).length).toBe(1) + expect(editor.tokensForScreenRow(1).length).toBe(2) // soft tab + expect(editor.lineTextForScreenRow(12)).toBe(buffer.lineForRow(12)) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + editor.insertText('hey"') + expect(editor.tokensForScreenRow(0).length).toBe(1) + expect(editor.tokensForScreenRow(1).length).toBe(2) + }) + }) + + describe('.copy()', () => { + it('returns a different editor with the same initial state', () => { + expect(editor.getAutoHeight()).toBeFalsy() + expect(editor.getAutoWidth()).toBeFalsy() + expect(editor.getShowCursorOnSelection()).toBeTruthy() + + const element = editor.getElement() + element.setHeight(100) + element.setWidth(100) + jasmine.attachToDOM(element) + + editor.update({showCursorOnSelection: false}) + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 8]], {reversed: true}) + editor.setScrollTopRow(3) + expect(editor.getScrollTopRow()).toBe(3) + editor.setScrollLeftColumn(4) + expect(editor.getScrollLeftColumn()).toBe(4) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + const editor2 = editor.copy() + const element2 = editor2.getElement() + element2.setHeight(100) + element2.setWidth(100) + jasmine.attachToDOM(element2) + expect(editor2.id).not.toBe(editor.id) + expect(editor2.getSelectedBufferRanges()).toEqual(editor.getSelectedBufferRanges()) + expect(editor2.getSelections()[1].isReversed()).toBeTruthy() + expect(editor2.getScrollTopRow()).toBe(3) + expect(editor2.getScrollLeftColumn()).toBe(4) + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor2.getAutoWidth()).toBe(false) + expect(editor2.getAutoHeight()).toBe(false) + expect(editor2.getShowCursorOnSelection()).toBeFalsy() + + // editor2 can now diverge from its origin edit session + editor2.getLastSelection().setBufferRange([[2, 1], [4, 3]]) + expect(editor2.getSelectedBufferRanges()).not.toEqual(editor.getSelectedBufferRanges()) + editor2.unfoldBufferRow(4) + expect(editor2.isFoldedAtBufferRow(4)).not.toBe(editor.isFoldedAtBufferRow(4)) + }) + }) + + describe('.update()', () => { + it('updates the editor with the supplied config parameters', () => { + let changeSpy + const { element } = editor // force element initialization + element.setUpdatedSynchronously(false) + editor.update({showInvisibles: true}) + editor.onDidChange(changeSpy = jasmine.createSpy('onDidChange')) + + const returnedPromise = editor.update({ + tabLength: 6, + softTabs: false, + softWrapped: true, + editorWidthInChars: 40, + showInvisibles: false, + mini: false, + lineNumberGutterVisible: false, + scrollPastEnd: true, + autoHeight: false, + maxScreenLineLength: 1000 + }) + + expect(returnedPromise).toBe(element.component.getNextUpdatePromise()) + expect(changeSpy.callCount).toBe(1) + expect(editor.getTabLength()).toBe(6) + expect(editor.getSoftTabs()).toBe(false) + expect(editor.isSoftWrapped()).toBe(true) + expect(editor.getEditorWidthInChars()).toBe(40) + expect(editor.getInvisibles()).toEqual({}) + expect(editor.isMini()).toBe(false) + expect(editor.isLineNumberGutterVisible()).toBe(false) + expect(editor.getScrollPastEnd()).toBe(true) + expect(editor.getAutoHeight()).toBe(false) + }) + }) + + describe('title', () => { + describe('.getTitle()', () => { + it("uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", () => { + expect(editor.getTitle()).toBe('sample.js') + buffer.setPath(undefined) + expect(editor.getTitle()).toBe('untitled') + }) + }) + + describe('.getLongTitle()', () => { + it('returns file name when there is no opened file with identical name', () => { + expect(editor.getLongTitle()).toBe('sample.js') + buffer.setPath(undefined) + expect(editor.getLongTitle()).toBe('untitled') + }) + + it("returns '' when opened files have identical file names", async () => { + const editor1 = await atom.workspace.open(path.join('sample-theme-1', 'readme')) + const editor2 = await atom.workspace.open(path.join('sample-theme-2', 'readme')) + expect(editor1.getLongTitle()).toBe('readme \u2014 sample-theme-1') + expect(editor2.getLongTitle()).toBe('readme \u2014 sample-theme-2') + }) + + it("returns '' when opened files have identical file names in subdirectories", async () => { + const path1 = path.join('sample-theme-1', 'src', 'js') + const path2 = path.join('sample-theme-2', 'src', 'js') + const editor1 = await atom.workspace.open(path.join(path1, 'main.js')) + const editor2 = await atom.workspace.open(path.join(path2, 'main.js')) + expect(editor1.getLongTitle()).toBe(`main.js \u2014 ${path1}`) + expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path2}`) + }) + + it("returns '' when opened files have identical file and same parent dir name", async () => { + const editor1 = await atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')) + const editor2 = await atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'plugin', 'main.js')) + expect(editor1.getLongTitle()).toBe('main.js \u2014 js') + expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path.join('js', 'plugin')}`) + }) + }) + + it('notifies ::onDidChangeTitle observers when the underlying buffer path changes', () => { + const observed = [] + editor.onDidChangeTitle(title => observed.push(title)) + + buffer.setPath('/foo/bar/baz.txt') + buffer.setPath(undefined) + + expect(observed).toEqual(['baz.txt', 'untitled']) + }) + }) + + describe('path', () => { + it('notifies ::onDidChangePath observers when the underlying buffer path changes', () => { + const observed = [] + editor.onDidChangePath(filePath => observed.push(filePath)) + + buffer.setPath(__filename) + buffer.setPath(undefined) + + expect(observed).toEqual([__filename, undefined]) + }) + }) + + describe('encoding', () => { + it('notifies ::onDidChangeEncoding observers when the editor encoding changes', () => { + const observed = [] + editor.onDidChangeEncoding(encoding => observed.push(encoding)) + + editor.setEncoding('utf16le') + editor.setEncoding('utf16le') + editor.setEncoding('utf16be') + editor.setEncoding() + editor.setEncoding() + + expect(observed).toEqual(['utf16le', 'utf16be', 'utf8']) + }) + }) + + describe('cursor', () => { + describe('.getLastCursor()', () => { + it('returns the most recently created cursor', () => { + editor.addCursorAtScreenPosition([1, 0]) + const lastCursor = editor.addCursorAtScreenPosition([2, 0]) + expect(editor.getLastCursor()).toBe(lastCursor) + }) + + it('creates a new cursor at (0, 0) if the last cursor has been destroyed', () => { + editor.getLastCursor().destroy() + expect(editor.getLastCursor().getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.getCursors()', () => { + it('creates a new cursor at (0, 0) if the last cursor has been destroyed', () => { + editor.getLastCursor().destroy() + expect(editor.getCursors()[0].getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('when the cursor moves', () => { + it('clears a goal column established by vertical movement', () => { + editor.setText('b') + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + editor.moveUp() + editor.insertText('a') + editor.moveDown() + expect(editor.getCursorBufferPosition()).toEqual([1, 1]) + }) + + it('emits an event with the old position, new position, and the cursor that moved', () => { + const cursorCallback = jasmine.createSpy('cursor-changed-position') + const editorCallback = jasmine.createSpy('editor-changed-cursor-position') + + editor.getLastCursor().onDidChangePosition(cursorCallback) + editor.onDidChangeCursorPosition(editorCallback) + + editor.setCursorBufferPosition([2, 4]) + + expect(editorCallback).toHaveBeenCalled() + expect(cursorCallback).toHaveBeenCalled() + const eventObject = editorCallback.mostRecentCall.args[0] + expect(cursorCallback.mostRecentCall.args[0]).toEqual(eventObject) + + expect(eventObject.oldBufferPosition).toEqual([0, 0]) + expect(eventObject.oldScreenPosition).toEqual([0, 0]) + expect(eventObject.newBufferPosition).toEqual([2, 4]) + expect(eventObject.newScreenPosition).toEqual([2, 4]) + expect(eventObject.cursor).toBe(editor.getLastCursor()) + }) + }) + + describe('.setCursorScreenPosition(screenPosition)', () => { + it('clears a goal column established by vertical movement', () => { + // set a goal column by moving down + editor.setCursorScreenPosition({row: 3, column: lineLengths[3]}) + editor.moveDown() + expect(editor.getCursorScreenPosition().column).not.toBe(6) + + // clear the goal column by explicitly setting the cursor position + editor.setCursorScreenPosition([4, 6]) + expect(editor.getCursorScreenPosition().column).toBe(6) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(6) + }) + + it('merges multiple cursors', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 1]) + const [cursor1, cursor2] = editor.getCursors() + editor.setCursorScreenPosition([4, 7]) + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursors()).toEqual([cursor1]) + expect(editor.getCursorScreenPosition()).toEqual([4, 7]) + }) + + describe('when soft-wrap is enabled and code is folded', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + editor.foldBufferRowRange(2, 3) + }) + + it('positions the cursor at the buffer position that corresponds to the given screen position', () => { + editor.setCursorScreenPosition([9, 0]) + expect(editor.getCursorBufferPosition()).toEqual([8, 11]) + }) + }) + }) + + describe('.moveUp()', () => { + it('moves the cursor up', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([1, 2]) + }) + + it('retains the goal column across lines of differing length', () => { + expect(lineLengths[6]).toBeGreaterThan(32) + editor.setCursorScreenPosition({row: 6, column: 32}) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[5]) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[4]) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(32) + }) + + describe('when the cursor is on the first line', () => { + it('moves the cursor to the beginning of the line, but retains the goal column', () => { + editor.setCursorScreenPosition([0, 4]) + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([1, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[4, 9], [5, 10]])) + + it('moves above the selection', () => { + const cursor = editor.getLastCursor() + editor.moveUp() + expect(cursor.getBufferPosition()).toEqual([3, 9]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.addCursorAtScreenPosition([1, 0]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveUp() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + }) + + describe('when the cursor was moved down from the beginning of an indented soft-wrapped line', () => { + it('moves to the beginning of the previous line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + + editor.setCursorScreenPosition([3, 0]) + editor.moveDown() + editor.moveDown() + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([4, 4]) + }) + }) + }) + + describe('.moveDown()', () => { + it('moves the cursor down', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([3, 2]) + }) + + it('retains the goal column across lines of differing length', () => { + editor.setCursorScreenPosition({row: 3, column: lineLengths[3]}) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[4]) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[5]) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[3]) + }) + + describe('when the cursor is on the last line', () => { + it('moves the cursor to the end of line, but retains the goal column when moving back up', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + editor.setCursorScreenPosition({row: lastLineIndex, column: editor.getTabLength()}) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual({row: lastLineIndex, column: lastLine.length}) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(editor.getTabLength()) + }) + + it('retains a goal column of 0 when moving back up', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + editor.setCursorScreenPosition({row: lastLineIndex, column: 0}) + editor.moveDown() + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(0) + }) + }) + + describe('when the cursor is at the beginning of an indented soft-wrapped line', () => { + it("moves to the beginning of the line's continuation on the next screen row", () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + + editor.setCursorScreenPosition([3, 0]) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([4, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[4, 9], [5, 10]])) + + it('moves below the selection', () => { + const cursor = editor.getLastCursor() + editor.moveDown() + expect(cursor.getBufferPosition()).toEqual([6, 10]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([11, 2]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveDown() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveLeft()', () => { + it('moves the cursor by one column to the left', () => { + editor.setCursorScreenPosition([1, 8]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([1, 7]) + }) + + it('moves the cursor by n columns to the left', () => { + editor.setCursorScreenPosition([1, 8]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([1, 4]) + }) + + it('moves the cursor by two rows up when the columnCount is longer than an entire line', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveLeft(34) + expect(editor.getCursorScreenPosition()).toEqual([0, 29]) + }) + + it('moves the cursor to the beginning columnCount is longer than the position in the buffer', () => { + editor.setCursorScreenPosition([1, 0]) + editor.moveLeft(100) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + + describe('when the cursor is in the first column', () => { + describe('when there is a previous line', () => { + it('wraps to the end of the previous line', () => { + editor.setCursorScreenPosition({row: 1, column: 0}) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual({row: 0, column: buffer.lineForRow(0).length}) + }) + + it('moves the cursor by one row up and n columns to the left', () => { + editor.setCursorScreenPosition([1, 0]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([0, 26]) + }) + }) + + describe('when the next line is empty', () => { + it('wraps to the beginning of the previous line', () => { + editor.setCursorScreenPosition([11, 0]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([10, 0]) + }) + }) + + describe('when line is wrapped and follow previous line indentation', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + }) + + it('wraps to the end of the previous line', () => { + editor.setCursorScreenPosition([4, 4]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([3, 46]) + }) + }) + + describe('when the cursor is on the first line', () => { + it('remains in the same position (0,0)', () => { + editor.setCursorScreenPosition({row: 0, column: 0}) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual({row: 0, column: 0}) + }) + + it('remains in the same position (0,0) when columnCount is specified', () => { + editor.setCursorScreenPosition([0, 0]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + }) + }) + + describe('when softTabs is enabled and the cursor is preceded by leading whitespace', () => { + it('skips tabLength worth of whitespace at a time', () => { + editor.setCursorBufferPosition([5, 6]) + + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([5, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[5, 22], [5, 27]])) + + it('moves to the left of the selection', () => { + const cursor = editor.getLastCursor() + editor.moveLeft() + expect(cursor.getBufferPosition()).toEqual([5, 22]) + + editor.moveLeft() + expect(cursor.getBufferPosition()).toEqual([5, 21]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 1]) + + const [cursor1, cursor2] = editor.getCursors() + editor.moveLeft() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveRight()', () => { + it('moves the cursor by one column to the right', () => { + editor.setCursorScreenPosition([3, 3]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([3, 4]) + }) + + it('moves the cursor by n columns to the right', () => { + editor.setCursorScreenPosition([3, 7]) + editor.moveRight(4) + expect(editor.getCursorScreenPosition()).toEqual([3, 11]) + }) + + it('moves the cursor by two rows down when the columnCount is longer than an entire line', () => { + editor.setCursorScreenPosition([0, 29]) + editor.moveRight(34) + expect(editor.getCursorScreenPosition()).toEqual([2, 2]) + }) + + it('moves the cursor to the end of the buffer when columnCount is longer than the number of characters following the cursor position', () => { + editor.setCursorScreenPosition([11, 5]) + editor.moveRight(100) + expect(editor.getCursorScreenPosition()).toEqual([12, 2]) + }) + + describe('when the cursor is on the last column of a line', () => { + describe('when there is a subsequent line', () => { + it('wraps to the beginning of the next line', () => { + editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([1, 0]) + }) + + it('moves the cursor by one row down and n columns to the right', () => { + editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) + editor.moveRight(4) + expect(editor.getCursorScreenPosition()).toEqual([1, 3]) + }) + }) + + describe('when the next line is empty', () => { + it('wraps to the beginning of the next line', () => { + editor.setCursorScreenPosition([9, 4]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([10, 0]) + }) + }) + + describe('when the cursor is on the last line', () => { + it('remains in the same position', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + const lastPosition = {row: lastLineIndex, column: lastLine.length} + editor.setCursorScreenPosition(lastPosition) + editor.moveRight() + + expect(editor.getCursorScreenPosition()).toEqual(lastPosition) + }) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[5, 22], [5, 27]])) + + it('moves to the left of the selection', () => { + const cursor = editor.getLastCursor() + editor.moveRight() + expect(cursor.getBufferPosition()).toEqual([5, 27]) + + editor.moveRight() + expect(cursor.getBufferPosition()).toEqual([5, 28]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([12, 1]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveRight() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveToTop()', () => { + it('moves the cursor to the top of the buffer', () => { + editor.setCursorScreenPosition([11, 1]) + editor.addCursorAtScreenPosition([12, 0]) + editor.moveToTop() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToBottom()', () => { + it('moves the cursor to the bottom of the buffer', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + editor.moveToBottom() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveToBeginningOfScreenLine()', () => { + describe('when soft wrap is on', () => { + it('moves cursor to the beginning of the screen line', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToBeginningOfScreenLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([1, 0]) + }) + }) + + describe('when soft wrap is off', () => { + it('moves cursor to the beginning of the line', () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + editor.moveToBeginningOfScreenLine() + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + }) + }) + }) + + describe('.moveToEndOfScreenLine()', () => { + describe('when soft wrap is on', () => { + it('moves cursor to the beginning of the screen line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToEndOfScreenLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([1, 9]) + }) + }) + + describe('when soft wrap is off', () => { + it('moves cursor to the end of line', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + editor.moveToEndOfScreenLine() + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 29]) + expect(cursor2.getBufferPosition()).toEqual([1, 30]) + }) + }) + }) + + describe('.moveToBeginningOfLine()', () => { + it('moves cursor to the beginning of the buffer line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToBeginningOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToEndOfLine()', () => { + it('moves cursor to the end of the buffer line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([0, 2]) + editor.moveToEndOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([4, 4]) + }) + }) + + describe('.moveToFirstCharacterOfLine()', () => { + describe('when soft wrap is on', () => { + it("moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([2, 5]) + editor.addCursorAtScreenPosition([8, 7]) + + editor.moveToFirstCharacterOfLine() + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getScreenPosition()).toEqual([2, 0]) + expect(cursor2.getScreenPosition()).toEqual([8, 2]) + + editor.moveToFirstCharacterOfLine() + expect(cursor1.getScreenPosition()).toEqual([2, 0]) + expect(cursor2.getScreenPosition()).toEqual([8, 2]) + }) + }) + + describe('when soft wrap is off', () => { + it("moves to the first character of the current line or the beginning of the line if it's already on the first character", () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + + editor.moveToFirstCharacterOfLine() + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 2]) + + editor.moveToFirstCharacterOfLine() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + }) + + it('moves to the beginning of the line if it only contains whitespace ', () => { + editor.setText('first\n \nthird') + editor.setCursorScreenPosition([1, 2]) + editor.moveToFirstCharacterOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getBufferPosition()).toEqual([1, 0]) + }) + + describe('when invisible characters are enabled with soft tabs', () => { + it('moves to the first character of the current line without being confused by the invisible characters', () => { + editor.update({showInvisibles: true}) + editor.setCursorScreenPosition([1, 7]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + }) + }) + + describe('when invisible characters are enabled with hard tabs', () => { + it('moves to the first character of the current line without being confused by the invisible characters', () => { + editor.update({showInvisibles: true}) + buffer.setTextInRange([[1, 0], [1, Infinity]], '\t\t\ta', {normalizeLineEndings: false}) + + editor.setCursorScreenPosition([1, 7]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 3]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + }) + }) + }) + }) + + describe('.moveToBeginningOfWord()', () => { + it('moves the cursor to the beginning of the word', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 12]) + editor.addCursorAtBufferPosition([3, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToBeginningOfWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([1, 11]) + expect(cursor3.getBufferPosition()).toEqual([2, 39]) + }) + + it('does not fail at position [0, 0]', () => { + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfWord() + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([11, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('treats lines with only whitespace as a word (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([11, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([9, 2]) + }) + + it('works when the current line is blank (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([9, 2]) + editor.buffer.setText(buffer.getText().replace(/\r\n/g, '\n')) + }) + }) + + describe('.moveToPreviousWordBoundary()', () => { + it('moves the cursor to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 0]) + editor.addCursorAtBufferPosition([2, 4]) + editor.addCursorAtBufferPosition([3, 14]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.moveToPreviousWordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([1, 30]) + expect(cursor3.getBufferPosition()).toEqual([2, 0]) + expect(cursor4.getBufferPosition()).toEqual([3, 13]) + }) + }) + + describe('.moveToNextWordBoundary()', () => { + it('moves the cursor to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 40]) + editor.addCursorAtBufferPosition([3, 0]) + editor.addCursorAtBufferPosition([3, 30]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.moveToNextWordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 0]) + expect(cursor3.getBufferPosition()).toEqual([3, 4]) + expect(cursor4.getBufferPosition()).toEqual([3, 31]) + }) + }) + + describe('.moveToEndOfWord()', () => { + it('moves the cursor to the end of the word', () => { + editor.setCursorBufferPosition([0, 6]) + editor.addCursorAtBufferPosition([1, 10]) + editor.addCursorAtBufferPosition([2, 40]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToEndOfWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([1, 12]) + expect(cursor3.getBufferPosition()).toEqual([3, 7]) + }) + + it('does not blow up when there is no next word', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + const endPosition = editor.getCursorBufferPosition() + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual(endPosition) + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([9, 4]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('treats lines with only whitespace as a word (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([9, 4]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 8]) + }) + + it('works when the current line is blank (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([10, 0]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 8]) + }) + }) + + describe('.moveToBeginningOfNextWord()', () => { + it('moves the cursor before the first character of the next word', () => { + editor.setCursorBufferPosition([0, 6]) + editor.addCursorAtBufferPosition([1, 11]) + editor.addCursorAtBufferPosition([2, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToBeginningOfNextWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([1, 13]) + expect(cursor3.getBufferPosition()).toEqual([2, 4]) + + // When the cursor is on whitespace + editor.setText('ab cde- ') + editor.setCursorBufferPosition([0, 2]) + const cursor = editor.getLastCursor() + editor.moveToBeginningOfNextWord() + + expect(cursor.getBufferPosition()).toEqual([0, 3]) + }) + + it('does not blow up when there is no next word', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + const endPosition = editor.getCursorBufferPosition() + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual(endPosition) + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([9, 4]) + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 9]) + }) + }) + + describe('.moveToPreviousSubwordBoundary', () => { + it('does not move the cursor when there is no previous subword boundary', () => { + editor.setText('') + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('stops at word and underscore boundaries', () => { + editor.setText('sub_word \n') + editor.setCursorBufferPosition([0, 9]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 8]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText(' word\n') + editor.setCursorBufferPosition([0, 3]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('stops at camelCase boundaries', () => { + editor.setText(' getPreviousWord\n') + editor.setCursorBufferPosition([0, 16]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 12]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('skips consecutive non-word characters', () => { + editor.setText('e, => \n') + editor.setCursorBufferPosition([0, 6]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('skips consecutive uppercase characters', () => { + editor.setText(' AAADF \n') + editor.setCursorBufferPosition([0, 7]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.setText('ALPhA\n') + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + + it('skips consecutive numbers', () => { + editor.setText(' 88 \n') + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('works with multiple cursors', () => { + editor.setText('curOp\ncursorOptions\n') + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 13]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveToPreviousSubwordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + }) + + it('works with non-English characters', () => { + editor.setText('supåTøåst \n') + editor.setCursorBufferPosition([0, 9]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.setText('supaÖast \n') + editor.setCursorBufferPosition([0, 8]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + }) + + describe('.moveToNextSubwordBoundary', () => { + it('does not move the cursor when there is no next subword boundary', () => { + editor.setText('') + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('stops at word and underscore boundaries', () => { + editor.setText(' sub_word \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 9]) + + editor.setText('word \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + + it('stops at camelCase boundaries', () => { + editor.setText('getPreviousWord \n') + editor.setCursorBufferPosition([0, 0]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 11]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 15]) + }) + + it('skips consecutive non-word characters', () => { + editor.setText(', => \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + + it('skips consecutive uppercase characters', () => { + editor.setText(' AAADF \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.setText('ALPhA\n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + + it('skips consecutive numbers', () => { + editor.setText(' 88 \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + }) + + it('works with multiple cursors', () => { + editor.setText('curOp\ncursorOptions\n') + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveToNextSubwordBoundary() + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + }) + + it('works with non-English characters', () => { + editor.setText('supåTøåst \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.setText('supaÖast \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + }) + + describe('.moveToBeginningOfNextParagraph()', () => { + it('moves the cursor before the first line of the next paragraph', () => { + editor.setCursorBufferPosition([0, 6]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('moves the cursor before the first line of the next paragraph (CRLF line endings)', () => { + editor.setText(editor.getText().replace(/\n/g, '\r\n')) + + editor.setCursorBufferPosition([0, 6]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToBeginningOfPreviousParagraph()', () => { + it('moves the cursor before the first line of the previous paragraph', () => { + editor.setCursorBufferPosition([10, 0]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('moves the cursor before the first line of the previous paragraph (CRLF line endings)', () => { + editor.setText(editor.getText().replace(/\n/g, '\r\n')) + + editor.setCursorBufferPosition([10, 0]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.getCurrentParagraphBufferRange()', () => { + it('returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file', () => { + buffer.setText(' ' + dedent` + I am the first paragraph, + bordered by the beginning of + the file + ${' '} + + I am the second paragraph + with blank lines above and below + me. + + I am the last paragraph, + bordered by the end of the file.\ + `) + + // in a paragraph + editor.setCursorBufferPosition([1, 7]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[0, 0], [2, 8]]) + + editor.setCursorBufferPosition([7, 1]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[5, 0], [7, 3]]) + + editor.setCursorBufferPosition([9, 10]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[9, 0], [10, 32]]) + + // between paragraphs + editor.setCursorBufferPosition([3, 1]) + expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() + }) + + it('will limit paragraph range to comments', () => { + editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) + editor.setText(dedent` + var quicksort = function () { + /* Single line comment block */ + var sort = function(items) {}; + + /* + A multiline + comment is here + */ + var sort = function(items) {}; + + // A comment + // + // Multiple comment + // lines + var sort = function(items) {}; + // comment line after fn + + var nosort = function(items) { + item; + } + + };\ + `) + + function paragraphBufferRangeForRow (row) { + editor.setCursorBufferPosition([row, 0]) + return editor.getLastCursor().getCurrentParagraphBufferRange() + } + + expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]]) + expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]]) + expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]]) + expect(paragraphBufferRangeForRow(3)).toBeFalsy() + expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]]) + expect(paragraphBufferRangeForRow(9)).toBeFalsy() + expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]]) + expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]]) + expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]]) + }) + }) + + describe('getCursorAtScreenPosition(screenPosition)', () => { + it('returns the cursor at the given screenPosition', () => { + const cursor1 = editor.addCursorAtScreenPosition([0, 2]) + const cursor2 = editor.getCursorAtScreenPosition(cursor1.getScreenPosition()) + expect(cursor2).toBe(cursor1) + }) + }) + + describe('::getCursorScreenPositions()', () => { + it('returns the cursor positions in the order they were added', () => { + editor.foldBufferRow(4) + const cursor1 = editor.addCursorAtBufferPosition([8, 5]) + const cursor2 = editor.addCursorAtBufferPosition([3, 5]) + expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [5, 5], [3, 5]]) + }) + }) + + describe('::getCursorsOrderedByBufferPosition()', () => { + it('returns all cursors ordered by buffer positions', () => { + const originalCursor = editor.getLastCursor() + const cursor1 = editor.addCursorAtBufferPosition([8, 5]) + const cursor2 = editor.addCursorAtBufferPosition([4, 5]) + expect(editor.getCursorsOrderedByBufferPosition()).toEqual([originalCursor, cursor2, cursor1]) + }) + }) + + describe('addCursorAtScreenPosition(screenPosition)', () => { + describe('when a cursor already exists at the position', () => { + it('returns the existing cursor', () => { + const cursor1 = editor.addCursorAtScreenPosition([0, 2]) + const cursor2 = editor.addCursorAtScreenPosition([0, 2]) + expect(cursor2).toBe(cursor1) + }) + }) + }) + + describe('addCursorAtBufferPosition(bufferPosition)', () => { + describe('when a cursor already exists at the position', () => { + it('returns the existing cursor', () => { + const cursor1 = editor.addCursorAtBufferPosition([1, 4]) + const cursor2 = editor.addCursorAtBufferPosition([1, 4]) + expect(cursor2.marker).toBe(cursor1.marker) + }) + }) + }) + + describe('.getCursorScope()', () => { + it('returns the current scope', () => { + const descriptor = editor.getCursorScope() + expect(descriptor.scopes).toContain('source.js') + }) + }) + }) + + describe('selection', () => { + let selection + + beforeEach(() => { + selection = editor.getLastSelection() + }) + + describe('.getLastSelection()', () => { + it('creates a new selection at (0, 0) if the last selection has been destroyed', () => { + editor.getLastSelection().destroy() + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + + it("doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", () => { + let callCount = 0 + editor.getLastSelection().destroy() + editor.onDidAddCursor(function (cursor) { + callCount++ + editor.getLastSelection() + }) + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) + expect(callCount).toBe(1) + }) + }) + + describe('.getSelections()', () => { + it('creates a new selection at (0, 0) if the last selection has been destroyed', () => { + editor.getLastSelection().destroy() + expect(editor.getSelections()[0].getBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + }) + + describe('when the selection range changes', () => { + it('emits an event with the old range, new range, and the selection that moved', () => { + let rangeChangedHandler + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + + editor.onDidChangeSelectionRange(rangeChangedHandler = jasmine.createSpy()) + editor.selectToBufferPosition([6, 2]) + + expect(rangeChangedHandler).toHaveBeenCalled() + const eventObject = rangeChangedHandler.mostRecentCall.args[0] + + expect(eventObject.oldBufferRange).toEqual([[3, 0], [4, 5]]) + expect(eventObject.oldScreenRange).toEqual([[3, 0], [4, 5]]) + expect(eventObject.newBufferRange).toEqual([[3, 0], [6, 2]]) + expect(eventObject.newScreenRange).toEqual([[3, 0], [6, 2]]) + expect(eventObject.selection).toBe(selection) + }) + }) + + describe('.selectUp/Down/Left/Right()', () => { + it("expands each selection to its cursor's new location", () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 14]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 22]]) + + editor.selectLeft() + editor.selectLeft() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + + editor.selectDown() + expect(selection1.getBufferRange()).toEqual([[0, 9], [1, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [4, 20]]) + + editor.selectUp() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + }) + + it('merges selections when they intersect when moving down', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]], [[2, 15], [3, 25]]]) + const [selection1, selection2, selection3] = editor.getSelections() + + editor.selectDown() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]) + expect(selection1.isReversed()).toBeFalsy() + }) + + it('merges selections when they intersect when moving up', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]]], {reversed: true}) + const [selection1, selection2] = editor.getSelections() + + editor.selectUp() + expect(editor.getSelections().length).toBe(1) + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]) + expect(selection1.isReversed()).toBeTruthy() + }) + + it('merges selections when they intersect when moving left', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[0, 13], [1, 20]]], {reversed: true}) + const [selection1, selection2] = editor.getSelections() + + editor.selectLeft() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]) + expect(selection1.isReversed()).toBeTruthy() + }) + + it('merges selections when they intersect when moving right', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 14]], [[0, 14], [1, 20]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) + expect(selection1.isReversed()).toBeFalsy() + }) + + describe('when counts are passed into the selection functions', () => { + it("expands each selection to its cursor's new location", () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight(2) + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 15]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 23]]) + + editor.selectLeft(3) + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + + editor.selectDown(3) + expect(selection1.getBufferRange()).toEqual([[0, 9], [3, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [6, 20]]) + + editor.selectUp(2) + expect(selection1.getBufferRange()).toEqual([[0, 9], [1, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [4, 20]]) + }) + }) + }) + + describe('.selectToBufferPosition(bufferPosition)', () => { + it('expands the last selection to the given position', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtBufferPosition([5, 6]) + editor.selectToBufferPosition([6, 2]) + + const selections = editor.getSelections() + expect(selections.length).toBe(2) + const [selection1, selection2] = selections + expect(selection1.getBufferRange()).toEqual([[3, 0], [4, 5]]) + expect(selection2.getBufferRange()).toEqual([[5, 6], [6, 2]]) + }) + }) + + describe('.selectToScreenPosition(screenPosition)', () => { + it('expands the last selection to the given position', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + const selections = editor.getSelections() + expect(selections.length).toBe(2) + const [selection1, selection2] = selections + expect(selection1.getScreenRange()).toEqual([[3, 0], [4, 5]]) + expect(selection2.getScreenRange()).toEqual([[5, 6], [6, 2]]) + }) + + describe('when selecting with an initial screen range', () => { + it('switches the direction of the selection when selecting to positions before/after the start of the initial range', () => { + editor.setCursorScreenPosition([5, 10]) + editor.selectWordsContainingCursors() + editor.selectToScreenPosition([3, 0]) + expect(editor.getLastSelection().isReversed()).toBe(true) + editor.selectToScreenPosition([9, 0]) + expect(editor.getLastSelection().isReversed()).toBe(false) + }) + }) + }) + + describe('.selectToBeginningOfNextParagraph()', () => { + it('selects from the cursor to first line of the next paragraph', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + editor.selectToBeginningOfNextParagraph() + + const selections = editor.getSelections() + expect(selections.length).toBe(1) + expect(selections[0].getScreenRange()).toEqual([[3, 0], [10, 0]]) + }) + }) + + describe('.selectToBeginningOfPreviousParagraph()', () => { + it('selects from the cursor to the first line of the previous paragraph', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + editor.selectToBeginningOfPreviousParagraph() + + const selections = editor.getSelections() + expect(selections.length).toBe(1) + expect(selections[0].getScreenRange()).toEqual([[0, 0], [5, 6]]) + }) + + it('merges selections if they intersect, maintaining the directionality of the last selection', () => { + editor.setCursorScreenPosition([4, 10]) + editor.selectToScreenPosition([5, 27]) + editor.addCursorAtScreenPosition([3, 10]) + editor.selectToScreenPosition([6, 27]) + + let selections = editor.getSelections() + expect(selections.length).toBe(1) + let [selection1] = selections + expect(selection1.getScreenRange()).toEqual([[3, 10], [6, 27]]) + expect(selection1.isReversed()).toBeFalsy() + + editor.addCursorAtScreenPosition([7, 4]) + editor.selectToScreenPosition([4, 11]) + + selections = editor.getSelections() + expect(selections.length).toBe(1); + [selection1] = selections + expect(selection1.getScreenRange()).toEqual([[3, 10], [7, 4]]) + expect(selection1.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToTop()', () => { + it('selects text from cursor position to the top of the buffer', () => { + editor.setCursorScreenPosition([11, 2]) + editor.addCursorAtScreenPosition([10, 0]) + editor.selectToTop() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [11, 2]]) + expect(editor.getLastSelection().isReversed()).toBeTruthy() + }) + }) + + describe('.selectToBottom()', () => { + it('selects text from cursor position to the bottom of the buffer', () => { + editor.setCursorScreenPosition([10, 0]) + editor.addCursorAtScreenPosition([9, 3]) + editor.selectToBottom() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([12, 2]) + expect(editor.getLastSelection().getBufferRange()).toEqual([[9, 3], [12, 2]]) + expect(editor.getLastSelection().isReversed()).toBeFalsy() + }) + }) + + describe('.selectAll()', () => { + it('selects the entire buffer', () => { + editor.selectAll() + expect(editor.getLastSelection().getBufferRange()).toEqual(buffer.getRange()) + }) + }) + + describe('.selectToBeginningOfLine()', () => { + it('selects text from cursor position to beginning of line', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([11, 3]) + + editor.selectToBeginningOfLine() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([12, 0]) + expect(cursor2.getBufferPosition()).toEqual([11, 0]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[12, 0], [12, 2]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[11, 0], [11, 3]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToEndOfLine()', () => { + it('selects text from cursor position to end of line', () => { + editor.setCursorScreenPosition([12, 0]) + editor.addCursorAtScreenPosition([11, 3]) + + editor.selectToEndOfLine() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + expect(cursor2.getBufferPosition()).toEqual([11, 44]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[12, 0], [12, 2]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[11, 3], [11, 44]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectLinesContainingCursors()', () => { + it('selects to the entire line (including newlines) at given row', () => { + editor.setCursorScreenPosition([1, 2]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[1, 0], [2, 0]]) + expect(editor.getSelectedText()).toBe(' var sort = function(items) {\n') + + editor.setCursorScreenPosition([12, 2]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[12, 0], [12, 2]]) + + editor.setCursorBufferPosition([0, 2]) + editor.selectLinesContainingCursors() + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [2, 0]]) + }) + + describe('when the selection spans multiple row', () => { + it('selects from the beginning of the first line to the last line', () => { + selection = editor.getLastSelection() + selection.setBufferRange([[1, 10], [3, 20]]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[1, 0], [4, 0]]) + }) + }) + }) + + describe('.selectToBeginningOfWord()', () => { + it('selects text from cursor position to beginning of word', () => { + editor.setCursorScreenPosition([0, 13]) + editor.addCursorAtScreenPosition([3, 49]) + + editor.selectToBeginningOfWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([3, 47]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 13]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[3, 47], [3, 49]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToEndOfWord()', () => { + it('selects text from cursor position to end of word', () => { + editor.setCursorScreenPosition([0, 4]) + editor.addCursorAtScreenPosition([3, 48]) + + editor.selectToEndOfWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 50]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 13]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[3, 48], [3, 50]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToBeginningOfNextWord()', () => { + it('selects text from cursor position to beginning of next word', () => { + editor.setCursorScreenPosition([0, 4]) + editor.addCursorAtScreenPosition([3, 48]) + + editor.selectToBeginningOfNextWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([3, 51]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 14]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[3, 48], [3, 51]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToPreviousWordBoundary()', () => { + it('select to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 0]) + editor.addCursorAtBufferPosition([3, 4]) + editor.addCursorAtBufferPosition([3, 14]) + + editor.selectToPreviousWordBoundary() + + expect(editor.getSelections().length).toBe(4) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 8], [0, 4]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[2, 0], [1, 30]]) + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual([[3, 4], [3, 0]]) + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual([[3, 14], [3, 13]]) + expect(selection4.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToNextWordBoundary()', () => { + it('select to the next word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 40]) + editor.addCursorAtBufferPosition([4, 0]) + editor.addCursorAtBufferPosition([3, 30]) + + editor.selectToNextWordBoundary() + + expect(editor.getSelections().length).toBe(4) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 8], [0, 13]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[2, 40], [3, 0]]) + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual([[4, 0], [4, 4]]) + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual([[3, 30], [3, 31]]) + expect(selection4.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToPreviousSubwordBoundary', () => { + it('selects subwords', () => { + editor.setText('') + editor.insertText('_word\n') + editor.insertText(' getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToPreviousSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) + expect(selection4.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToNextSubwordBoundary', () => { + it('selects subwords', () => { + editor.setText('') + editor.insertText('word_\n') + editor.insertText('getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 1]) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToNextSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) + expect(selection4.isReversed()).toBeFalsy() + }) + }) + + describe('.deleteToBeginningOfSubword', () => { + it('deletes subwords', () => { + editor.setText('') + editor.insertText('_word\n') + editor.insertText(' getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe(' getviousWord') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 1]) + expect(cursor2.getBufferPosition()).toEqual([1, 4]) + expect(cursor3.getBufferPosition()).toEqual([2, 3]) + expect(cursor4.getBufferPosition()).toEqual([3, 1]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe(' viousWord') + expect(buffer.lineForRow(2)).toBe('e ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 1]) + expect(cursor3.getBufferPosition()).toEqual([2, 1]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('viousWord') + expect(buffer.lineForRow(2)).toBe(' ') + expect(buffer.lineForRow(3)).toBe('') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 0]) + expect(cursor4.getBufferPosition()).toEqual([2, 1]) + }) + }) + + describe('.deleteToEndOfSubword', () => { + it('deletes subwords', () => { + editor.setText('') + editor.insertText('word_\n') + editor.insertText('getPreviousWord \n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 0]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe('PreviousWord ') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe('88 ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('Word ') + expect(buffer.lineForRow(2)).toBe('e,') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + }) + }) + + describe('.selectWordsContainingCursors()', () => { + describe('when the cursor is inside a word', () => { + it('selects the entire word', () => { + editor.setCursorScreenPosition([0, 8]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('quicksort') + }) + }) + + describe('when the cursor is between two words', () => { + it('selects the word the cursor is on', () => { + editor.setCursorScreenPosition([0, 4]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('quicksort') + + editor.setCursorScreenPosition([0, 3]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('var') + }) + }) + + describe('when the cursor is inside a region of whitespace', () => { + it('selects the whitespace region', () => { + editor.setCursorScreenPosition([5, 2]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[5, 0], [5, 6]]) + + editor.setCursorScreenPosition([5, 0]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[5, 0], [5, 6]]) + }) + }) + + describe('when the cursor is at the end of the text', () => { + it('select the previous word', () => { + editor.buffer.append('word') + editor.moveToBottom() + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[12, 2], [12, 6]]) + }) + }) + + it("selects words based on the non-word characters configured at the cursor's current scope", () => { + editor.setText("one-one; 'two-two'; three-three") + + editor.setCursorBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([0, 12]) + + const scopeDescriptors = editor.getCursors().map(c => c.getScopeDescriptor()) + expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js']) + expect(scopeDescriptors[1].getScopesArray()).toEqual(['source.js', 'string.quoted.single.js']) + + editor.setScopedSettingsDelegate({ + getNonWordCharacters (scopes) { + const result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?' + if (scopes.some(scope => scope.startsWith('string'))) { + return result + } else { + return result + '-' + } + } + }) + + editor.selectWordsContainingCursors() + + expect(editor.getSelections()[0].getText()).toBe('one') + expect(editor.getSelections()[1].getText()).toBe('two-two') + }) + }) + + describe('.selectToFirstCharacterOfLine()', () => { + it("moves to the first character of the current line or the beginning of the line if it's already on the first character", () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + + editor.selectToFirstCharacterOfLine() + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 2]) + + expect(editor.getSelections().length).toBe(2) + let [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 0], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 2], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + + editor.selectToFirstCharacterOfLine(); + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 0], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 0], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.setSelectedBufferRanges(ranges)', () => { + it('clears existing selections and creates selections for each of the given ranges', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + + editor.setSelectedBufferRanges([[[5, 5], [6, 6]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[5, 5], [6, 6]]]) + }) + + it('merges intersecting selections', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [5, 5]]]) + }) + + it('does not merge non-empty adjacent selections', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) + }) + + it('recycles existing selection instances', () => { + selection = editor.getLastSelection() + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1).toBe(selection) + expect(selection1.getBufferRange()).toEqual([[2, 2], [3, 3]]) + }) + + describe("when the 'preserveFolds' option is false (the default)", () => { + 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) + editor.foldBufferRowRange(6, 8) + editor.foldBufferRowRange(10, 11) + + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) + expect(editor.isFoldedAtScreenRow(1)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() + 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]]) + editor.foldBufferRowRange(1, 4) + editor.foldBufferRowRange(6, 8) + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], {preserveFolds: true}) + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + }) + }) + }) + + describe('.setSelectedScreenRanges(ranges)', () => { + beforeEach(() => editor.foldBufferRow(4)) + + it('clears existing selections and creates selections for each of the given ranges', () => { + editor.setSelectedScreenRanges([[[3, 4], [3, 7]], [[5, 4], [5, 7]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[3, 4], [3, 7]], [[8, 4], [8, 7]]]) + + editor.setSelectedScreenRanges([[[6, 2], [6, 4]]]) + expect(editor.getSelectedScreenRanges()).toEqual([[[6, 2], [6, 4]]]) + }) + + it('merges intersecting selections and unfolds the fold which contain them', () => { + editor.foldBufferRow(0) + + // Use buffer ranges because only the first line is on screen + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [5, 5]]]) + }) + + it('recycles existing selection instances', () => { + selection = editor.getLastSelection() + editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1).toBe(selection) + expect(selection1.getScreenRange()).toEqual([[2, 2], [3, 4]]) + }) + }) + + describe('.selectMarker(marker)', () => { + describe('if the marker is valid', () => { + it("selects the marker's range and returns the selected range", () => { + const marker = editor.markBufferRange([[0, 1], [3, 3]]) + expect(editor.selectMarker(marker)).toEqual([[0, 1], [3, 3]]) + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 3]]) + }) + }) + + describe('if the marker is invalid', () => { + it('does not change the selection and returns a falsy value', () => { + const marker = editor.markBufferRange([[0, 1], [3, 3]]) + marker.destroy() + expect(editor.selectMarker(marker)).toBeFalsy() + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + }) + }) + + describe('.addSelectionForBufferRange(bufferRange)', () => { + it('adds a selection for the specified buffer range', () => { + editor.addSelectionForBufferRange([[3, 4], [5, 6]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 0]], [[3, 4], [5, 6]]]) + }) + }) + + describe('.addSelectionBelow()', () => { + describe('when the selection is non-empty', () => { + it('selects the same region of the line below current selections if possible', () => { + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + editor.addSelectionForBufferRange([[3, 25], [3, 34]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 16], [3, 21]], + [[3, 25], [3, 34]], + [[4, 16], [4, 21]], + [[4, 25], [4, 29]] + ]) + }) + + it('skips lines that are too short to create a non-empty selection', () => { + editor.setSelectedBufferRange([[3, 31], [3, 38]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 31], [3, 38]], + [[6, 31], [6, 38]] + ]) + }) + + it("honors the original selection's range (goal range) when adding across shorter lines", () => { + editor.setSelectedBufferRange([[3, 22], [3, 38]]) + editor.addSelectionBelow() + editor.addSelectionBelow() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 38]], + [[4, 22], [4, 29]], + [[5, 22], [5, 30]], + [[6, 22], [6, 38]] + ]) + }) + + it('clears selection goal ranges when the selection changes', () => { + editor.setSelectedBufferRange([[3, 22], [3, 38]]) + editor.addSelectionBelow() + editor.selectLeft() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 37]], + [[4, 22], [4, 29]], + [[5, 22], [5, 28]] + ]) + + // goal range from previous add selection is honored next time + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 37]], + [[4, 22], [4, 29]], + [[5, 22], [5, 30]], // select to end of line 5 because line 4's goal range was reset by line 3 previously + [[6, 22], [6, 28]] + ]) + }) + + it('can add selections to soft-wrapped line segments', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(40) + editor.setDefaultCharWidth(1) + + editor.setSelectedScreenRange([[3, 10], [3, 15]]) + editor.addSelectionBelow() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 10], [3, 15]], + [[4, 10], [4, 15]] + ]) + }) + + it('takes atomic tokens into account', async () => { + editor = await atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', {autoIndent: false}) + editor.setSelectedBufferRange([[2, 1], [2, 3]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 1], [2, 3]], + [[3, 1], [3, 2]] + ]) + }) + }) + + describe('when the selection is empty', () => { + describe('when lines are soft-wrapped', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + }) + + it('skips soft-wrap indentation tokens', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addSelectionBelow() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 0], [3, 0]], + [[4, 4], [4, 4]] + ]) + }) + + it("does not skip them if they're shorter than the current column", () => { + editor.setCursorScreenPosition([3, 37]) + editor.addSelectionBelow() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 37], [3, 37]], + [[4, 26], [4, 26]] + ]) + }) + }) + + it('does not skip lines that are shorter than the current column', () => { + editor.setCursorBufferPosition([3, 36]) + editor.addSelectionBelow() + editor.addSelectionBelow() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 36], [3, 36]], + [[4, 29], [4, 29]], + [[5, 30], [5, 30]], + [[6, 36], [6, 36]] + ]) + }) + + it('skips empty lines when the column is non-zero', () => { + editor.setCursorBufferPosition([9, 4]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[9, 4], [9, 4]], + [[11, 4], [11, 4]] + ]) + }) + + it('does not skip empty lines when the column is zero', () => { + editor.setCursorBufferPosition([9, 0]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[9, 0], [9, 0]], + [[10, 0], [10, 0]] + ]) + }) + }) + }) + + describe('.addSelectionAbove()', () => { + describe('when the selection is non-empty', () => { + it('selects the same region of the line above current selections if possible', () => { + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + editor.addSelectionForBufferRange([[3, 37], [3, 44]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 16], [3, 21]], + [[3, 37], [3, 44]], + [[2, 16], [2, 21]], + [[2, 37], [2, 40]] + ]) + }) + + it('skips lines that are too short to create a non-empty selection', () => { + editor.setSelectedBufferRange([[6, 31], [6, 38]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 31], [6, 38]], + [[3, 31], [3, 38]] + ]) + }) + + it("honors the original selection's range (goal range) when adding across shorter lines", () => { + editor.setSelectedBufferRange([[6, 22], [6, 38]]) + editor.addSelectionAbove() + editor.addSelectionAbove() + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 22], [6, 38]], + [[5, 22], [5, 30]], + [[4, 22], [4, 29]], + [[3, 22], [3, 38]] + ]) + }) + + it('can add selections to soft-wrapped line segments', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + + editor.setSelectedScreenRange([[4, 10], [4, 15]]) + editor.addSelectionAbove() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[4, 10], [4, 15]], + [[3, 10], [3, 15]] + ]) + }) + + it('takes atomic tokens into account', async () => { + editor = await atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', {autoIndent: false}) + editor.setSelectedBufferRange([[3, 1], [3, 2]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 1], [3, 2]], + [[2, 1], [2, 3]] + ]) + }) + }) + + describe('when the selection is empty', () => { + describe('when lines are soft-wrapped', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + }) + + it('skips soft-wrap indentation tokens', () => { + editor.setCursorScreenPosition([5, 0]) + editor.addSelectionAbove() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[5, 0], [5, 0]], + [[4, 4], [4, 4]] + ]) + }) + + it("does not skip them if they're shorter than the current column", () => { + editor.setCursorScreenPosition([5, 29]) + editor.addSelectionAbove() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[5, 29], [5, 29]], + [[4, 26], [4, 26]] + ]) + }) + }) + + it('does not skip lines that are shorter than the current column', () => { + editor.setCursorBufferPosition([6, 36]) + editor.addSelectionAbove() + editor.addSelectionAbove() + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 36], [6, 36]], + [[5, 30], [5, 30]], + [[4, 29], [4, 29]], + [[3, 36], [3, 36]] + ]) + }) + + it('skips empty lines when the column is non-zero', () => { + editor.setCursorBufferPosition([11, 4]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[11, 4], [11, 4]], + [[9, 4], [9, 4]] + ]) + }) + + it('does not skip empty lines when the column is zero', () => { + editor.setCursorBufferPosition([10, 0]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[10, 0], [10, 0]], + [[9, 0], [9, 0]] + ]) + }) + }) + }) + + describe('.splitSelectionsIntoLines()', () => { + it('splits all multi-line selections into one selection per line', () => { + editor.setSelectedBufferRange([[0, 3], [2, 4]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 29]], + [[1, 0], [1, 30]], + [[2, 0], [2, 4]] + ]) + + editor.setSelectedBufferRange([[0, 3], [1, 10]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 29]], + [[1, 0], [1, 10]] + ]) + + editor.setSelectedBufferRange([[0, 0], [0, 3]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]]]) + }) + }) + + describe('::consolidateSelections()', () => { + const makeMultipleSelections = () => { + selection.setBufferRange([[3, 16], [3, 21]]) + const selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]]) + const selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]]) + const selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]]) + expect(editor.getSelections()).toEqual([selection, selection2, selection3, selection4]) + return [selection, selection2, selection3, selection4] + } + + it('destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed', () => { + const [selection1] = makeMultipleSelections() + + const autoscrollEvents = [] + editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)) + + expect(editor.consolidateSelections()).toBeTruthy() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.isEmpty()).toBeFalsy() + expect(editor.consolidateSelections()).toBeFalsy() + expect(editor.getSelections()).toEqual([selection1]) + + expect(autoscrollEvents).toEqual([ + {screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}} + ]) + }) + }) + + describe('when the cursor is moved while there is a selection', () => { + const makeSelection = () => selection.setBufferRange([[1, 2], [1, 5]]) + + it('clears the selection', () => { + makeSelection() + editor.moveDown() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveUp() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveLeft() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveRight() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.setCursorScreenPosition([3, 3]) + expect(selection.isEmpty()).toBeTruthy() + }) + }) + + it('does not share selections between different edit sessions for the same buffer', async () => { + atom.workspace.getActivePane().splitRight() + const editor2 = await atom.workspace.open(editor.getPath()) + + expect(editor2.getText()).toBe(editor.getText()) + editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) + editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) + expect(editor2.getSelectedBufferRanges()).not.toEqual(editor.getSelectedBufferRanges()) + }) + }) + + describe('buffer manipulation', () => { + describe('.moveLineUp', () => { + it('moves the line under the cursor up', () => { + editor.setCursorBufferPosition([1, 0]) + editor.moveLineUp() + expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe(' var sort = function(items) {') + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + it("updates the line's indentation when the the autoIndent setting is true", () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([1, 0]) + editor.moveLineUp() + expect(editor.indentationForBufferRow(0)).toBe(0) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + describe('when there is a single selection', () => { + describe('when the selection spans a single line', () => { + describe('when there is no fold in the preceeding row', () => + it('moves the line to the preceding row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.setSelectedBufferRange([[3, 2], [3, 9]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [2, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is at the beginning of a fold', () => + it('moves the line to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [4, 9]], {preserveFolds: true}) + expect(editor.getSelectedBufferRange()).toEqual([[4, 2], [4, 9]]) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + }) + ) + + describe('when the preceding row consists of folded code', () => + it('moves the line above the folded row and perseveres the correct folds', () => { + expect(editor.lineTextForBufferRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[8, 0], [8, 4]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [4, 4]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when the selection spans multiple lines', () => { + it('moves the lines spanned by the selection to the preceding row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[3, 2], [4, 9]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' if (items.length <= 1) return items;') + }) + + describe("when the selection's end intersects a fold", () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[3, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' if (items.length <= 1) return items;') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + }) + ) + + describe("when the selection's start intersects a fold", () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [8, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [7, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(8)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + }) + ) + }) + + describe('when the selection spans multiple lines, but ends at column 0', () => { + it('does not move the last line of the selection', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[3, 2], [4, 0]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 0]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + }) + }) + + describe('when the preceeding row is a folded row', () => { + it('moves the lines spanned by the selection to the preceeding row, but preserves the folded code', () => { + expect(editor.lineTextForBufferRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + editor.foldBufferRowRange(4, 7) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[8, 0], [9, 2]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [5, 2]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' };') + expect(editor.lineTextForBufferRow(6)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(5)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + }) + }) + }) + + describe('when there are multiple selections', () => { + describe('when all the selections span different lines', () => { + describe('when there is no folds', () => + it('moves all lines that are spanned by a selection to the preceding row', () => { + editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + expect(editor.lineTextForBufferRow(0)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(1)).toBe('var quicksort = function () {') + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + }) + ) + + describe('when one selection intersects a fold', () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRanges([ + [[2, 2], [2, 9]], + [[4, 2], [4, 9]] + ], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 2], [1, 9]], + [[3, 2], [3, 9]] + ]) + + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + }) + ) + + describe('when there is a fold', () => + it('moves all lines that spanned by a selection to preceding row, preserving all folds', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[8, 0], [8, 3]], [[11, 0], [11, 5]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[4, 0], [4, 3]], [[10, 0], [10, 5]]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when there are many folds', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-many-folds.js', {autoIndent: false}) + }) + + describe('and many selections intersects folded rows', () => + it('moves and preserves all the folds', () => { + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(7, 9) + + editor.setSelectedBufferRanges([ + [[1, 0], [5, 4]], + [[7, 0], [7, 4]] + ], {preserveFolds: true}) + + editor.moveLineUp() + + expect(editor.lineTextForBufferRow(1)).toEqual('function f3() {') + expect(editor.lineTextForBufferRow(4)).toEqual('6;') + expect(editor.lineTextForBufferRow(5)).toEqual('1;') + expect(editor.lineTextForBufferRow(6)).toEqual('function f8() {') + expect(editor.lineTextForBufferRow(9)).toEqual('7;') + + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when some of the selections span the same lines', () => { + it('moves lines that contain multiple selections correctly', () => { + editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [2, 9]], [[2, 12], [2, 13]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + + describe('when one of the selections spans line 0', () => { + it("doesn't move any lines, since line 0 can't move", () => { + editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + expect(buffer.isModified()).toBe(false) + }) + }) + + describe('when one of the selections spans the last line, and it is empty', () => { + it("doesn't move any lines, since the last line can't move", () => { + buffer.append('\n') + editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) + }) + }) + }) + }) + + describe('.moveLineDown', () => { + it('moves the line under the cursor down', () => { + editor.setCursorBufferPosition([0, 0]) + editor.moveLineDown() + expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe('var quicksort = function () {') + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + it("updates the line's indentation when the editor.autoIndent setting is true", () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([0, 0]) + editor.moveLineDown() + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(2) + }) + + describe('when there is a single selection', () => { + describe('when the selection spans a single line', () => { + describe('when there is no fold in the following row', () => + it('moves the line to the following row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.setSelectedBufferRange([[2, 2], [2, 9]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is at the beginning of a fold', () => + it('moves the line to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[5, 2], [5, 9]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + + describe('when the following row is a folded row', () => + it('moves the line below the folded row and preserves the fold', () => { + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[3, 0], [3, 4]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[7, 0], [7, 4]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + ) + }) + + describe('when the selection spans multiple lines', () => { + it('moves the lines spanned by the selection to the following row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[2, 2], [3, 9]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [4, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + + describe('when the selection spans multiple lines, but ends at column 0', () => { + it('does not move the last line of the selection', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[2, 2], [3, 0]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [4, 0]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + }) + }) + + describe("when the selection's end intersects a fold", () => { + it('moves the lines to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[3, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 2], [5, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + }) + + describe("when the selection's start intersects a fold", () => { + it('moves the lines to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [8, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[5, 2], [9, 9]]) + expect(editor.lineTextForBufferRow(4)).toBe(' };') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(9)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + }) + }) + + describe('when the following row is a folded row', () => { + it('moves the lines spanned by the selection to the following row, but preserves the folded code', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.foldBufferRowRange(4, 7) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[2, 0], [3, 2]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[6, 0], [7, 2]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + }) + }) + + describe('when the last line of selection does not end with a valid line ending', () => { + it('appends line ending to last line and moves the lines spanned by the selection to the preceeding row', () => { + expect(editor.lineTextForBufferRow(9)).toBe(' };') + expect(editor.lineTextForBufferRow(10)).toBe('') + expect(editor.lineTextForBufferRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.lineTextForBufferRow(12)).toBe('};') + + editor.setSelectedBufferRange([[10, 0], [12, 2]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[9, 0], [11, 2]]) + expect(editor.lineTextForBufferRow(9)).toBe('') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.lineTextForBufferRow(11)).toBe('};') + expect(editor.lineTextForBufferRow(12)).toBe(' };') + }) + }) + }) + + describe('when there are multiple selections', () => { + describe('when all the selections span different lines', () => { + describe('when there is no folds', () => + it('moves all lines that are spanned by a selection to the following row', () => { + editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[6, 2], [6, 9]], [[4, 2], [4, 9]], [[2, 2], [2, 9]]]) + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(6)).toBe(' current = items.shift();') + }) + ) + + describe('when there are many folds', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-many-folds.js', {autoIndent: false}) + }) + + describe('and many selections intersects folded rows', () => + it('moves and preserves all the folds', () => { + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(7, 9) + + editor.setSelectedBufferRanges([ + [[2, 0], [2, 4]], + [[6, 0], [10, 4]] + ], {preserveFolds: true}) + + editor.moveLineDown() + + expect(editor.lineTextForBufferRow(2)).toEqual('6;') + expect(editor.lineTextForBufferRow(3)).toEqual('function f3() {') + expect(editor.lineTextForBufferRow(6)).toEqual('12;') + expect(editor.lineTextForBufferRow(7)).toEqual('7;') + expect(editor.lineTextForBufferRow(8)).toEqual('function f8() {') + expect(editor.lineTextForBufferRow(11)).toEqual('11;') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(11)).toBeFalsy() + }) + ) + }) + + describe('when there is a fold below one of the selected row', () => + it('moves all lines spanned by a selection to the following row, preserving the fold', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[1, 2], [1, 6]], [[3, 0], [3, 4]], [[8, 0], [8, 3]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[9, 0], [9, 3]], [[7, 0], [7, 4]], [[2, 2], [2, 6]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(9)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + }) + ) + + describe('when there is a fold below a group of multiple selections without any lines with no selection in-between', () => + it('moves all the lines below the fold, preserving the fold', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[7, 0], [7, 4]], [[6, 2], [6, 6]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + ) + }) + + describe('when one selection intersects a fold', () => { + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRanges([ + [[2, 2], [2, 9]], + [[4, 2], [4, 9]] + ], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[5, 2], [5, 9]], + [[3, 2], [3, 9]] + ]) + + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + }) + + describe('when some of the selections span the same lines', () => { + it('moves lines that contain multiple selections correctly', () => { + editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[4, 12], [4, 13]], [[4, 2], [4, 9]]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + }) + }) + + describe('when the selections are above a wrapped line', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(80) + editor.setText(`\ +1 +2 +Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. +3 +4\ +`) + }) + + it('moves the lines past the soft wrapped line', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]) + + editor.moveLineDown() + + expect(editor.lineTextForBufferRow(0)).not.toBe('2') + expect(editor.lineTextForBufferRow(1)).toBe('1') + expect(editor.lineTextForBufferRow(2)).toBe('2') + }) + }) + }) + + describe('when the line is the last buffer row', () => { + it("doesn't move it", () => { + editor.setText('abc\ndef') + editor.setCursorBufferPosition([1, 0]) + editor.moveLineDown() + expect(editor.getText()).toBe('abc\ndef') + }) + }) + }) + + describe('.insertText(text)', () => { + describe('when there is a single selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 0], [1, 2]])) + + it('replaces the selection with the given text', () => { + const range = editor.insertText('xxx') + expect(range).toEqual([ [[1, 0], [1, 3]] ]) + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + }) + }) + + describe('when there are multiple empty selections', () => { + describe('when the cursors are on the same line', () => { + it("inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", () => { + editor.setCursorScreenPosition([1, 2]) + editor.addCursorAtScreenPosition([1, 5]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe(' xxxvarxxx sort = function(items) {') + const [cursor1, cursor2] = editor.getCursors() + + expect(cursor1.getBufferPosition()).toEqual([1, 5]) + expect(cursor2.getBufferPosition()).toEqual([1, 11]) + }) + }) + + describe('when the cursors are on different lines', () => { + it("inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", () => { + editor.setCursorScreenPosition([1, 2]) + editor.addCursorAtScreenPosition([2, 4]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe(' xxxvar sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' xxxif (items.length <= 1) return items;') + const [cursor1, cursor2] = editor.getCursors() + + expect(cursor1.getBufferPosition()).toEqual([1, 5]) + expect(cursor2.getBufferPosition()).toEqual([2, 7]) + }) + }) + }) + + describe('when there are multiple non-empty selections', () => { + describe('when the selections are on the same line', () => { + it('replaces each selection range with the inserted characters', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 22], [0, 24]]]) + editor.insertText('x') + + const [cursor1, cursor2] = editor.getCursors() + const [selection1, selection2] = editor.getSelections() + + expect(cursor1.getScreenPosition()).toEqual([0, 5]) + expect(cursor2.getScreenPosition()).toEqual([0, 15]) + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + + expect(editor.lineTextForBufferRow(0)).toBe('var x = functix () {') + }) + }) + + describe('when the selections are on different lines', () => { + it("replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", () => { + editor.setSelectedBufferRanges([[[1, 0], [1, 2]], [[2, 0], [2, 4]]]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + expect(buffer.lineForRow(2)).toBe('xxxif (items.length <= 1) return items;') + const [selection1, selection2] = editor.getSelections() + + expect(selection1.isEmpty()).toBeTruthy() + expect(selection1.cursor.getBufferPosition()).toEqual([1, 3]) + expect(selection2.isEmpty()).toBeTruthy() + expect(selection2.cursor.getBufferPosition()).toEqual([2, 3]) + }) + }) + }) + + describe('when there is a selection that ends on a folded line', () => { + it('destroys the selection', () => { + editor.foldBufferRowRange(2, 4) + editor.setSelectedBufferRange([[1, 0], [2, 0]]) + editor.insertText('holy cow') + expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() + }) + }) + + describe('when there are ::onWillInsertText and ::onDidInsertText observers', () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 0], [1, 2]])) + + it('notifies the observers when inserting text', () => { + const willInsertSpy = jasmine.createSpy().andCallFake(() => expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {')) + + const didInsertSpy = jasmine.createSpy().andCallFake(() => expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {')) + + editor.onWillInsertText(willInsertSpy) + editor.onDidInsertText(didInsertSpy) + + expect(editor.insertText('xxx')).toBeTruthy() + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + + expect(willInsertSpy).toHaveBeenCalled() + expect(didInsertSpy).toHaveBeenCalled() + + let options = willInsertSpy.mostRecentCall.args[0] + expect(options.text).toBe('xxx') + expect(options.cancel).toBeDefined() + + options = didInsertSpy.mostRecentCall.args[0] + expect(options.text).toBe('xxx') + }) + + it('cancels text insertion when an ::onWillInsertText observer calls cancel on an event', () => { + const willInsertSpy = jasmine.createSpy().andCallFake(({cancel}) => cancel()) + + const didInsertSpy = jasmine.createSpy() + + editor.onWillInsertText(willInsertSpy) + editor.onDidInsertText(didInsertSpy) + + expect(editor.insertText('xxx')).toBe(false) + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + expect(willInsertSpy).toHaveBeenCalled() + expect(didInsertSpy).not.toHaveBeenCalled() + }) + }) + + describe("when the undo option is set to 'skip'", () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 2], [1, 2]])) + + it('does not undo the skipped operation', () => { + let range = editor.insertText('x') + range = editor.insertText('y', {undo: 'skip'}) + editor.undo() + expect(buffer.lineForRow(1)).toBe(' yvar sort = function(items) {') + }) + }) + }) + + describe('.insertNewline()', () => { + describe('when there is a single cursor', () => { + describe('when the cursor is at the beginning of a line', () => { + it('inserts an empty line before it', () => { + editor.setCursorScreenPosition({row: 1, column: 0}) + + editor.insertNewline() + + expect(buffer.lineForRow(1)).toBe('') + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + + describe('when the cursor is in the middle of a line', () => { + it('splits the current line to form a new line', () => { + editor.setCursorScreenPosition({row: 1, column: 6}) + const originalLine = buffer.lineForRow(1) + const lineBelowOriginalLine = buffer.lineForRow(2) + + editor.insertNewline() + + expect(buffer.lineForRow(1)).toBe(originalLine.slice(0, 6)) + expect(buffer.lineForRow(2)).toBe(originalLine.slice(6)) + expect(buffer.lineForRow(3)).toBe(lineBelowOriginalLine) + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + + describe('when the cursor is on the end of a line', () => { + it('inserts an empty line after it', () => { + editor.setCursorScreenPosition({row: 1, column: buffer.lineForRow(1).length}) + + editor.insertNewline() + + expect(buffer.lineForRow(2)).toBe('') + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when the cursors are on the same line', () => { + it('breaks the line at the cursor locations', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.insertNewline() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot') + expect(editor.lineTextForBufferRow(4)).toBe(' = items.shift(), current') + expect(editor.lineTextForBufferRow(5)).toBe(', left = [], right = [];') + expect(editor.lineTextForBufferRow(6)).toBe(' while(items.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([4, 0]) + expect(cursor2.getBufferPosition()).toEqual([5, 0]) + }) + }) + + describe('when the cursors are on different lines', () => { + it('inserts newlines at each cursor location', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addCursorAtScreenPosition([6, 0]) + + editor.insertText('\n') + expect(editor.lineTextForBufferRow(3)).toBe('') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(6)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(7)).toBe('') + expect(editor.lineTextForBufferRow(8)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(9)).toBe(' }') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([4, 0]) + expect(cursor2.getBufferPosition()).toEqual([8, 0]) + }) + }) + }) + }) + + describe('.insertNewlineBelow()', () => { + describe('when the operation is undone', () => { + it('places the cursor back at the previous location', () => { + editor.setCursorBufferPosition([0, 2]) + editor.insertNewlineBelow() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.undo() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + }) + + it("inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", () => { + editor.update({autoIndent: true}) + editor.insertNewlineBelow() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' ') + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + }) + }) + + describe('.insertNewlineAbove()', () => { + describe('when the cursor is on first line', () => { + it('inserts a newline on the first line and moves the cursor to the first line', () => { + editor.setCursorBufferPosition([0]) + editor.insertNewlineAbove() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + expect(editor.lineTextForBufferRow(0)).toBe('') + expect(editor.lineTextForBufferRow(1)).toBe('var quicksort = function () {') + expect(editor.buffer.getLineCount()).toBe(14) + }) + }) + + describe('when the cursor is not on the first line', () => { + it('inserts a newline above the current line and moves the cursor to the inserted line', () => { + editor.setCursorBufferPosition([3, 4]) + editor.insertNewlineAbove() + expect(editor.getCursorBufferPosition()).toEqual([3, 0]) + expect(editor.lineTextForBufferRow(3)).toBe('') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.buffer.getLineCount()).toBe(14) + + editor.undo() + expect(editor.getCursorBufferPosition()).toEqual([3, 4]) + }) + }) + + it('indents the new line to the correct level when editor.autoIndent is true', () => { + editor.update({autoIndent: true}) + + editor.setText(' var test') + editor.setCursorBufferPosition([0, 2]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + expect(editor.lineTextForBufferRow(0)).toBe(' ') + expect(editor.lineTextForBufferRow(1)).toBe(' var test') + + editor.setText('\n var test') + editor.setCursorBufferPosition([1, 2]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + expect(editor.lineTextForBufferRow(0)).toBe('') + expect(editor.lineTextForBufferRow(1)).toBe(' ') + expect(editor.lineTextForBufferRow(2)).toBe(' var test') + + editor.setText('function() {\n}') + editor.setCursorBufferPosition([1, 1]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + expect(editor.lineTextForBufferRow(0)).toBe('function() {') + expect(editor.lineTextForBufferRow(1)).toBe(' ') + expect(editor.lineTextForBufferRow(2)).toBe('}') + }) + }) + + describe('.insertNewLine()', () => { + describe('when a new line is appended before a closing tag (e.g. by pressing enter before a selection)', () => { + it('moves the line down and keeps the indentation level the same when editor.autoIndent is true', () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([9, 2]) + editor.insertNewline() + expect(editor.lineTextForBufferRow(10)).toBe(' };') + }) + }) + + describe('when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)', () => { + it('indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language', () => { + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.js')) + editor.setText('var test = () => {\n return true;};') + editor.setCursorBufferPosition([1, 14]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + }) + + it('indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified', () => { + editor.setGrammar(atom.grammars.selectGrammar('file')) + editor.update({autoIndent: true}) + editor.setText(' if true') + editor.setCursorBufferPosition([0, 8]) + editor.insertNewline() + expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar) + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(1) + }) + + it('indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language', async () => { + await atom.packages.activatePackage('language-coffee-script') + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.coffee')) + editor.setText('if true\n return trueelse\n return false') + editor.setCursorBufferPosition([1, 13]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + expect(editor.indentationForBufferRow(3)).toBe(1) + }) + }) + + describe('when a newline is appended on a line that matches the decreaseNextIndentPattern', () => { + it('indents the new line to the correct level when editor.autoIndent is true', async () => { + await atom.packages.activatePackage('language-go') + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.go')) + editor.setText('fmt.Printf("some%s",\n "thing")') + editor.setCursorBufferPosition([1, 10]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + }) + }) + }) + + describe('.backspace()', () => { + describe('when there is a single cursor', () => { + let changeScreenRangeHandler = null + + beforeEach(() => { + const selection = editor.getLastSelection() + changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') + selection.onDidChangeRange(changeScreenRangeHandler) + }) + + describe('when the cursor is on the middle of the line', () => { + it('removes the character before the cursor', () => { + editor.setCursorScreenPosition({row: 1, column: 7}) + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + editor.backspace() + + const line = buffer.lineForRow(1) + expect(line).toBe(' var ort = function(items) {') + expect(editor.getCursorScreenPosition()).toEqual({row: 1, column: 6}) + expect(changeScreenRangeHandler).toHaveBeenCalled() + }) + }) + + describe('when the cursor is at the beginning of a line', () => { + it('joins it with the line above', () => { + const originalLine0 = buffer.lineForRow(0) + expect(originalLine0).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + editor.setCursorScreenPosition({row: 1, column: 0}) + editor.backspace() + + const line0 = buffer.lineForRow(0) + const line1 = buffer.lineForRow(1) + expect(line0).toBe('var quicksort = function () { var sort = function(items) {') + expect(line1).toBe(' if (items.length <= 1) return items;') + expect(editor.getCursorScreenPosition()).toEqual([0, originalLine0.length]) + + expect(changeScreenRangeHandler).toHaveBeenCalled() + }) + }) + + describe('when the cursor is at the first column of the first line', () => { + it("does nothing, but doesn't raise an error", () => { + editor.setCursorScreenPosition({row: 0, column: 0}) + editor.backspace() + }) + }) + + describe('when the cursor is after a fold', () => { + it('deletes the folded range', () => { + editor.foldBufferRange([[4, 7], [5, 8]]) + editor.setCursorBufferPosition([5, 8]) + editor.backspace() + + expect(buffer.lineForRow(4)).toBe(' whirrent = items.shift();') + expect(editor.isFoldedAtBufferRow(4)).toBe(false) + }) + }) + + describe('when the cursor is in the middle of a line below a fold', () => { + it('backspaces as normal', () => { + editor.setCursorScreenPosition([4, 0]) + editor.foldCurrentRow() + editor.setCursorScreenPosition([5, 5]) + editor.backspace() + + expect(buffer.lineForRow(7)).toBe(' }') + expect(buffer.lineForRow(8)).toBe(' eturn sort(left).concat(pivot).concat(sort(right));') + }) + }) + + describe('when the cursor is on a folded screen line', () => { + it('deletes the contents of the fold before the cursor', () => { + editor.setCursorBufferPosition([3, 0]) + editor.foldCurrentRow() + editor.backspace() + + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getCursorScreenPosition()).toEqual([1, 29]) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when cursors are on the same line', () => { + it('removes the characters preceding each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.backspace() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivo = items.shift(), curren, left = [], right = [];') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 12]) + expect(cursor2.getBufferPosition()).toEqual([3, 36]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + }) + + describe('when cursors are on different lines', () => { + describe('when the cursors are in the middle of their lines', () => + it('removes the characters preceding each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([4, 10]) + + editor.backspace() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivo = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' whileitems.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 12]) + expect(cursor2.getBufferPosition()).toEqual([4, 9]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + ) + + describe('when the cursors are on the first column of their lines', () => + it('removes the newlines preceding each cursor', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addCursorAtScreenPosition([6, 0]) + + editor.backspace() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' current = items.shift(); current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(5)).toBe(' }') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([2, 40]) + expect(cursor2.getBufferPosition()).toEqual([4, 30]) + }) + ) + }) + }) + + describe('when there is a single selection', () => { + it('deletes the selection, but not the character before it', () => { + editor.setSelectedBufferRange([[0, 5], [0, 9]]) + editor.backspace() + expect(editor.buffer.lineForRow(0)).toBe('var qsort = function () {') + }) + + describe('when the selection ends on a folded line', () => { + it('preserves the fold', () => { + editor.setSelectedBufferRange([[3, 0], [4, 0]]) + editor.foldBufferRow(4) + editor.backspace() + + expect(buffer.lineForRow(3)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtScreenRow(3)).toBe(true) + }) + }) + }) + + describe('when there are multiple selections', () => { + it('removes all selected text', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + editor.backspace() + expect(editor.lineTextForBufferRow(0)).toBe('var = () {') + }) + }) + }) + + describe('.deleteToPreviousWordBoundary()', () => { + describe('when no text is selected', () => { + it('deletes to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 16]) + editor.addCursorAtBufferPosition([1, 21]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = (items) {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 13]) + + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort function () {') + expect(buffer.lineForRow(1)).toBe(' var sort =(items) {') + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([1, 12]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.deleteToNextWordBoundary()', () => { + describe('when no text is selected', () => { + it('deletes to the next word boundary', () => { + editor.setCursorBufferPosition([0, 15]) + editor.addCursorAtBufferPosition([1, 24]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort = () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =() {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it{') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.deleteToBeginningOfWord()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the beginning of the word', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([3, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(ems) {') + expect(buffer.lineForRow(3)).toBe(' ar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 22]) + expect(cursor2.getBufferPosition()).toEqual([3, 4]) + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = functionems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 21]) + expect(cursor2.getBufferPosition()).toEqual([2, 39]) + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = ems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 13]) + expect(cursor2.getBufferPosition()).toEqual([2, 34]) + + editor.setText(' var sort') + editor.setCursorBufferPosition([0, 2]) + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(0)).toBe('var sort') + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + }) + }) + }) + + describe('.deleteToEndOfLine()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the end of the line', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it') + expect(buffer.lineForRow(2)).toBe(' i') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + }) + + describe('when at the end of the line', () => { + it('deletes the next newline', () => { + editor.setCursorBufferPosition([1, 30]) + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + }) + + describe('when text is selected', () => { + it('deletes only the text in the selection', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + }) + }) + }) + + describe('.deleteToBeginningOfLine()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the beginning of the line', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe('ems) {') + expect(buffer.lineForRow(2)).toBe('f (items.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 0]) + expect(cursor2.getBufferPosition()).toEqual([2, 0]) + }) + + describe('when at the beginning of the line', () => { + it('deletes the newline', () => { + editor.setCursorBufferPosition([2]) + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + }) + + describe('when text is selected', () => { + it('still deletes all text to beginning of the line', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe('ems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + }) + }) + }) + + describe('.delete()', () => { + describe('when there is a single cursor', () => { + describe('when the cursor is on the middle of a line', () => { + it('deletes the character following the cursor', () => { + editor.setCursorScreenPosition([1, 6]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var ort = function(items) {') + }) + }) + + describe('when the cursor is on the end of a line', () => { + it('joins the line with the following line', () => { + editor.setCursorScreenPosition([1, buffer.lineForRow(1).length]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + + describe('when the cursor is on the last column of the last line', () => { + it("does nothing, but doesn't raise an error", () => { + editor.setCursorScreenPosition([12, buffer.lineForRow(12).length]) + editor.delete() + expect(buffer.lineForRow(12)).toBe('};') + }) + }) + + describe('when the cursor is before a fold', () => { + it('only deletes the lines inside the fold', () => { + editor.foldBufferRange([[3, 6], [4, 8]]) + editor.setCursorScreenPosition([3, 6]) + const cursorPositionBefore = editor.getCursorScreenPosition() + + editor.delete() + + expect(buffer.lineForRow(3)).toBe(' vae(items.length > 0) {') + expect(buffer.lineForRow(4)).toBe(' current = items.shift();') + expect(editor.getCursorScreenPosition()).toEqual(cursorPositionBefore) + }) + }) + + describe('when the cursor is in the middle a line above a fold', () => { + it('deletes as normal', () => { + editor.foldBufferRow(4) + editor.setCursorScreenPosition([3, 4]) + const cursorPositionBefore = editor.getCursorScreenPosition() + + editor.delete() + + expect(buffer.lineForRow(3)).toBe(' ar pivot = items.shift(), current, left = [], right = [];') + expect(editor.isFoldedAtScreenRow(4)).toBe(true) + expect(editor.getCursorScreenPosition()).toEqual([3, 4]) + }) + }) + + describe('when the cursor is inside a fold', () => { + it('removes the folded content after the cursor', () => { + editor.foldBufferRange([[2, 6], [6, 21]]) + editor.setCursorBufferPosition([4, 9]) + + editor.delete() + + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(buffer.lineForRow(4)).toBe(' while ? left.push(current) : right.push(current);') + expect(buffer.lineForRow(5)).toBe(' }') + expect(editor.getCursorBufferPosition()).toEqual([4, 9]) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when cursors are on the same line', () => { + it('removes the characters following each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.delete() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot= items.shift(), current left = [], right = [];') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 37]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + }) + + describe('when cursors are on different lines', () => { + describe('when the cursors are in the middle of the lines', () => + it('removes the characters following each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([4, 10]) + + editor.delete() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot= items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(tems.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 13]) + expect(cursor2.getBufferPosition()).toEqual([4, 10]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + ) + + describe('when the cursors are at the end of their lines', () => + it('removes the newlines following each cursor', () => { + editor.setCursorScreenPosition([0, 29]) + editor.addCursorAtScreenPosition([1, 30]) + + editor.delete() + + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 29]) + expect(cursor2.getBufferPosition()).toEqual([0, 59]) + }) + ) + }) + }) + + describe('when there is a single selection', () => { + it('deletes the selection, but not the character following it', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + expect(editor.getLastSelection().isEmpty()).toBeTruthy() + }) + }) + + describe('when there are multiple selections', () => + describe('when selections are on the same line', () => { + it('removes all selected text', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + editor.delete() + expect(editor.lineTextForBufferRow(0)).toBe('var = () {') + }) + }) + ) + }) + + describe('.deleteToEndOfWord()', () => { + describe('when no text is selected', () => { + it('deletes to the end of the word', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe(' i (items.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it {') + expect(buffer.lineForRow(2)).toBe(' iitems.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.indent()', () => { + describe('when the selection is empty', () => { + describe('when autoIndent is disabled', () => { + describe("if 'softTabs' is true (the default)", () => { + it("inserts 'tabLength' spaces into the buffer", () => { + const tabRegex = new RegExp(`^[ ]{${editor.getTabLength()}}`) + expect(buffer.lineForRow(0)).not.toMatch(tabRegex) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(tabRegex) + }) + + it('respects the tab stops when cursor is in the middle of a tab', () => { + editor.setTabLength(4) + buffer.insert([12, 2], '\n ') + editor.setCursorBufferPosition([13, 1]) + editor.indent() + expect(buffer.lineForRow(13)).toMatch(/^\s+$/) + expect(buffer.lineForRow(13).length).toBe(4) + expect(editor.getCursorBufferPosition()).toEqual([13, 4]) + + buffer.insert([13, 0], ' ') + editor.setCursorBufferPosition([13, 6]) + editor.indent() + expect(buffer.lineForRow(13).length).toBe(8) + }) + }) + + describe("if 'softTabs' is false", () => + it('insert a \t into the buffer', () => { + editor.setSoftTabs(false) + expect(buffer.lineForRow(0)).not.toMatch(/^\t/) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t/) + }) + ) + }) + + describe('when autoIndent is enabled', () => { + describe("when the cursor's column is less than the suggested level of indentation", () => { + describe("when 'softTabs' is true (the default)", () => { + it('moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation', () => { + buffer.insert([5, 0], ' \n') + editor.setCursorBufferPosition([5, 0]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(5)).toMatch(/^\s+$/) + expect(buffer.lineForRow(5).length).toBe(6) + expect(editor.getCursorBufferPosition()).toEqual([5, 6]) + }) + + it('respects the tab stops when cursor is in the middle of a tab', () => { + editor.setTabLength(4) + buffer.insert([12, 2], '\n ') + editor.setCursorBufferPosition([13, 1]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(13)).toMatch(/^\s+$/) + expect(buffer.lineForRow(13).length).toBe(4) + expect(editor.getCursorBufferPosition()).toEqual([13, 4]) + + buffer.insert([13, 0], ' ') + editor.setCursorBufferPosition([13, 6]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(13).length).toBe(8) + }) + }) + + describe("when 'softTabs' is false", () => { + it('moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + buffer.insert([5, 0], '\t\n') + editor.setCursorBufferPosition([5, 0]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(5)).toMatch(/^\t\t\t$/) + expect(editor.getCursorBufferPosition()).toEqual([5, 3]) + }) + + describe('when the difference between the suggested level of indentation and the current level of indentation is greater than 0 but less than 1', () => + it('inserts one tab', () => { + editor.setSoftTabs(false) + buffer.setText(' \ntest') + editor.setCursorBufferPosition([1, 0]) + + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(1)).toBe('\ttest') + expect(editor.getCursorBufferPosition()).toEqual([1, 1]) + }) + ) + }) + }) + + describe("when the line's indent level is greater than the suggested level of indentation", () => { + describe("when 'softTabs' is true (the default)", () => + it("moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", () => { + buffer.insert([7, 0], ' \n') + editor.setCursorBufferPosition([7, 2]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(7)).toMatch(/^\s+$/) + expect(buffer.lineForRow(7).length).toBe(8) + expect(editor.getCursorBufferPosition()).toEqual([7, 8]) + }) + ) + + describe("when 'softTabs' is false", () => + it('moves the cursor to the end of the leading whitespace and inserts \t into the buffer', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + buffer.insert([7, 0], '\t\t\t\n') + editor.setCursorBufferPosition([7, 1]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(7)).toMatch(/^\t\t\t\t$/) + expect(editor.getCursorBufferPosition()).toEqual([7, 4]) + }) + ) + }) + }) + }) + + describe('when the selection is not empty', () => { + it('indents the selected lines', () => { + editor.setSelectedBufferRange([[0, 0], [10, 0]]) + const selection = editor.getLastSelection() + spyOn(selection, 'indentSelectedRows') + editor.indent() + expect(selection.indentSelectedRows).toHaveBeenCalled() + }) + }) + + describe('if editor.softTabs is false', () => { + it('inserts a tab character into the buffer', () => { + editor.setSoftTabs(false) + expect(buffer.lineForRow(0)).not.toMatch(/^\t/) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t/) + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + expect(editor.getCursorScreenPosition()).toEqual([0, editor.getTabLength()]) + + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t\t/) + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + expect(editor.getCursorScreenPosition()).toEqual([0, editor.getTabLength() * 2]) + }) + }) + }) + + describe('clipboard operations', () => { + describe('.cutSelectedText()', () => { + it('removes the selected text from the buffer and places it on the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + editor.cutSelectedText() + expect(buffer.lineForRow(0)).toBe('var = function () {') + expect(buffer.lineForRow(1)).toBe(' var = function(items) {') + expect(clipboard.readText()).toBe('quicksort\nsort') + }) + + describe('when no text is selected', () => { + beforeEach(() => + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[5, 0], [5, 0]] + ]) + ) + + it('cuts the lines on which there are cursors', () => { + editor.cutSelectedText() + expect(buffer.getLineCount()).toBe(11) + expect(buffer.lineForRow(1)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(4)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(atom.clipboard.read()).toEqual([ + 'var quicksort = function () {', + '', + ' current = items.shift();', + '' + ].join('\n')) + }) + }) + + describe('when many selections get added in shuffle order', () => { + it('cuts them in order', () => { + editor.setSelectedBufferRanges([ + [[2, 8], [2, 13]], + [[0, 4], [0, 13]], + [[1, 6], [1, 10]] + ]) + editor.cutSelectedText() + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + }) + + describe('.cutToEndOfLine()', () => { + describe('when soft wrap is on', () => { + it('cuts up to the end of the line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(25) + editor.setCursorScreenPosition([2, 6]) + editor.cutToEndOfLine() + expect(editor.lineTextForScreenRow(2)).toBe(' var function(items) {') + }) + }) + + describe('when soft wrap is off', () => { + describe('when nothing is selected', () => + it('cuts up to the end of the line', () => { + editor.setCursorBufferPosition([2, 20]) + editor.addCursorAtBufferPosition([3, 20]) + editor.cutToEndOfLine() + expect(buffer.lineForRow(2)).toBe(' if (items.length') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) return items;\ns.shift(), current, left = [], right = [];') + }) + ) + + describe('when text is selected', () => + it('only cuts the selected text, not to the end of the line', () => { + editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) + editor.cutToEndOfLine() + expect(buffer.lineForRow(2)).toBe(' if (items.lengthurn items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) ret\ns.shift(), current, left = [], right = [];') + }) + ) + }) + }) + + describe('.cutToEndOfBufferLine()', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + }) + + describe('when nothing is selected', () => { + it('cuts up to the end of the buffer line', () => { + editor.setCursorBufferPosition([2, 20]) + editor.addCursorAtBufferPosition([3, 20]) + editor.cutToEndOfBufferLine() + expect(buffer.lineForRow(2)).toBe(' if (items.length') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) return items;\ns.shift(), current, left = [], right = [];') + }) + }) + + describe('when text is selected', () => { + it('only cuts the selected text, not to the end of the buffer line', () => { + editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) + editor.cutToEndOfBufferLine() + expect(buffer.lineForRow(2)).toBe(' if (items.lengthurn items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) ret\ns.shift(), current, left = [], right = [];') + }) + }) + }) + + describe('.copySelectedText()', () => { + it('copies selected text onto the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) + editor.copySelectedText() + + 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;') + expect(clipboard.readText()).toBe('quicksort\nsort\nitems') + expect(atom.clipboard.read()).toEqual('quicksort\nsort\nitems') + }) + + describe('when no text is selected', () => { + beforeEach(() => { + editor.setSelectedBufferRanges([ + [[1, 5], [1, 5]], + [[5, 8], [5, 8]] + ]) + }) + + it('copies the lines on which there are cursors', () => { + editor.copySelectedText() + expect(atom.clipboard.read()).toEqual([ + ' var sort = function(items) {\n', + ' current = items.shift();\n' + ].join('\n')) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 5], [1, 5]], + [[5, 8], [5, 8]] + ]) + }) + }) + + describe('when many selections get added in shuffle order', () => { + it('copies them in order', () => { + editor.setSelectedBufferRanges([ + [[2, 8], [2, 13]], + [[0, 4], [0, 13]], + [[1, 6], [1, 10]] + ]) + editor.copySelectedText() + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + }) + + describe('.copyOnlySelectedText()', () => { + describe('when thee are multiple selections', () => { + it('copies selected text onto the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) + + editor.copyOnlySelectedText() + 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;') + expect(clipboard.readText()).toBe('quicksort\nsort\nitems') + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + + describe('when no text is selected', () => { + it('does not copy anything', () => { + editor.setCursorBufferPosition([1, 5]) + editor.copyOnlySelectedText() + expect(atom.clipboard.read()).toEqual('initial clipboard content') + }) + }) + }) + + describe('.pasteText()', () => { + const copyText = function (text, {startColumn, textEditor} = {}) { + if (startColumn == null) startColumn = 0 + if (textEditor == null) textEditor = editor + textEditor.setCursorBufferPosition([0, 0]) + textEditor.insertText(text) + const numberOfNewlines = text.match(/\n/g).length + const endColumn = text.match(/[^\n]*$/)[0].length + textEditor.getLastSelection().setBufferRange([[0, startColumn], [numberOfNewlines, endColumn]]) + return textEditor.cutSelectedText() + } + + it('pastes text into the buffer', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + atom.clipboard.write('first') + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var first = function () {') + expect(editor.lineTextForBufferRow(1)).toBe(' var first = function(items) {') + }) + + it('notifies ::onWillInsertText observers', () => { + const insertedStrings = [] + editor.onWillInsertText(function ({text, cancel}) { + insertedStrings.push(text) + cancel() + }) + + atom.clipboard.write('hello') + editor.pasteText() + + expect(insertedStrings).toEqual(['hello']) + }) + + it('notifies ::onDidInsertText observers', () => { + const insertedStrings = [] + editor.onDidInsertText(({text, range}) => insertedStrings.push(text)) + + atom.clipboard.write('hello') + editor.pasteText() + + expect(insertedStrings).toEqual(['hello']) + }) + + describe('when `autoIndentOnPaste` is true', () => { + beforeEach(() => editor.update({autoIndentOnPaste: true})) + + describe('when pasting multiple lines before any non-whitespace characters', () => { + it('auto-indents the lines spanned by the pasted text, based on the first pasted line', () => { + atom.clipboard.write('a(x);\n b(x);\n c(x);\n', {indentBasis: 0}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + // Adjust the indentation of the pasted lines while preserving + // their indentation relative to each other. Also preserve the + // indentation of the following line. + expect(editor.lineTextForBufferRow(5)).toBe(' a(x);') + expect(editor.lineTextForBufferRow(6)).toBe(' b(x);') + expect(editor.lineTextForBufferRow(7)).toBe(' c(x);') + expect(editor.lineTextForBufferRow(8)).toBe(' current = items.shift();') + }) + + it('auto-indents lines with a mix of hard tabs and spaces without removing spaces', () => { + editor.setSoftTabs(false) + expect(editor.indentationForBufferRow(5)).toBe(3) + + atom.clipboard.write('/**\n\t * testing\n\t * indent\n\t **/\n', {indentBasis: 1}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + // Do not lose the alignment spaces + expect(editor.lineTextForBufferRow(5)).toBe('\t\t\t/**') + expect(editor.lineTextForBufferRow(6)).toBe('\t\t\t * testing') + expect(editor.lineTextForBufferRow(7)).toBe('\t\t\t * indent') + expect(editor.lineTextForBufferRow(8)).toBe('\t\t\t **/') + }) + }) + + describe('when pasting line(s) above a line that matches the decreaseIndentPattern', () => + it('auto-indents based on the pasted line(s) only', () => { + atom.clipboard.write('a(x);\n b(x);\n c(x);\n', {indentBasis: 0}) + editor.setCursorBufferPosition([7, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(7)).toBe(' a(x);') + expect(editor.lineTextForBufferRow(8)).toBe(' b(x);') + expect(editor.lineTextForBufferRow(9)).toBe(' c(x);') + expect(editor.lineTextForBufferRow(10)).toBe(' }') + }) + ) + + describe('when pasting a line of text without line ending', () => + it('does not auto-indent the text', () => { + atom.clipboard.write('a(x);', {indentBasis: 0}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(5)).toBe('a(x); current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' current < pivot ? left.push(current) : right.push(current);') + }) + ) + + describe('when pasting on a line after non-whitespace characters', () => + it('does not auto-indent the affected line', () => { + // Before the paste, the indentation is non-standard. + editor.setText(dedent`\ + if (x) { + y(); + }\ + `) + + atom.clipboard.write(' z();\n h();') + editor.setCursorBufferPosition([1, Infinity]) + + // The indentation of the non-standard line is unchanged. + editor.pasteText() + expect(editor.lineTextForBufferRow(1)).toBe(' y(); z();') + expect(editor.lineTextForBufferRow(2)).toBe(' h();') + }) + ) + }) + + describe('when `autoIndentOnPaste` is false', () => { + beforeEach(() => editor.update({autoIndentOnPaste: false})) + + describe('when the cursor is indented further than the original copied text', () => + it('increases the indentation of the copied lines to match', () => { + editor.setSelectedBufferRange([[1, 2], [3, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([5, 6]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(5)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is indented less far than the original copied text', () => + it('decreases the indentation of the copied lines to match', () => { + editor.setSelectedBufferRange([[6, 6], [8, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([1, 2]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(1)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(2)).toBe('}') + }) + ) + + describe('when the first copied line has leading whitespace', () => + it("preserves the line's leading whitespace", () => { + editor.setSelectedBufferRange([[4, 0], [6, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([0, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(0)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(1)).toBe(' current = items.shift();') + }) + ) + }) + + describe('when the clipboard has many selections', () => { + beforeEach(() => { + editor.update({autoIndentOnPaste: false}) + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + editor.copySelectedText() + }) + + it('pastes each selection in order separately into the buffer', () => { + editor.setSelectedBufferRanges([ + [[1, 6], [1, 10]], + [[0, 4], [0, 13]] + ]) + + editor.moveRight() + editor.insertText('_') + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort_quicksort = function () {') + expect(editor.lineTextForBufferRow(1)).toBe(' var sort_sort = function(items) {') + }) + + describe('and the selections count does not match', () => { + beforeEach(() => editor.setSelectedBufferRanges([[[0, 4], [0, 13]]])) + + it('pastes the whole text into the buffer', () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort') + expect(editor.lineTextForBufferRow(1)).toBe('sort = function () {') + }) + }) + }) + + describe('when a full line was cut', () => { + beforeEach(() => { + editor.setCursorBufferPosition([2, 13]) + editor.cutSelectedText() + editor.setCursorBufferPosition([2, 13]) + }) + + it("pastes the line above the cursor and retains the cursor's column", () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getCursorBufferPosition()).toEqual([3, 13]) + }) + }) + + describe('when a full line was copied', () => { + beforeEach(() => { + editor.setCursorBufferPosition([2, 13]) + editor.copySelectedText() + }) + + describe('when there is a selection', () => + it('overwrites the selection as with any copied text', () => { + editor.setSelectedBufferRange([[1, 2], [1, Infinity]]) + editor.pasteText() + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe('') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.getCursorBufferPosition()).toEqual([2, 0]) + }) + ) + + describe('when there is no selection', () => + it("pastes the line above the cursor and retains the cursor's column", () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + 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', () => { + it('indents line and retains selection', () => { + editor.setSelectedBufferRange([[0, 3], [0, 3]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3 + editor.getTabLength()], [0, 3 + editor.getTabLength()]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents line and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[0, 3], [0, 3]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3 + 1], [0, 3 + 1]]) + }) + }) + }) + + describe('when one line is selected', () => { + describe('when softTabs is enabled', () => { + it('indents line and retains selection', () => { + editor.setSelectedBufferRange([[0, 4], [0, 14]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe(`${editor.getTabText()}var quicksort = function () {`) + expect(editor.getSelectedBufferRange()).toEqual([[0, 4 + editor.getTabLength()], [0, 14 + editor.getTabLength()]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents line and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[0, 4], [0, 14]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 4 + 1], [0, 14 + 1]]) + }) + }) + }) + + describe('when multiple lines are selected', () => { + describe('when softTabs is enabled', () => { + it('indents selected lines (that are not empty) and retains selection', () => { + editor.setSelectedBufferRange([[9, 1], [11, 15]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe(' };') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + editor.getTabLength()], [11, 15 + editor.getTabLength()]]) + }) + + it('does not indent the last row if the selection ends at column 0', () => { + editor.setSelectedBufferRange([[9, 1], [11, 0]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe(' };') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + editor.getTabLength()], [11, 0]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents selected lines (that are not empty) and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[9, 1], [11, 15]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe('\t\t};') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe('\t\treturn sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + 1], [11, 15 + 1]]) + }) + }) + }) + }) + + describe('.outdentSelectedRows()', () => { + describe('when nothing is selected', () => { + it('outdents line and retains selection', () => { + editor.setSelectedBufferRange([[1, 3], [1, 3]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[1, 3 - editor.getTabLength()], [1, 3 - editor.getTabLength()]]) + }) + + it('outdents when indent is less than a tab length', () => { + editor.insertText(' ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs', () => { + editor.insertText('\t\t') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents when a mix of hard tabs and soft tabs are used', () => { + editor.insertText('\t ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents only up to the first non-space non-tab character', () => { + editor.insertText(' \tfoo\t ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tfoo\t var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('foo\t var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('foo\t var quicksort = function () {') + }) + }) + + describe('when one line is selected', () => { + it('outdents line and retains editor', () => { + editor.setSelectedBufferRange([[1, 4], [1, 14]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[1, 4 - editor.getTabLength()], [1, 14 - editor.getTabLength()]]) + }) + }) + + describe('when multiple lines are selected', () => { + it('outdents selected lines and retains editor', () => { + editor.setSelectedBufferRange([[0, 1], [3, 15]]) + editor.outdentSelectedRows() + 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;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 15 - editor.getTabLength()]]) + }) + + it('does not outdent the last line of the selection if it ends at column 0', () => { + editor.setSelectedBufferRange([[0, 1], [3, 0]]) + editor.outdentSelectedRows() + 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;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 0]]) + }) + }) + }) + + describe('.autoIndentSelectedRows', () => { + it('auto-indents the selection', () => { + editor.setCursorBufferPosition([2, 0]) + editor.insertText('function() {\ninside=true\n}\n i=1\n') + editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]) + editor.autoIndentSelectedRows() + + expect(editor.lineTextForBufferRow(2)).toBe(' function() {') + expect(editor.lineTextForBufferRow(3)).toBe(' inside=true') + expect(editor.lineTextForBufferRow(4)).toBe(' }') + expect(editor.lineTextForBufferRow(5)).toBe(' i=1') + }) + }) + + describe('.undo() and .redo()', () => { + it('undoes/redoes the last change', () => { + editor.insertText('foo') + editor.undo() + expect(buffer.lineForRow(0)).not.toContain('foo') + + editor.redo() + expect(buffer.lineForRow(0)).toContain('foo') + }) + + it('batches the undo / redo of changes caused by multiple cursors', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + + editor.insertText('foo') + editor.backspace() + + expect(buffer.lineForRow(0)).toContain('fovar') + expect(buffer.lineForRow(1)).toContain('fo ') + + editor.undo() + + expect(buffer.lineForRow(0)).toContain('foo') + expect(buffer.lineForRow(1)).toContain('foo') + + editor.redo() + + expect(buffer.lineForRow(0)).not.toContain('foo') + expect(buffer.lineForRow(0)).toContain('fovar') + }) + + it('restores cursors and selections to their states before and after undone and redone changes', () => { + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[1, 0], [1, 3]] + ]) + editor.insertText('abc') + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 3]], + [[1, 3], [1, 3]] + ]) + + editor.setCursorBufferPosition([0, 0]) + editor.setSelectedBufferRanges([ + [[2, 0], [2, 0]], + [[3, 0], [3, 0]], + [[4, 0], [4, 3]] + ]) + editor.insertText('def') + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 3], [2, 3]], + [[3, 3], [3, 3]], + [[4, 3], [4, 3]] + ]) + + editor.setCursorBufferPosition([0, 0]) + editor.undo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 0], [2, 0]], + [[3, 0], [3, 0]], + [[4, 0], [4, 3]] + ]) + + editor.undo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 0], [0, 0]], + [[1, 0], [1, 3]] + ]) + + editor.redo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 3]], + [[1, 3], [1, 3]] + ]) + + editor.redo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 3], [2, 3]], + [[3, 3], [3, 3]], + [[4, 3], [4, 3]] + ]) + }) + + it('restores the selected ranges after undo and redo', () => { + editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) + editor.delete() + editor.delete() + + const selections = editor.getSelections() + expect(buffer.lineForRow(1)).toBe(' var = function( {') + + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 17], [1, 17]]]) + + editor.undo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 18], [1, 18]]]) + + editor.undo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) + + editor.redo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 18], [1, 18]]]) + }) + + xit('restores folds after undo and redo', () => { + editor.foldBufferRow(1) + editor.setSelectedBufferRange([[1, 0], [10, Infinity]], {preserveFolds: true}) + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + + editor.insertText(dedent`\ + // testing + function foo() { + return 1 + 2; + }\ + `) + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + editor.foldBufferRow(2) + + editor.undo() + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + + editor.redo() + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + }) + }) + + describe('::transact', () => { + it('restores the selection when the transaction is undone/redone', () => { + buffer.setText('1234') + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + + editor.transact(() => { + editor.delete() + editor.moveToEndOfLine() + editor.insertText('5') + expect(buffer.getText()).toBe('145') + }) + + editor.undo() + expect(buffer.getText()).toBe('1234') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 3]]) + + editor.redo() + expect(buffer.getText()).toBe('145') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 3]]) + }) + }) + + describe('when the buffer is changed (via its direct api, rather than via than edit session)', () => { + it('moves the cursor so it is in the same relative position of the buffer', () => { + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + editor.addCursorAtScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + buffer.insert([0, 1], 'abc') + + expect(cursor1.getScreenPosition()).toEqual([0, 0]) + expect(cursor2.getScreenPosition()).toEqual([0, 8]) + expect(cursor3.getScreenPosition()).toEqual([1, 0]) + }) + + it('does not destroy cursors or selections when a change encompasses them', () => { + const cursor = editor.getLastCursor() + cursor.setBufferPosition([3, 3]) + editor.buffer.delete([[3, 1], [3, 5]]) + expect(cursor.getBufferPosition()).toEqual([3, 1]) + expect(editor.getCursors().indexOf(cursor)).not.toBe(-1) + + const selection = editor.getLastSelection() + selection.setBufferRange([[3, 5], [3, 10]]) + editor.buffer.delete([[3, 3], [3, 8]]) + expect(selection.getBufferRange()).toEqual([[3, 3], [3, 5]]) + expect(editor.getSelections().indexOf(selection)).not.toBe(-1) + }) + + it('merges cursors when the change causes them to overlap', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 2]) + editor.addCursorAtScreenPosition([1, 2]) + + const [cursor1, cursor2, cursor3] = editor.getCursors() + expect(editor.getCursors().length).toBe(3) + + buffer.delete([[0, 0], [0, 2]]) + + expect(editor.getCursors().length).toBe(2) + expect(editor.getCursors()).toEqual([cursor1, cursor3]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor3.getBufferPosition()).toEqual([1, 2]) + }) + }) + + describe('.moveSelectionLeft()', () => { + it('moves one active selection on one line one column to the left', () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]) + expect(editor.getSelectedText()).toBe('quicksort') + + editor.moveSelectionLeft() + + expect(editor.getSelectedText()).toBe('quicksort') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 12]]) + }) + + it('moves multiple active selections on one line one column to the left', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 3], [0, 12]], [[0, 15], [0, 23]]]) + }) + + it('moves multiple active selections on multiple lines one column to the left', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 3], [0, 12]], [[1, 5], [1, 9]]]) + }) + + describe('when a selection is at the first column of a line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe(' v') + + editor.moveSelectionLeft() + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe(' v') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) + }) + + describe('when multiple selections are active on one line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe('quicksort') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe('quicksort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) + }) + }) + }) + }) + + describe('.moveSelectionRight()', () => { + it('moves one active selection on one line one column to the right', () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]) + expect(editor.getSelectedText()).toBe('quicksort') + + editor.moveSelectionRight() + + expect(editor.getSelectedText()).toBe('quicksort') + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 14]]) + }) + + it('moves multiple active selections on one line one column to the right', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 5], [0, 14]], [[0, 17], [0, 25]]]) + }) + + it('moves multiple active selections on multiple lines one column to the right', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 5], [0, 14]], [[1, 7], [1, 11]]]) + }) + + describe('when a selection is at the last column of a line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('items;') + expect(selections[1].getText()).toBe('shift();') + + editor.moveSelectionRight() + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('items;') + expect(selections[1].getText()).toBe('shift();') + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) + }) + + describe('when multiple selections are active on one line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('return') + expect(selections[1].getText()).toBe('items;') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('return') + expect(selections[1].getText()).toBe('items;') + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) + }) + }) + }) + }) + }) + + describe('reading text', () => { + it('.lineTextForScreenRow(row)', () => { + editor.foldBufferRow(4) + expect(editor.lineTextForScreenRow(5)).toEqual(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForScreenRow(9)).toEqual('};') + expect(editor.lineTextForScreenRow(10)).toBeUndefined() + }) + }) + + describe('.deleteLine()', () => { + it('deletes the first line when the cursor is there', () => { + editor.getLastCursor().moveToTop() + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line1) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('deletes the last line when the cursor is there', () => { + const count = buffer.getLineCount() + const secondToLastLine = buffer.lineForRow(count - 2) + expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine) + editor.getLastCursor().moveToBottom() + editor.deleteLine() + const newCount = buffer.getLineCount() + expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine) + expect(newCount).toBe(count - 1) + }) + + it('deletes whole lines when partial lines are selected', () => { + editor.setSelectedBufferRange([[0, 2], [1, 2]]) + const line2 = buffer.lineForRow(2) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line2) + expect(buffer.lineForRow(1)).not.toBe(line2) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line2) + expect(buffer.getLineCount()).toBe(count - 2) + }) + + it('deletes a line only once when multiple selections are on the same line', () => { + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + editor.setSelectedBufferRanges([ + [[0, 1], [0, 2]], + [[0, 4], [0, 5]] + ]) + expect(buffer.lineForRow(0)).not.toBe(line1) + + editor.deleteLine() + + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('only deletes first line if only newline is selected on second line', () => { + editor.setSelectedBufferRange([[0, 2], [1, 0]]) + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line1) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('deletes the entire region when invoke on a folded region', () => { + editor.foldBufferRow(1) + editor.getLastCursor().moveToTop() + editor.getLastCursor().moveDown() + expect(buffer.getLineCount()).toBe(13) + editor.deleteLine() + expect(buffer.getLineCount()).toBe(4) + }) + + it('deletes the entire file from the bottom up', () => { + const count = buffer.getLineCount() + expect(count).toBeGreaterThan(0) + for (let i = 0; i < count; i++) { + editor.getLastCursor().moveToBottom() + editor.deleteLine() + } + expect(buffer.getLineCount()).toBe(1) + expect(buffer.getText()).toBe('') + }) + + it('deletes the entire file from the top down', () => { + const count = buffer.getLineCount() + expect(count).toBeGreaterThan(0) + for (let i = 0; i < count; i++) { + editor.getLastCursor().moveToTop() + editor.deleteLine() + } + expect(buffer.getLineCount()).toBe(1) + expect(buffer.getText()).toBe('') + }) + + describe('when soft wrap is enabled', () => { + it('deletes the entire line that the cursor is on', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + editor.setCursorBufferPosition([6]) + + const line7 = buffer.lineForRow(7) + const count = buffer.getLineCount() + expect(buffer.lineForRow(6)).not.toBe(line7) + editor.deleteLine() + expect(buffer.lineForRow(6)).toBe(line7) + expect(buffer.getLineCount()).toBe(count - 1) + }) + }) + + describe('when the line being deleted precedes a fold, and the command is undone', () => { + it('restores the line and preserves the fold', () => { + editor.setCursorBufferPosition([4]) + editor.foldCurrentRow() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + editor.setCursorBufferPosition([3]) + editor.deleteLine() + expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe(' while(items.length > 0) {') + editor.undo() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + }) + + describe('.replaceSelectedText(options, fn)', () => { + describe('when no text is selected', () => { + it('inserts the text returned from the function at the cursor position', () => { + editor.replaceSelectedText({}, () => '123') + expect(buffer.lineForRow(0)).toBe('123var quicksort = function () {') + + editor.setCursorBufferPosition([0]) + editor.replaceSelectedText({selectWordIfEmpty: true}, () => 'var') + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + + editor.setCursorBufferPosition([10]) + editor.replaceSelectedText(null, () => '') + expect(buffer.lineForRow(10)).toBe('') + }) + }) + + describe('when text is selected', () => { + it('replaces the selected text with the text returned from the function', () => { + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.replaceSelectedText({}, () => 'ia') + expect(buffer.lineForRow(0)).toBe('via quicksort = function () {') + }) + + it('replaces the selected text and selects the replacement text', () => { + editor.setSelectedBufferRange([[0, 4], [0, 9]]) + editor.replaceSelectedText({}, () => 'whatnot') + expect(buffer.lineForRow(0)).toBe('var whatnotsort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 4], [0, 11]]) + }) + }) + }) + + describe('.transpose()', () => { + it('swaps two characters', () => { + editor.buffer.setText('abc') + editor.setCursorScreenPosition([0, 1]) + editor.transpose() + expect(editor.lineTextForBufferRow(0)).toBe('bac') + }) + + it('reverses a selection', () => { + editor.buffer.setText('xabcz') + editor.setSelectedBufferRange([[0, 1], [0, 4]]) + editor.transpose() + expect(editor.lineTextForBufferRow(0)).toBe('xcbaz') + }) + }) + + describe('.upperCase()', () => { + describe('when there is no selection', () => { + it('upper cases the current word', () => { + editor.buffer.setText('aBc') + editor.setCursorScreenPosition([0, 1]) + editor.upperCase() + expect(editor.lineTextForBufferRow(0)).toBe('ABC') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when there is a selection', () => { + it('upper cases the current selection', () => { + editor.buffer.setText('abc') + editor.setSelectedBufferRange([[0, 0], [0, 2]]) + editor.upperCase() + expect(editor.lineTextForBufferRow(0)).toBe('ABc') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 2]]) + }) + }) + }) + + describe('.lowerCase()', () => { + describe('when there is no selection', () => { + it('lower cases the current word', () => { + editor.buffer.setText('aBC') + editor.setCursorScreenPosition([0, 1]) + editor.lowerCase() + expect(editor.lineTextForBufferRow(0)).toBe('abc') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when there is a selection', () => { + it('lower cases the current selection', () => { + editor.buffer.setText('ABC') + editor.setSelectedBufferRange([[0, 0], [0, 2]]) + editor.lowerCase() + expect(editor.lineTextForBufferRow(0)).toBe('abC') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 2]]) + }) + }) + }) + + describe('.setTabLength(tabLength)', () => { + it('clips atomic soft tabs to the given tab length', () => { + expect(editor.getTabLength()).toBe(2) + expect(editor.clipScreenPosition([5, 1], {clipDirection: 'forward'})).toEqual([5, 2]) + + editor.setTabLength(6) + expect(editor.getTabLength()).toBe(6) + expect(editor.clipScreenPosition([5, 1], {clipDirection: 'forward'})).toEqual([5, 6]) + + const changeHandler = jasmine.createSpy('changeHandler') + editor.onDidChange(changeHandler) + editor.setTabLength(6) + expect(changeHandler).not.toHaveBeenCalled() + }) + + it('does not change its tab length when the given tab length is null', () => { + editor.setTabLength(4) + editor.setTabLength(null) + expect(editor.getTabLength()).toBe(4) + }) + }) + + describe('.indentLevelForLine(line)', () => { + it('returns the indent level when the line has only leading whitespace', () => { + expect(editor.indentLevelForLine(' hello')).toBe(2) + expect(editor.indentLevelForLine(' hello')).toBe(1.5) + }) + + it('returns the indent level when the line has only leading tabs', () => expect(editor.indentLevelForLine('\t\thello')).toBe(2)) + + it('returns the indent level based on the character starting the line when the leading whitespace contains both spaces and tabs', () => { + expect(editor.indentLevelForLine('\t hello')).toBe(2) + expect(editor.indentLevelForLine(' \thello')).toBe(2) + expect(editor.indentLevelForLine(' \t hello')).toBe(2.5) + expect(editor.indentLevelForLine(' \t \thello')).toBe(4) + expect(editor.indentLevelForLine(' \t \thello')).toBe(4) + expect(editor.indentLevelForLine(' \t \t hello')).toBe(4.5) + }) + }) + + describe('when a better-matched grammar is added to syntax', () => { + it('switches to the better-matched grammar and re-tokenizes the buffer', async () => { + editor.destroy() + + const jsGrammar = atom.grammars.selectGrammar('a.js') + atom.grammars.removeGrammar(jsGrammar) + + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + + expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar) + expect(editor.tokensForScreenRow(0).length).toBe(1) + + atom.grammars.addGrammar(jsGrammar) + expect(editor.getGrammar()).toBe(jsGrammar) + expect(editor.tokensForScreenRow(0).length).toBeGreaterThan(1) + }) + }) + + describe('editor.autoIndent', () => { + describe('when editor.autoIndent is false (default)', () => { + describe('when `indent` is triggered', () => { + it('does not auto-indent the line', () => { + editor.setCursorBufferPosition([1, 30]) + editor.insertText('\n ') + expect(editor.lineTextForBufferRow(2)).toBe(' ') + + editor.update({autoIndent: false}) + editor.indent() + expect(editor.lineTextForBufferRow(2)).toBe(' ') + }) + }) + }) + + describe('when editor.autoIndent is true', () => { + beforeEach(() => editor.update({autoIndent: true})) + + describe('when `indent` is triggered', () => { + it('auto-indents the line', () => { + editor.setCursorBufferPosition([1, 30]) + editor.insertText('\n ') + expect(editor.lineTextForBufferRow(2)).toBe(' ') + + editor.update({autoIndent: true}) + editor.indent() + expect(editor.lineTextForBufferRow(2)).toBe(' ') + }) + }) + + describe('when a newline is added', () => { + describe('when the line preceding the newline adds a new level of indentation', () => { + it('indents the newline to one additional level of indentation beyond the preceding line', () => { + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + }) + }) + + describe("when the line preceding the newline doesn't add a level of indentation", () => { + it('indents the new line to the same level as the preceding line', () => { + editor.setCursorBufferPosition([5, 14]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(6)).toBe(editor.indentationForBufferRow(5)) + }) + }) + + describe('when the line preceding the newline is a comment', () => { + it('maintains the indent of the commented line', () => { + editor.setCursorBufferPosition([0, 0]) + editor.insertText(' //') + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(1)).toBe(2) + }) + }) + + describe('when the line preceding the newline contains only whitespace', () => { + it("bases the new line's indentation on only the preceding line", () => { + editor.setCursorBufferPosition([6, Infinity]) + editor.insertText('\n ') + expect(editor.getCursorBufferPosition()).toEqual([7, 2]) + + editor.insertNewline() + expect(editor.lineTextForBufferRow(8)).toBe(' ') + }) + }) + + it('does not indent the line preceding the newline', () => { + editor.setCursorBufferPosition([2, 0]) + editor.insertText(' var this-line-should-be-indented-more\n') + expect(editor.indentationForBufferRow(1)).toBe(1) + + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([2, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(1) + }) + + describe('when the cursor is before whitespace', () => { + it('retains the whitespace following the cursor on the new line', () => { + editor.setText(' var sort = function() {}') + editor.setCursorScreenPosition([0, 12]) + editor.insertNewline() + + expect(buffer.lineForRow(0)).toBe(' var sort =') + expect(buffer.lineForRow(1)).toBe(' function() {}') + expect(editor.getCursorScreenPosition()).toEqual([1, 2]) + }) + }) + }) + + describe('when inserted text matches a decrease indent pattern', () => { + describe('when the preceding line matches an increase indent pattern', () => { + it('decreases the indentation to match that of the preceding line', () => { + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + editor.insertText('}') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1)) + }) + }) + + describe("when the preceding line doesn't match an increase indent pattern", () => { + it('decreases the indentation to be one level below that of the preceding line', () => { + editor.setCursorBufferPosition([3, Infinity]) + editor.insertText('\n ') + expect(editor.indentationForBufferRow(4)).toBe(editor.indentationForBufferRow(3)) + editor.insertText('}') + expect(editor.indentationForBufferRow(4)).toBe(editor.indentationForBufferRow(3) - 1) + }) + + it("doesn't break when decreasing the indentation on a row that has no indentation", () => { + editor.setCursorBufferPosition([12, Infinity]) + editor.insertText('\n}; # too many closing brackets!') + expect(editor.lineTextForBufferRow(13)).toBe('}; # too many closing brackets!') + }) + }) + }) + + describe('when inserted text does not match a decrease indent pattern', () => { + it('does not decrease the indentation', () => { + editor.setCursorBufferPosition([12, 0]) + editor.insertText(' ') + expect(editor.lineTextForBufferRow(12)).toBe(' };') + editor.insertText('\t\t') + expect(editor.lineTextForBufferRow(12)).toBe(' \t\t};') + }) + }) + + describe('when the current line does not match a decrease indent pattern', () => { + it('leaves the line unchanged', () => { + editor.setCursorBufferPosition([2, 4]) + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + editor.insertText('foo') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + }) + }) + }) + }) + + describe('atomic soft tabs', () => { + it('skips tab-length runs of leading whitespace when moving the cursor', () => { + editor.update({tabLength: 4, atomicSoftTabs: true}) + + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 4]) + + editor.update({atomicSoftTabs: false}) + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 3]) + + editor.update({atomicSoftTabs: true}) + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 4]) + }) + }) + + describe('.destroy()', () => { + it('destroys marker layers associated with the text editor', () => { + buffer.retain() + const selectionsMarkerLayerId = editor.selectionsMarkerLayer.id + const foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id + editor.destroy() + expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() + expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() + buffer.release() + }) + + it('notifies ::onDidDestroy observers when the editor is destroyed', () => { + let destroyObserverCalled = false + editor.onDidDestroy(() => destroyObserverCalled = true) + + editor.destroy() + expect(destroyObserverCalled).toBe(true) + }) + + it('does not blow up when query methods are called afterward', () => { + editor.destroy() + editor.getGrammar() + editor.getLastCursor() + editor.lineTextForBufferRow(0) + }) + + it("emits the destroy event after destroying the editor's buffer", () => { + const events = [] + editor.getBuffer().onDidDestroy(() => { + expect(editor.isDestroyed()).toBe(true) + events.push('buffer-destroyed') + }) + editor.onDidDestroy(() => { + expect(buffer.isDestroyed()).toBe(true) + events.push('editor-destroyed') + }) + editor.destroy() + expect(events).toEqual(['buffer-destroyed', 'editor-destroyed']) + }) + }) + + describe('.joinLines()', () => { + describe('when no text is selected', () => { + describe("when the line below isn't empty", () => { + it('joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up', () => { + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText(' ') + editor.setCursorBufferPosition([0]) + editor.joinLines() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) {') + expect(editor.getCursorBufferPosition()).toEqual([0, 29]) + }) + }) + + describe('when the line below is empty', () => { + it('deletes the line below and moves the cursor to the end of the line', () => { + editor.setCursorBufferPosition([9]) + editor.joinLines() + expect(editor.lineTextForBufferRow(9)).toBe(' };') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getCursorBufferPosition()).toEqual([9, 4]) + }) + }) + + describe('when the cursor is on the last row', () => { + it('does nothing', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + editor.joinLines() + expect(editor.lineTextForBufferRow(12)).toBe('};') + }) + }) + + describe('when the line is empty', () => { + it('joins the line below with the current line with no added space', () => { + editor.setCursorBufferPosition([10]) + editor.joinLines() + expect(editor.lineTextForBufferRow(10)).toBe('return sort(Array.apply(this, arguments));') + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + }) + }) + + describe('when text is selected', () => { + describe('when the selection does not span multiple lines', () => { + it('joins the line below with the current line separated by a space and retains the selected text', () => { + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.joinLines() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 3]]) + }) + }) + + describe('when the selection spans multiple lines', () => { + it('joins all selected lines separated by a space and retains the selected text', () => { + editor.setSelectedBufferRange([[9, 3], [12, 1]]) + editor.joinLines() + expect(editor.lineTextForBufferRow(9)).toBe(' }; return sort(Array.apply(this, arguments)); };') + expect(editor.getSelectedBufferRange()).toEqual([[9, 3], [9, 49]]) + }) + }) + }) + }) + + describe('.duplicateLines()', () => { + it('for each selection, duplicates all buffer lines intersected by the selection', () => { + editor.foldBufferRow(4) + editor.setCursorBufferPosition([2, 5]) + editor.addSelectionForBufferRange([[3, 0], [8, 0]], {preserveFolds: true}) + + editor.duplicateLines() + + expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe(`\ +\ if (items.length <= 1) return items; + if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + }\ +` + ) + expect(editor.getSelectedBufferRanges()).toEqual([[[3, 5], [3, 5]], [[9, 0], [14, 0]]]) + + // folds are also duplicated + expect(editor.isFoldedAtScreenRow(5)).toBe(true) + expect(editor.isFoldedAtScreenRow(7)).toBe(true) + expect(editor.lineTextForScreenRow(7)).toBe(` while(items.length > 0) {${editor.displayLayer.foldCharacter}`) + expect(editor.lineTextForScreenRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + }) + + it('duplicates all folded lines for empty selections on lines containing folds', () => { + editor.foldBufferRow(4) + editor.setCursorBufferPosition([4, 0]) + + editor.duplicateLines() + + expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe(`\ +\ if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + }\ +` + ) + expect(editor.getSelectedBufferRange()).toEqual([[8, 0], [8, 0]]) + }) + + it('can duplicate the last line of the buffer', () => { + editor.setSelectedBufferRange([[11, 0], [12, 2]]) + editor.duplicateLines() + expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe(`\ +\ return sort(Array.apply(this, arguments)); +}; + return sort(Array.apply(this, arguments)); +};\ +` + ) + expect(editor.getSelectedBufferRange()).toEqual([[13, 0], [14, 2]]) + }) + + it('only duplicates lines containing multiple selections once', () => { + editor.setText(`\ +aaaaaa +bbbbbb +cccccc +dddddd\ +`) + editor.setSelectedBufferRanges([ + [[0, 1], [0, 2]], + [[0, 3], [0, 4]], + [[2, 1], [2, 2]], + [[2, 3], [3, 1]], + [[3, 3], [3, 4]] + ]) + editor.duplicateLines() + expect(editor.getText()).toBe(`\ +aaaaaa +aaaaaa +bbbbbb +cccccc +dddddd +cccccc +dddddd\ +`) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 1], [1, 2]], + [[1, 3], [1, 4]], + [[5, 1], [5, 2]], + [[5, 3], [6, 1]], + [[6, 3], [6, 4]] + ]) + }) + }) + + describe('when the editor contains surrogate pair characters', () => { + it('correctly backspaces over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') + editor.moveToBottom() + editor.backspace() + expect(editor.getText()).toBe('\uD835\uDF97\uD835\uDF97') + editor.backspace() + expect(editor.getText()).toBe('\uD835\uDF97') + editor.backspace() + expect(editor.getText()).toBe('') + }) + + it('correctly deletes over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') + editor.moveToTop() + editor.delete() + expect(editor.getText()).toBe('\uD835\uDF97\uD835\uDF97') + editor.delete() + expect(editor.getText()).toBe('\uD835\uDF97') + editor.delete() + expect(editor.getText()).toBe('') + }) + + it('correctly moves over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n') + editor.moveToTop() + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('when the editor contains variation sequence character pairs', () => { + it('correctly backspaces over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') + editor.moveToBottom() + editor.backspace() + expect(editor.getText()).toBe('\u2714\uFE0E\u2714\uFE0E') + editor.backspace() + expect(editor.getText()).toBe('\u2714\uFE0E') + editor.backspace() + expect(editor.getText()).toBe('') + }) + + it('correctly deletes over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') + editor.moveToTop() + editor.delete() + expect(editor.getText()).toBe('\u2714\uFE0E\u2714\uFE0E') + editor.delete() + expect(editor.getText()).toBe('\u2714\uFE0E') + editor.delete() + expect(editor.getText()).toBe('') + }) + + it('correctly moves over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E\n') + editor.moveToTop() + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.setIndentationForBufferRow', () => { + describe('when the editor uses soft tabs but the row has hard tabs', () => { + it('only replaces whitespace characters', () => { + editor.setSoftWrapped(true) + editor.setText('\t1\n\t2') + editor.setCursorBufferPosition([0, 0]) + editor.setIndentationForBufferRow(0, 2) + expect(editor.getText()).toBe(' 1\n\t2') + }) + }) + + describe('when the indentation level is a non-integer', () => { + it('does not throw an exception', () => { + editor.setSoftWrapped(true) + editor.setText('\t1\n\t2') + editor.setCursorBufferPosition([0, 0]) + editor.setIndentationForBufferRow(0, 2.1) + expect(editor.getText()).toBe(' 1\n\t2') + }) + }) + }) + + describe("when the editor's grammar has an injection selector", () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-text') + await atom.packages.activatePackage('language-javascript') + }) + + it("includes the grammar's patterns when the selector matches the current scope in other grammars", async () => { + await atom.packages.activatePackage('language-hyperlink') + + const grammar = atom.grammars.selectGrammar('text.js') + const {line, tags} = grammar.tokenizeLine('var i; // http://github.com') + + const tokens = atom.grammars.decodeTokens(line, tags) + expect(tokens[0].value).toBe('var') + expect(tokens[0].scopes).toEqual(['source.js', 'storage.type.var.js']) + expect(tokens[6].value).toBe('http://github.com') + expect(tokens[6].scopes).toEqual(['source.js', 'comment.line.double-slash.js', 'markup.underline.link.http.hyperlink']) + }) + + describe('when the grammar is added', () => { + it('retokenizes existing buffers that contain tokens that match the injection selector', async () => { + editor = await atom.workspace.open('sample.js') + editor.setText('// http://github.com') + let tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('language-hyperlink') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']} + ]) + }) + + describe('when the grammar is updated', () => { + it('retokenizes existing buffers that contain tokens that match the injection selector', async () => { + editor = await atom.workspace.open('sample.js') + editor.setText('// SELECT * FROM OCTOCATS') + let tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('package-with-injection-selector') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('language-sql') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, + {text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + }) + }) + }) + }) + + describe('.normalizeTabsInBufferRange()', () => { + it("normalizes tabs depending on the editor's soft tab/tab length settings", () => { + editor.setTabLength(1) + editor.setSoftTabs(true) + editor.setText('\t\t\t') + editor.normalizeTabsInBufferRange([[0, 0], [0, 1]]) + expect(editor.getText()).toBe(' \t\t') + + editor.setTabLength(2) + editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) + expect(editor.getText()).toBe(' ') + + editor.setSoftTabs(false) + editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) + expect(editor.getText()).toBe(' ') + }) + }) + + describe('.pageUp/Down()', () => { + it('moves the cursor down one page length', () => { + editor.update({autoHeight: false}) + const element = editor.getElement() + jasmine.attachToDOM(element) + element.style.height = (element.component.getLineHeight() * 5) + 'px' + element.measureDimensions() + + expect(editor.getCursorBufferPosition().row).toBe(0) + + editor.pageDown() + expect(editor.getCursorBufferPosition().row).toBe(5) + + editor.pageDown() + expect(editor.getCursorBufferPosition().row).toBe(10) + + editor.pageUp() + expect(editor.getCursorBufferPosition().row).toBe(5) + + editor.pageUp() + expect(editor.getCursorBufferPosition().row).toBe(0) + }) + }) + + describe('.selectPageUp/Down()', () => { + it('selects one screen height of text up or down', () => { + editor.update({autoHeight: false}) + const element = editor.getElement() + jasmine.attachToDOM(element) + element.style.height = (element.component.getLineHeight() * 5) + 'px' + element.measureDimensions() + + expect(editor.getCursorBufferPosition().row).toBe(0) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [5, 0]]]) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [10, 0]]]) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [12, 2]]]) + + editor.moveToBottom() + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[7, 0], [12, 2]]]) + + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 0], [12, 2]]]) + + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [12, 2]]]) + }) + }) + + describe('::scrollToScreenPosition(position, [options])', () => { + it('triggers ::onDidRequestAutoscroll with the logical coordinates along with the options', () => { + const scrollSpy = jasmine.createSpy('::onDidRequestAutoscroll') + editor.onDidRequestAutoscroll(scrollSpy) + + editor.scrollToScreenPosition([8, 20]) + editor.scrollToScreenPosition([8, 20], {center: true}) + editor.scrollToScreenPosition([8, 20], {center: false, reversed: true}) + + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {}}) + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {center: true}}) + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}}) + }) + }) + + describe('scroll past end', () => { + it('returns false by default but can be customized', () => { + expect(editor.getScrollPastEnd()).toBe(false) + editor.update({scrollPastEnd: true}) + expect(editor.getScrollPastEnd()).toBe(true) + editor.update({scrollPastEnd: false}) + expect(editor.getScrollPastEnd()).toBe(false) + }) + + it('always returns false when autoHeight is on', () => { + editor.update({autoHeight: true, scrollPastEnd: true}) + expect(editor.getScrollPastEnd()).toBe(false) + editor.update({autoHeight: false}) + expect(editor.getScrollPastEnd()).toBe(true) + }) + }) + + describe('auto height', () => { + it('returns true by default but can be customized', () => { + editor = new TextEditor() + expect(editor.getAutoHeight()).toBe(true) + editor.update({autoHeight: false}) + expect(editor.getAutoHeight()).toBe(false) + editor.update({autoHeight: true}) + expect(editor.getAutoHeight()).toBe(true) + editor.destroy() + }) + }) + + describe('auto width', () => { + it('returns false by default but can be customized', () => { + expect(editor.getAutoWidth()).toBe(false) + editor.update({autoWidth: true}) + expect(editor.getAutoWidth()).toBe(true) + editor.update({autoWidth: false}) + expect(editor.getAutoWidth()).toBe(false) + }) + }) + + describe('.get/setPlaceholderText()', () => { + it('can be created with placeholderText', () => { + const newEditor = new TextEditor({ + mini: true, + placeholderText: 'yep' + }) + expect(newEditor.getPlaceholderText()).toBe('yep') + }) + + it('models placeholderText and emits an event when changed', () => { + let handler + editor.onDidChangePlaceholderText(handler = jasmine.createSpy()) + + expect(editor.getPlaceholderText()).toBeUndefined() + + editor.setPlaceholderText('OK') + expect(handler).toHaveBeenCalledWith('OK') + expect(editor.getPlaceholderText()).toBe('OK') + }) + }) + + describe('gutters', () => { + describe('the TextEditor constructor', () => { + it('creates a line-number gutter', () => { + expect(editor.getGutters().length).toBe(1) + const lineNumberGutter = editor.gutterWithName('line-number') + expect(lineNumberGutter.name).toBe('line-number') + expect(lineNumberGutter.priority).toBe(0) + }) + }) + + describe('::addGutter', () => { + it('can add a gutter', () => { + expect(editor.getGutters().length).toBe(1) // line-number gutter + const options = { + name: 'test-gutter', + priority: 1 + } + const gutter = editor.addGutter(options) + expect(editor.getGutters().length).toBe(2) + expect(editor.getGutters()[1]).toBe(gutter) + }) + + it("does not allow a custom gutter with the 'line-number' name.", () => expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow()) + }) + + describe('::decorateMarker', () => { + let marker + + beforeEach(() => marker = editor.markBufferRange([[1, 0], [1, 0]])) + + it('reflects an added decoration when one of its custom gutters is decorated.', () => { + const gutter = editor.addGutter({'name': 'custom-gutter'}) + const decoration = gutter.decorateMarker(marker, {class: 'custom-class'}) + const gutterDecorations = editor.getDecorations({ + type: 'gutter', + gutterName: 'custom-gutter', + class: 'custom-class' + }) + expect(gutterDecorations.length).toBe(1) + expect(gutterDecorations[0]).toBe(decoration) + }) + + it('reflects an added decoration when its line-number gutter is decorated.', () => { + const decoration = editor.gutterWithName('line-number').decorateMarker(marker, {class: 'test-class'}) + const gutterDecorations = editor.getDecorations({ + type: 'line-number', + gutterName: 'line-number', + class: 'test-class' + }) + expect(gutterDecorations.length).toBe(1) + expect(gutterDecorations[0]).toBe(decoration) + }) + }) + + describe('::observeGutters', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback immediately with each existing gutter, and with each added gutter after that.', () => { + const lineNumberGutter = editor.gutterWithName('line-number') + editor.observeGutters(callback) + expect(payloads).toEqual([lineNumberGutter]) + const gutter1 = editor.addGutter({name: 'test-gutter-1'}) + expect(payloads).toEqual([lineNumberGutter, gutter1]) + const gutter2 = editor.addGutter({name: 'test-gutter-2'}) + expect(payloads).toEqual([lineNumberGutter, gutter1, gutter2]) + }) + + it('does not call the callback when a gutter is removed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + editor.observeGutters(callback) + payloads = [] + gutter.destroy() + expect(payloads).toEqual([]) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const subscription = editor.observeGutters(callback) + payloads = [] + subscription.dispose() + editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([]) + }) + }) + + describe('::onDidAddGutter', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback with each newly-added gutter, but not with existing gutters.', () => { + editor.onDidAddGutter(callback) + expect(payloads).toEqual([]) + const gutter = editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([gutter]) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const subscription = editor.onDidAddGutter(callback) + payloads = [] + subscription.dispose() + editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([]) + }) + }) + + describe('::onDidRemoveGutter', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback when a gutter is removed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + editor.onDidRemoveGutter(callback) + expect(payloads).toEqual([]) + gutter.destroy() + expect(payloads).toEqual(['test-gutter']) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + const subscription = editor.onDidRemoveGutter(callback) + subscription.dispose() + gutter.destroy() + expect(payloads).toEqual([]) + }) + }) + }) + + describe('decorations', () => { + describe('::decorateMarker', () => { + it('includes the decoration in the object returned from ::decorationsStateForScreenRowRange', () => { + const marker = editor.markBufferRange([[2, 4], [6, 8]]) + const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'foo'}) + expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker.getScreenRange(), + bufferRange: marker.getBufferRange(), + rangeIsReversed: false + }) + }) + + it("does not throw errors after the marker's containing layer is destroyed", () => { + const layer = editor.addMarkerLayer() + const marker = layer.markBufferRange([[2, 4], [6, 8]]) + const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'foo'}) + layer.destroy() + editor.decorationsStateForScreenRowRange(0, 5) + }) + }) + + describe('::decorateMarkerLayer', () => { + it('based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange', () => { + const layer1 = editor.getBuffer().addMarkerLayer() + const marker1 = layer1.markRange([[2, 4], [6, 8]]) + const marker2 = layer1.markRange([[11, 0], [11, 12]]) + const layer2 = editor.getBuffer().addMarkerLayer() + const marker3 = layer2.markRange([[8, 0], [9, 0]]) + + const layer1Decoration1 = editor.decorateMarkerLayer(layer1, {type: 'highlight', class: 'foo'}) + const layer1Decoration2 = editor.decorateMarkerLayer(layer1, {type: 'highlight', class: 'bar'}) + const layer2Decoration = editor.decorateMarkerLayer(layer2, {type: 'highlight', class: 'baz'}) + + let decorationState = editor.decorationsStateForScreenRowRange(0, 13) + + expect(decorationState[`${layer1Decoration1.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration1.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer2Decoration.id}-${marker3.id}`]).toEqual({ + properties: {type: 'highlight', class: 'baz'}, + screenRange: marker3.getRange(), + bufferRange: marker3.getRange(), + rangeIsReversed: false + }) + + layer1Decoration1.destroy() + + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration1.id}-${marker1.id}`]).toBeUndefined() + expect(decorationState[`${layer1Decoration1.id}-${marker2.id}`]).toBeUndefined() + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer2Decoration.id}-${marker3.id}`]).toEqual({ + properties: {type: 'highlight', class: 'baz'}, + screenRange: marker3.getRange(), + bufferRange: marker3.getRange(), + rangeIsReversed: false + }) + + layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'}) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'quux'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + + layer1Decoration2.setPropertiesForMarker(marker1, null) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + }) + }) + }) + + describe('invisibles', () => { + beforeEach(() => { + editor.update({showInvisibles: true}) + }) + + it('substitutes invisible characters according to the given rules', () => { + const previousLineText = editor.lineTextForScreenRow(0) + editor.update({invisibles: {eol: '?'}}) + expect(editor.lineTextForScreenRow(0)).not.toBe(previousLineText) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) + expect(editor.getInvisibles()).toEqual({eol: '?'}) + }) + + it('does not use invisibles if showInvisibles is set to false', () => { + editor.update({invisibles: {eol: '?'}}) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) + + editor.update({showInvisibles: false}) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(false) + }) + }) + + describe('indent guides', () => { + it('shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini', () => { + editor.setText(' foo') + editor.setTabLength(2) + + editor.update({showIndentGuide: false}) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + + editor.update({showIndentGuide: true}) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + + editor.setMini(true) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + }) + }) + + describe('when the editor is constructed with the grammar option set', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-coffee-script') + }) + + it('sets the grammar', () => { + editor = new TextEditor({grammar: atom.grammars.grammarForScopeName('source.coffee')}) + expect(editor.getGrammar().name).toBe('CoffeeScript') + }) + }) + + describe('softWrapAtPreferredLineLength', () => { + it('soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini', () => { + editor.update({ + editorWidthInChars: 30, + softWrapped: true, + softWrapAtPreferredLineLength: true, + preferredLineLength: 20 + }) + + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = ') + + editor.update({editorWidthInChars: 10}) + expect(editor.lineTextForScreenRow(0)).toBe('var ') + + editor.update({mini: true}) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + }) + }) + + describe('softWrapHangingIndentLength', () => { + it('controls how much extra indentation is applied to soft-wrapped lines', () => { + editor.setText('123456789') + editor.update({ + editorWidthInChars: 8, + softWrapped: true, + softWrapHangingIndentLength: 2 + }) + expect(editor.lineTextForScreenRow(1)).toEqual(' 9') + + editor.update({softWrapHangingIndentLength: 4}) + expect(editor.lineTextForScreenRow(1)).toEqual(' 9') + }) + }) + + describe('::getElement', () => { + it('returns an element', () => expect(editor.getElement() instanceof HTMLElement).toBe(true)) + }) + + describe('setMaxScreenLineLength', () => { + it('sets the maximum line length in the editor before soft wrapping is forced', () => { + expect(editor.getSoftWrapColumn()).toBe(500) + editor.update({ + maxScreenLineLength: 1500 + }) + expect(editor.getSoftWrapColumn()).toBe(1500) + }) + }) +}) describe('TextEditor', () => { let editor @@ -539,3 +7185,7 @@ describe('TextEditor', () => { }) }) }) + +function convertToHardTabs (buffer) { + buffer.setText(buffer.getText().replace(/[ ]{2}/g, '\t')) +} From af82dff75bfebe79b056ddae744f99d4fa499f38 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 16:15:32 -0700 Subject: [PATCH 243/301] Fix error in .getLongTitle when editors have no path --- src/text-editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor.js b/src/text-editor.js index 4d7d94de0..033cea5d6 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1059,11 +1059,11 @@ class TextEditor { let myPathSegments const openEditorPathSegmentsWithSameFilename = [] for (const textEditor of atom.workspace.getTextEditors()) { - const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) if (textEditor.getFileName() === fileName) { + const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) openEditorPathSegmentsWithSameFilename.push(pathSegments) + if (textEditor === this) myPathSegments = pathSegments } - if (textEditor === this) myPathSegments = pathSegments } if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName From 887975c4034f6ed3ef5dfa5ad7473b167cf0a317 Mon Sep 17 00:00:00 2001 From: Roy Giladi Date: Thu, 2 Nov 2017 01:33:39 +0200 Subject: [PATCH 244/301] Remove duplicate variable declaration Hey, just noticed that "Project" has already been declared on line 36 --- src/atom-environment.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index a32c4424b..af61ffb36 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -42,7 +42,6 @@ PaneContainer = require './pane-container' PaneAxis = require './pane-axis' Pane = require './pane' Dock = require './dock' -Project = require './project' TextEditor = require './text-editor' TextBuffer = require 'text-buffer' Gutter = require './gutter' From 96e6b3a2ce467193b6671bff3b532ff501a1ce0c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 16:51:01 -0700 Subject: [PATCH 245/301] Fix error in .getLongTitle when editor isn't in the workspace --- spec/text-editor-spec.js | 9 +++++++++ src/text-editor.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index b2cc41ab7..cece5d753 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -218,6 +218,15 @@ describe('TextEditor', () => { expect(editor1.getLongTitle()).toBe('main.js \u2014 js') expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path.join('js', 'plugin')}`) }) + + it('returns the filename when the editor is not in the workspace', async () => { + editor.onDidDestroy(() => { + expect(editor.getLongTitle()).toBe('sample.js') + }) + + await atom.workspace.getActivePane().close() + expect(editor.isDestroyed()).toBe(true) + }) }) it('notifies ::onDidChangeTitle observers when the underlying buffer path changes', () => { diff --git a/src/text-editor.js b/src/text-editor.js index 033cea5d6..a0b9d19a0 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1066,7 +1066,7 @@ class TextEditor { } } - if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName + if (!myPathSegments || openEditorPathSegmentsWithSameFilename.length === 1) return fileName let commonPathSegmentCount for (let i = 0, {length} = myPathSegments; i < length; i++) { From d5445db78465645a6e8d4121262feeffea8d11ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 16:59:52 -0700 Subject: [PATCH 246/301] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2716fac9f..6b5d407eb 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.8.0", + "text-buffer": "13.8.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 7f48c140ba61c9cf72d1848514ab610b16643413 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 17:37:29 -0700 Subject: [PATCH 247/301] :arrow_up: tabs for spec fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72250311d..584ea0af6 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.14", "styleguide": "0.49.8", "symbols-view": "0.118.1", - "tabs": "0.109.0", + "tabs": "0.109.1", "timecop": "0.36.0", "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", From 9540d3f33ebb248c61e6ed92edb402024e35f510 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 21:42:04 -0700 Subject: [PATCH 248/301] :arrow_up: whitespace, snippets --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 584ea0af6..1dc4406aa 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.2", - "snippets": "1.1.8", + "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.14", "styleguide": "0.49.8", @@ -134,7 +134,7 @@ "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", - "whitespace": "0.37.4", + "whitespace": "0.37.5", "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", From 7054eefe1d7f249e951103c7640c61ee84e1c3b4 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 1 Nov 2017 09:21:17 -0400 Subject: [PATCH 249/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/theme-manager-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/theme-manager-spec.coffee | 437 ---------------------------- spec/theme-manager-spec.js | 503 +++++++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+), 437 deletions(-) delete mode 100644 spec/theme-manager-spec.coffee create mode 100644 spec/theme-manager-spec.js diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee deleted file mode 100644 index 86237b71d..000000000 --- a/spec/theme-manager-spec.coffee +++ /dev/null @@ -1,437 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -temp = require('temp').track() - -describe "atom.themes", -> - beforeEach -> - spyOn(atom, 'inSpecMode').andReturn(false) - spyOn(console, 'warn') - - afterEach -> - waitsForPromise -> - atom.themes.deactivateThemes() - runs -> - try - temp.cleanupSync() - - describe "theme getters and setters", -> - beforeEach -> - jasmine.snapshotDeprecations() - atom.packages.loadPackages() - - afterEach -> - jasmine.restoreDeprecationsSnapshot() - - describe 'getLoadedThemes', -> - it 'gets all the loaded themes', -> - themes = atom.themes.getLoadedThemes() - expect(themes.length).toBeGreaterThan(2) - - describe "getActiveThemes", -> - it 'gets all the active themes', -> - waitsForPromise -> atom.themes.activateThemes() - - runs -> - names = atom.config.get('core.themes') - expect(names.length).toBeGreaterThan(0) - themes = atom.themes.getActiveThemes() - expect(themes).toHaveLength(names.length) - - describe "when the core.themes config value contains invalid entry", -> - it "ignores theme", -> - atom.config.set 'core.themes', [ - 'atom-light-ui' - null - undefined - '' - false - 4 - {} - [] - 'atom-dark-ui' - ] - - expect(atom.themes.getEnabledThemeNames()).toEqual ['atom-dark-ui', 'atom-light-ui'] - - describe "::getImportPaths()", -> - it "returns the theme directories before the themes are loaded", -> - atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui', 'atom-light-ui']) - - paths = atom.themes.getImportPaths() - - # syntax theme is not a dir at this time, so only two. - expect(paths.length).toBe 2 - expect(paths[0]).toContain 'atom-light-ui' - expect(paths[1]).toContain 'atom-dark-ui' - - it "ignores themes that cannot be resolved to a directory", -> - atom.config.set('core.themes', ['definitely-not-a-theme']) - expect(-> atom.themes.getImportPaths()).not.toThrow() - - describe "when the core.themes config value changes", -> - it "add/removes stylesheets to reflect the new config value", -> - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - spyOn(atom.styles, 'getUserStyleSheetPath').andCallFake -> null - - waitsForPromise -> - atom.themes.activateThemes() - - runs -> - didChangeActiveThemesHandler.reset() - atom.config.set('core.themes', []) - - waitsFor 'a', -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - didChangeActiveThemesHandler.reset() - expect(document.querySelectorAll('style.theme')).toHaveLength 0 - atom.config.set('core.themes', ['atom-dark-ui']) - - waitsFor 'b', -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - didChangeActiveThemesHandler.reset() - expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2 - expect(document.querySelector('style[priority="1"]').getAttribute('source-path')).toMatch /atom-dark-ui/ - atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']) - - waitsFor 'c', -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - didChangeActiveThemesHandler.reset() - expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2 - expect(document.querySelectorAll('style[priority="1"]')[0].getAttribute('source-path')).toMatch /atom-dark-ui/ - expect(document.querySelectorAll('style[priority="1"]')[1].getAttribute('source-path')).toMatch /atom-light-ui/ - atom.config.set('core.themes', []) - - waitsFor -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - didChangeActiveThemesHandler.reset() - expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2 - # atom-dark-ui has an directory path, the syntax one doesn't - atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui']) - - waitsFor -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - expect(document.querySelectorAll('style[priority="1"]')).toHaveLength 2 - importPaths = atom.themes.getImportPaths() - expect(importPaths.length).toBe 1 - expect(importPaths[0]).toContain 'atom-dark-ui' - - it 'adds theme-* classes to the workspace for each active theme', -> - atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax']) - workspaceElement = atom.workspace.getElement() - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - - waitsForPromise -> - atom.themes.activateThemes() - - runs -> - expect(workspaceElement).toHaveClass 'theme-atom-dark-ui' - - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables']) - - waitsFor -> - didChangeActiveThemesHandler.callCount > 0 - - runs -> - # `theme-` twice as it prefixes the name with `theme-` - expect(workspaceElement).toHaveClass 'theme-theme-with-ui-variables' - expect(workspaceElement).toHaveClass 'theme-theme-with-syntax-variables' - expect(workspaceElement).not.toHaveClass 'theme-atom-dark-ui' - expect(workspaceElement).not.toHaveClass 'theme-atom-dark-syntax' - - describe "when a theme fails to load", -> - it "logs a warning", -> - console.warn.reset() - atom.packages.activatePackage('a-theme-that-will-not-be-found').then((->), (->)) - expect(console.warn.callCount).toBe 1 - expect(console.warn.argsForCall[0][0]).toContain "Could not resolve 'a-theme-that-will-not-be-found'" - - describe "::requireStylesheet(path)", -> - beforeEach -> - jasmine.snapshotDeprecations() - - afterEach -> - jasmine.restoreDeprecationsSnapshot() - - it "synchronously loads css at the given path and installs a style tag for it in the head", -> - atom.styles.onDidAddStyleElement styleElementAddedHandler = jasmine.createSpy("styleElementAddedHandler") - - cssPath = atom.project.getDirectories()[0]?.resolve('css.css') - lengthBefore = document.querySelectorAll('head style').length - - atom.themes.requireStylesheet(cssPath) - expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 - - expect(styleElementAddedHandler).toHaveBeenCalled() - - element = document.querySelector('head style[source-path*="css.css"]') - expect(element.getAttribute('source-path')).toEqualPath cssPath - expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8') - - # doesn't append twice - styleElementAddedHandler.reset() - atom.themes.requireStylesheet(cssPath) - expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 - expect(styleElementAddedHandler).not.toHaveBeenCalled() - - for styleElement in document.querySelectorAll('head style[id*="css.css"]') - styleElement.remove() - - it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", -> - lessPath = atom.project.getDirectories()[0]?.resolve('sample.less') - lengthBefore = document.querySelectorAll('head style').length - atom.themes.requireStylesheet(lessPath) - expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 - - element = document.querySelector('head style[source-path*="sample.less"]') - expect(element.getAttribute('source-path')).toEqualPath lessPath - expect(element.textContent.toLowerCase()).toBe """ - #header { - color: #4d926f; - } - h2 { - color: #4d926f; - } - - """ - - # doesn't append twice - atom.themes.requireStylesheet(lessPath) - expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 - for styleElement in document.querySelectorAll('head style[id*="sample.less"]') - styleElement.remove() - - it "supports requiring css and less stylesheets without an explicit extension", -> - atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css') - expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.project.getDirectories()[0]?.resolve('css.css') - atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample') - expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.project.getDirectories()[0]?.resolve('sample.less') - - document.querySelector('head style[source-path*="css.css"]').remove() - document.querySelector('head style[source-path*="sample.less"]').remove() - - it "returns a disposable allowing styles applied by the given path to be removed", -> - cssPath = require.resolve('./fixtures/css.css') - - expect(getComputedStyle(document.body).fontWeight).not.toBe("bold") - disposable = atom.themes.requireStylesheet(cssPath) - expect(getComputedStyle(document.body).fontWeight).toBe("bold") - - atom.styles.onDidRemoveStyleElement styleElementRemovedHandler = jasmine.createSpy("styleElementRemovedHandler") - - disposable.dispose() - - expect(getComputedStyle(document.body).fontWeight).not.toBe("bold") - - expect(styleElementRemovedHandler).toHaveBeenCalled() - - - describe "base style sheet loading", -> - beforeEach -> - workspaceElement = atom.workspace.getElement() - jasmine.attachToDOM(atom.workspace.getElement()) - workspaceElement.appendChild document.createElement('atom-text-editor') - - waitsForPromise -> - atom.themes.activateThemes() - - it "loads the correct values from the theme's ui-variables file", -> - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables']) - - waitsFor -> - didChangeActiveThemesHandler.callCount > 0 - - runs -> - # an override loaded in the base css - expect(getComputedStyle(atom.workspace.getElement())["background-color"]).toBe "rgb(0, 0, 255)" - - # from within the theme itself - expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingTop).toBe "150px" - expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingRight).toBe "150px" - expect(getComputedStyle(document.querySelector("atom-text-editor")).paddingBottom).toBe "150px" - - describe "when there is a theme with incomplete variables", -> - it "loads the correct values from the fallback ui-variables", -> - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() - atom.config.set('core.themes', ['theme-with-incomplete-ui-variables', 'theme-with-syntax-variables']) - - waitsFor -> - didChangeActiveThemesHandler.callCount > 0 - - runs -> - # an override loaded in the base css - expect(getComputedStyle(atom.workspace.getElement())["background-color"]).toBe "rgb(0, 0, 255)" - - # from within the theme itself - expect(getComputedStyle(document.querySelector("atom-text-editor")).backgroundColor).toBe "rgb(0, 152, 255)" - - describe "user stylesheet", -> - userStylesheetPath = null - beforeEach -> - userStylesheetPath = path.join(temp.mkdirSync("atom"), 'styles.less') - fs.writeFileSync(userStylesheetPath, 'body {border-style: dotted !important;}') - spyOn(atom.styles, 'getUserStyleSheetPath').andReturn userStylesheetPath - - describe "when the user stylesheet changes", -> - beforeEach -> - jasmine.snapshotDeprecations() - - afterEach -> - jasmine.restoreDeprecationsSnapshot() - - it "reloads it", -> - [styleElementAddedHandler, styleElementRemovedHandler] = [] - - waitsForPromise -> - atom.themes.activateThemes() - - runs -> - atom.styles.onDidRemoveStyleElement styleElementRemovedHandler = jasmine.createSpy("styleElementRemovedHandler") - atom.styles.onDidAddStyleElement styleElementAddedHandler = jasmine.createSpy("styleElementAddedHandler") - - spyOn(atom.themes, 'loadUserStylesheet').andCallThrough() - - expect(getComputedStyle(document.body).borderStyle).toBe 'dotted' - fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}') - - waitsFor -> - atom.themes.loadUserStylesheet.callCount is 1 - - runs -> - expect(getComputedStyle(document.body).borderStyle).toBe 'dashed' - - expect(styleElementRemovedHandler).toHaveBeenCalled() - expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain 'dotted' - - expect(styleElementAddedHandler).toHaveBeenCalled() - expect(styleElementAddedHandler.argsForCall[0][0].textContent).toContain 'dashed' - - styleElementRemovedHandler.reset() - fs.removeSync(userStylesheetPath) - - waitsFor -> - atom.themes.loadUserStylesheet.callCount is 2 - - runs -> - expect(styleElementRemovedHandler).toHaveBeenCalled() - expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain 'dashed' - expect(getComputedStyle(document.body).borderStyle).toBe 'none' - - describe "when there is an error reading the stylesheet", -> - addErrorHandler = null - beforeEach -> - atom.themes.loadUserStylesheet() - spyOn(atom.themes.lessCache, 'cssForFile').andCallFake -> - throw new Error('EACCES permission denied "styles.less"') - atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() - - it "creates an error notification and does not add the stylesheet", -> - atom.themes.loadUserStylesheet() - expect(addErrorHandler).toHaveBeenCalled() - note = addErrorHandler.mostRecentCall.args[0] - expect(note.getType()).toBe 'error' - expect(note.getMessage()).toContain 'Error loading' - expect(atom.styles.styleElementsBySourcePath[atom.styles.getUserStyleSheetPath()]).toBeUndefined() - - describe "when there is an error watching the user stylesheet", -> - addErrorHandler = null - beforeEach -> - {File} = require 'pathwatcher' - spyOn(File::, 'on').andCallFake (event) -> - if event.indexOf('contents-changed') > -1 - throw new Error('Unable to watch path') - spyOn(atom.themes, 'loadStylesheet').andReturn '' - atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() - - it "creates an error notification", -> - atom.themes.loadUserStylesheet() - expect(addErrorHandler).toHaveBeenCalled() - note = addErrorHandler.mostRecentCall.args[0] - expect(note.getType()).toBe 'error' - expect(note.getMessage()).toContain 'Unable to watch path' - - it "adds a notification when a theme's stylesheet is invalid", -> - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> atom.packages.activatePackage('theme-with-invalid-styles').then((->), (->))).not.toThrow() - expect(addErrorHandler.callCount).toBe 2 - expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to activate the theme-with-invalid-styles theme") - - describe "when a non-existent theme is present in the config", -> - beforeEach -> - console.warn.reset() - atom.config.set('core.themes', ['non-existent-dark-ui', 'non-existent-dark-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the default dark UI and syntax themes and logs a warning', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(console.warn.callCount).toBe 2 - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') - - describe "when in safe mode", -> - describe 'when the enabled UI and syntax themes are bundled with Atom', -> - beforeEach -> - atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the enabled themes', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-light-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') - - describe 'when the enabled UI and syntax themes are not bundled with Atom', -> - beforeEach -> - atom.config.set('core.themes', ['installed-dark-ui', 'installed-dark-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the default dark UI and syntax themes', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') - - describe 'when the enabled UI theme is not bundled with Atom', -> - beforeEach -> - atom.config.set('core.themes', ['installed-dark-ui', 'atom-light-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the default dark UI theme', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-dark-ui') - expect(activeThemeNames).toContain('atom-light-syntax') - - describe 'when the enabled syntax theme is not bundled with Atom', -> - beforeEach -> - atom.config.set('core.themes', ['atom-light-ui', 'installed-dark-syntax']) - - waitsForPromise -> - atom.themes.activateThemes() - - it 'uses the default dark syntax theme', -> - activeThemeNames = atom.themes.getActiveThemeNames() - expect(activeThemeNames.length).toBe(2) - expect(activeThemeNames).toContain('atom-light-ui') - expect(activeThemeNames).toContain('atom-dark-syntax') diff --git a/spec/theme-manager-spec.js b/spec/theme-manager-spec.js new file mode 100644 index 000000000..f4ed3b9f5 --- /dev/null +++ b/spec/theme-manager-spec.js @@ -0,0 +1,503 @@ +const path = require('path') +const fs = require('fs-plus') +const temp = require('temp').track() + +describe('atom.themes', function () { + beforeEach(function () { + spyOn(atom, 'inSpecMode').andReturn(false) + spyOn(console, 'warn') + }) + + afterEach(function () { + waitsForPromise(() => atom.themes.deactivateThemes()) + runs(function () { + try { + temp.cleanupSync() + } catch (error) {} + }) + }) + + describe('theme getters and setters', function () { + beforeEach(function () { + jasmine.snapshotDeprecations() + atom.packages.loadPackages() + }) + + afterEach(() => jasmine.restoreDeprecationsSnapshot()) + + describe('getLoadedThemes', () => + it('gets all the loaded themes', function () { + const themes = atom.themes.getLoadedThemes() + expect(themes.length).toBeGreaterThan(2) + }) + ) + + describe('getActiveThemes', () => + it('gets all the active themes', function () { + waitsForPromise(() => atom.themes.activateThemes()) + + runs(function () { + const names = atom.config.get('core.themes') + expect(names.length).toBeGreaterThan(0) + const themes = atom.themes.getActiveThemes() + expect(themes).toHaveLength(names.length) + }) + }) + ) + }) + + describe('when the core.themes config value contains invalid entry', () => + it('ignores theme', function () { + atom.config.set('core.themes', [ + 'atom-light-ui', + null, + undefined, + '', + false, + 4, + {}, + [], + 'atom-dark-ui' + ]) + + expect(atom.themes.getEnabledThemeNames()).toEqual(['atom-dark-ui', 'atom-light-ui']) + }) +) + + describe('::getImportPaths()', function () { + it('returns the theme directories before the themes are loaded', function () { + atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui', 'atom-light-ui']) + + const paths = atom.themes.getImportPaths() + + // syntax theme is not a dir at this time, so only two. + expect(paths.length).toBe(2) + expect(paths[0]).toContain('atom-light-ui') + expect(paths[1]).toContain('atom-dark-ui') + }) + + it('ignores themes that cannot be resolved to a directory', function () { + atom.config.set('core.themes', ['definitely-not-a-theme']) + expect(() => atom.themes.getImportPaths()).not.toThrow() + }) + }) + + describe('when the core.themes config value changes', function () { + it('add/removes stylesheets to reflect the new config value', function () { + let didChangeActiveThemesHandler + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + spyOn(atom.styles, 'getUserStyleSheetPath').andCallFake(() => null) + + waitsForPromise(() => atom.themes.activateThemes()) + + runs(function () { + didChangeActiveThemesHandler.reset() + atom.config.set('core.themes', []) + }) + + waitsFor('a', () => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + didChangeActiveThemesHandler.reset() + expect(document.querySelectorAll('style.theme')).toHaveLength(0) + atom.config.set('core.themes', ['atom-dark-ui']) + }) + + waitsFor('b', () => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + didChangeActiveThemesHandler.reset() + expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2) + expect(document.querySelector('style[priority="1"]').getAttribute('source-path')).toMatch(/atom-dark-ui/) + atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-ui']) + }) + + waitsFor('c', () => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + didChangeActiveThemesHandler.reset() + expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2) + expect(document.querySelectorAll('style[priority="1"]')[0].getAttribute('source-path')).toMatch(/atom-dark-ui/) + expect(document.querySelectorAll('style[priority="1"]')[1].getAttribute('source-path')).toMatch(/atom-light-ui/) + atom.config.set('core.themes', []) + }) + + waitsFor(() => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + didChangeActiveThemesHandler.reset() + expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2) + // atom-dark-ui has a directory path, the syntax one doesn't + atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui']) + }) + + waitsFor(() => didChangeActiveThemesHandler.callCount === 1) + + runs(function () { + expect(document.querySelectorAll('style[priority="1"]')).toHaveLength(2) + const importPaths = atom.themes.getImportPaths() + expect(importPaths.length).toBe(1) + expect(importPaths[0]).toContain('atom-dark-ui') + }) + }) + + it('adds theme-* classes to the workspace for each active theme', function () { + atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax']) + + let didChangeActiveThemesHandler + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + waitsForPromise(() => atom.themes.activateThemes()) + + const workspaceElement = atom.workspace.getElement() + runs(function () { + expect(workspaceElement).toHaveClass('theme-atom-dark-ui') + + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables']) + }) + + waitsFor(() => didChangeActiveThemesHandler.callCount > 0) + + runs(function () { + // `theme-` twice as it prefixes the name with `theme-` + expect(workspaceElement).toHaveClass('theme-theme-with-ui-variables') + expect(workspaceElement).toHaveClass('theme-theme-with-syntax-variables') + expect(workspaceElement).not.toHaveClass('theme-atom-dark-ui') + expect(workspaceElement).not.toHaveClass('theme-atom-dark-syntax') + }) + }) + }) + + describe('when a theme fails to load', () => + it('logs a warning', function () { + console.warn.reset() + atom.packages.activatePackage('a-theme-that-will-not-be-found').then(function () {}, function () {}) + expect(console.warn.callCount).toBe(1) + expect(console.warn.argsForCall[0][0]).toContain("Could not resolve 'a-theme-that-will-not-be-found'") + }) + ) + + describe('::requireStylesheet(path)', function () { + beforeEach(() => jasmine.snapshotDeprecations()) + + afterEach(() => jasmine.restoreDeprecationsSnapshot()) + + it('synchronously loads css at the given path and installs a style tag for it in the head', function () { + let styleElementAddedHandler + atom.styles.onDidAddStyleElement(styleElementAddedHandler = jasmine.createSpy('styleElementAddedHandler')) + + const cssPath = getAbsolutePath(atom.project.getDirectories()[0], 'css.css') + const lengthBefore = document.querySelectorAll('head style').length + + atom.themes.requireStylesheet(cssPath) + expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1) + + expect(styleElementAddedHandler).toHaveBeenCalled() + + const element = document.querySelector('head style[source-path*="css.css"]') + expect(element.getAttribute('source-path')).toEqualPath(cssPath) + expect(element.textContent).toBe(fs.readFileSync(cssPath, 'utf8')) + + // doesn't append twice + styleElementAddedHandler.reset() + atom.themes.requireStylesheet(cssPath) + expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1) + expect(styleElementAddedHandler).not.toHaveBeenCalled() + + document.querySelectorAll('head style[id*="css.css"]').forEach((styleElement) => { + styleElement.remove() + }) + }) + + it('synchronously loads and parses less files at the given path and installs a style tag for it in the head', function () { + const lessPath = getAbsolutePath(atom.project.getDirectories()[0], 'sample.less') + const lengthBefore = document.querySelectorAll('head style').length + atom.themes.requireStylesheet(lessPath) + expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1) + + const element = document.querySelector('head style[source-path*="sample.less"]') + expect(element.getAttribute('source-path')).toEqualPath(lessPath) + expect(element.textContent.toLowerCase()).toBe(`\ +#header { + color: #4d926f; +} +h2 { + color: #4d926f; +} +\ +` + ) + + // doesn't append twice + atom.themes.requireStylesheet(lessPath) + expect(document.querySelectorAll('head style').length).toBe(lengthBefore + 1) + document.querySelectorAll('head style[id*="sample.less"]').forEach((styleElement) => { + styleElement.remove() + }) + }) + + it('supports requiring css and less stylesheets without an explicit extension', function () { + atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'css')) + expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')) + .toEqualPath(getAbsolutePath(atom.project.getDirectories()[0], 'css.css')) + atom.themes.requireStylesheet(path.join(__dirname, 'fixtures', 'sample')) + expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')) + .toEqualPath(getAbsolutePath(atom.project.getDirectories()[0], 'sample.less')) + + document.querySelector('head style[source-path*="css.css"]').remove() + document.querySelector('head style[source-path*="sample.less"]').remove() + }) + + it('returns a disposable allowing styles applied by the given path to be removed', function () { + const cssPath = require.resolve('./fixtures/css.css') + + expect(getComputedStyle(document.body).fontWeight).not.toBe('bold') + const disposable = atom.themes.requireStylesheet(cssPath) + expect(getComputedStyle(document.body).fontWeight).toBe('bold') + + let styleElementRemovedHandler + atom.styles.onDidRemoveStyleElement(styleElementRemovedHandler = jasmine.createSpy('styleElementRemovedHandler')) + + disposable.dispose() + + expect(getComputedStyle(document.body).fontWeight).not.toBe('bold') + + expect(styleElementRemovedHandler).toHaveBeenCalled() + }) + }) + + describe('base style sheet loading', function () { + beforeEach(function () { + const workspaceElement = atom.workspace.getElement() + jasmine.attachToDOM(atom.workspace.getElement()) + workspaceElement.appendChild(document.createElement('atom-text-editor')) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it("loads the correct values from the theme's ui-variables file", function () { + let didChangeActiveThemesHandler + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + atom.config.set('core.themes', ['theme-with-ui-variables', 'theme-with-syntax-variables']) + + waitsFor(() => didChangeActiveThemesHandler.callCount > 0) + + runs(function () { + // an override loaded in the base css + expect(getComputedStyle(atom.workspace.getElement())['background-color']).toBe('rgb(0, 0, 255)') + + // from within the theme itself + expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingTop).toBe('150px') + expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingRight).toBe('150px') + expect(getComputedStyle(document.querySelector('atom-text-editor')).paddingBottom).toBe('150px') + }) + }) + + describe('when there is a theme with incomplete variables', () => + it('loads the correct values from the fallback ui-variables', function () { + let didChangeActiveThemesHandler + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler = jasmine.createSpy()) + atom.config.set('core.themes', ['theme-with-incomplete-ui-variables', 'theme-with-syntax-variables']) + + waitsFor(() => didChangeActiveThemesHandler.callCount > 0) + + runs(function () { + // an override loaded in the base css + expect(getComputedStyle(atom.workspace.getElement())['background-color']).toBe('rgb(0, 0, 255)') + + // from within the theme itself + expect(getComputedStyle(document.querySelector('atom-text-editor')).backgroundColor).toBe('rgb(0, 152, 255)') + }) + }) + ) + }) + + describe('user stylesheet', function () { + let userStylesheetPath + beforeEach(function () { + userStylesheetPath = path.join(temp.mkdirSync('atom'), 'styles.less') + fs.writeFileSync(userStylesheetPath, 'body {border-style: dotted !important;}') + spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(userStylesheetPath) + }) + + describe('when the user stylesheet changes', function () { + beforeEach(() => jasmine.snapshotDeprecations()) + + afterEach(() => jasmine.restoreDeprecationsSnapshot()) + + it('reloads it', function () { + let styleElementAddedHandler, styleElementRemovedHandler + + waitsForPromise(() => atom.themes.activateThemes()) + + runs(function () { + atom.styles.onDidRemoveStyleElement(styleElementRemovedHandler = jasmine.createSpy('styleElementRemovedHandler')) + atom.styles.onDidAddStyleElement(styleElementAddedHandler = jasmine.createSpy('styleElementAddedHandler')) + + spyOn(atom.themes, 'loadUserStylesheet').andCallThrough() + + expect(getComputedStyle(document.body).borderStyle).toBe('dotted') + fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}') + }) + + waitsFor(() => atom.themes.loadUserStylesheet.callCount === 1) + + runs(function () { + expect(getComputedStyle(document.body).borderStyle).toBe('dashed') + + expect(styleElementRemovedHandler).toHaveBeenCalled() + expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain('dotted') + + expect(styleElementAddedHandler).toHaveBeenCalled() + expect(styleElementAddedHandler.argsForCall[0][0].textContent).toContain('dashed') + + styleElementRemovedHandler.reset() + fs.removeSync(userStylesheetPath) + }) + + waitsFor(() => atom.themes.loadUserStylesheet.callCount === 2) + + runs(function () { + expect(styleElementRemovedHandler).toHaveBeenCalled() + expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain('dashed') + expect(getComputedStyle(document.body).borderStyle).toBe('none') + }) + }) + }) + + describe('when there is an error reading the stylesheet', function () { + let addErrorHandler = null + beforeEach(function () { + atom.themes.loadUserStylesheet() + spyOn(atom.themes.lessCache, 'cssForFile').andCallFake(function () { + throw new Error('EACCES permission denied "styles.less"') + }) + atom.notifications.onDidAddNotification(addErrorHandler = jasmine.createSpy()) + }) + + it('creates an error notification and does not add the stylesheet', function () { + atom.themes.loadUserStylesheet() + expect(addErrorHandler).toHaveBeenCalled() + const note = addErrorHandler.mostRecentCall.args[0] + expect(note.getType()).toBe('error') + expect(note.getMessage()).toContain('Error loading') + expect(atom.styles.styleElementsBySourcePath[atom.styles.getUserStyleSheetPath()]).toBeUndefined() + }) + }) + + describe('when there is an error watching the user stylesheet', function () { + let addErrorHandler = null + beforeEach(function () { + const {File} = require('pathwatcher') + spyOn(File.prototype, 'on').andCallFake(function (event) { + if (event.indexOf('contents-changed') > -1) { + throw new Error('Unable to watch path') + } + }) + spyOn(atom.themes, 'loadStylesheet').andReturn('') + atom.notifications.onDidAddNotification(addErrorHandler = jasmine.createSpy()) + }) + + it('creates an error notification', function () { + atom.themes.loadUserStylesheet() + expect(addErrorHandler).toHaveBeenCalled() + const note = addErrorHandler.mostRecentCall.args[0] + expect(note.getType()).toBe('error') + expect(note.getMessage()).toContain('Unable to watch path') + }) + }) + + it("adds a notification when a theme's stylesheet is invalid", function () { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => atom.packages.activatePackage('theme-with-invalid-styles').then(function () {}, function () {})).not.toThrow() + expect(addErrorHandler.callCount).toBe(2) + expect(addErrorHandler.argsForCall[1][0].message).toContain('Failed to activate the theme-with-invalid-styles theme') + }) + }) + + describe('when a non-existent theme is present in the config', function () { + beforeEach(function () { + console.warn.reset() + atom.config.set('core.themes', ['non-existent-dark-ui', 'non-existent-dark-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the default dark UI and syntax themes and logs a warning', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(console.warn.callCount).toBe(2) + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-dark-ui') + expect(activeThemeNames).toContain('atom-dark-syntax') + }) + }) + + describe('when in safe mode', function () { + describe('when the enabled UI and syntax themes are bundled with Atom', function () { + beforeEach(function () { + atom.config.set('core.themes', ['atom-light-ui', 'atom-dark-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the enabled themes', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-light-ui') + expect(activeThemeNames).toContain('atom-dark-syntax') + }) + }) + + describe('when the enabled UI and syntax themes are not bundled with Atom', function () { + beforeEach(function () { + atom.config.set('core.themes', ['installed-dark-ui', 'installed-dark-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the default dark UI and syntax themes', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-dark-ui') + expect(activeThemeNames).toContain('atom-dark-syntax') + }) + }) + + describe('when the enabled UI theme is not bundled with Atom', function () { + beforeEach(function () { + atom.config.set('core.themes', ['installed-dark-ui', 'atom-light-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the default dark UI theme', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-dark-ui') + expect(activeThemeNames).toContain('atom-light-syntax') + }) + }) + + describe('when the enabled syntax theme is not bundled with Atom', function () { + beforeEach(function () { + atom.config.set('core.themes', ['atom-light-ui', 'installed-dark-syntax']) + + waitsForPromise(() => atom.themes.activateThemes()) + }) + + it('uses the default dark syntax theme', function () { + const activeThemeNames = atom.themes.getActiveThemeNames() + expect(activeThemeNames.length).toBe(2) + expect(activeThemeNames).toContain('atom-light-ui') + expect(activeThemeNames).toContain('atom-dark-syntax') + }) + }) + }) +}) + +function getAbsolutePath (directory, relativePath) { + if (directory) { + return directory.resolve(relativePath) + } +} From 1e9753d8a54a027c5902be0c9f8b10c665f271ec Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 10:22:22 -0600 Subject: [PATCH 250/301] Fix select-word command between word and non-word chararacters In #15776, we accidentally stopped passing an option to the wordRegExp method that caused us to prefer word characters when selecting words at a boundary between word and non-word characters. --- spec/text-editor-spec.js | 8 ++++++-- src/cursor.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index cece5d753..382d020d4 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -2007,13 +2007,17 @@ describe('TextEditor', () => { describe('when the cursor is between two words', () => { it('selects the word the cursor is on', () => { - editor.setCursorScreenPosition([0, 4]) + editor.setCursorBufferPosition([0, 4]) editor.selectWordsContainingCursors() expect(editor.getSelectedText()).toBe('quicksort') - editor.setCursorScreenPosition([0, 3]) + editor.setCursorBufferPosition([0, 3]) editor.selectWordsContainingCursors() expect(editor.getSelectedText()).toBe('var') + + editor.setCursorBufferPosition([1, 22]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('items') }) }) diff --git a/src/cursor.js b/src/cursor.js index 6cd0cc623..10bdef804 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -594,7 +594,7 @@ class Cursor extends Model { getCurrentWordBufferRange (options = {}) { const position = this.getBufferPosition() const ranges = this.editor.buffer.findAllInRangeSync( - options.wordRegex || this.wordRegExp(), + options.wordRegex || this.wordRegExp(options), new Range(new Point(position.row, 0), new Point(position.row, Infinity)) ) const range = ranges.find(range => From 4ce351d0f34fc36cfd4cfc3b304e6af026257ed6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Nov 2017 09:22:58 -0700 Subject: [PATCH 251/301] Convert Selection to JS --- src/selection.coffee | 840 ------------------------------------- src/selection.js | 975 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 975 insertions(+), 840 deletions(-) delete mode 100644 src/selection.coffee create mode 100644 src/selection.js diff --git a/src/selection.coffee b/src/selection.coffee deleted file mode 100644 index e55f17e88..000000000 --- a/src/selection.coffee +++ /dev/null @@ -1,840 +0,0 @@ -{Point, Range} = require 'text-buffer' -{pick} = require 'underscore-plus' -{Emitter} = require 'event-kit' -Model = require './model' - -NonWhitespaceRegExp = /\S/ - -# Extended: Represents a selection in the {TextEditor}. -module.exports = -class Selection extends Model - cursor: null - marker: null - editor: null - initialScreenRange: null - wordwise: false - - constructor: ({@cursor, @marker, @editor, id}) -> - @emitter = new Emitter - - @assignId(id) - @cursor.selection = this - @decoration = @editor.decorateMarker(@marker, type: 'highlight', class: 'selection') - - @marker.onDidChange (e) => @markerDidChange(e) - @marker.onDidDestroy => @markerDidDestroy() - - destroy: -> - @marker.destroy() - - isLastSelection: -> - this is @editor.getLastSelection() - - ### - Section: Event Subscription - ### - - # Extended: Calls your `callback` when the selection was moved. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferRange` {Range} - # * `oldScreenRange` {Range} - # * `newBufferRange` {Range} - # * `newScreenRange` {Range} - # * `selection` {Selection} that triggered the event - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeRange: (callback) -> - @emitter.on 'did-change-range', callback - - # Extended: Calls your `callback` when the selection was destroyed - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - ### - Section: Managing the selection range - ### - - # Public: Returns the screen {Range} for the selection. - getScreenRange: -> - @marker.getScreenRange() - - # Public: Modifies the screen range for the selection. - # - # * `screenRange` The new {Range} to use. - # * `options` (optional) {Object} options matching those found in {::setBufferRange}. - setScreenRange: (screenRange, options) -> - @setBufferRange(@editor.bufferRangeForScreenRange(screenRange), options) - - # Public: Returns the buffer {Range} for the selection. - getBufferRange: -> - @marker.getBufferRange() - - # Public: Modifies the buffer {Range} for the selection. - # - # * `bufferRange` The new {Range} to select. - # * `options` (optional) {Object} with the keys: - # * `preserveFolds` if `true`, the fold settings are preserved after the - # selection moves. - # * `autoscroll` {Boolean} indicating whether to autoscroll to the new - # range. Defaults to `true` if this is the most recently added selection, - # `false` otherwise. - setBufferRange: (bufferRange, options={}) -> - bufferRange = Range.fromObject(bufferRange) - options.reversed ?= @isReversed() - @editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) unless options.preserveFolds - @modifySelection => - needsFlash = options.flash - delete options.flash if options.flash? - @marker.setBufferRange(bufferRange, options) - @autoscroll() if options?.autoscroll ? @isLastSelection() - @decoration.flash('flash', @editor.selectionFlashDuration) if needsFlash - - # Public: Returns the starting and ending buffer rows the selection is - # highlighting. - # - # Returns an {Array} of two {Number}s: the starting row, and the ending row. - getBufferRowRange: -> - range = @getBufferRange() - start = range.start.row - end = range.end.row - end = Math.max(start, end - 1) if range.end.column is 0 - [start, end] - - getTailScreenPosition: -> - @marker.getTailScreenPosition() - - getTailBufferPosition: -> - @marker.getTailBufferPosition() - - getHeadScreenPosition: -> - @marker.getHeadScreenPosition() - - getHeadBufferPosition: -> - @marker.getHeadBufferPosition() - - ### - Section: Info about the selection - ### - - # Public: Determines if the selection contains anything. - isEmpty: -> - @getBufferRange().isEmpty() - - # Public: Determines if the ending position of a marker is greater than the - # starting position. - # - # This can happen when, for example, you highlight text "up" in a {TextBuffer}. - isReversed: -> - @marker.isReversed() - - # Public: Returns whether the selection is a single line or not. - isSingleScreenLine: -> - @getScreenRange().isSingleLine() - - # Public: Returns the text in the selection. - getText: -> - @editor.buffer.getTextInRange(@getBufferRange()) - - # Public: Identifies if a selection intersects with a given buffer range. - # - # * `bufferRange` A {Range} to check against. - # - # Returns a {Boolean} - intersectsBufferRange: (bufferRange) -> - @getBufferRange().intersectsWith(bufferRange) - - intersectsScreenRowRange: (startRow, endRow) -> - @getScreenRange().intersectsRowRange(startRow, endRow) - - intersectsScreenRow: (screenRow) -> - @getScreenRange().intersectsRow(screenRow) - - # Public: Identifies if a selection intersects with another selection. - # - # * `otherSelection` A {Selection} to check against. - # - # Returns a {Boolean} - intersectsWith: (otherSelection, exclusive) -> - @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive) - - ### - Section: Modifying the selected range - ### - - # Public: Clears the selection, moving the marker to the head. - # - # * `options` (optional) {Object} with the following keys: - # * `autoscroll` {Boolean} indicating whether to autoscroll to the new - # range. Defaults to `true` if this is the most recently added selection, - # `false` otherwise. - clear: (options) -> - @goalScreenRange = null - @marker.clearTail() unless @retainSelection - @autoscroll() if options?.autoscroll ? @isLastSelection() - @finalize() - - # Public: Selects the text from the current cursor position to a given screen - # position. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToScreenPosition: (position, options) -> - position = Point.fromObject(position) - - @modifySelection => - if @initialScreenRange - if position.isLessThan(@initialScreenRange.start) - @marker.setScreenRange([position, @initialScreenRange.end], reversed: true) - else - @marker.setScreenRange([@initialScreenRange.start, position], reversed: false) - else - @cursor.setScreenPosition(position, options) - - if @linewise - @expandOverLine(options) - else if @wordwise - @expandOverWord(options) - - # Public: Selects the text from the current cursor position to a given buffer - # position. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToBufferPosition: (position) -> - @modifySelection => @cursor.setBufferPosition(position) - - # Public: Selects the text one position right of the cursor. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - selectRight: (columnCount) -> - @modifySelection => @cursor.moveRight(columnCount) - - # Public: Selects the text one position left of the cursor. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - selectLeft: (columnCount) -> - @modifySelection => @cursor.moveLeft(columnCount) - - # Public: Selects all the text one position above the cursor. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - selectUp: (rowCount) -> - @modifySelection => @cursor.moveUp(rowCount) - - # Public: Selects all the text one position below the cursor. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - selectDown: (rowCount) -> - @modifySelection => @cursor.moveDown(rowCount) - - # Public: Selects all the text from the current cursor position to the top of - # the buffer. - selectToTop: -> - @modifySelection => @cursor.moveToTop() - - # Public: Selects all the text from the current cursor position to the bottom - # of the buffer. - selectToBottom: -> - @modifySelection => @cursor.moveToBottom() - - # Public: Selects all the text in the buffer. - selectAll: -> - @setBufferRange(@editor.buffer.getRange(), autoscroll: false) - - # Public: Selects all the text from the current cursor position to the - # beginning of the line. - selectToBeginningOfLine: -> - @modifySelection => @cursor.moveToBeginningOfLine() - - # Public: Selects all the text from the current cursor position to the first - # character of the line. - selectToFirstCharacterOfLine: -> - @modifySelection => @cursor.moveToFirstCharacterOfLine() - - # Public: Selects all the text from the current cursor position to the end of - # the screen line. - selectToEndOfLine: -> - @modifySelection => @cursor.moveToEndOfScreenLine() - - # Public: Selects all the text from the current cursor position to the end of - # the buffer line. - selectToEndOfBufferLine: -> - @modifySelection => @cursor.moveToEndOfLine() - - # Public: Selects all the text from the current cursor position to the - # beginning of the word. - selectToBeginningOfWord: -> - @modifySelection => @cursor.moveToBeginningOfWord() - - # Public: Selects all the text from the current cursor position to the end of - # the word. - selectToEndOfWord: -> - @modifySelection => @cursor.moveToEndOfWord() - - # Public: Selects all the text from the current cursor position to the - # beginning of the next word. - selectToBeginningOfNextWord: -> - @modifySelection => @cursor.moveToBeginningOfNextWord() - - # Public: Selects text to the previous word boundary. - selectToPreviousWordBoundary: -> - @modifySelection => @cursor.moveToPreviousWordBoundary() - - # Public: Selects text to the next word boundary. - selectToNextWordBoundary: -> - @modifySelection => @cursor.moveToNextWordBoundary() - - # Public: Selects text to the previous subword boundary. - selectToPreviousSubwordBoundary: -> - @modifySelection => @cursor.moveToPreviousSubwordBoundary() - - # Public: Selects text to the next subword boundary. - selectToNextSubwordBoundary: -> - @modifySelection => @cursor.moveToNextSubwordBoundary() - - # Public: Selects all the text from the current cursor position to the - # beginning of the next paragraph. - selectToBeginningOfNextParagraph: -> - @modifySelection => @cursor.moveToBeginningOfNextParagraph() - - # Public: Selects all the text from the current cursor position to the - # beginning of the previous paragraph. - selectToBeginningOfPreviousParagraph: -> - @modifySelection => @cursor.moveToBeginningOfPreviousParagraph() - - # Public: Modifies the selection to encompass the current word. - # - # Returns a {Range}. - selectWord: (options={}) -> - options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace() - if @cursor.isBetweenWordAndNonWord() - options.includeNonWordCharacters = false - - @setBufferRange(@cursor.getCurrentWordBufferRange(options), options) - @wordwise = true - @initialScreenRange = @getScreenRange() - - # Public: Expands the newest selection to include the entire word on which - # the cursors rests. - expandOverWord: (options) -> - @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()), autoscroll: false) - @cursor.autoscroll() if options?.autoscroll ? true - - # Public: Selects an entire line in the buffer. - # - # * `row` The line {Number} to select (default: the row of the cursor). - selectLine: (row, options) -> - if row? - @setBufferRange(@editor.bufferRangeForBufferRow(row, includeNewline: true), options) - else - startRange = @editor.bufferRangeForBufferRow(@marker.getStartBufferPosition().row) - endRange = @editor.bufferRangeForBufferRow(@marker.getEndBufferPosition().row, includeNewline: true) - @setBufferRange(startRange.union(endRange), options) - - @linewise = true - @wordwise = false - @initialScreenRange = @getScreenRange() - - # Public: Expands the newest selection to include the entire line on which - # the cursor currently rests. - # - # It also includes the newline character. - expandOverLine: (options) -> - range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true)) - @setBufferRange(range, autoscroll: false) - @cursor.autoscroll() if options?.autoscroll ? true - - ### - Section: Modifying the selected text - ### - - # Public: Replaces text at the current selection. - # - # * `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 - # (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. - insertText: (text, options={}) -> - oldBufferRange = @getBufferRange() - wasReversed = @isReversed() - @clear(options) - - autoIndentFirstLine = false - precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start]) - remainingLines = text.split('\n') - firstInsertedLine = remainingLines.shift() - - if options.indentBasis? and not options.preserveTrailingLineIndentation - indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis - @adjustIndent(remainingLines, indentAdjustment) - - textIsAutoIndentable = text is '\n' or text is '\r\n' or NonWhitespaceRegExp.test(text) - if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0 - autoIndentFirstLine = true - firstLine = precedingText + firstInsertedLine - desiredIndentLevel = @editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) - indentAdjustment = desiredIndentLevel - @editor.indentLevelForLine(firstLine) - @adjustIndent(remainingLines, indentAdjustment) - - text = firstInsertedLine - text += '\n' + remainingLines.join('\n') if remainingLines.length > 0 - - newBufferRange = @editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings')) - - if options.select - @setBufferRange(newBufferRange, reversed: wasReversed) - else - @cursor.setBufferPosition(newBufferRange.end) if wasReversed - - if autoIndentFirstLine - @editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel) - - if options.autoIndentNewline and text is '\n' - @editor.autoIndentBufferRow(newBufferRange.end.row, preserveLeadingWhitespace: true, skipBlankLines: false) - else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text) - @editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row) - - @autoscroll() if options.autoscroll ? @isLastSelection() - - newBufferRange - - # Public: Removes the first character before the selection if the selection - # is empty otherwise it deletes the selection. - backspace: -> - @selectLeft() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or, if nothing is selected, then all - # characters from the start of the selection back to the previous word - # boundary. - deleteToPreviousWordBoundary: -> - @selectToPreviousWordBoundary() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or, if nothing is selected, then all - # characters from the start of the selection up to the next word - # boundary. - deleteToNextWordBoundary: -> - @selectToNextWordBoundary() if @isEmpty() - @deleteSelectedText() - - # Public: Removes from the start of the selection to the beginning of the - # current word if the selection is empty otherwise it deletes the selection. - deleteToBeginningOfWord: -> - @selectToBeginningOfWord() if @isEmpty() - @deleteSelectedText() - - # Public: Removes from the beginning of the line which the selection begins on - # all the way through to the end of the selection. - deleteToBeginningOfLine: -> - if @isEmpty() and @cursor.isAtBeginningOfLine() - @selectLeft() - else - @selectToBeginningOfLine() - @deleteSelectedText() - - # Public: Removes the selection or the next character after the start of the - # selection if the selection is empty. - delete: -> - @selectRight() if @isEmpty() - @deleteSelectedText() - - # Public: If the selection is empty, removes all text from the cursor to the - # end of the line. If the cursor is already at the end of the line, it - # removes the following newline. If the selection isn't empty, only deletes - # the contents of the selection. - deleteToEndOfLine: -> - return @delete() if @isEmpty() and @cursor.isAtEndOfLine() - @selectToEndOfLine() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or all characters from the start of the - # selection to the end of the current word if nothing is selected. - deleteToEndOfWord: -> - @selectToEndOfWord() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or all characters from the start of the - # selection to the end of the current word if nothing is selected. - deleteToBeginningOfSubword: -> - @selectToPreviousSubwordBoundary() if @isEmpty() - @deleteSelectedText() - - # Public: Removes the selection or all characters from the start of the - # selection to the end of the current word if nothing is selected. - deleteToEndOfSubword: -> - @selectToNextSubwordBoundary() if @isEmpty() - @deleteSelectedText() - - # Public: Removes only the selected text. - deleteSelectedText: -> - bufferRange = @getBufferRange() - @editor.buffer.delete(bufferRange) unless bufferRange.isEmpty() - @cursor?.setBufferPosition(bufferRange.start) - - # Public: Removes the line at the beginning of the selection if the selection - # is empty unless the selection spans multiple lines in which case all lines - # are removed. - deleteLine: -> - if @isEmpty() - start = @cursor.getScreenRow() - range = @editor.bufferRowsForScreenRows(start, start + 1) - if range[1] > range[0] - @editor.buffer.deleteRows(range[0], range[1] - 1) - else - @editor.buffer.deleteRow(range[0]) - else - range = @getBufferRange() - start = range.start.row - end = range.end.row - if end isnt @editor.buffer.getLastRow() and range.end.column is 0 - end-- - @editor.buffer.deleteRows(start, end) - - # Public: Joins the current line with the one below it. Lines will - # be separated by a single space. - # - # If there selection spans more than one line, all the lines are joined together. - joinLines: -> - selectedRange = @getBufferRange() - if selectedRange.isEmpty() - return if selectedRange.start.row is @editor.buffer.getLastRow() - else - joinMarker = @editor.markBufferRange(selectedRange, invalidate: 'never') - - rowCount = Math.max(1, selectedRange.getRowCount() - 1) - for [0...rowCount] - @cursor.setBufferPosition([selectedRange.start.row]) - @cursor.moveToEndOfLine() - - # Remove trailing whitespace from the current line - scanRange = @cursor.getCurrentLineBufferRange() - trailingWhitespaceRange = null - @editor.scanInBufferRange /[ \t]+$/, scanRange, ({range}) -> - trailingWhitespaceRange = range - if trailingWhitespaceRange? - @setBufferRange(trailingWhitespaceRange) - @deleteSelectedText() - - currentRow = selectedRange.start.row - nextRow = currentRow + 1 - insertSpace = nextRow <= @editor.buffer.getLastRow() and - @editor.buffer.lineLengthForRow(nextRow) > 0 and - @editor.buffer.lineLengthForRow(currentRow) > 0 - @insertText(' ') if insertSpace - - @cursor.moveToEndOfLine() - - # Remove leading whitespace from the line below - @modifySelection => - @cursor.moveRight() - @cursor.moveToFirstCharacterOfLine() - @deleteSelectedText() - - @cursor.moveLeft() if insertSpace - - if joinMarker? - newSelectedRange = joinMarker.getBufferRange() - @setBufferRange(newSelectedRange) - joinMarker.destroy() - - # Public: Removes one level of indent from the currently selected rows. - outdentSelectedRows: -> - [start, end] = @getBufferRowRange() - buffer = @editor.buffer - leadingTabRegex = new RegExp("^( {1,#{@editor.getTabLength()}}|\t)") - for row in [start..end] - if matchLength = buffer.lineForRow(row).match(leadingTabRegex)?[0].length - buffer.delete [[row, 0], [row, matchLength]] - return - - # Public: Sets the indentation level of all selected rows to values suggested - # by the relevant grammars. - autoIndentSelectedRows: -> - [start, end] = @getBufferRowRange() - @editor.autoIndentBufferRows(start, end) - - # Public: Wraps the selected lines in comments if they aren't currently part - # of a comment. - # - # Removes the comment if they are currently wrapped in a comment. - toggleLineComments: -> - @editor.toggleLineCommentsForBufferRows(@getBufferRowRange()...) - - # Public: Cuts the selection until the end of the screen line. - cutToEndOfLine: (maintainClipboard) -> - @selectToEndOfLine() if @isEmpty() - @cut(maintainClipboard) - - # Public: Cuts the selection until the end of the buffer line. - cutToEndOfBufferLine: (maintainClipboard) -> - @selectToEndOfBufferLine() if @isEmpty() - @cut(maintainClipboard) - - # Public: Copies the selection to the clipboard and then deletes it. - # - # * `maintainClipboard` {Boolean} (default: false) See {::copy} - # * `fullLine` {Boolean} (default: false) See {::copy} - cut: (maintainClipboard=false, fullLine=false) -> - @copy(maintainClipboard, fullLine) - @delete() - - # Public: Copies the current selection to the clipboard. - # - # * `maintainClipboard` {Boolean} if `true`, a specific metadata property - # is created to store each content copied to the clipboard. The clipboard - # `text` still contains the concatenation of the clipboard with the - # current selection. (default: false) - # * `fullLine` {Boolean} if `true`, the copied text will always be pasted - # at the beginning of the line containing the cursor, regardless of the - # cursor's horizontal position. (default: false) - copy: (maintainClipboard=false, fullLine=false) -> - return if @isEmpty() - {start, end} = @getBufferRange() - selectionText = @editor.getTextInRange([start, end]) - precedingText = @editor.getTextInRange([[start.row, 0], start]) - startLevel = @editor.indentLevelForLine(precedingText) - - if maintainClipboard - {text: clipboardText, metadata} = @editor.constructor.clipboard.readWithMetadata() - metadata ?= {} - unless metadata.selections? - metadata.selections = [{ - text: clipboardText, - indentBasis: metadata.indentBasis, - fullLine: metadata.fullLine, - }] - metadata.selections.push({ - text: selectionText, - indentBasis: startLevel, - fullLine: fullLine - }) - @editor.constructor.clipboard.write([clipboardText, selectionText].join("\n"), metadata) - else - @editor.constructor.clipboard.write(selectionText, { - indentBasis: startLevel, - fullLine: fullLine - }) - - # Public: Creates a fold containing the current selection. - fold: -> - range = @getBufferRange() - unless range.isEmpty() - @editor.foldBufferRange(range) - @cursor.setBufferPosition(range.end) - - # Private: Increase the indentation level of the given text by given number - # of levels. Leaves the first line unchanged. - adjustIndent: (lines, indentAdjustment) -> - for line, i in lines - if indentAdjustment is 0 or line is '' - continue - else if indentAdjustment > 0 - lines[i] = @editor.buildIndentString(indentAdjustment) + line - else - currentIndentLevel = @editor.indentLevelForLine(lines[i]) - indentLevel = Math.max(0, currentIndentLevel + indentAdjustment) - lines[i] = line.replace(/^[\t ]+/, @editor.buildIndentString(indentLevel)) - return - - # Indent the current line(s). - # - # If the selection is empty, indents the current line if the cursor precedes - # non-whitespace characters, and otherwise inserts a tab. If the selection is - # non empty, calls {::indentSelectedRows}. - # - # * `options` (optional) {Object} with the keys: - # * `autoIndent` If `true`, the line is indented to an automatically-inferred - # level. Otherwise, {TextEditor::getTabText} is inserted. - indent: ({autoIndent}={}) -> - {row} = @cursor.getBufferPosition() - - if @isEmpty() - @cursor.skipLeadingWhitespace() - desiredIndent = @editor.suggestedIndentForBufferRow(row) - delta = desiredIndent - @cursor.getIndentLevel() - - if autoIndent and delta > 0 - delta = Math.max(delta, 1) unless @editor.getSoftTabs() - @insertText(@editor.buildIndentString(delta)) - else - @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn())) - else - @indentSelectedRows() - - # Public: If the selection spans multiple rows, indent all of them. - indentSelectedRows: -> - [start, end] = @getBufferRowRange() - for row in [start..end] - @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) is 0 - return - - ### - Section: Managing multiple selections - ### - - # Public: Moves the selection down one row. - addSelectionBelow: -> - range = @getGoalScreenRange().copy() - nextRow = range.end.row + 1 - - for row in [nextRow..@editor.getLastScreenRow()] - range.start.row = row - range.end.row = row - clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true) - - if range.isEmpty() - continue if range.end.column > 0 and clippedRange.end.column is 0 - else - continue if clippedRange.isEmpty() - - selection = @editor.addSelectionForScreenRange(clippedRange) - selection.setGoalScreenRange(range) - break - - return - - # Public: Moves the selection up one row. - addSelectionAbove: -> - range = @getGoalScreenRange().copy() - previousRow = range.end.row - 1 - - for row in [previousRow..0] - range.start.row = row - range.end.row = row - clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true) - - if range.isEmpty() - continue if range.end.column > 0 and clippedRange.end.column is 0 - else - continue if clippedRange.isEmpty() - - selection = @editor.addSelectionForScreenRange(clippedRange) - selection.setGoalScreenRange(range) - break - - return - - # Public: Combines the given selection into this selection and then destroys - # the given selection. - # - # * `otherSelection` A {Selection} to merge with. - # * `options` (optional) {Object} options matching those found in {::setBufferRange}. - merge: (otherSelection, options = {}) -> - myGoalScreenRange = @getGoalScreenRange() - otherGoalScreenRange = otherSelection.getGoalScreenRange() - - if myGoalScreenRange? and otherGoalScreenRange? - options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange) - else - options.goalScreenRange = myGoalScreenRange ? otherGoalScreenRange - - @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), Object.assign(autoscroll: false, options)) - otherSelection.destroy() - - ### - Section: Comparing to other selections - ### - - # Public: Compare this selection's buffer range to another selection's buffer - # range. - # - # See {Range::compare} for more details. - # - # * `otherSelection` A {Selection} to compare against - compare: (otherSelection) -> - @marker.compare(otherSelection.marker) - - ### - Section: Private Utilities - ### - - setGoalScreenRange: (range) -> - @goalScreenRange = Range.fromObject(range) - - getGoalScreenRange: -> - @goalScreenRange ? @getScreenRange() - - markerDidChange: (e) -> - {oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e - {oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e - {textChanged} = e - - unless oldHeadScreenPosition.isEqual(newHeadScreenPosition) - @cursor.goalColumn = null - cursorMovedEvent = { - oldBufferPosition: oldHeadBufferPosition - oldScreenPosition: oldHeadScreenPosition - newBufferPosition: newHeadBufferPosition - newScreenPosition: newHeadScreenPosition - textChanged: textChanged - cursor: @cursor - } - @cursor.emitter.emit('did-change-position', cursorMovedEvent) - @editor.cursorMoved(cursorMovedEvent) - - @emitter.emit 'did-change-range' - @editor.selectionRangeChanged( - oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition) - oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition) - newBufferRange: @getBufferRange() - newScreenRange: @getScreenRange() - selection: this - ) - - markerDidDestroy: -> - return if @editor.isDestroyed() - - @destroyed = true - @cursor.destroyed = true - - @editor.removeSelection(this) - - @cursor.emitter.emit 'did-destroy' - @emitter.emit 'did-destroy' - - @cursor.emitter.dispose() - @emitter.dispose() - - finalize: -> - @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange()) - if @isEmpty() - @wordwise = false - @linewise = false - - autoscroll: (options) -> - if @marker.hasTail() - @editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options)) - else - @cursor.autoscroll(options) - - clearAutoscroll: -> - - modifySelection: (fn) -> - @retainSelection = true - @plantTail() - fn() - @retainSelection = false - - # Sets the marker's tail to the same position as the marker's head. - # - # This only works if there isn't already a tail position. - # - # Returns a {Point} representing the new tail position. - plantTail: -> - @marker.plantTail() diff --git a/src/selection.js b/src/selection.js new file mode 100644 index 000000000..20561fd64 --- /dev/null +++ b/src/selection.js @@ -0,0 +1,975 @@ +const {Point, Range} = require('text-buffer') +const {pick} = require('underscore-plus') +const {Emitter} = require('event-kit') + +const NonWhitespaceRegExp = /\S/ +let nextId = 0 + +// Extended: Represents a selection in the {TextEditor}. +module.exports = +class Selection { + constructor ({cursor, marker, editor, id}) { + this.id = (id != null) ? id : nextId++ + this.cursor = cursor + this.marker = marker + this.editor = editor + this.emitter = new Emitter() + this.initialScreenRange = null + this.wordwise = false + this.cursor.selection = this + this.decoration = this.editor.decorateMarker(this.marker, {type: 'highlight', class: 'selection'}) + this.marker.onDidChange(e => this.markerDidChange(e)) + this.marker.onDidDestroy(() => this.markerDidDestroy()) + } + + destroy () { + this.marker.destroy() + } + + isLastSelection () { + return this === this.editor.getLastSelection() + } + + /* + Section: Event Subscription + */ + + // Extended: Calls your `callback` when the selection was moved. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldBufferRange` {Range} + // * `oldScreenRange` {Range} + // * `newBufferRange` {Range} + // * `newScreenRange` {Range} + // * `selection` {Selection} that triggered the event + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeRange (callback) { + return this.emitter.on('did-change-range', callback) + } + + // Extended: Calls your `callback` when the selection was destroyed + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + /* + Section: Managing the selection range + */ + + // Public: Returns the screen {Range} for the selection. + getScreenRange () { + return this.marker.getScreenRange() + } + + // Public: Modifies the screen range for the selection. + // + // * `screenRange` The new {Range} to use. + // * `options` (optional) {Object} options matching those found in {::setBufferRange}. + setScreenRange (screenRange, options) { + return this.setBufferRange(this.editor.bufferRangeForScreenRange(screenRange), options) + } + + // Public: Returns the buffer {Range} for the selection. + getBufferRange () { + return this.marker.getBufferRange() + } + + // Public: Modifies the buffer {Range} for the selection. + // + // * `bufferRange` The new {Range} to select. + // * `options` (optional) {Object} with the keys: + // * `preserveFolds` if `true`, the fold settings are preserved after the + // selection moves. + // * `autoscroll` {Boolean} indicating whether to autoscroll to the new + // range. Defaults to `true` if this is the most recently added selection, + // `false` otherwise. + setBufferRange (bufferRange, options = {}) { + bufferRange = Range.fromObject(bufferRange) + if (options.reversed == null) options.reversed = this.isReversed() + if (!options.preserveFolds) this.editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) + this.modifySelection(() => { + const needsFlash = options.flash + options.flash = null + this.marker.setBufferRange(bufferRange, options) + const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection() + if (autoscroll) this.autoscroll() + if (needsFlash) this.decoration.flash('flash', this.editor.selectionFlashDuration) + }) + } + + // Public: Returns the starting and ending buffer rows the selection is + // highlighting. + // + // Returns an {Array} of two {Number}s: the starting row, and the ending row. + getBufferRowRange () { + const range = this.getBufferRange() + const start = range.start.row + let end = range.end.row + if (range.end.column === 0) end = Math.max(start, end - 1) + return [start, end] + } + + getTailScreenPosition () { + return this.marker.getTailScreenPosition() + } + + getTailBufferPosition () { + return this.marker.getTailBufferPosition() + } + + getHeadScreenPosition () { + return this.marker.getHeadScreenPosition() + } + + getHeadBufferPosition () { + return this.marker.getHeadBufferPosition() + } + + /* + Section: Info about the selection + */ + + // Public: Determines if the selection contains anything. + isEmpty () { + return this.getBufferRange().isEmpty() + } + + // Public: Determines if the ending position of a marker is greater than the + // starting position. + // + // This can happen when, for example, you highlight text "up" in a {TextBuffer}. + isReversed () { + return this.marker.isReversed() + } + + // Public: Returns whether the selection is a single line or not. + isSingleScreenLine () { + return this.getScreenRange().isSingleLine() + } + + // Public: Returns the text in the selection. + getText () { + return this.editor.buffer.getTextInRange(this.getBufferRange()) + } + + // Public: Identifies if a selection intersects with a given buffer range. + // + // * `bufferRange` A {Range} to check against. + // + // Returns a {Boolean} + intersectsBufferRange (bufferRange) { + return this.getBufferRange().intersectsWith(bufferRange) + } + + intersectsScreenRowRange (startRow, endRow) { + return this.getScreenRange().intersectsRowRange(startRow, endRow) + } + + intersectsScreenRow (screenRow) { + return this.getScreenRange().intersectsRow(screenRow) + } + + // Public: Identifies if a selection intersects with another selection. + // + // * `otherSelection` A {Selection} to check against. + // + // Returns a {Boolean} + intersectsWith (otherSelection, exclusive) { + return this.getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive) + } + + /* + Section: Modifying the selected range + */ + + // Public: Clears the selection, moving the marker to the head. + // + // * `options` (optional) {Object} with the following keys: + // * `autoscroll` {Boolean} indicating whether to autoscroll to the new + // range. Defaults to `true` if this is the most recently added selection, + // `false` otherwise. + clear (options) { + this.goalScreenRange = null + if (!this.retainSelection) this.marker.clearTail() + const autoscroll = options && options.autoscroll != null + ? options.autoscroll + : this.isLastSelection() + if (autoscroll) this.autoscroll() + this.finalize() + } + + // Public: Selects the text from the current cursor position to a given screen + // position. + // + // * `position` An instance of {Point}, with a given `row` and `column`. + selectToScreenPosition (position, options) { + position = Point.fromObject(position) + + this.modifySelection(() => { + if (this.initialScreenRange) { + if (position.isLessThan(this.initialScreenRange.start)) { + this.marker.setScreenRange([position, this.initialScreenRange.end], {reversed: true}) + } else { + this.marker.setScreenRange([this.initialScreenRange.start, position], {reversed: false}) + } + } else { + this.cursor.setScreenPosition(position, options) + } + + if (this.linewise) { + this.expandOverLine(options) + } else if (this.wordwise) { + this.expandOverWord(options) + } + }) + } + + // Public: Selects the text from the current cursor position to a given buffer + // position. + // + // * `position` An instance of {Point}, with a given `row` and `column`. + selectToBufferPosition (position) { + this.modifySelection(() => this.cursor.setBufferPosition(position)) + } + + // Public: Selects the text one position right of the cursor. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + selectRight (columnCount) { + this.modifySelection(() => this.cursor.moveRight(columnCount)) + } + + // Public: Selects the text one position left of the cursor. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + selectLeft (columnCount) { + this.modifySelection(() => this.cursor.moveLeft(columnCount)) + } + + // Public: Selects all the text one position above the cursor. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + selectUp (rowCount) { + this.modifySelection(() => this.cursor.moveUp(rowCount)) + } + + // Public: Selects all the text one position below the cursor. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + selectDown (rowCount) { + this.modifySelection(() => this.cursor.moveDown(rowCount)) + } + + // Public: Selects all the text from the current cursor position to the top of + // the buffer. + selectToTop () { + this.modifySelection(() => this.cursor.moveToTop()) + } + + // Public: Selects all the text from the current cursor position to the bottom + // of the buffer. + selectToBottom () { + this.modifySelection(() => this.cursor.moveToBottom()) + } + + // Public: Selects all the text in the buffer. + selectAll () { + this.setBufferRange(this.editor.buffer.getRange(), {autoscroll: false}) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the line. + selectToBeginningOfLine () { + this.modifySelection(() => this.cursor.moveToBeginningOfLine()) + } + + // Public: Selects all the text from the current cursor position to the first + // character of the line. + selectToFirstCharacterOfLine () { + this.modifySelection(() => this.cursor.moveToFirstCharacterOfLine()) + } + + // Public: Selects all the text from the current cursor position to the end of + // the screen line. + selectToEndOfLine () { + this.modifySelection(() => this.cursor.moveToEndOfScreenLine()) + } + + // Public: Selects all the text from the current cursor position to the end of + // the buffer line. + selectToEndOfBufferLine () { + this.modifySelection(() => this.cursor.moveToEndOfLine()) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the word. + selectToBeginningOfWord () { + this.modifySelection(() => this.cursor.moveToBeginningOfWord()) + } + + // Public: Selects all the text from the current cursor position to the end of + // the word. + selectToEndOfWord () { + this.modifySelection(() => this.cursor.moveToEndOfWord()) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the next word. + selectToBeginningOfNextWord () { + this.modifySelection(() => this.cursor.moveToBeginningOfNextWord()) + } + + // Public: Selects text to the previous word boundary. + selectToPreviousWordBoundary () { + this.modifySelection(() => this.cursor.moveToPreviousWordBoundary()) + } + + // Public: Selects text to the next word boundary. + selectToNextWordBoundary () { + this.modifySelection(() => this.cursor.moveToNextWordBoundary()) + } + + // Public: Selects text to the previous subword boundary. + selectToPreviousSubwordBoundary () { + this.modifySelection(() => this.cursor.moveToPreviousSubwordBoundary()) + } + + // Public: Selects text to the next subword boundary. + selectToNextSubwordBoundary () { + this.modifySelection(() => this.cursor.moveToNextSubwordBoundary()) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the next paragraph. + selectToBeginningOfNextParagraph () { + this.modifySelection(() => this.cursor.moveToBeginningOfNextParagraph()) + } + + // Public: Selects all the text from the current cursor position to the + // beginning of the previous paragraph. + selectToBeginningOfPreviousParagraph () { + this.modifySelection(() => this.cursor.moveToBeginningOfPreviousParagraph()) + } + + // Public: Modifies the selection to encompass the current word. + // + // Returns a {Range}. + selectWord (options = {}) { + if (this.cursor.isSurroundedByWhitespace()) options.wordRegex = /[\t ]*/ + if (this.cursor.isBetweenWordAndNonWord()) { + options.includeNonWordCharacters = false + } + + this.setBufferRange(this.cursor.getCurrentWordBufferRange(options), options) + this.wordwise = true + this.initialScreenRange = this.getScreenRange() + } + + // Public: Expands the newest selection to include the entire word on which + // the cursors rests. + expandOverWord (options) { + this.setBufferRange(this.getBufferRange().union(this.cursor.getCurrentWordBufferRange()), {autoscroll: false}) + const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection() + if (autoscroll) this.cursor.autoscroll() + } + + // Public: Selects an entire line in the buffer. + // + // * `row` The line {Number} to select (default: the row of the cursor). + selectLine (row, options) { + if (row != null) { + this.setBufferRange(this.editor.bufferRangeForBufferRow(row, {includeNewline: true}), options) + } else { + const startRange = this.editor.bufferRangeForBufferRow(this.marker.getStartBufferPosition().row) + const endRange = this.editor.bufferRangeForBufferRow(this.marker.getEndBufferPosition().row, {includeNewline: true}) + this.setBufferRange(startRange.union(endRange), options) + } + + this.linewise = true + this.wordwise = false + this.initialScreenRange = this.getScreenRange() + } + + // Public: Expands the newest selection to include the entire line on which + // the cursor currently rests. + // + // It also includes the newline character. + expandOverLine (options) { + const range = this.getBufferRange().union(this.cursor.getCurrentLineBufferRange({includeNewline: true})) + this.setBufferRange(range, {autoscroll: false}) + const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection() + if (autoscroll) this.cursor.autoscroll() + } + + /* + Section: Modifying the selected text + */ + + // Public: Replaces text at the current selection. + // + // * `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 + // (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. + insertText (text, options = {}) { + let desiredIndentLevel, indentAdjustment + const oldBufferRange = this.getBufferRange() + const wasReversed = this.isReversed() + this.clear(options) + + let autoIndentFirstLine = false + const precedingText = this.editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start]) + const remainingLines = text.split('\n') + const firstInsertedLine = remainingLines.shift() + + if (options.indentBasis != null && !options.preserveTrailingLineIndentation) { + indentAdjustment = this.editor.indentLevelForLine(precedingText) - options.indentBasis + this.adjustIndent(remainingLines, indentAdjustment) + } + + const textIsAutoIndentable = (text === '\n') || (text === '\r\n') || NonWhitespaceRegExp.test(text) + if (options.autoIndent && textIsAutoIndentable && !NonWhitespaceRegExp.test(precedingText) && (remainingLines.length > 0)) { + autoIndentFirstLine = true + const firstLine = precedingText + firstInsertedLine + desiredIndentLevel = this.editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) + indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine) + this.adjustIndent(remainingLines, indentAdjustment) + } + + text = firstInsertedLine + if (remainingLines.length > 0) text += `\n${remainingLines.join('\n')}` + + const newBufferRange = this.editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings')) + + if (options.select) { + this.setBufferRange(newBufferRange, {reversed: wasReversed}) + } else { + if (wasReversed) this.cursor.setBufferPosition(newBufferRange.end) + } + + if (autoIndentFirstLine) { + this.editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel) + } + + if (options.autoIndentNewline && (text === '\n')) { + this.editor.autoIndentBufferRow(newBufferRange.end.row, {preserveLeadingWhitespace: true, skipBlankLines: false}) + } else if (options.autoDecreaseIndent && NonWhitespaceRegExp.test(text)) { + this.editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row) + } + + const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection() + if (autoscroll) this.autoscroll() + + return newBufferRange + } + + // Public: Removes the first character before the selection if the selection + // is empty otherwise it deletes the selection. + backspace () { + if (this.isEmpty()) this.selectLeft() + this.deleteSelectedText() + } + + // Public: Removes the selection or, if nothing is selected, then all + // characters from the start of the selection back to the previous word + // boundary. + deleteToPreviousWordBoundary () { + if (this.isEmpty()) this.selectToPreviousWordBoundary() + this.deleteSelectedText() + } + + // Public: Removes the selection or, if nothing is selected, then all + // characters from the start of the selection up to the next word + // boundary. + deleteToNextWordBoundary () { + if (this.isEmpty()) this.selectToNextWordBoundary() + this.deleteSelectedText() + } + + // Public: Removes from the start of the selection to the beginning of the + // current word if the selection is empty otherwise it deletes the selection. + deleteToBeginningOfWord () { + if (this.isEmpty()) this.selectToBeginningOfWord() + this.deleteSelectedText() + } + + // Public: Removes from the beginning of the line which the selection begins on + // all the way through to the end of the selection. + deleteToBeginningOfLine () { + if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) { + this.selectLeft() + } else { + this.selectToBeginningOfLine() + } + this.deleteSelectedText() + } + + // Public: Removes the selection or the next character after the start of the + // selection if the selection is empty. + delete () { + if (this.isEmpty()) this.selectRight() + this.deleteSelectedText() + } + + // Public: If the selection is empty, removes all text from the cursor to the + // end of the line. If the cursor is already at the end of the line, it + // removes the following newline. If the selection isn't empty, only deletes + // the contents of the selection. + deleteToEndOfLine () { + if (this.isEmpty()) { + if (this.cursor.isAtEndOfLine()) { + this.delete() + return + } + this.selectToEndOfLine() + } + this.deleteSelectedText() + } + + // Public: Removes the selection or all characters from the start of the + // selection to the end of the current word if nothing is selected. + deleteToEndOfWord () { + if (this.isEmpty()) this.selectToEndOfWord() + this.deleteSelectedText() + } + + // Public: Removes the selection or all characters from the start of the + // selection to the end of the current word if nothing is selected. + deleteToBeginningOfSubword () { + if (this.isEmpty()) this.selectToPreviousSubwordBoundary() + this.deleteSelectedText() + } + + // Public: Removes the selection or all characters from the start of the + // selection to the end of the current word if nothing is selected. + deleteToEndOfSubword () { + if (this.isEmpty()) this.selectToNextSubwordBoundary() + this.deleteSelectedText() + } + + // Public: Removes only the selected text. + deleteSelectedText () { + const bufferRange = this.getBufferRange() + if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange) + if (this.cursor) this.cursor.setBufferPosition(bufferRange.start) + } + + // Public: Removes the line at the beginning of the selection if the selection + // is empty unless the selection spans multiple lines in which case all lines + // are removed. + deleteLine () { + if (this.isEmpty()) { + const start = this.cursor.getScreenRow() + const range = this.editor.bufferRowsForScreenRows(start, start + 1) + if (range[1] > range[0]) { + this.editor.buffer.deleteRows(range[0], range[1] - 1) + } else { + this.editor.buffer.deleteRow(range[0]) + } + } else { + const range = this.getBufferRange() + const start = range.start.row + let end = range.end.row + if (end !== this.editor.buffer.getLastRow() && range.end.column === 0) end-- + this.editor.buffer.deleteRows(start, end) + } + } + + // Public: Joins the current line with the one below it. Lines will + // be separated by a single space. + // + // If there selection spans more than one line, all the lines are joined together. + joinLines () { + let joinMarker + const selectedRange = this.getBufferRange() + if (selectedRange.isEmpty()) { + if (selectedRange.start.row === this.editor.buffer.getLastRow()) return + } else { + joinMarker = this.editor.markBufferRange(selectedRange, {invalidate: 'never'}) + } + + const rowCount = Math.max(1, selectedRange.getRowCount() - 1) + for (let i = 0; i < rowCount; i++) { + this.cursor.setBufferPosition([selectedRange.start.row]) + this.cursor.moveToEndOfLine() + + // Remove trailing whitespace from the current line + const scanRange = this.cursor.getCurrentLineBufferRange() + let trailingWhitespaceRange = null + this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({range}) => trailingWhitespaceRange = range) + if (trailingWhitespaceRange) { + this.setBufferRange(trailingWhitespaceRange) + this.deleteSelectedText() + } + + const currentRow = selectedRange.start.row + const nextRow = currentRow + 1 + const insertSpace = + (nextRow <= this.editor.buffer.getLastRow()) && + (this.editor.buffer.lineLengthForRow(nextRow) > 0) && + (this.editor.buffer.lineLengthForRow(currentRow) > 0) + if (insertSpace) this.insertText(' ') + + this.cursor.moveToEndOfLine() + + // Remove leading whitespace from the line below + this.modifySelection(() => { + this.cursor.moveRight() + this.cursor.moveToFirstCharacterOfLine() + }) + this.deleteSelectedText() + + if (insertSpace) this.cursor.moveLeft() + } + + if (joinMarker) { + const newSelectedRange = joinMarker.getBufferRange() + this.setBufferRange(newSelectedRange) + joinMarker.destroy() + } + } + + // Public: Removes one level of indent from the currently selected rows. + outdentSelectedRows () { + const [start, end] = this.getBufferRowRange() + const {buffer} = this.editor + const leadingTabRegex = new RegExp(`^( {1,${this.editor.getTabLength()}}|\t)`) + for (let row = start; row <= end; row++) { + const match = buffer.lineForRow(row).match(leadingTabRegex) + if (match && match[0].length > 0) { + buffer.delete([[row, 0], [row, match[0].length]]) + } + } + } + + // Public: Sets the indentation level of all selected rows to values suggested + // by the relevant grammars. + autoIndentSelectedRows () { + const [start, end] = this.getBufferRowRange() + return this.editor.autoIndentBufferRows(start, end) + } + + // Public: Wraps the selected lines in comments if they aren't currently part + // of a comment. + // + // Removes the comment if they are currently wrapped in a comment. + toggleLineComments () { + this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || [])) + } + + // Public: Cuts the selection until the end of the screen line. + cutToEndOfLine (maintainClipboard) { + if (this.isEmpty()) this.selectToEndOfLine() + return this.cut(maintainClipboard) + } + + // Public: Cuts the selection until the end of the buffer line. + cutToEndOfBufferLine (maintainClipboard) { + if (this.isEmpty()) this.selectToEndOfBufferLine() + this.cut(maintainClipboard) + } + + // Public: Copies the selection to the clipboard and then deletes it. + // + // * `maintainClipboard` {Boolean} (default: false) See {::copy} + // * `fullLine` {Boolean} (default: false) See {::copy} + cut (maintainClipboard = false, fullLine = false) { + this.copy(maintainClipboard, fullLine) + this.delete() + } + + // Public: Copies the current selection to the clipboard. + // + // * `maintainClipboard` {Boolean} if `true`, a specific metadata property + // is created to store each content copied to the clipboard. The clipboard + // `text` still contains the concatenation of the clipboard with the + // current selection. (default: false) + // * `fullLine` {Boolean} if `true`, the copied text will always be pasted + // at the beginning of the line containing the cursor, regardless of the + // cursor's horizontal position. (default: false) + copy (maintainClipboard = false, fullLine = false) { + if (this.isEmpty()) return + const {start, end} = this.getBufferRange() + const selectionText = this.editor.getTextInRange([start, end]) + const precedingText = this.editor.getTextInRange([[start.row, 0], start]) + const startLevel = this.editor.indentLevelForLine(precedingText) + + if (maintainClipboard) { + let {text: clipboardText, metadata} = this.editor.constructor.clipboard.readWithMetadata() + if (!metadata) metadata = {} + if (!metadata.selections) { + metadata.selections = [{ + text: clipboardText, + indentBasis: metadata.indentBasis, + fullLine: metadata.fullLine + }] + } + metadata.selections.push({ + text: selectionText, + indentBasis: startLevel, + fullLine + }) + this.editor.constructor.clipboard.write([clipboardText, selectionText].join('\n'), metadata) + } else { + this.editor.constructor.clipboard.write(selectionText, { + indentBasis: startLevel, + fullLine + }) + } + } + + // Public: Creates a fold containing the current selection. + fold () { + const range = this.getBufferRange() + if (!range.isEmpty()) { + this.editor.foldBufferRange(range) + this.cursor.setBufferPosition(range.end) + } + } + + // Private: Increase the indentation level of the given text by given number + // of levels. Leaves the first line unchanged. + adjustIndent (lines, indentAdjustment) { + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (indentAdjustment === 0 || line === '') { + continue + } else if (indentAdjustment > 0) { + lines[i] = this.editor.buildIndentString(indentAdjustment) + line + } else { + const currentIndentLevel = this.editor.indentLevelForLine(lines[i]) + const indentLevel = Math.max(0, currentIndentLevel + indentAdjustment) + lines[i] = line.replace(/^[\t ]+/, this.editor.buildIndentString(indentLevel)) + } + } + } + + // Indent the current line(s). + // + // If the selection is empty, indents the current line if the cursor precedes + // non-whitespace characters, and otherwise inserts a tab. If the selection is + // non empty, calls {::indentSelectedRows}. + // + // * `options` (optional) {Object} with the keys: + // * `autoIndent` If `true`, the line is indented to an automatically-inferred + // level. Otherwise, {TextEditor::getTabText} is inserted. + indent ({autoIndent} = {}) { + const {row} = this.cursor.getBufferPosition() + + if (this.isEmpty()) { + this.cursor.skipLeadingWhitespace() + const desiredIndent = this.editor.suggestedIndentForBufferRow(row) + let delta = desiredIndent - this.cursor.getIndentLevel() + + if (autoIndent && delta > 0) { + if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1) + this.insertText(this.editor.buildIndentString(delta)) + } else { + this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn())) + } + } else { + this.indentSelectedRows() + } + } + + // Public: If the selection spans multiple rows, indent all of them. + indentSelectedRows () { + const [start, end] = this.getBufferRowRange() + for (let row = start; row <= end; row++) { + if (this.editor.buffer.lineLengthForRow(row) !== 0) { + this.editor.buffer.insert([row, 0], this.editor.getTabText()) + } + } + } + + /* + Section: Managing multiple selections + */ + + // Public: Moves the selection down one row. + addSelectionBelow () { + const range = this.getGoalScreenRange().copy() + const nextRow = range.end.row + 1 + + for (let row = nextRow, end = this.editor.getLastScreenRow(); row <= end; row++) { + range.start.row = row + range.end.row = row + const clippedRange = this.editor.clipScreenRange(range, {skipSoftWrapIndentation: true}) + + if (range.isEmpty()) { + if (range.end.column > 0 && clippedRange.end.column === 0) continue + } else { + if (clippedRange.isEmpty()) continue + } + + const selection = this.editor.addSelectionForScreenRange(clippedRange) + selection.setGoalScreenRange(range) + break + } + } + + // Public: Moves the selection up one row. + addSelectionAbove () { + const range = this.getGoalScreenRange().copy() + const previousRow = range.end.row - 1 + + for (let row = previousRow; row >= 0; row--) { + range.start.row = row + range.end.row = row + const clippedRange = this.editor.clipScreenRange(range, {skipSoftWrapIndentation: true}) + + if (range.isEmpty()) { + if (range.end.column > 0 && clippedRange.end.column === 0) continue + } else { + if (clippedRange.isEmpty()) continue + } + + const selection = this.editor.addSelectionForScreenRange(clippedRange) + selection.setGoalScreenRange(range) + break + } + } + + // Public: Combines the given selection into this selection and then destroys + // the given selection. + // + // * `otherSelection` A {Selection} to merge with. + // * `options` (optional) {Object} options matching those found in {::setBufferRange}. + merge (otherSelection, options = {}) { + const myGoalScreenRange = this.getGoalScreenRange() + const otherGoalScreenRange = otherSelection.getGoalScreenRange() + + if (myGoalScreenRange && otherGoalScreenRange) { + options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange) + } else { + options.goalScreenRange = myGoalScreenRange || otherGoalScreenRange + } + + const bufferRange = this.getBufferRange().union(otherSelection.getBufferRange()) + this.setBufferRange(bufferRange, Object.assign({autoscroll: false}, options)) + otherSelection.destroy() + } + + /* + Section: Comparing to other selections + */ + + // Public: Compare this selection's buffer range to another selection's buffer + // range. + // + // See {Range::compare} for more details. + // + // * `otherSelection` A {Selection} to compare against + compare (otherSelection) { + return this.marker.compare(otherSelection.marker) + } + + /* + Section: Private Utilities + */ + + setGoalScreenRange (range) { + return this.goalScreenRange = Range.fromObject(range) + } + + getGoalScreenRange () { + return this.goalScreenRange || this.getScreenRange() + } + + markerDidChange (e) { + const {oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e + const {oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e + const {textChanged} = e + + if (!oldHeadScreenPosition.isEqual(newHeadScreenPosition)) { + this.cursor.goalColumn = null + const cursorMovedEvent = { + oldBufferPosition: oldHeadBufferPosition, + oldScreenPosition: oldHeadScreenPosition, + newBufferPosition: newHeadBufferPosition, + newScreenPosition: newHeadScreenPosition, + textChanged, + cursor: this.cursor + } + this.cursor.emitter.emit('did-change-position', cursorMovedEvent) + this.editor.cursorMoved(cursorMovedEvent) + } + + this.emitter.emit('did-change-range') + this.editor.selectionRangeChanged({ + oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition), + oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition), + newBufferRange: this.getBufferRange(), + newScreenRange: this.getScreenRange(), + selection: this + }) + } + + markerDidDestroy () { + if (this.editor.isDestroyed()) return + + this.destroyed = true + this.cursor.destroyed = true + + this.editor.removeSelection(this) + + this.cursor.emitter.emit('did-destroy') + this.emitter.emit('did-destroy') + + this.cursor.emitter.dispose() + this.emitter.dispose() + } + + finalize () { + if (!this.initialScreenRange || !this.initialScreenRange.isEqual(this.getScreenRange())) { + this.initialScreenRange = null + } + if (this.isEmpty()) { + this.wordwise = false + this.linewise = false + } + } + + autoscroll (options) { + if (this.marker.hasTail()) { + this.editor.scrollToScreenRange(this.getScreenRange(), Object.assign({reversed: this.isReversed()}, options)) + } else { + this.cursor.autoscroll(options) + } + } + + clearAutoscroll () {} + + modifySelection (fn) { + this.retainSelection = true + this.plantTail() + fn() + this.retainSelection = false + } + + // Sets the marker's tail to the same position as the marker's head. + // + // This only works if there isn't already a tail position. + // + // Returns a {Point} representing the new tail position. + plantTail () { + this.marker.plantTail() + } +} From 99f90af42729593e42337203b8c7c5f22c68801b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Nov 2017 09:26:58 -0700 Subject: [PATCH 252/301] Convert Selection spec to JS --- spec/selection-spec.coffee | 128 ------------------------------ spec/selection-spec.js | 157 +++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 128 deletions(-) delete mode 100644 spec/selection-spec.coffee create mode 100644 spec/selection-spec.js diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee deleted file mode 100644 index b0e65be30..000000000 --- a/spec/selection-spec.coffee +++ /dev/null @@ -1,128 +0,0 @@ -TextEditor = require '../src/text-editor' - -describe "Selection", -> - [buffer, editor, selection] = [] - - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - editor = new TextEditor({buffer: buffer, tabLength: 2}) - selection = editor.getLastSelection() - - afterEach -> - buffer.destroy() - - describe ".deleteSelectedText()", -> - describe "when nothing is selected", -> - it "deletes nothing", -> - selection.setBufferRange [[0, 3], [0, 3]] - selection.deleteSelectedText() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - describe "when one line is selected", -> - it "deletes selected text and clears the selection", -> - selection.setBufferRange [[0, 4], [0, 14]] - selection.deleteSelectedText() - expect(buffer.lineForRow(0)).toBe "var = function () {" - - endOfLine = buffer.lineForRow(0).length - selection.setBufferRange [[0, 0], [0, endOfLine]] - selection.deleteSelectedText() - expect(buffer.lineForRow(0)).toBe "" - - expect(selection.isEmpty()).toBeTruthy() - - describe "when multiple lines are selected", -> - it "deletes selected text and clears the selection", -> - selection.setBufferRange [[0, 1], [2, 39]] - selection.deleteSelectedText() - expect(buffer.lineForRow(0)).toBe "v;" - expect(selection.isEmpty()).toBeTruthy() - - describe "when the cursor precedes the tail", -> - it "deletes selected text and clears the selection", -> - selection.cursor.setScreenPosition [0, 13] - selection.selectToScreenPosition [0, 4] - - selection.delete() - expect(buffer.lineForRow(0)).toBe "var = function () {" - expect(selection.isEmpty()).toBeTruthy() - - describe ".isReversed()", -> - it "returns true if the cursor precedes the tail", -> - selection.cursor.setScreenPosition([0, 20]) - selection.selectToScreenPosition([0, 10]) - expect(selection.isReversed()).toBeTruthy() - - selection.selectToScreenPosition([0, 25]) - expect(selection.isReversed()).toBeFalsy() - - describe ".selectLine(row)", -> - describe "when passed a row", -> - it "selects the specified row", -> - selection.setBufferRange([[2, 4], [3, 4]]) - selection.selectLine(5) - expect(selection.getBufferRange()).toEqual [[5, 0], [6, 0]] - - describe "when not passed a row", -> - it "selects all rows spanned by the selection", -> - selection.setBufferRange([[2, 4], [3, 4]]) - selection.selectLine() - expect(selection.getBufferRange()).toEqual [[2, 0], [4, 0]] - - describe "when only the selection's tail is moved (regression)", -> - it "notifies ::onDidChangeRange observers", -> - selection.setBufferRange([[2, 0], [2, 10]], reversed: true) - changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') - selection.onDidChangeRange changeScreenRangeHandler - - buffer.insert([2, 5], 'abc') - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the selection is destroyed", -> - it "destroys its marker", -> - selection.setBufferRange([[2, 0], [2, 10]]) - marker = selection.marker - selection.destroy() - expect(marker.isDestroyed()).toBeTruthy() - - describe ".insertText(text, options)", -> - it "allows pasting white space only lines when autoIndent is enabled", -> - selection.setBufferRange [[0, 0], [0, 0]] - selection.insertText(" \n \n\n", autoIndent: true) - expect(buffer.lineForRow(0)).toBe " " - expect(buffer.lineForRow(1)).toBe " " - expect(buffer.lineForRow(2)).toBe "" - - it "auto-indents if only a newline is inserted", -> - selection.setBufferRange [[2, 0], [3, 0]] - selection.insertText("\n", autoIndent: true) - expect(buffer.lineForRow(2)).toBe " " - - it "auto-indents if only a carriage return + newline is inserted", -> - selection.setBufferRange [[2, 0], [3, 0]] - 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]]) - selection.fold() - - expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]]) - expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]]) - expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {" - expect(editor.isFoldedAtBufferRow(0)).toBe(true) - - it "doesn't create a fold when the selection is empty", -> - selection.setBufferRange([[0, 3], [0, 3]]) - selection.fold() - - expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]]) - expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]]) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" - expect(editor.isFoldedAtBufferRow(0)).toBe(false) diff --git a/spec/selection-spec.js b/spec/selection-spec.js new file mode 100644 index 000000000..cb586da26 --- /dev/null +++ b/spec/selection-spec.js @@ -0,0 +1,157 @@ +const TextEditor = require('../src/text-editor') + +describe('Selection', () => { + let buffer, editor, selection + + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + editor = new TextEditor({buffer, tabLength: 2}) + selection = editor.getLastSelection() + }) + + afterEach(() => buffer.destroy()) + + describe('.deleteSelectedText()', () => { + describe('when nothing is selected', () => { + it('deletes nothing', () => { + selection.setBufferRange([[0, 3], [0, 3]]) + selection.deleteSelectedText() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + }) + + describe('when one line is selected', () => { + it('deletes selected text and clears the selection', () => { + selection.setBufferRange([[0, 4], [0, 14]]) + selection.deleteSelectedText() + expect(buffer.lineForRow(0)).toBe('var = function () {') + + const endOfLine = buffer.lineForRow(0).length + selection.setBufferRange([[0, 0], [0, endOfLine]]) + selection.deleteSelectedText() + expect(buffer.lineForRow(0)).toBe('') + + expect(selection.isEmpty()).toBeTruthy() + }) + }) + + describe('when multiple lines are selected', () => { + it('deletes selected text and clears the selection', () => { + selection.setBufferRange([[0, 1], [2, 39]]) + selection.deleteSelectedText() + expect(buffer.lineForRow(0)).toBe('v;') + expect(selection.isEmpty()).toBeTruthy() + }) + }) + + describe('when the cursor precedes the tail', () => { + it('deletes selected text and clears the selection', () => { + selection.cursor.setScreenPosition([0, 13]) + selection.selectToScreenPosition([0, 4]) + + selection.delete() + expect(buffer.lineForRow(0)).toBe('var = function () {') + expect(selection.isEmpty()).toBeTruthy() + }) + }) + }) + + describe('.isReversed()', () => { + it('returns true if the cursor precedes the tail', () => { + selection.cursor.setScreenPosition([0, 20]) + selection.selectToScreenPosition([0, 10]) + expect(selection.isReversed()).toBeTruthy() + + selection.selectToScreenPosition([0, 25]) + expect(selection.isReversed()).toBeFalsy() + }) + }) + + describe('.selectLine(row)', () => { + describe('when passed a row', () => { + it('selects the specified row', () => { + selection.setBufferRange([[2, 4], [3, 4]]) + selection.selectLine(5) + expect(selection.getBufferRange()).toEqual([[5, 0], [6, 0]]) + }) + }) + + describe('when not passed a row', () => { + it('selects all rows spanned by the selection', () => { + selection.setBufferRange([[2, 4], [3, 4]]) + selection.selectLine() + expect(selection.getBufferRange()).toEqual([[2, 0], [4, 0]]) + }) + }) + }) + + describe("when only the selection's tail is moved (regression)", () => { + it('notifies ::onDidChangeRange observers', () => { + selection.setBufferRange([[2, 0], [2, 10]], {reversed: true}) + const changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') + selection.onDidChangeRange(changeScreenRangeHandler) + + buffer.insert([2, 5], 'abc') + expect(changeScreenRangeHandler).toHaveBeenCalled() + }) + }) + + describe('when the selection is destroyed', () => { + it('destroys its marker', () => { + selection.setBufferRange([[2, 0], [2, 10]]) + const { marker } = selection + selection.destroy() + expect(marker.isDestroyed()).toBeTruthy() + }) + }) + + describe('.insertText(text, options)', () => { + it('allows pasting white space only lines when autoIndent is enabled', () => { + selection.setBufferRange([[0, 0], [0, 0]]) + selection.insertText(' \n \n\n', {autoIndent: true}) + expect(buffer.lineForRow(0)).toBe(' ') + expect(buffer.lineForRow(1)).toBe(' ') + expect(buffer.lineForRow(2)).toBe('') + }) + + it('auto-indents if only a newline is inserted', () => { + selection.setBufferRange([[2, 0], [3, 0]]) + selection.insertText('\n', {autoIndent: true}) + expect(buffer.lineForRow(2)).toBe(' ') + }) + + it('auto-indents if only a carriage return + newline is inserted', () => { + selection.setBufferRange([[2, 0], [3, 0]]) + 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]]) + selection.fold() + + expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]]) + expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]]) + expect(editor.lineTextForScreenRow(0)).toBe(`var${editor.displayLayer.foldCharacter}sort = function(items) {`) + expect(editor.isFoldedAtBufferRow(0)).toBe(true) + }) + + it("doesn't create a fold when the selection is empty", () => { + selection.setBufferRange([[0, 3], [0, 3]]) + selection.fold() + + expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]]) + expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]]) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + expect(editor.isFoldedAtBufferRow(0)).toBe(false) + }) + }) +}) From 3b6f98b446b7ac581f555542677afd986523d1f9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Nov 2017 09:29:33 -0700 Subject: [PATCH 253/301] Fix lint errors --- src/selection.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/selection.js b/src/selection.js index 20561fd64..a54ba68b8 100644 --- a/src/selection.js +++ b/src/selection.js @@ -613,7 +613,9 @@ class Selection { // Remove trailing whitespace from the current line const scanRange = this.cursor.getCurrentLineBufferRange() let trailingWhitespaceRange = null - this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({range}) => trailingWhitespaceRange = range) + this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({range}) => { + trailingWhitespaceRange = range + }) if (trailingWhitespaceRange) { this.setBufferRange(trailingWhitespaceRange) this.deleteSelectedText() @@ -886,7 +888,7 @@ class Selection { */ setGoalScreenRange (range) { - return this.goalScreenRange = Range.fromObject(range) + this.goalScreenRange = Range.fromObject(range) } getGoalScreenRange () { From 3d3042baf2086e306845e383b863a6c8b5eb36d6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 12:18:46 -0600 Subject: [PATCH 254/301] :arrow_up: command-palette --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8681e18e..ebf9448c0 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "background-tips": "0.27.1", "bookmarks": "0.44.4", "bracket-matcher": "0.88.0", - "command-palette": "0.41.1", + "command-palette": "0.42.0", "dalek": "0.2.1", "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", From cbf2d24d9ea726f1edbc3605080b0072ba69978e Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 11:31:06 -0700 Subject: [PATCH 255/301] :keyboard: fix typo --- src/main-process/atom-application.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index a5e3e6b0b..f6802705e 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -162,7 +162,7 @@ class AtomApplication # Public: Removes the {AtomWindow} from the global window list. removeWindow: (window) -> - @windowStacktack.removeWindow(window) + @windowStack.removeWindow(window) if @getAllWindows().length is 0 @applicationMenu?.enableWindowSpecificItems(false) if process.platform in ['win32', 'linux'] From 303cf30b51b68d78d6d440aa10aa8cfc7afd115b Mon Sep 17 00:00:00 2001 From: hansonw Date: Wed, 1 Nov 2017 15:40:00 -0700 Subject: [PATCH 256/301] Allow directory providers to implement onDidChangeFiles for custom pathwatchers --- spec/project-spec.js | 28 ++++++++++++++++++++++++++++ src/project.js | 12 ++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/spec/project-spec.js b/spec/project-spec.js index 63c065fa6..0f003b26b 100644 --- a/spec/project-spec.js +++ b/spec/project-spec.js @@ -335,9 +335,14 @@ describe('Project', () => { isRoot () { return true } existsSync () { return this.path.endsWith('does-exist') } contains (filePath) { return filePath.startsWith(this.path) } + onDidChangeFiles (callback) { + onDidChangeFilesCallback = callback + return {dispose: () => {}} + } } let serviceDisposable = null + let onDidChangeFilesCallback = null beforeEach(() => { serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { @@ -349,6 +354,7 @@ describe('Project', () => { } } }) + onDidChangeFilesCallback = null waitsFor(() => atom.project.directoryProviders.length > 0) }) @@ -383,6 +389,28 @@ describe('Project', () => { atom.project.setPaths(['ssh://foreign-directory:8080/does-exist']) expect(atom.project.getDirectories().length).toBe(0) }) + + it('uses the custom onDidChangeFiles as the watcher if available', () => { + // Ensure that all preexisting watchers are stopped + waitsForPromise(() => stopAllWatchers()) + + const remotePath = 'ssh://another-directory:8080/does-exist' + runs(() => atom.project.setPaths([remotePath])) + waitsForPromise(() => atom.project.getWatcherPromise(remotePath)) + + runs(() => { + expect(onDidChangeFilesCallback).not.toBeNull() + + const changeSpy = jasmine.createSpy('atom.project.onDidChangeFiles') + const disposable = atom.project.onDidChangeFiles(changeSpy) + + const events = [{action: 'created', path: remotePath + '/test.txt'}] + onDidChangeFilesCallback(events) + + expect(changeSpy).toHaveBeenCalledWith(events) + disposable.dispose() + }) + }) }) describe('.open(path)', () => { diff --git a/src/project.js b/src/project.js index 48541c395..92a11ec7a 100644 --- a/src/project.js +++ b/src/project.js @@ -338,13 +338,21 @@ class Project extends Model { } this.rootDirectories.push(directory) - this.watcherPromisesByPath[directory.getPath()] = watchPath(directory.getPath(), {}, events => { + + const didChangeCallback = events => { // Stop event delivery immediately on removal of a rootDirectory, even if its watcher // promise has yet to resolve at the time of removal if (this.rootDirectories.includes(directory)) { this.emitter.emit('did-change-files', events) } - }) + } + // We'll use the directory's custom onDidChangeFiles callback, if available. + // CustomDirectory::onDidChangeFiles should match the signature of + // Project::onDidChangeFiles below (although it may resolve asynchronously) + this.watcherPromisesByPath[directory.getPath()] = + directory.onDidChangeFiles != null + ? Promise.resolve(directory.onDidChangeFiles(didChangeCallback)) + : watchPath(directory.getPath(), {}, didChangeCallback) for (let watchedPath in this.watcherPromisesByPath) { if (!this.rootDirectories.find(dir => dir.getPath() === watchedPath)) { From 76ac08c49cd829ecb3b1383f7d9725acc9490628 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 2 Nov 2017 20:40:23 +0100 Subject: [PATCH 257/301] :arrow_up: open-on-github@1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ebf9448c0..b19548235 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "markdown-preview": "0.159.18", "metrics": "1.2.6", "notifications": "0.69.2", - "open-on-github": "1.2.1", + "open-on-github": "1.3.0", "package-generator": "1.1.1", "settings-view": "0.252.2", "snippets": "1.1.9", From db0fd527cea4ff6bc203a599f772303c6e950dbc Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 13:35:29 -0700 Subject: [PATCH 258/301] Add test for core URI handler window-selection logic --- spec/main-process/atom-application.test.js | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 6434710ce..a209b4301 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -461,6 +461,30 @@ describe('AtomApplication', function () { assert.equal(reached, true); windows[0].close(); }) + + it('triggers /core/open/file in the correct window', async function() { + const dirAPath = makeTempDir('a') + const dirBPath = makeTempDir('b') + + const atomApplication = buildAtomApplication() + const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath)])) + await focusWindow(window1) + const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath)])) + await focusWindow(window2) + + await new Promise(res => setTimeout(res, 2000)) + + const fileA = path.join(dirAPath, 'file-a') + const fileB = path.join(dirBPath, 'file-b') + + atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) + await new Promise(res => setTimeout(res, 1000)) + assert.equal(atomApplication.getLastFocusedWindow(), window1) + + atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) + await new Promise(res => setTimeout(res, 1000)) + assert.equal(atomApplication.getLastFocusedWindow(), window2) + }) }) }) From 668397c1d0cac12e61c1894cfa1d832f37cd8f59 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 14:11:52 -0700 Subject: [PATCH 259/301] Fix flaky test --- spec/main-process/atom-application.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index a209b4301..c97c70746 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -479,11 +479,11 @@ describe('AtomApplication', function () { atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) await new Promise(res => setTimeout(res, 1000)) - assert.equal(atomApplication.getLastFocusedWindow(), window1) + await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window1) atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) await new Promise(res => setTimeout(res, 1000)) - assert.equal(atomApplication.getLastFocusedWindow(), window2) + await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window2) }) }) }) From 7639afe684c490db7aa612e6c3095551e5e4bf4d Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 2 Nov 2017 12:50:42 -0600 Subject: [PATCH 260/301] Judge resize of overlay by contentRect changing --- src/text-editor-component.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 2a77e30f8..91ea18361 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -126,7 +126,6 @@ class TextEditorComponent { this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this)) this.lineComponentsByScreenLineId = new Map() this.overlayComponents = new Set() - this.overlayDimensionsByElement = new WeakMap() this.shouldRenderDummyScrollbars = true this.remeasureScrollbars = false this.pendingAutoscroll = null @@ -803,15 +802,9 @@ class TextEditorComponent { { key: overlayProps.element, overlayComponents: this.overlayComponents, - measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element), didResize: (overlayComponent) => { this.updateOverlayToRender(overlayProps) - overlayComponent.update(Object.assign( - { - measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) - }, - overlayProps - )) + overlayComponent.update(overlayProps) } }, overlayProps @@ -1357,7 +1350,6 @@ class TextEditorComponent { 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) @@ -4226,17 +4218,26 @@ class OverlayComponent { this.element.style.zIndex = 4 this.element.style.top = (this.props.pixelTop || 0) + 'px' this.element.style.left = (this.props.pixelLeft || 0) + 'px' + this.currentContentRect = null // Synchronous DOM updates in response to resize events might trigger a // "loop limit exceeded" error. We disconnect the observer before // potentially mutating the DOM, and then reconnect it on the next tick. + // Note: ResizeObserver calls its callback when .observe is called this.resizeObserver = new ResizeObserver((entries) => { const {contentRect} = entries[0] - if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) { + + if ( + this.currentContentRect && + (this.currentContentRect.width !== contentRect.width || + this.currentContentRect.height !== contentRect.height) + ) { this.resizeObserver.disconnect() this.props.didResize(this) process.nextTick(() => { this.resizeObserver.observe(this.props.element) }) } + + this.currentContentRect = contentRect }) this.didAttach() this.props.overlayComponents.add(this) From ada75ed1dd1ee354bcf43982cd2a9ee957952727 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 14:45:14 -0700 Subject: [PATCH 261/301] Fix bug in test --- spec/main-process/atom-application.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index c97c70746..70ebf686c 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -479,11 +479,11 @@ describe('AtomApplication', function () { atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) await new Promise(res => setTimeout(res, 1000)) - await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window1) + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1) atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) await new Promise(res => setTimeout(res, 1000)) - await conditionPromise(() => window.atomApplication.getLastFocusedWindow() === window2) + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2) }) }) }) From 667634191eeaa4629cad63f01fc6ceff2fbf8af3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Nov 2017 16:15:36 -0600 Subject: [PATCH 262/301] Document hiddenInCommandPalette option in atom.commands.add --- src/command-registry.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/command-registry.js b/src/command-registry.js index ba75918ab..9e6d8c2e1 100644 --- a/src/command-registry.js +++ b/src/command-registry.js @@ -107,6 +107,13 @@ module.exports = class CommandRegistry { // otherwise be generated from the event name. // * `description`: Used by consumers to display detailed information about // the command. + // * `hiddenInCommandPalette`: If `true`, this command will not appear in + // the bundled command palette by default, but can still be shown with. + // the `Command Palette: Show Hidden Commands` command. This is a good + // option when you need to register large numbers of commands that don't + // make sense to be executed from the command palette. Please use this + // option conservatively, as it could reduce the discoverability of your + // package's commands. // // ## Arguments: Registering Multiple Commands // From 178756b62ae1245745f40473e1d44a689ae1a1fc Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 15:27:59 -0700 Subject: [PATCH 263/301] :white_check_mark: update test --- spec/main-process/atom-application.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 70ebf686c..69ddb3539 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -472,17 +472,13 @@ describe('AtomApplication', function () { const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath)])) await focusWindow(window2) - await new Promise(res => setTimeout(res, 2000)) - const fileA = path.join(dirAPath, 'file-a') const fileB = path.join(dirBPath, 'file-b') atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) - await new Promise(res => setTimeout(res, 1000)) await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1) atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) - await new Promise(res => setTimeout(res, 1000)) await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2) }) }) From 444597c8455501d8421d5dbe766adf28d4070187 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 15:49:06 -0700 Subject: [PATCH 264/301] Let's add some debugging --- spec/async-spec-helpers.js | 4 ++-- spec/main-process/atom-application.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js index 56550cd9f..73002c049 100644 --- a/spec/async-spec-helpers.js +++ b/spec/async-spec-helpers.js @@ -34,7 +34,7 @@ export function afterEach (fn) { } }) -export async function conditionPromise (condition) { +export async function conditionPromise (condition, description = 'anonymous condition') { const startTime = Date.now() while (true) { @@ -45,7 +45,7 @@ export async function conditionPromise (condition) { } if (Date.now() - startTime > 5000) { - throw new Error('Timed out waiting on condition') + throw new Error('Timed out waiting on ' + description) } } } diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 69ddb3539..39863aa34 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -476,10 +476,10 @@ describe('AtomApplication', function () { const fileB = path.join(dirBPath, 'file-b') atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1) + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1, 'window1 to be focused') atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2) + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2, 'window2 to be focused') }) }) }) From 99bef8e7d1b2fdb1fe670a54fce4c3623b241343 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 16:09:57 -0700 Subject: [PATCH 265/301] More debugging --- spec/main-process/atom-application.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 39863aa34..a7096e49c 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -476,10 +476,10 @@ describe('AtomApplication', function () { const fileB = path.join(dirBPath, 'file-b') atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1, 'window1 to be focused') + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1, `window1 to be focused from ${fileA}`) atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2, 'window2 to be focused') + await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2, `window2 to be focused from ${fileB}`) }) }) }) From 07ac7041d9385ea8aab7e7a89b75299ad1f66d2a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 16:19:19 -0700 Subject: [PATCH 266/301] :arrow_up: settings-view@0.253.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b19548235..87b37cfb1 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.2", "open-on-github": "1.3.0", "package-generator": "1.1.1", - "settings-view": "0.252.2", + "settings-view": "0.253.0", "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.14", From 026782921145e492bfb1660f1d5cee763fa0ca78 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 2 Nov 2017 16:45:52 -0700 Subject: [PATCH 267/301] Change the way we test this --- spec/main-process/atom-application.test.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index a7096e49c..01d052b96 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -5,6 +5,7 @@ import dedent from 'dedent' import electron from 'electron' import fs from 'fs-plus' import path from 'path' +import sinon from 'sinon' import AtomApplication from '../../src/main-process/atom-application' import parseCommandLine from '../../src/main-process/parse-command-line' import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers' @@ -473,13 +474,18 @@ describe('AtomApplication', function () { await focusWindow(window2) const fileA = path.join(dirAPath, 'file-a') + const uriA = `atom://core/open/file?filename=${fileA}` const fileB = path.join(dirBPath, 'file-b') + const uriB = `atom://core/open/file?filename=${fileB}` - atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileA}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window1, `window1 to be focused from ${fileA}`) + sinon.spy(window1, 'sendURIMessage') + sinon.spy(window2, 'sendURIMessage') - atomApplication.launch(parseCommandLine(['--uri-handler', `atom://core/open/file?filename=${fileB}`])) - await conditionPromise(() => atomApplication.getLastFocusedWindow() === window2, `window2 to be focused from ${fileB}`) + atomApplication.launch(parseCommandLine(['--uri-handler', uriA])) + await conditionPromise(() => window1.sendURIMessage.calledWith(uriA), `window1 to be focused from ${fileA}`) + + atomApplication.launch(parseCommandLine(['--uri-handler', uriB])) + await conditionPromise(() => window2.sendURIMessage.calledWith(uriB), `window2 to be focused from ${fileB}`) }) }) }) From 1ee1c6c30e60428a2727ecdd9679b48ccc7ab247 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 3 Nov 2017 07:44:06 -0400 Subject: [PATCH 268/301] =?UTF-8?q?=E2=98=A0=E2=98=95=E2=98=95=20Decaffein?= =?UTF-8?q?ate=20src/token-iterator.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/token-iterator.coffee | 56 --------------------------- src/token-iterator.js | 79 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 56 deletions(-) delete mode 100644 src/token-iterator.coffee create mode 100644 src/token-iterator.js diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee deleted file mode 100644 index f836d33d4..000000000 --- a/src/token-iterator.coffee +++ /dev/null @@ -1,56 +0,0 @@ -module.exports = -class TokenIterator - constructor: (@tokenizedBuffer) -> - - reset: (@line) -> - @index = null - @startColumn = 0 - @endColumn = 0 - @scopes = @line.openScopes.map (id) => @tokenizedBuffer.grammar.scopeForId(id) - @scopeStarts = @scopes.slice() - @scopeEnds = [] - this - - next: -> - {tags} = @line - - if @index? - @startColumn = @endColumn - @scopeEnds.length = 0 - @scopeStarts.length = 0 - @index++ - else - @index = 0 - - while @index < tags.length - tag = tags[@index] - if tag < 0 - scope = @tokenizedBuffer.grammar.scopeForId(tag) - if tag % 2 is 0 - if @scopeStarts[@scopeStarts.length - 1] is scope - @scopeStarts.pop() - else - @scopeEnds.push(scope) - @scopes.pop() - else - @scopeStarts.push(scope) - @scopes.push(scope) - @index++ - else - @endColumn += tag - @text = @line.text.substring(@startColumn, @endColumn) - return true - - false - - getScopes: -> @scopes - - getScopeStarts: -> @scopeStarts - - getScopeEnds: -> @scopeEnds - - getText: -> @text - - getBufferStart: -> @startColumn - - getBufferEnd: -> @endColumn diff --git a/src/token-iterator.js b/src/token-iterator.js new file mode 100644 index 000000000..a698fc748 --- /dev/null +++ b/src/token-iterator.js @@ -0,0 +1,79 @@ +module.exports = +class TokenIterator { + constructor (tokenizedBuffer) { + this.tokenizedBuffer = tokenizedBuffer + } + + reset (line) { + this.line = line + this.index = null + this.startColumn = 0 + this.endColumn = 0 + this.scopes = this.line.openScopes.map(id => this.tokenizedBuffer.grammar.scopeForId(id)) + this.scopeStarts = this.scopes.slice() + this.scopeEnds = [] + return this + } + + next () { + const {tags} = this.line + + if (this.index != null) { + this.startColumn = this.endColumn + this.scopeEnds.length = 0 + this.scopeStarts.length = 0 + this.index++ + } else { + this.index = 0 + } + + while (this.index < tags.length) { + const tag = tags[this.index] + if (tag < 0) { + const scope = this.tokenizedBuffer.grammar.scopeForId(tag) + if ((tag % 2) === 0) { + if (this.scopeStarts[this.scopeStarts.length - 1] === scope) { + this.scopeStarts.pop() + } else { + this.scopeEnds.push(scope) + } + this.scopes.pop() + } else { + this.scopeStarts.push(scope) + this.scopes.push(scope) + } + this.index++ + } else { + this.endColumn += tag + this.text = this.line.text.substring(this.startColumn, this.endColumn) + return true + } + } + + return false + } + + getScopes () { + return this.scopes + } + + getScopeStarts () { + return this.scopeStarts + } + + getScopeEnds () { + return this.scopeEnds + } + + getText () { + return this.text + } + + getBufferStart () { + return this.startColumn + } + + getBufferEnd () { + return this.endColumn + } +} From 7b5837afb896294ce4600a06a4ac212e1c8613f2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 3 Nov 2017 07:53:28 -0400 Subject: [PATCH 269/301] =?UTF-8?q?=E2=98=A0=E2=98=95=E2=98=95=20Decaffein?= =?UTF-8?q?ate=20spec/token-iterator-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/token-iterator-spec.coffee | 37 ---------------------------- spec/token-iterator-spec.js | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 37 deletions(-) delete mode 100644 spec/token-iterator-spec.coffee create mode 100644 spec/token-iterator-spec.js diff --git a/spec/token-iterator-spec.coffee b/spec/token-iterator-spec.coffee deleted file mode 100644 index 6ae01cd30..000000000 --- a/spec/token-iterator-spec.coffee +++ /dev/null @@ -1,37 +0,0 @@ -TextBuffer = require 'text-buffer' -TokenizedBuffer = require '../src/tokenized-buffer' - -describe "TokenIterator", -> - it "correctly terminates scopes at the beginning of the line (regression)", -> - grammar = atom.grammars.createGrammar('test', { - 'scopeName': 'text.broken' - 'name': 'Broken grammar' - 'patterns': [ - { - 'begin': 'start' - 'end': '(?=end)' - 'name': 'blue.broken' - } - { - 'match': '.' - 'name': 'yellow.broken' - } - ] - }) - - buffer = new TextBuffer(text: """ - start x - end x - x - """) - tokenizedBuffer = new TokenizedBuffer({ - buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert - }) - tokenizedBuffer.setGrammar(grammar) - - tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator() - tokenIterator.next() - - expect(tokenIterator.getBufferStart()).toBe 0 - expect(tokenIterator.getScopeEnds()).toEqual [] - expect(tokenIterator.getScopeStarts()).toEqual ['text.broken', 'yellow.broken'] diff --git a/spec/token-iterator-spec.js b/spec/token-iterator-spec.js new file mode 100644 index 000000000..f6d43395c --- /dev/null +++ b/spec/token-iterator-spec.js @@ -0,0 +1,43 @@ +const TextBuffer = require('text-buffer') +const TokenizedBuffer = require('../src/tokenized-buffer') + +describe('TokenIterator', () => + it('correctly terminates scopes at the beginning of the line (regression)', () => { + const grammar = atom.grammars.createGrammar('test', { + 'scopeName': 'text.broken', + 'name': 'Broken grammar', + 'patterns': [ + { + 'begin': 'start', + 'end': '(?=end)', + 'name': 'blue.broken' + }, + { + 'match': '.', + 'name': 'yellow.broken' + } + ] + }) + + const buffer = new TextBuffer({text: `\ +start x +end x +x\ +`}) + const tokenizedBuffer = new TokenizedBuffer({ + buffer, + config: atom.config, + grammarRegistry: atom.grammars, + packageManager: atom.packages, + assert: atom.assert + }) + tokenizedBuffer.setGrammar(grammar) + + const tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator() + tokenIterator.next() + + expect(tokenIterator.getBufferStart()).toBe(0) + expect(tokenIterator.getScopeEnds()).toEqual([]) + expect(tokenIterator.getScopeStarts()).toEqual(['text.broken', 'yellow.broken']) + }) +) From 208e3293f3c2456f9e43a219cf719e59d675c8ca Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 3 Nov 2017 08:14:24 -0400 Subject: [PATCH 270/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src?= =?UTF-8?q?/text-utils.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/text-utils.coffee | 121 --------------------------------------- src/text-utils.js | 130 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 121 deletions(-) delete mode 100644 src/text-utils.coffee create mode 100644 src/text-utils.js diff --git a/src/text-utils.coffee b/src/text-utils.coffee deleted file mode 100644 index f4d62772e..000000000 --- a/src/text-utils.coffee +++ /dev/null @@ -1,121 +0,0 @@ -isHighSurrogate = (charCode) -> - 0xD800 <= charCode <= 0xDBFF - -isLowSurrogate = (charCode) -> - 0xDC00 <= charCode <= 0xDFFF - -isVariationSelector = (charCode) -> - 0xFE00 <= charCode <= 0xFE0F - -isCombiningCharacter = (charCode) -> - 0x0300 <= charCode <= 0x036F or - 0x1AB0 <= charCode <= 0x1AFF or - 0x1DC0 <= charCode <= 0x1DFF or - 0x20D0 <= charCode <= 0x20FF or - 0xFE20 <= charCode <= 0xFE2F - -# Are the given character codes a high/low surrogate pair? -# -# * `charCodeA` The first character code {Number}. -# * `charCode2` The second character code {Number}. -# -# Return a {Boolean}. -isSurrogatePair = (charCodeA, charCodeB) -> - isHighSurrogate(charCodeA) and isLowSurrogate(charCodeB) - -# Are the given character codes a variation sequence? -# -# * `charCodeA` The first character code {Number}. -# * `charCode2` The second character code {Number}. -# -# Return a {Boolean}. -isVariationSequence = (charCodeA, charCodeB) -> - not isVariationSelector(charCodeA) and isVariationSelector(charCodeB) - -# Are the given character codes a combined character pair? -# -# * `charCodeA` The first character code {Number}. -# * `charCode2` The second character code {Number}. -# -# Return a {Boolean}. -isCombinedCharacter = (charCodeA, charCodeB) -> - not isCombiningCharacter(charCodeA) and isCombiningCharacter(charCodeB) - -# Is the character at the given index the start of high/low surrogate pair -# a variation sequence, or a combined character? -# -# * `string` The {String} to check for a surrogate pair, variation sequence, -# or combined character. -# * `index` The {Number} index to look for a surrogate pair, variation -# sequence, or combined character. -# -# Return a {Boolean}. -isPairedCharacter = (string, index=0) -> - charCodeA = string.charCodeAt(index) - charCodeB = string.charCodeAt(index + 1) - isSurrogatePair(charCodeA, charCodeB) or - isVariationSequence(charCodeA, charCodeB) or - isCombinedCharacter(charCodeA, charCodeB) - -IsJapaneseKanaCharacter = (charCode) -> - 0x3000 <= charCode <= 0x30FF - -isCJKUnifiedIdeograph = (charCode) -> - 0x4E00 <= charCode <= 0x9FFF - -isFullWidthForm = (charCode) -> - 0xFF01 <= charCode <= 0xFF5E or - 0xFFE0 <= charCode <= 0xFFE6 - -isDoubleWidthCharacter = (character) -> - charCode = character.charCodeAt(0) - - IsJapaneseKanaCharacter(charCode) or - isCJKUnifiedIdeograph(charCode) or - isFullWidthForm(charCode) - -isHalfWidthCharacter = (character) -> - charCode = character.charCodeAt(0) - - 0xFF65 <= charCode <= 0xFFDC or - 0xFFE8 <= charCode <= 0xFFEE - -isKoreanCharacter = (character) -> - charCode = character.charCodeAt(0) - - 0xAC00 <= charCode <= 0xD7A3 or - 0x1100 <= charCode <= 0x11FF or - 0x3130 <= charCode <= 0x318F or - 0xA960 <= charCode <= 0xA97F or - 0xD7B0 <= charCode <= 0xD7FF - -isCJKCharacter = (character) -> - isDoubleWidthCharacter(character) or - isHalfWidthCharacter(character) or - isKoreanCharacter(character) - -isWordStart = (previousCharacter, character) -> - (previousCharacter is ' ' or previousCharacter is '\t') and - (character isnt ' ' and character isnt '\t') - -isWrapBoundary = (previousCharacter, character) -> - isWordStart(previousCharacter, character) or isCJKCharacter(character) - -# Does the given string contain at least surrogate pair, variation sequence, -# or combined character? -# -# * `string` The {String} to check for the presence of paired characters. -# -# Returns a {Boolean}. -hasPairedCharacter = (string) -> - index = 0 - while index < string.length - return true if isPairedCharacter(string, index) - index++ - false - -module.exports = { - isPairedCharacter, hasPairedCharacter, - isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, - isWrapBoundary -} diff --git a/src/text-utils.js b/src/text-utils.js new file mode 100644 index 000000000..7dde49fd7 --- /dev/null +++ b/src/text-utils.js @@ -0,0 +1,130 @@ +const isHighSurrogate = (charCode) => + charCode >= 0xD800 && charCode <= 0xDBFF + +const isLowSurrogate = (charCode) => + charCode >= 0xDC00 && charCode <= 0xDFFF + +const isVariationSelector = (charCode) => + charCode >= 0xFE00 && charCode <= 0xFE0F + +const isCombiningCharacter = charCode => + (charCode >= 0x0300 && charCode <= 0x036F) || + (charCode >= 0x1AB0 && charCode <= 0x1AFF) || + (charCode >= 0x1DC0 && charCode <= 0x1DFF) || + (charCode >= 0x20D0 && charCode <= 0x20FF) || + (charCode >= 0xFE20 && charCode <= 0xFE2F) + +// Are the given character codes a high/low surrogate pair? +// +// * `charCodeA` The first character code {Number}. +// * `charCode2` The second character code {Number}. +// +// Return a {Boolean}. +const isSurrogatePair = (charCodeA, charCodeB) => + isHighSurrogate(charCodeA) && isLowSurrogate(charCodeB) + +// Are the given character codes a variation sequence? +// +// * `charCodeA` The first character code {Number}. +// * `charCode2` The second character code {Number}. +// +// Return a {Boolean}. +const isVariationSequence = (charCodeA, charCodeB) => + !isVariationSelector(charCodeA) && isVariationSelector(charCodeB) + +// Are the given character codes a combined character pair? +// +// * `charCodeA` The first character code {Number}. +// * `charCode2` The second character code {Number}. +// +// Return a {Boolean}. +const isCombinedCharacter = (charCodeA, charCodeB) => + !isCombiningCharacter(charCodeA) && isCombiningCharacter(charCodeB) + +// Is the character at the given index the start of high/low surrogate pair +// a variation sequence, or a combined character? +// +// * `string` The {String} to check for a surrogate pair, variation sequence, +// or combined character. +// * `index` The {Number} index to look for a surrogate pair, variation +// sequence, or combined character. +// +// Return a {Boolean}. +const isPairedCharacter = (string, index = 0) => { + const charCodeA = string.charCodeAt(index) + const charCodeB = string.charCodeAt(index + 1) + return isSurrogatePair(charCodeA, charCodeB) || + isVariationSequence(charCodeA, charCodeB) || + isCombinedCharacter(charCodeA, charCodeB) +} + +const IsJapaneseKanaCharacter = charCode => + charCode >= 0x3000 && charCode <= 0x30FF + +const isCJKUnifiedIdeograph = charCode => + charCode >= 0x4E00 && charCode <= 0x9FFF + +const isFullWidthForm = charCode => + (charCode >= 0xFF01 && charCode <= 0xFF5E) || + (charCode >= 0xFFE0 && charCode <= 0xFFE6) + +const isDoubleWidthCharacter = (character) => { + const charCode = character.charCodeAt(0) + + return IsJapaneseKanaCharacter(charCode) || + isCJKUnifiedIdeograph(charCode) || + isFullWidthForm(charCode) +} + +const isHalfWidthCharacter = (character) => { + const charCode = character.charCodeAt(0) + + return (charCode >= 0xFF65 && charCode <= 0xFFDC) || + (charCode >= 0xFFE8 && charCode <= 0xFFEE) +} + +const isKoreanCharacter = (character) => { + const charCode = character.charCodeAt(0) + + return (charCode >= 0xAC00 && charCode <= 0xD7A3) || + (charCode >= 0x1100 && charCode <= 0x11FF) || + (charCode >= 0x3130 && charCode <= 0x318F) || + (charCode >= 0xA960 && charCode <= 0xA97F) || + (charCode >= 0xD7B0 && charCode <= 0xD7FF) +} + +const isCJKCharacter = (character) => + isDoubleWidthCharacter(character) || + isHalfWidthCharacter(character) || + isKoreanCharacter(character) + +const isWordStart = (previousCharacter, character) => + ((previousCharacter === ' ') || (previousCharacter === '\t')) && + ((character !== ' ') && (character !== '\t')) + +const isWrapBoundary = (previousCharacter, character) => + isWordStart(previousCharacter, character) || isCJKCharacter(character) + +// Does the given string contain at least surrogate pair, variation sequence, +// or combined character? +// +// * `string` The {String} to check for the presence of paired characters. +// +// Returns a {Boolean}. +const hasPairedCharacter = (string) => { + let index = 0 + while (index < string.length) { + if (isPairedCharacter(string, index)) { return true } + index++ + } + return false +} + +module.exports = { + isPairedCharacter, + hasPairedCharacter, + isDoubleWidthCharacter, + isHalfWidthCharacter, + isKoreanCharacter, + isWrapBoundary +} From b7504a2a72838b3de293ae88d48e5ad7731d2c9d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 3 Nov 2017 06:17:07 -0600 Subject: [PATCH 271/301] :arrow_up: tree-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 87b37cfb1..d6656725a 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.109.1", "timecop": "0.36.0", - "tree-view": "0.221.1", + "tree-view": "0.221.2", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.5", From 3a42785678621eedb45870c5f6b24d30ffd1cf10 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 3 Nov 2017 08:21:01 -0400 Subject: [PATCH 272/301] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20spe?= =?UTF-8?q?c/text-utils-spec.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/text-utils-spec.coffee | 97 ------------------------------- spec/text-utils-spec.js | 110 ++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 97 deletions(-) delete mode 100644 spec/text-utils-spec.coffee create mode 100644 spec/text-utils-spec.js diff --git a/spec/text-utils-spec.coffee b/spec/text-utils-spec.coffee deleted file mode 100644 index bae7f5997..000000000 --- a/spec/text-utils-spec.coffee +++ /dev/null @@ -1,97 +0,0 @@ -textUtils = require '../src/text-utils' - -describe 'text utilities', -> - describe '.hasPairedCharacter(string)', -> - it 'returns true when the string contains a surrogate pair, variation sequence, or combined character', -> - expect(textUtils.hasPairedCharacter('abc')).toBe false - expect(textUtils.hasPairedCharacter('a\uD835\uDF97b\uD835\uDF97c')).toBe true - expect(textUtils.hasPairedCharacter('\uD835\uDF97')).toBe true - expect(textUtils.hasPairedCharacter('\u2714\uFE0E')).toBe true - expect(textUtils.hasPairedCharacter('e\u0301')).toBe true - - expect(textUtils.hasPairedCharacter('\uD835')).toBe false - expect(textUtils.hasPairedCharacter('\uDF97')).toBe false - expect(textUtils.hasPairedCharacter('\uFE0E')).toBe false - expect(textUtils.hasPairedCharacter('\u0301')).toBe false - - expect(textUtils.hasPairedCharacter('\uFE0E\uFE0E')).toBe false - expect(textUtils.hasPairedCharacter('\u0301\u0301')).toBe false - - describe '.isPairedCharacter(string, index)', -> - it 'returns true when the index is the start of a high/low surrogate pair, variation sequence, or combined character', -> - expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 0)).toBe false - expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 1)).toBe true - expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 2)).toBe false - expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 3)).toBe false - expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 4)).toBe true - expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 5)).toBe false - expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 6)).toBe false - - expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 0)).toBe false - expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 1)).toBe true - expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 2)).toBe false - expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 3)).toBe false - - expect(textUtils.isPairedCharacter('\uD835')).toBe false - expect(textUtils.isPairedCharacter('\uDF97')).toBe false - expect(textUtils.isPairedCharacter('\uFE0E')).toBe false - expect(textUtils.isPairedCharacter('\uFE0E')).toBe false - - expect(textUtils.isPairedCharacter('\uFE0E\uFE0E')).toBe false - - expect(textUtils.isPairedCharacter('ae\u0301c', 0)).toBe false - expect(textUtils.isPairedCharacter('ae\u0301c', 1)).toBe true - expect(textUtils.isPairedCharacter('ae\u0301c', 2)).toBe false - expect(textUtils.isPairedCharacter('ae\u0301c', 3)).toBe false - expect(textUtils.isPairedCharacter('ae\u0301c', 4)).toBe false - - describe ".isDoubleWidthCharacter(character)", -> - it "returns true when the character is either japanese, chinese or a full width form", -> - expect(textUtils.isDoubleWidthCharacter("我")).toBe(true) - - expect(textUtils.isDoubleWidthCharacter("私")).toBe(true) - - expect(textUtils.isDoubleWidthCharacter("B")).toBe(true) - expect(textUtils.isDoubleWidthCharacter(",")).toBe(true) - expect(textUtils.isDoubleWidthCharacter("¢")).toBe(true) - - expect(textUtils.isDoubleWidthCharacter("a")).toBe(false) - - describe ".isHalfWidthCharacter(character)", -> - it "returns true when the character is an half width form", -> - expect(textUtils.isHalfWidthCharacter("ハ")).toBe(true) - expect(textUtils.isHalfWidthCharacter("ヒ")).toBe(true) - expect(textUtils.isHalfWidthCharacter("ᆲ")).toBe(true) - expect(textUtils.isHalfWidthCharacter("■")).toBe(true) - - expect(textUtils.isHalfWidthCharacter("B")).toBe(false) - - describe ".isKoreanCharacter(character)", -> - it "returns true when the character is a korean character", -> - expect(textUtils.isKoreanCharacter("우")).toBe(true) - expect(textUtils.isKoreanCharacter("가")).toBe(true) - expect(textUtils.isKoreanCharacter("ㅢ")).toBe(true) - expect(textUtils.isKoreanCharacter("ㄼ")).toBe(true) - - expect(textUtils.isKoreanCharacter("O")).toBe(false) - - describe ".isWrapBoundary(previousCharacter, character)", -> - it "returns true when the character is CJK or when the previous character is a space/tab", -> - anyCharacter = 'x' - expect(textUtils.isWrapBoundary(anyCharacter, "我")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "私")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "B")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, ",")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "¢")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "ハ")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "ヒ")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "ᆲ")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "■")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "우")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "가")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "ㅢ")).toBe(true) - expect(textUtils.isWrapBoundary(anyCharacter, "ㄼ")).toBe(true) - - expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true) - expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true) - expect(textUtils.isWrapBoundary('a', 'h')).toBe(false) diff --git a/spec/text-utils-spec.js b/spec/text-utils-spec.js new file mode 100644 index 000000000..3a4b29866 --- /dev/null +++ b/spec/text-utils-spec.js @@ -0,0 +1,110 @@ +const textUtils = require('../src/text-utils') + +describe('text utilities', () => { + describe('.hasPairedCharacter(string)', () => + it('returns true when the string contains a surrogate pair, variation sequence, or combined character', () => { + expect(textUtils.hasPairedCharacter('abc')).toBe(false) + expect(textUtils.hasPairedCharacter('a\uD835\uDF97b\uD835\uDF97c')).toBe(true) + expect(textUtils.hasPairedCharacter('\uD835\uDF97')).toBe(true) + expect(textUtils.hasPairedCharacter('\u2714\uFE0E')).toBe(true) + expect(textUtils.hasPairedCharacter('e\u0301')).toBe(true) + + expect(textUtils.hasPairedCharacter('\uD835')).toBe(false) + expect(textUtils.hasPairedCharacter('\uDF97')).toBe(false) + expect(textUtils.hasPairedCharacter('\uFE0E')).toBe(false) + expect(textUtils.hasPairedCharacter('\u0301')).toBe(false) + + expect(textUtils.hasPairedCharacter('\uFE0E\uFE0E')).toBe(false) + expect(textUtils.hasPairedCharacter('\u0301\u0301')).toBe(false) + }) + ) + + describe('.isPairedCharacter(string, index)', () => + it('returns true when the index is the start of a high/low surrogate pair, variation sequence, or combined character', () => { + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 0)).toBe(false) + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 1)).toBe(true) + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 2)).toBe(false) + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 3)).toBe(false) + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 4)).toBe(true) + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 5)).toBe(false) + expect(textUtils.isPairedCharacter('a\uD835\uDF97b\uD835\uDF97c', 6)).toBe(false) + + expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 0)).toBe(false) + expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 1)).toBe(true) + expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 2)).toBe(false) + expect(textUtils.isPairedCharacter('a\u2714\uFE0E', 3)).toBe(false) + + expect(textUtils.isPairedCharacter('\uD835')).toBe(false) + expect(textUtils.isPairedCharacter('\uDF97')).toBe(false) + expect(textUtils.isPairedCharacter('\uFE0E')).toBe(false) + expect(textUtils.isPairedCharacter('\uFE0E')).toBe(false) + + expect(textUtils.isPairedCharacter('\uFE0E\uFE0E')).toBe(false) + + expect(textUtils.isPairedCharacter('ae\u0301c', 0)).toBe(false) + expect(textUtils.isPairedCharacter('ae\u0301c', 1)).toBe(true) + expect(textUtils.isPairedCharacter('ae\u0301c', 2)).toBe(false) + expect(textUtils.isPairedCharacter('ae\u0301c', 3)).toBe(false) + expect(textUtils.isPairedCharacter('ae\u0301c', 4)).toBe(false) + }) + ) + + describe('.isDoubleWidthCharacter(character)', () => + it('returns true when the character is either japanese, chinese or a full width form', () => { + expect(textUtils.isDoubleWidthCharacter('我')).toBe(true) + + expect(textUtils.isDoubleWidthCharacter('私')).toBe(true) + + expect(textUtils.isDoubleWidthCharacter('B')).toBe(true) + expect(textUtils.isDoubleWidthCharacter(',')).toBe(true) + expect(textUtils.isDoubleWidthCharacter('¢')).toBe(true) + + expect(textUtils.isDoubleWidthCharacter('a')).toBe(false) + }) + ) + + describe('.isHalfWidthCharacter(character)', () => + it('returns true when the character is an half width form', () => { + expect(textUtils.isHalfWidthCharacter('ハ')).toBe(true) + expect(textUtils.isHalfWidthCharacter('ヒ')).toBe(true) + expect(textUtils.isHalfWidthCharacter('ᆲ')).toBe(true) + expect(textUtils.isHalfWidthCharacter('■')).toBe(true) + + expect(textUtils.isHalfWidthCharacter('B')).toBe(false) + }) + ) + + describe('.isKoreanCharacter(character)', () => + it('returns true when the character is a korean character', () => { + expect(textUtils.isKoreanCharacter('우')).toBe(true) + expect(textUtils.isKoreanCharacter('가')).toBe(true) + expect(textUtils.isKoreanCharacter('ㅢ')).toBe(true) + expect(textUtils.isKoreanCharacter('ㄼ')).toBe(true) + + expect(textUtils.isKoreanCharacter('O')).toBe(false) + }) + ) + + describe('.isWrapBoundary(previousCharacter, character)', () => + it('returns true when the character is CJK or when the previous character is a space/tab', () => { + const anyCharacter = 'x' + expect(textUtils.isWrapBoundary(anyCharacter, '我')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, '私')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, 'B')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, ',')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, '¢')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, 'ハ')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, 'ヒ')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, 'ᆲ')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, '■')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, '우')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, '가')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, 'ㅢ')).toBe(true) + expect(textUtils.isWrapBoundary(anyCharacter, 'ㄼ')).toBe(true) + + expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true) + expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true) + expect(textUtils.isWrapBoundary('a', 'h')).toBe(false) + }) + ) +}) From 5dd5b29ca9c2bb3bfa9c7170fb66227dbeaa515e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 3 Nov 2017 09:28:59 -0600 Subject: [PATCH 273/301] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6656725a..6a7f57e33 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", "find-and-replace": "0.213.0", - "fuzzy-finder": "1.7.2", + "fuzzy-finder": "1.7.3", "github": "0.8.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 6465f54bcd29aff07b462332d92d7b27db2101e5 Mon Sep 17 00:00:00 2001 From: Hubot Date: Fri, 3 Nov 2017 10:57:02 -0500 Subject: [PATCH 274/301] 1.24.0-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a7f57e33..09e5da8fe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.23.0-dev", + "version": "1.24.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { From a1e8d25e55a2456be91c46ec0b785a8c9a288dce Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 3 Nov 2017 09:57:29 -0600 Subject: [PATCH 275/301] Autoscroll to cursor position after folding or unfolding There were several different fold/unfold code paths, so I decided to weave simple autoscroll assertions into the existing tests. --- spec/text-editor-spec.js | 26 +++++++++++++++++++++++--- src/text-editor.js | 28 ++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 382d020d4..84eea43ef 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -115,12 +115,12 @@ describe('TextEditor', () => { editor.update({showCursorOnSelection: false}) editor.setSelectedBufferRange([[1, 2], [3, 4]]) editor.addSelectionForBufferRange([[5, 6], [7, 8]], {reversed: true}) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() editor.setScrollTopRow(3) expect(editor.getScrollTopRow()).toBe(3) editor.setScrollLeftColumn(4) expect(editor.getScrollLeftColumn()).toBe(4) - editor.foldBufferRow(4) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() const editor2 = editor.copy() const element2 = editor2.getElement() @@ -7002,15 +7002,19 @@ describe('TextEditor', () => { }) describe('.unfoldAll()', () => { - it('unfolds every folded line', async () => { + it('unfolds every folded line and autoscrolls', async () => { editor = await atom.workspace.open('sample.js', {autoIndent: false}) + const autoscrollEvents = [] + editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)) const initialScreenLineCount = editor.getScreenLineCount() editor.foldBufferRow(0) editor.foldBufferRow(1) expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + expect(autoscrollEvents.length).toBe(1) editor.unfoldAll() expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + expect(autoscrollEvents.length).toBe(2) }) it('unfolds every folded line with comments', async () => { @@ -7028,8 +7032,11 @@ describe('TextEditor', () => { describe('.foldAll()', () => { it('folds every foldable line', async () => { editor = await atom.workspace.open('sample.js', {autoIndent: false}) + const autoscrollEvents = [] + editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)) editor.foldAll() + expect(autoscrollEvents.length).toBe(1) const [fold1, fold2, fold3] = editor.unfoldAll() expect([fold1.start.row, fold1.end.row]).toEqual([0, 12]) expect([fold2.start.row, fold2.end.row]).toEqual([1, 9]) @@ -7060,7 +7067,11 @@ describe('TextEditor', () => { describe('when bufferRow can be folded', () => { it('creates a fold based on the syntactic region starting at the given row', () => { + const autoscrollEvents = [] + editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)) + editor.foldBufferRow(1) + expect(autoscrollEvents.length).toBe(1) const [fold] = editor.unfoldAll() expect([fold.start.row, fold.end.row]).toEqual([1, 9]) }) @@ -7107,10 +7118,14 @@ 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) + const autoscrollEvents = [] + editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)) editor.foldCurrentRow() + expect(autoscrollEvents.length).toBe(1) expect(editor.getScreenLineCount()).toBe(3) }) @@ -7127,21 +7142,26 @@ describe('TextEditor', () => { describe('.foldAllAtIndentLevel(indentLevel)', () => { it('folds blocks of text at the given indentation level', async () => { editor = await atom.workspace.open('sample.js', {autoIndent: false}) + const autoscrollEvents = [] + editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)) editor.foldAllAtIndentLevel(0) expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`) expect(editor.getLastScreenRow()).toBe(0) + expect(autoscrollEvents.length).toBe(1) editor.foldAllAtIndentLevel(1) expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') expect(editor.lineTextForScreenRow(1)).toBe(` var sort = function(items) {${editor.displayLayer.foldCharacter}`) expect(editor.getLastScreenRow()).toBe(4) + expect(autoscrollEvents.length).toBe(2) editor.foldAllAtIndentLevel(2) expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') expect(editor.lineTextForScreenRow(1)).toBe(' var sort = function(items) {') expect(editor.lineTextForScreenRow(2)).toBe(' if (items.length <= 1) return items;') expect(editor.getLastScreenRow()).toBe(9) + expect(autoscrollEvents.length).toBe(3) }) it('folds every foldable range at a given indentLevel', async () => { diff --git a/src/text-editor.js b/src/text-editor.js index a0b9d19a0..8eee5c140 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3750,13 +3750,19 @@ class TextEditor { foldCurrentRow () { const {row} = this.getCursorBufferPosition() const range = this.tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) - if (range) return this.displayLayer.foldBufferRange(range) + if (range) { + const result = this.displayLayer.foldBufferRange(range) + this.scrollToCursorPosition() + return result + } } // Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow () { const {row} = this.getCursorBufferPosition() - return this.displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) + const result = this.displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) + this.scrollToCursorPosition() + return result } // Essential: Fold the given row in buffer coordinates based on its indentation @@ -3774,6 +3780,7 @@ class TextEditor { const existingFolds = this.displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) if (existingFolds.length === 0) { this.displayLayer.foldBufferRange(foldableRange) + this.scrollToCursorPosition() } else { const firstExistingFoldRange = this.displayLayer.bufferRangeForFold(existingFolds[0]) if (firstExistingFoldRange.start.isLessThan(position)) { @@ -3791,7 +3798,9 @@ class TextEditor { // * `bufferRow` A {Number} unfoldBufferRow (bufferRow) { const position = Point(bufferRow, Infinity) - return this.displayLayer.destroyFoldsContainingBufferPositions([position]) + const result = this.displayLayer.destroyFoldsContainingBufferPositions([position]) + this.scrollToCursorPosition() + return result } // Extended: For each selection, fold the rows it intersects. @@ -3807,6 +3816,7 @@ class TextEditor { for (let range of this.tokenizedBuffer.getFoldableRanges(this.getTabLength())) { this.displayLayer.foldBufferRange(range) } + this.scrollToCursorPosition() } // Extended: Unfold all existing folds. @@ -3824,6 +3834,7 @@ class TextEditor { for (let range of this.tokenizedBuffer.getFoldableRangesAtIndentLevel(level, this.getTabLength())) { this.displayLayer.foldBufferRange(range) } + this.scrollToCursorPosition() } // Extended: Determine whether the given row in buffer coordinates is foldable. @@ -3851,11 +3862,14 @@ class TextEditor { // Extended: Fold the given buffer row if it isn't currently folded, and unfold // it otherwise. toggleFoldAtBufferRow (bufferRow) { + let result if (this.isFoldedAtBufferRow(bufferRow)) { - return this.unfoldBufferRow(bufferRow) + result = this.unfoldBufferRow(bufferRow) } else { - return this.foldBufferRow(bufferRow) + result = this.foldBufferRow(bufferRow) } + this.scrollToCursorPosition() + return result } // Extended: Determine whether the most recently added cursor's row is folded. @@ -3894,7 +3908,9 @@ class TextEditor { // // Returns the new {Fold}. foldBufferRowRange (startRow, endRow) { - return this.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) + const result = this.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) + this.scrollToCursorPosition() + return result } foldBufferRange (range) { From ead3fdb46245625376ff3ca61cb337386a32ba03 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 3 Nov 2017 15:36:46 -0700 Subject: [PATCH 276/301] :arrow_up: github@0.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09e5da8fe..dc814e954 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.41.5", "find-and-replace": "0.213.0", "fuzzy-finder": "1.7.3", - "github": "0.8.0", + "github": "0.8.1", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.8", From d89a8162621b1474cce27e5a4df60e941c2bbab8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 4 Nov 2017 20:44:30 +0100 Subject: [PATCH 277/301] :arrow_up: timecop@0.36.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc814e954..b080c53d0 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "styleguide": "0.49.8", "symbols-view": "0.118.1", "tabs": "0.109.1", - "timecop": "0.36.0", + "timecop": "0.36.1", "tree-view": "0.221.2", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", From 4fd0345e8488d2fa09151316b4e99656e7861644 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sun, 5 Nov 2017 02:49:56 +0100 Subject: [PATCH 278/301] :arrow_up: timecop@0.36.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b080c53d0..92b80089a 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "styleguide": "0.49.8", "symbols-view": "0.118.1", "tabs": "0.109.1", - "timecop": "0.36.1", + "timecop": "0.36.2", "tree-view": "0.221.2", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", From 3f264fa2b0435fc7c2f7fa9d2f579dfe1d818fb9 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 6 Nov 2017 10:24:57 +0100 Subject: [PATCH 279/301] :arrow_up: styleguide@0.49.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 92b80089a..9aadf1484 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.14", - "styleguide": "0.49.8", + "styleguide": "0.49.9", "symbols-view": "0.118.1", "tabs": "0.109.1", "timecop": "0.36.2", From 05fc82cb8653d0fbc1463acd3a50e340f074ce3a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 6 Nov 2017 15:30:25 +0100 Subject: [PATCH 280/301] :arrow_up: status-bar@1.8.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9aadf1484..b010cd4a0 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "settings-view": "0.253.0", "snippets": "1.1.9", "spell-check": "0.72.3", - "status-bar": "1.8.14", + "status-bar": "1.8.15", "styleguide": "0.49.9", "symbols-view": "0.118.1", "tabs": "0.109.1", From 85f8b13a62ac79f2933ba9c784c07d1ba1c33d90 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Nov 2017 09:44:05 -0800 Subject: [PATCH 281/301] :art: clean up git-repository.js --- src/git-repository.js | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/git-repository.js b/src/git-repository.js index 057c5fcb7..55d70c12c 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -1,15 +1,7 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -const {join} = require('path') +const path = require('path') +const fs = require('fs-plus') const _ = require('underscore-plus') const {Emitter, Disposable, CompositeDisposable} = require('event-kit') -const fs = require('fs-plus') -const path = require('path') const GitUtils = require('git-utils') let nextId = 0 @@ -241,15 +233,15 @@ class GitRepository { // * `path` The {String} path to check. // // Returns a {Boolean}. - isSubmodule (path) { - if (!path) return false + isSubmodule (filePath) { + if (!filePath) return false - const repo = this.getRepo(path) - if (repo.isSubmodule(repo.relativize(path))) { + const repo = this.getRepo(filePath) + if (repo.isSubmodule(repo.relativize(filePath))) { return true } else { - // Check if the path is a working directory in a repo that isn't the root. - return repo !== this.getRepo() && repo.relativize(join(path, 'dir')) === 'dir' + // Check if the filePath is a working directory in a repo that isn't the root. + return repo !== this.getRepo() && repo.relativize(path.join(filePath, 'dir')) === 'dir' } } From 275fb0eb3645a883b5af0781f0137617f6867d7a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Nov 2017 10:25:31 -0800 Subject: [PATCH 282/301] Convert GitRepository spec to JS --- spec/git-repository-spec.coffee | 371 --------------------------- spec/git-repository-spec.js | 433 ++++++++++++++++++++++++++++++++ 2 files changed, 433 insertions(+), 371 deletions(-) delete mode 100644 spec/git-repository-spec.coffee create mode 100644 spec/git-repository-spec.js diff --git a/spec/git-repository-spec.coffee b/spec/git-repository-spec.coffee deleted file mode 100644 index e4d1e0c7f..000000000 --- a/spec/git-repository-spec.coffee +++ /dev/null @@ -1,371 +0,0 @@ -temp = require('temp').track() -GitRepository = require '../src/git-repository' -fs = require 'fs-plus' -path = require 'path' -Project = require '../src/project' - -copyRepository = -> - workingDirPath = temp.mkdirSync('atom-spec-git') - fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) - fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) - workingDirPath - -describe "GitRepository", -> - repo = null - - beforeEach -> - gitPath = path.join(temp.dir, '.git') - fs.removeSync(gitPath) if fs.isDirectorySync(gitPath) - - afterEach -> - repo.destroy() if repo?.repo? - try - temp.cleanupSync() # These tests sometimes lag at shutting down resources - - describe "@open(path)", -> - it "returns null when no repository is found", -> - expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull() - - describe "new GitRepository(path)", -> - it "throws an exception when no repository is found", -> - expect(-> new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() - - describe ".getPath()", -> - it "returns the repository path for a .git directory path with a directory", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) - expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') - - it "returns the repository path for a repository path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) - expect(repo.getPath()).toBe path.join(__dirname, 'fixtures', 'git', 'master.git') - - describe ".isPathIgnored(path)", -> - it "returns true for an ignored path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) - expect(repo.isPathIgnored('a.txt')).toBeTruthy() - - it "returns false for a non-ignored path", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) - expect(repo.isPathIgnored('b.txt')).toBeFalsy() - - describe ".isPathModified(path)", -> - [repo, filePath, newPath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - newPath = path.join(workingDirPath, 'new-path.txt') - - describe "when the path is unstaged", -> - it "returns false if the path has not been modified", -> - expect(repo.isPathModified(filePath)).toBeFalsy() - - it "returns true if the path is modified", -> - fs.writeFileSync(filePath, "change") - expect(repo.isPathModified(filePath)).toBeTruthy() - - it "returns true if the path is deleted", -> - fs.removeSync(filePath) - expect(repo.isPathModified(filePath)).toBeTruthy() - - it "returns false if the path is new", -> - expect(repo.isPathModified(newPath)).toBeFalsy() - - describe ".isPathNew(path)", -> - [filePath, newPath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - newPath = path.join(workingDirPath, 'new-path.txt') - fs.writeFileSync(newPath, "i'm new here") - - describe "when the path is unstaged", -> - it "returns true if the path is new", -> - expect(repo.isPathNew(newPath)).toBeTruthy() - - it "returns false if the path isn't new", -> - expect(repo.isPathNew(filePath)).toBeFalsy() - - describe ".checkoutHead(path)", -> - [filePath] = [] - - beforeEach -> - workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath) - filePath = path.join(workingDirPath, 'a.txt') - - it "no longer reports a path as modified after checkout", -> - expect(repo.isPathModified(filePath)).toBeFalsy() - fs.writeFileSync(filePath, 'ch ch changes') - expect(repo.isPathModified(filePath)).toBeTruthy() - expect(repo.checkoutHead(filePath)).toBeTruthy() - expect(repo.isPathModified(filePath)).toBeFalsy() - - it "restores the contents of the path to the original text", -> - fs.writeFileSync(filePath, 'ch ch changes') - expect(repo.checkoutHead(filePath)).toBeTruthy() - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - - it "fires a status-changed event if the checkout completes successfully", -> - fs.writeFileSync(filePath, 'ch ch changes') - repo.getPathStatus(filePath) - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatus statusHandler - repo.checkoutHead(filePath) - expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: 0} - - repo.checkoutHead(filePath) - expect(statusHandler.callCount).toBe 1 - - describe ".checkoutHeadForEditor(editor)", -> - [filePath, editor] = [] - - beforeEach -> - spyOn(atom, "confirm") - - workingDirPath = copyRepository() - repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) - filePath = path.join(workingDirPath, 'a.txt') - fs.writeFileSync(filePath, 'ch ch changes') - - waitsForPromise -> - atom.workspace.open(filePath) - - runs -> - editor = atom.workspace.getActiveTextEditor() - - it "displays a confirmation dialog by default", -> - return if process.platform is 'win32' # Permissions issues with this test on Windows - - atom.confirm.andCallFake ({buttons}) -> buttons.OK() - atom.config.set('editor.confirmCheckoutHeadRevision', true) - - repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - - it "does not display a dialog when confirmation is disabled", -> - return if process.platform is 'win32' # Flakey EPERM opening a.txt on Win32 - atom.config.set('editor.confirmCheckoutHeadRevision', false) - - repo.checkoutHeadForEditor(editor) - - expect(fs.readFileSync(filePath, 'utf8')).toBe '' - expect(atom.confirm).not.toHaveBeenCalled() - - describe ".destroy()", -> - it "throws an exception when any method is called after it is called", -> - repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) - repo.destroy() - expect(-> repo.getShortHead()).toThrow() - - describe ".getPathStatus(path)", -> - [filePath] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) - filePath = path.join(workingDirectory, 'file.txt') - - it "trigger a status-changed event when the new status differs from the last cached one", -> - statusHandler = jasmine.createSpy("statusHandler") - repo.onDidChangeStatus statusHandler - fs.writeFileSync(filePath, '') - status = repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe 1 - expect(statusHandler.argsForCall[0][0]).toEqual {path: filePath, pathStatus: status} - - fs.writeFileSync(filePath, 'abc') - status = repo.getPathStatus(filePath) - expect(statusHandler.callCount).toBe 1 - - describe ".getDirectoryStatus(path)", -> - [directoryPath, filePath] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory) - directoryPath = path.join(workingDirectory, 'dir') - filePath = path.join(directoryPath, 'b.txt') - - it "gets the status based on the files inside the directory", -> - expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe false - fs.writeFileSync(filePath, 'abc') - repo.getPathStatus(filePath) - expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe true - - describe ".refreshStatus()", -> - [newPath, modifiedPath, cleanPath, workingDirectory] = [] - - beforeEach -> - workingDirectory = copyRepository() - repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config}) - modifiedPath = path.join(workingDirectory, 'file.txt') - newPath = path.join(workingDirectory, 'untracked.txt') - cleanPath = path.join(workingDirectory, 'other.txt') - fs.writeFileSync(cleanPath, 'Full of text') - fs.writeFileSync(newPath, '') - newPath = fs.absolute newPath # specs could be running under symbol path. - - it "returns status information for all new and modified files", -> - fs.writeFileSync(modifiedPath, 'making this path modified') - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses statusHandler - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 - - runs -> - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - - it 'caches the proper statuses when a subdir is open', -> - subDir = path.join(workingDirectory, 'dir') - fs.mkdirSync(subDir) - - filePath = path.join(subDir, 'b.txt') - fs.writeFileSync(filePath, '') - - atom.project.setPaths([subDir]) - - waitsForPromise -> - atom.workspace.open('b.txt') - - statusHandler = null - runs -> - repo = atom.project.getRepositories()[0] - - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses statusHandler - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 - - runs -> - status = repo.getCachedPathStatus(filePath) - expect(repo.isStatusModified(status)).toBe false - expect(repo.isStatusNew(status)).toBe false - - it "works correctly when the project has multiple folders (regression)", -> - atom.project.addPath(workingDirectory) - atom.project.addPath(path.join(__dirname, 'fixtures', 'dir')) - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses statusHandler - - repo.refreshStatus() - - waitsFor -> - statusHandler.callCount > 0 - - runs -> - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - - it 'caches statuses that were looked up synchronously', -> - originalContent = 'undefined' - fs.writeFileSync(modifiedPath, 'making this path modified') - repo.getPathStatus('file.txt') - - fs.writeFileSync(modifiedPath, originalContent) - waitsForPromise -> repo.refreshStatus() - runs -> - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() - - describe "buffer events", -> - [editor] = [] - - beforeEach -> - statusRefreshed = false - atom.project.setPaths([copyRepository()]) - atom.project.getRepositories()[0].onDidChangeStatuses -> statusRefreshed = true - - waitsForPromise -> - atom.workspace.open('other.txt').then (o) -> editor = o - - waitsFor 'repo to refresh', -> statusRefreshed - - it "emits a status-changed event when a buffer is saved", -> - editor.insertNewline() - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler - - waitsForPromise -> - editor.save() - - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - - it "emits a status-changed event when a buffer is reloaded", -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler - - waitsForPromise -> - editor.getBuffer().reload() - - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - - waitsForPromise -> - editor.getBuffer().reload() - - runs -> - expect(statusHandler.callCount).toBe 1 - - it "emits a status-changed event when a buffer's path changes", -> - fs.writeFileSync(editor.getPath(), 'changed') - - statusHandler = jasmine.createSpy('statusHandler') - atom.project.getRepositories()[0].onDidChangeStatus statusHandler - editor.getBuffer().emitter.emit 'did-change-path' - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256} - editor.getBuffer().emitter.emit 'did-change-path' - expect(statusHandler.callCount).toBe 1 - - it "stops listening to the buffer when the repository is destroyed (regression)", -> - atom.project.getRepositories()[0].destroy() - expect(-> editor.save()).not.toThrow() - - describe "when a project is deserialized", -> - [buffer, project2, statusHandler] = [] - - afterEach -> - project2?.destroy() - - it "subscribes to all the serialized buffers in the project", -> - atom.project.setPaths([copyRepository()]) - - waitsForPromise -> - atom.workspace.open('file.txt') - - waitsForPromise -> - project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate}) - project2.deserialize(atom.project.serialize({isUnloading: false})) - - waitsFor -> - buffer = project2.getBuffers()[0] - - waitsForPromise -> - originalContent = buffer.getText() - buffer.append('changes') - - statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].onDidChangeStatus statusHandler - buffer.save() - - runs -> - expect(statusHandler.callCount).toBe 1 - expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256} diff --git a/spec/git-repository-spec.js b/spec/git-repository-spec.js new file mode 100644 index 000000000..1dfad182d --- /dev/null +++ b/spec/git-repository-spec.js @@ -0,0 +1,433 @@ +const path = require('path') +const fs = require('fs-plus') +const temp = require('temp').track() +const GitRepository = require('../src/git-repository') +const Project = require('../src/project') + +describe('GitRepository', () => { + let repo + + beforeEach(() => { + const gitPath = path.join(temp.dir, '.git') + if (fs.isDirectorySync(gitPath)) fs.removeSync(gitPath) + }) + + afterEach(() => { + if (repo && !repo.isDestroyed()) repo.destroy() + + // These tests sometimes lag at shutting down resources + try { + temp.cleanupSync() + } catch (error) {} + }) + + describe('@open(path)', () => { + it('returns null when no repository is found', () => { + expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull() + }) + }) + + describe('new GitRepository(path)', () => { + it('throws an exception when no repository is found', () => { + expect(() => new GitRepository(path.join(temp.dir, 'nogit.txt'))).toThrow() + }) + }) + + describe('.getPath()', () => { + it('returns the repository path for a .git directory path with a directory', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) + expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) + }) + + it('returns the repository path for a repository path', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) + expect(repo.getPath()).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git')) + }) + }) + + describe('.isPathIgnored(path)', () => { + it('returns true for an ignored path', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) + expect(repo.isPathIgnored('a.txt')).toBeTruthy() + }) + + it('returns false for a non-ignored path', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'ignore.git')) + expect(repo.isPathIgnored('b.txt')).toBeFalsy() + }) + }) + + describe('.isPathModified(path)', () => { + let filePath, newPath + + beforeEach(() => { + const workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + }) + + describe('when the path is unstaged', () => { + it('returns false if the path has not been modified', () => { + expect(repo.isPathModified(filePath)).toBeFalsy() + }) + + it('returns true if the path is modified', () => { + fs.writeFileSync(filePath, 'change') + expect(repo.isPathModified(filePath)).toBeTruthy() + }) + + it('returns true if the path is deleted', () => { + fs.removeSync(filePath) + expect(repo.isPathModified(filePath)).toBeTruthy() + }) + + it('returns false if the path is new', () => { + expect(repo.isPathModified(newPath)).toBeFalsy() + }) + }) + }) + + describe('.isPathNew(path)', () => { + let filePath, newPath + + beforeEach(() => { + const workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + newPath = path.join(workingDirPath, 'new-path.txt') + fs.writeFileSync(newPath, "i'm new here") + }) + + describe('when the path is unstaged', () => { + it('returns true if the path is new', () => { + expect(repo.isPathNew(newPath)).toBeTruthy() + }) + + it("returns false if the path isn't new", () => { + expect(repo.isPathNew(filePath)).toBeFalsy() + }) + }) + }) + + describe('.checkoutHead(path)', () => { + let filePath + + beforeEach(() => { + const workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath) + filePath = path.join(workingDirPath, 'a.txt') + }) + + it('no longer reports a path as modified after checkout', () => { + expect(repo.isPathModified(filePath)).toBeFalsy() + fs.writeFileSync(filePath, 'ch ch changes') + expect(repo.isPathModified(filePath)).toBeTruthy() + expect(repo.checkoutHead(filePath)).toBeTruthy() + expect(repo.isPathModified(filePath)).toBeFalsy() + }) + + it('restores the contents of the path to the original text', () => { + fs.writeFileSync(filePath, 'ch ch changes') + expect(repo.checkoutHead(filePath)).toBeTruthy() + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + }) + + it('fires a status-changed event if the checkout completes successfully', () => { + fs.writeFileSync(filePath, 'ch ch changes') + repo.getPathStatus(filePath) + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus(statusHandler) + repo.checkoutHead(filePath) + expect(statusHandler.callCount).toBe(1) + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: 0}) + + repo.checkoutHead(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + + describe('.checkoutHeadForEditor(editor)', () => { + let filePath, editor + + beforeEach(() => { + spyOn(atom, 'confirm') + + const workingDirPath = copyRepository() + repo = new GitRepository(workingDirPath, {project: atom.project, config: atom.config, confirm: atom.confirm}) + filePath = path.join(workingDirPath, 'a.txt') + fs.writeFileSync(filePath, 'ch ch changes') + + waitsForPromise(() => atom.workspace.open(filePath)) + + runs(() => editor = atom.workspace.getActiveTextEditor()) + }) + + it('displays a confirmation dialog by default', () => { + // Permissions issues with this test on Windows + if (process.platform === 'win32') return + + atom.confirm.andCallFake(({buttons}) => buttons.OK()) + atom.config.set('editor.confirmCheckoutHeadRevision', true) + + repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + }) + + it('does not display a dialog when confirmation is disabled', () => { + // Flakey EPERM opening a.txt on Win32 + if (process.platform === 'win32') return + atom.config.set('editor.confirmCheckoutHeadRevision', false) + + repo.checkoutHeadForEditor(editor) + + expect(fs.readFileSync(filePath, 'utf8')).toBe('') + expect(atom.confirm).not.toHaveBeenCalled() + }) + }) + + describe('.destroy()', () => { + it('throws an exception when any method is called after it is called', () => { + repo = new GitRepository(path.join(__dirname, 'fixtures', 'git', 'master.git')) + repo.destroy() + expect(() => repo.getShortHead()).toThrow() + }) + }) + + describe('.getPathStatus(path)', () => { + let filePath + + beforeEach(() => { + const workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory) + filePath = path.join(workingDirectory, 'file.txt') + }) + + it('trigger a status-changed event when the new status differs from the last cached one', () => { + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatus(statusHandler) + fs.writeFileSync(filePath, '') + let status = repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe(1) + expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status}) + + fs.writeFileSync(filePath, 'abc') + status = repo.getPathStatus(filePath) + expect(statusHandler.callCount).toBe(1) + }) + }) + + describe('.getDirectoryStatus(path)', () => { + let directoryPath, filePath + + beforeEach(() => { + const workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory) + directoryPath = path.join(workingDirectory, 'dir') + filePath = path.join(directoryPath, 'b.txt') + }) + + it('gets the status based on the files inside the directory', () => { + expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(false) + fs.writeFileSync(filePath, 'abc') + repo.getPathStatus(filePath) + expect(repo.isStatusModified(repo.getDirectoryStatus(directoryPath))).toBe(true) + }) + }) + + describe('.refreshStatus()', () => { + let newPath, modifiedPath, cleanPath, workingDirectory + + beforeEach(() => { + workingDirectory = copyRepository() + repo = new GitRepository(workingDirectory, {project: atom.project, config: atom.config}) + modifiedPath = path.join(workingDirectory, 'file.txt') + newPath = path.join(workingDirectory, 'untracked.txt') + cleanPath = path.join(workingDirectory, 'other.txt') + fs.writeFileSync(cleanPath, 'Full of text') + fs.writeFileSync(newPath, '') + newPath = fs.absolute(newPath) + }) // specs could be running under symbol path. + + it('returns status information for all new and modified files', () => { + fs.writeFileSync(modifiedPath, 'making this path modified') + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses(statusHandler) + repo.refreshStatus() + + waitsFor(() => statusHandler.callCount > 0) + + runs(() => { + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + }) + }) + + it('caches the proper statuses when a subdir is open', () => { + const subDir = path.join(workingDirectory, 'dir') + fs.mkdirSync(subDir) + + const filePath = path.join(subDir, 'b.txt') + fs.writeFileSync(filePath, '') + + atom.project.setPaths([subDir]) + + waitsForPromise(() => atom.workspace.open('b.txt')) + + let statusHandler = null + runs(() => { + repo = atom.project.getRepositories()[0] + + statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses(statusHandler) + repo.refreshStatus() + }) + + waitsFor(() => statusHandler.callCount > 0) + + runs(() => { + const status = repo.getCachedPathStatus(filePath) + expect(repo.isStatusModified(status)).toBe(false) + expect(repo.isStatusNew(status)).toBe(false) + }) + }) + + it('works correctly when the project has multiple folders (regression)', () => { + atom.project.addPath(workingDirectory) + atom.project.addPath(path.join(__dirname, 'fixtures', 'dir')) + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses(statusHandler) + + repo.refreshStatus() + + waitsFor(() => statusHandler.callCount > 0) + + runs(() => { + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + }) + }) + + it('caches statuses that were looked up synchronously', () => { + const originalContent = 'undefined' + fs.writeFileSync(modifiedPath, 'making this path modified') + repo.getPathStatus('file.txt') + + fs.writeFileSync(modifiedPath, originalContent) + waitsForPromise(() => repo.refreshStatus()) + runs(() => { + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() + }) + }) + }) + + describe('buffer events', () => { + let editor + + beforeEach(() => { + let statusRefreshed = false + atom.project.setPaths([copyRepository()]) + atom.project.getRepositories()[0].onDidChangeStatuses(() => statusRefreshed = true) + + waitsForPromise(() => atom.workspace.open('other.txt').then(o => editor = o)) + + waitsFor('repo to refresh', () => statusRefreshed) + }) + + it('emits a status-changed event when a buffer is saved', () => { + editor.insertNewline() + + const statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) + + waitsForPromise(() => editor.save()) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + }) + }) + + it('emits a status-changed event when a buffer is reloaded', () => { + fs.writeFileSync(editor.getPath(), 'changed') + + const statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) + + waitsForPromise(() => editor.getBuffer().reload()) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + }) + + waitsForPromise(() => editor.getBuffer().reload()) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + }) + }) + + it("emits a status-changed event when a buffer's path changes", () => { + fs.writeFileSync(editor.getPath(), 'changed') + + const statusHandler = jasmine.createSpy('statusHandler') + atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) + editor.getBuffer().emitter.emit('did-change-path') + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) + editor.getBuffer().emitter.emit('did-change-path') + expect(statusHandler.callCount).toBe(1) + }) + + it('stops listening to the buffer when the repository is destroyed (regression)', () => { + atom.project.getRepositories()[0].destroy() + expect(() => editor.save()).not.toThrow() + }) + }) + + describe('when a project is deserialized', () => { + let buffer, project2, statusHandler + + afterEach(() => { + if (project2) project2.destroy() + }) + + it('subscribes to all the serialized buffers in the project', () => { + atom.project.setPaths([copyRepository()]) + + waitsForPromise(() => atom.workspace.open('file.txt')) + + waitsForPromise(() => { + project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate}) + return project2.deserialize(atom.project.serialize({isUnloading: false})) + }) + + waitsFor(() => buffer = project2.getBuffers()[0]) + + waitsForPromise(() => { + const originalContent = buffer.getText() + buffer.append('changes') + + statusHandler = jasmine.createSpy('statusHandler') + project2.getRepositories()[0].onDidChangeStatus(statusHandler) + return buffer.save() + }) + + runs(() => { + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) + }) + }) + }) +}) + +function copyRepository () { + const workingDirPath = temp.mkdirSync('atom-spec-git') + fs.copySync(path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath) + fs.renameSync(path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git')) + return workingDirPath +} From 6e0b629389610120ae817cd79e6bd4f511889a1a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Nov 2017 10:57:46 -0800 Subject: [PATCH 283/301] Use async and await in git-repository-spec --- spec/git-repository-spec.js | 160 ++++++++++++++---------------------- 1 file changed, 60 insertions(+), 100 deletions(-) diff --git a/spec/git-repository-spec.js b/spec/git-repository-spec.js index 1dfad182d..e03a9788a 100644 --- a/spec/git-repository-spec.js +++ b/spec/git-repository-spec.js @@ -1,3 +1,4 @@ +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') const path = require('path') const fs = require('fs-plus') const temp = require('temp').track() @@ -150,7 +151,7 @@ describe('GitRepository', () => { describe('.checkoutHeadForEditor(editor)', () => { let filePath, editor - beforeEach(() => { + beforeEach(async () => { spyOn(atom, 'confirm') const workingDirPath = copyRepository() @@ -158,9 +159,7 @@ describe('GitRepository', () => { filePath = path.join(workingDirPath, 'a.txt') fs.writeFileSync(filePath, 'ch ch changes') - waitsForPromise(() => atom.workspace.open(filePath)) - - runs(() => editor = atom.workspace.getActiveTextEditor()) + editor = await atom.workspace.open(filePath) }) it('displays a confirmation dialog by default', () => { @@ -248,127 +247,89 @@ describe('GitRepository', () => { fs.writeFileSync(cleanPath, 'Full of text') fs.writeFileSync(newPath, '') newPath = fs.absolute(newPath) - }) // specs could be running under symbol path. - - it('returns status information for all new and modified files', () => { - fs.writeFileSync(modifiedPath, 'making this path modified') - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - repo.refreshStatus() - - waitsFor(() => statusHandler.callCount > 0) - - runs(() => { - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - }) }) - it('caches the proper statuses when a subdir is open', () => { + it('returns status information for all new and modified files', async () => { + const statusHandler = jasmine.createSpy('statusHandler') + repo.onDidChangeStatuses(statusHandler) + fs.writeFileSync(modifiedPath, 'making this path modified') + + await repo.refreshStatus() + expect(statusHandler.callCount).toBe(1) + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath) )).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() + }) + + it('caches the proper statuses when a subdir is open', async () => { const subDir = path.join(workingDirectory, 'dir') fs.mkdirSync(subDir) - const filePath = path.join(subDir, 'b.txt') fs.writeFileSync(filePath, '') - atom.project.setPaths([subDir]) + await atom.workspace.open('b.txt') + repo = atom.project.getRepositories()[0] - waitsForPromise(() => atom.workspace.open('b.txt')) - - let statusHandler = null - runs(() => { - repo = atom.project.getRepositories()[0] - - statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - repo.refreshStatus() - }) - - waitsFor(() => statusHandler.callCount > 0) - - runs(() => { - const status = repo.getCachedPathStatus(filePath) - expect(repo.isStatusModified(status)).toBe(false) - expect(repo.isStatusNew(status)).toBe(false) - }) + await repo.refreshStatus() + const status = repo.getCachedPathStatus(filePath) + expect(repo.isStatusModified(status)).toBe(false) + expect(repo.isStatusNew(status)).toBe(false) }) - it('works correctly when the project has multiple folders (regression)', () => { + it('works correctly when the project has multiple folders (regression)', async () => { atom.project.addPath(workingDirectory) atom.project.addPath(path.join(__dirname, 'fixtures', 'dir')) - const statusHandler = jasmine.createSpy('statusHandler') - repo.onDidChangeStatuses(statusHandler) - repo.refreshStatus() - - waitsFor(() => statusHandler.callCount > 0) - - runs(() => { - expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() - expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() - }) + await repo.refreshStatus() + expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined() + expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeTruthy() }) - it('caches statuses that were looked up synchronously', () => { + it('caches statuses that were looked up synchronously', async () => { const originalContent = 'undefined' fs.writeFileSync(modifiedPath, 'making this path modified') repo.getPathStatus('file.txt') fs.writeFileSync(modifiedPath, originalContent) - waitsForPromise(() => repo.refreshStatus()) - runs(() => { - expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() - }) + await repo.refreshStatus() + expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy() }) }) describe('buffer events', () => { let editor - beforeEach(() => { - let statusRefreshed = false + beforeEach(async () => { atom.project.setPaths([copyRepository()]) - atom.project.getRepositories()[0].onDidChangeStatuses(() => statusRefreshed = true) - - waitsForPromise(() => atom.workspace.open('other.txt').then(o => editor = o)) - - waitsFor('repo to refresh', () => statusRefreshed) + const refreshPromise = new Promise(resolve => atom.project.getRepositories()[0].onDidChangeStatuses(resolve)) + editor = await atom.workspace.open('other.txt') + await refreshPromise }) - it('emits a status-changed event when a buffer is saved', () => { + it('emits a status-changed event when a buffer is saved', async () => { editor.insertNewline() const statusHandler = jasmine.createSpy('statusHandler') atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) - waitsForPromise(() => editor.save()) - - runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - }) + await editor.save() + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) }) - it('emits a status-changed event when a buffer is reloaded', () => { + it('emits a status-changed event when a buffer is reloaded', async () => { fs.writeFileSync(editor.getPath(), 'changed') const statusHandler = jasmine.createSpy('statusHandler') atom.project.getRepositories()[0].onDidChangeStatus(statusHandler) - waitsForPromise(() => editor.getBuffer().reload()) + await editor.getBuffer().reload() + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: editor.getPath(), pathStatus: 256}) - }) - - waitsForPromise(() => editor.getBuffer().reload()) - - runs(() => { - expect(statusHandler.callCount).toBe(1) - }) + await editor.getBuffer().reload() + expect(statusHandler.callCount).toBe(1) }) it("emits a status-changed event when a buffer's path changes", () => { @@ -396,31 +357,30 @@ describe('GitRepository', () => { if (project2) project2.destroy() }) - it('subscribes to all the serialized buffers in the project', () => { + it('subscribes to all the serialized buffers in the project', async () => { atom.project.setPaths([copyRepository()]) - waitsForPromise(() => atom.workspace.open('file.txt')) + await atom.workspace.open('file.txt') - waitsForPromise(() => { - project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate}) - return project2.deserialize(atom.project.serialize({isUnloading: false})) + project2 = new Project({ + notificationManager: atom.notifications, + packageManager: atom.packages, + confirm: atom.confirm, + applicationDelegate: atom.applicationDelegate }) + await project2.deserialize(atom.project.serialize({isUnloading: false})) - waitsFor(() => buffer = project2.getBuffers()[0]) + buffer = project2.getBuffers()[0] - waitsForPromise(() => { - const originalContent = buffer.getText() - buffer.append('changes') + const originalContent = buffer.getText() + buffer.append('changes') - statusHandler = jasmine.createSpy('statusHandler') - project2.getRepositories()[0].onDidChangeStatus(statusHandler) - return buffer.save() - }) + statusHandler = jasmine.createSpy('statusHandler') + project2.getRepositories()[0].onDidChangeStatus(statusHandler) + await buffer.save() - runs(() => { - expect(statusHandler.callCount).toBe(1) - expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) - }) + expect(statusHandler.callCount).toBe(1) + expect(statusHandler).toHaveBeenCalledWith({path: buffer.getPath(), pathStatus: 256}) }) }) }) From 3b5948c11059fde31a73bc8c767b3519471fb2b7 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 6 Nov 2017 15:08:51 -0800 Subject: [PATCH 284/301] :arrow_up: github@0.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b010cd4a0..e87d07cb0 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.41.5", "find-and-replace": "0.213.0", "fuzzy-finder": "1.7.3", - "github": "0.8.1", + "github": "0.8.2", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.8", From 31eafc4622c55c5db50df5fd50664f9f9b5336eb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Nov 2017 17:32:29 -0800 Subject: [PATCH 285/301] Convert atom-environment-spec to JS --- spec/atom-environment-spec.coffee | 711 ------------------------- spec/atom-environment-spec.js | 830 ++++++++++++++++++++++++++++++ 2 files changed, 830 insertions(+), 711 deletions(-) delete mode 100644 spec/atom-environment-spec.coffee create mode 100644 spec/atom-environment-spec.js diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee deleted file mode 100644 index f178bbb6c..000000000 --- a/spec/atom-environment-spec.coffee +++ /dev/null @@ -1,711 +0,0 @@ -_ = require 'underscore-plus' -path = require 'path' -temp = require('temp').track() -AtomEnvironment = require '../src/atom-environment' -StorageFolder = require '../src/storage-folder' - -describe "AtomEnvironment", -> - afterEach -> - try - temp.cleanupSync() - - describe 'window sizing methods', -> - describe '::getPosition and ::setPosition', -> - originalPosition = null - beforeEach -> - originalPosition = atom.getPosition() - - afterEach -> - atom.setPosition(originalPosition.x, originalPosition.y) - - it 'sets the position of the window, and can retrieve the position just set', -> - atom.setPosition(22, 45) - expect(atom.getPosition()).toEqual x: 22, y: 45 - - describe '::getSize and ::setSize', -> - originalSize = null - beforeEach -> - originalSize = atom.getSize() - afterEach -> - atom.setSize(originalSize.width, originalSize.height) - - it 'sets the size of the window, and can retrieve the size just set', -> - newWidth = originalSize.width - 12 - newHeight = originalSize.height - 23 - waitsForPromise -> - atom.setSize(newWidth, newHeight) - runs -> - expect(atom.getSize()).toEqual width: newWidth, height: newHeight - - describe ".isReleasedVersion()", -> - it "returns false if the version is a SHA and true otherwise", -> - version = '0.1.0' - spyOn(atom, 'getVersion').andCallFake -> version - expect(atom.isReleasedVersion()).toBe true - version = '36b5518' - expect(atom.isReleasedVersion()).toBe false - - describe "loading default config", -> - it 'loads the default core config schema', -> - expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true - expect(atom.config.get('core.followSymlinks')).toBe true - expect(atom.config.get('editor.showInvisibles')).toBe false - - describe "window onerror handler", -> - devToolsPromise = null - beforeEach -> - devToolsPromise = Promise.resolve() - spyOn(atom, 'openDevTools').andReturn(devToolsPromise) - spyOn(atom, 'executeJavaScriptInDevTools') - - it "will open the dev tools when an error is triggered", -> - try - a + 1 - catch e - window.onerror.call(window, e.toString(), 'abc', 2, 3, e) - - waitsForPromise -> devToolsPromise - runs -> - expect(atom.openDevTools).toHaveBeenCalled() - expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled() - - describe "::onWillThrowError", -> - willThrowSpy = null - beforeEach -> - willThrowSpy = jasmine.createSpy() - - it "is called when there is an error", -> - error = null - atom.onWillThrowError(willThrowSpy) - try - a + 1 - catch e - error = e - window.onerror.call(window, e.toString(), 'abc', 2, 3, e) - - delete willThrowSpy.mostRecentCall.args[0].preventDefault - expect(willThrowSpy).toHaveBeenCalledWith - message: error.toString() - url: 'abc' - line: 2 - column: 3 - originalError: error - - it "will not show the devtools when preventDefault() is called", -> - willThrowSpy.andCallFake (errorObject) -> errorObject.preventDefault() - atom.onWillThrowError(willThrowSpy) - - try - a + 1 - catch e - window.onerror.call(window, e.toString(), 'abc', 2, 3, e) - - expect(willThrowSpy).toHaveBeenCalled() - expect(atom.openDevTools).not.toHaveBeenCalled() - expect(atom.executeJavaScriptInDevTools).not.toHaveBeenCalled() - - describe "::onDidThrowError", -> - didThrowSpy = null - beforeEach -> - didThrowSpy = jasmine.createSpy() - - it "is called when there is an error", -> - error = null - atom.onDidThrowError(didThrowSpy) - try - a + 1 - catch e - error = e - window.onerror.call(window, e.toString(), 'abc', 2, 3, e) - expect(didThrowSpy).toHaveBeenCalledWith - message: error.toString() - url: 'abc' - line: 2 - column: 3 - originalError: error - - describe ".assert(condition, message, callback)", -> - errors = null - - beforeEach -> - errors = [] - spyOn(atom, 'isReleasedVersion').andReturn(true) - atom.onDidFailAssertion (error) -> errors.push(error) - - describe "if the condition is false", -> - it "notifies onDidFailAssertion handlers with an error object based on the call site of the assertion", -> - result = atom.assert(false, "a == b") - expect(result).toBe false - expect(errors.length).toBe 1 - expect(errors[0].message).toBe "Assertion failed: a == b" - expect(errors[0].stack).toContain('atom-environment-spec') - - describe "if passed a callback function", -> - it "calls the callback with the assertion failure's error object", -> - error = null - atom.assert(false, "a == b", (e) -> error = e) - expect(error).toBe errors[0] - - describe "if passed metadata", -> - it "assigns the metadata on the assertion failure's error object", -> - atom.assert(false, "a == b", {foo: 'bar'}) - expect(errors[0].metadata).toEqual {foo: 'bar'} - - describe "when Atom has been built from source", -> - it "throws an error", -> - atom.isReleasedVersion.andReturn(false) - expect(-> atom.assert(false, 'testing')).toThrow('Assertion failed: testing') - - describe "if the condition is true", -> - it "does nothing", -> - result = atom.assert(true, "a == b") - expect(result).toBe true - expect(errors).toEqual [] - - describe "saving and loading", -> - beforeEach -> - atom.enablePersistence = true - - afterEach -> - atom.enablePersistence = false - - it "selects the state based on the current project paths", -> - jasmine.useRealClock() - - [dir1, dir2] = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")] - - loadSettings = _.extend atom.getLoadSettings(), - initialPaths: [dir1] - windowState: null - - spyOn(atom, 'getLoadSettings').andCallFake -> loadSettings - spyOn(atom, 'serialize').andReturn({stuff: 'cool'}) - - atom.project.setPaths([dir1, dir2]) - # State persistence will fail if other Atom instances are running - waitsForPromise -> - atom.stateStore.connect().then (isConnected) -> - expect(isConnected).toBe true - - waitsForPromise -> - atom.saveState().then -> - atom.loadState().then (state) -> - expect(state).toBeFalsy() - - waitsForPromise -> - loadSettings.initialPaths = [dir2, dir1] - atom.loadState().then (state) -> - expect(state).toEqual({stuff: 'cool'}) - - it "loads state from the storage folder when it can't be found in atom.stateStore", -> - jasmine.useRealClock() - - storageFolderState = {foo: 1, bar: 2} - serializedState = {someState: 42} - loadSettings = _.extend(atom.getLoadSettings(), {initialPaths: [temp.mkdirSync("project-directory")]}) - spyOn(atom, 'getLoadSettings').andReturn(loadSettings) - spyOn(atom, 'serialize').andReturn(serializedState) - spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync("config-directory"))) - atom.project.setPaths(atom.getLoadSettings().initialPaths) - - waitsForPromise -> - atom.stateStore.connect() - - runs -> - atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState) - - waitsForPromise -> - atom.loadState().then (state) -> expect(state).toEqual(storageFolderState) - - waitsForPromise -> - atom.saveState() - - waitsForPromise -> - atom.loadState().then (state) -> expect(state).toEqual(serializedState) - - it "saves state when the CPU is idle after a keydown or mousedown event", -> - atomEnv = new AtomEnvironment({ - applicationDelegate: global.atom.applicationDelegate, - }) - idleCallbacks = [] - atomEnv.initialize({ - window: { - requestIdleCallback: (callback) -> idleCallbacks.push(callback), - addEventListener: -> - removeEventListener: -> - }, - document: document.implementation.createHTMLDocument() - }) - - spyOn(atomEnv, 'saveState') - - keydown = new KeyboardEvent('keydown') - atomEnv.document.dispatchEvent(keydown) - advanceClock atomEnv.saveStateDebounceInterval - idleCallbacks.shift()() - expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false}) - expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true}) - - atomEnv.saveState.reset() - mousedown = new MouseEvent('mousedown') - atomEnv.document.dispatchEvent(mousedown) - advanceClock atomEnv.saveStateDebounceInterval - idleCallbacks.shift()() - expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false}) - expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true}) - - atomEnv.destroy() - - it "ignores mousedown/keydown events happening after calling unloadEditorWindow", -> - atomEnv = new AtomEnvironment({ - applicationDelegate: global.atom.applicationDelegate, - }) - idleCallbacks = [] - atomEnv.initialize({ - window: { - requestIdleCallback: (callback) -> idleCallbacks.push(callback), - addEventListener: -> - removeEventListener: -> - }, - document: document.implementation.createHTMLDocument() - }) - - spyOn(atomEnv, 'saveState') - - mousedown = new MouseEvent('mousedown') - atomEnv.document.dispatchEvent(mousedown) - atomEnv.unloadEditorWindow() - expect(atomEnv.saveState).not.toHaveBeenCalled() - - advanceClock atomEnv.saveStateDebounceInterval - idleCallbacks.shift()() - expect(atomEnv.saveState).not.toHaveBeenCalled() - - mousedown = new MouseEvent('mousedown') - atomEnv.document.dispatchEvent(mousedown) - advanceClock atomEnv.saveStateDebounceInterval - idleCallbacks.shift()() - expect(atomEnv.saveState).not.toHaveBeenCalled() - - atomEnv.destroy() - - it "serializes the project state with all the options supplied in saveState", -> - spyOn(atom.project, 'serialize').andReturn({foo: 42}) - - waitsForPromise -> atom.saveState({anyOption: 'any option'}) - runs -> - expect(atom.project.serialize.calls.length).toBe(1) - expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'}) - - it "serializes the text editor registry", -> - editor = null - - waitsForPromise -> - atom.workspace.open('sample.js').then (e) -> editor = e - - waitsForPromise -> - atom.textEditors.setGrammarOverride(editor, 'text.plain') - - atom2 = new AtomEnvironment({ - applicationDelegate: atom.applicationDelegate, - window: document.createElement('div'), - document: Object.assign( - document.createElement('div'), - { - body: document.createElement('div'), - head: document.createElement('div'), - } - ) - }) - atom2.initialize({document, window}) - atom2.deserialize(atom.serialize()).then -> - expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain') - atom2.destroy() - - describe "deserialization failures", -> - - it "propagates project state restoration failures", -> - spyOn(atom.project, 'deserialize').andCallFake -> - err = new Error('deserialization failure') - err.missingProjectPaths = ['/foo'] - Promise.reject(err) - spyOn(atom.notifications, 'addError') - - waitsForPromise -> atom.deserialize({project: 'should work'}) - runs -> - expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open project directory', - {description: 'Project directory `/foo` is no longer on disk.'} - - it "accumulates and reports two errors with one notification", -> - spyOn(atom.project, 'deserialize').andCallFake -> - err = new Error('deserialization failure') - err.missingProjectPaths = ['/foo', '/wat'] - Promise.reject(err) - spyOn(atom.notifications, 'addError') - - waitsForPromise -> atom.deserialize({project: 'should work'}) - runs -> - expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 2 project directories', - {description: 'Project directories `/foo` and `/wat` are no longer on disk.'} - - it "accumulates and reports three+ errors with one notification", -> - spyOn(atom.project, 'deserialize').andCallFake -> - err = new Error('deserialization failure') - err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things'] - Promise.reject(err) - spyOn(atom.notifications, 'addError') - - waitsForPromise -> atom.deserialize({project: 'should work'}) - runs -> - expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 4 project directories', - {description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'} - - describe "openInitialEmptyEditorIfNecessary", -> - describe "when there are no paths set", -> - beforeEach -> - spyOn(atom, 'getLoadSettings').andReturn(initialPaths: []) - - it "opens an empty buffer", -> - spyOn(atom.workspace, 'open') - atom.openInitialEmptyEditorIfNecessary() - expect(atom.workspace.open).toHaveBeenCalledWith(null) - - describe "when there is already a buffer open", -> - beforeEach -> - waitsForPromise -> atom.workspace.open() - - it "does not open an empty buffer", -> - spyOn(atom.workspace, 'open') - atom.openInitialEmptyEditorIfNecessary() - expect(atom.workspace.open).not.toHaveBeenCalled() - - describe "when the project has a path", -> - beforeEach -> - spyOn(atom, 'getLoadSettings').andReturn(initialPaths: ['something']) - spyOn(atom.workspace, 'open') - - it "does not open an empty buffer", -> - atom.openInitialEmptyEditorIfNecessary() - expect(atom.workspace.open).not.toHaveBeenCalled() - - describe "adding a project folder", -> - it "does nothing if the user dismisses the file picker", -> - initialPaths = atom.project.getPaths() - tempDirectory = temp.mkdirSync("a-new-directory") - spyOn(atom, "pickFolder").andCallFake (callback) -> callback(null) - atom.addProjectFolder() - expect(atom.project.getPaths()).toEqual(initialPaths) - - describe "when there is no saved state for the added folders", -> - beforeEach -> - spyOn(atom, 'loadState').andReturn(Promise.resolve(null)) - spyOn(atom, 'attemptRestoreProjectStateForPaths') - - it "adds the selected folder to the project", -> - initialPaths = atom.project.setPaths([]) - tempDirectory = temp.mkdirSync("a-new-directory") - spyOn(atom, "pickFolder").andCallFake (callback) -> - callback([tempDirectory]) - waitsForPromise -> - atom.addProjectFolder() - runs -> - expect(atom.project.getPaths()).toEqual([tempDirectory]) - expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() - - describe "when there is saved state for the relevant directories", -> - state = Symbol('savedState') - - beforeEach -> - spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':') - spyOn(atom, "loadState").andCallFake (key) -> - if key is __dirname then Promise.resolve(state) else Promise.resolve(null) - spyOn(atom, "attemptRestoreProjectStateForPaths") - spyOn(atom, "pickFolder").andCallFake (callback) -> - callback([__dirname]) - atom.project.setPaths([]) - - describe "when there are no project folders", -> - it "attempts to restore the project state", -> - waitsForPromise -> - atom.addProjectFolder() - runs -> - expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname]) - expect(atom.project.getPaths()).toEqual([]) - - describe "when there are already project folders", -> - openedPath = path.join(__dirname, 'fixtures') - beforeEach -> - atom.project.setPaths([openedPath]) - - it "does not attempt to restore the project state, instead adding the project paths", -> - waitsForPromise -> - atom.addProjectFolder() - runs -> - expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() - expect(atom.project.getPaths()).toEqual([openedPath, __dirname]) - - describe "attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)", -> - describe "when the window is clean (empty or has only unnamed, unmodified buffers)", -> - beforeEach -> - # Unnamed, unmodified buffer doesn't count toward "clean"-ness - waitsForPromise -> atom.workspace.open() - - it "automatically restores the saved state into the current environment", -> - state = Symbol() - spyOn(atom.workspace, 'open') - spyOn(atom, 'restoreStateIntoThisEnvironment') - - atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) - expect(atom.restoreStateIntoThisEnvironment).toHaveBeenCalledWith(state) - expect(atom.workspace.open.callCount).toBe(1) - expect(atom.workspace.open).toHaveBeenCalledWith(__filename) - - describe "when a dock has a non-text editor", -> - it "doesn't prompt the user to restore state", -> - dock = atom.workspace.getLeftDock() - dock.getActivePane().addItem - getTitle: -> 'title' - element: document.createElement 'div' - state = Symbol() - spyOn(atom, 'confirm') - atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) - expect(atom.confirm).not.toHaveBeenCalled() - - describe "when the window is dirty", -> - editor = null - - beforeEach -> - waitsForPromise -> atom.workspace.open().then (e) -> - editor = e - editor.setText('new editor') - - describe "when a dock has a modified editor", -> - it "prompts the user to restore the state", -> - dock = atom.workspace.getLeftDock() - dock.getActivePane().addItem editor - spyOn(atom, "confirm").andReturn(1) - spyOn(atom.project, 'addPath') - spyOn(atom.workspace, 'open') - state = Symbol() - atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) - expect(atom.confirm).toHaveBeenCalled() - - it "prompts the user to restore the state in a new window, discarding it and adding folder to current window", -> - spyOn(atom, "confirm").andReturn(1) - spyOn(atom.project, 'addPath') - spyOn(atom.workspace, 'open') - state = Symbol() - - atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) - expect(atom.confirm).toHaveBeenCalled() - expect(atom.project.addPath.callCount).toBe(1) - expect(atom.project.addPath).toHaveBeenCalledWith(__dirname) - expect(atom.workspace.open.callCount).toBe(1) - expect(atom.workspace.open).toHaveBeenCalledWith(__filename) - - it "prompts the user to restore the state in a new window, opening a new window", -> - spyOn(atom, "confirm").andReturn(0) - spyOn(atom, "open") - state = Symbol() - - atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) - expect(atom.confirm).toHaveBeenCalled() - expect(atom.open).toHaveBeenCalledWith - pathsToOpen: [__dirname, __filename] - newWindow: true - devMode: atom.inDevMode() - safeMode: atom.inSafeMode() - - describe "::unloadEditorWindow()", -> - it "saves the BlobStore so it can be loaded after reload", -> - configDirPath = temp.mkdirSync('atom-spec-environment') - fakeBlobStore = jasmine.createSpyObj("blob store", ["save"]) - atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true}) - atomEnvironment.initialize({configDirPath, blobStore: fakeBlobStore, window, document}) - - atomEnvironment.unloadEditorWindow() - - expect(fakeBlobStore.save).toHaveBeenCalled() - - atomEnvironment.destroy() - - describe "::destroy()", -> - it "does not throw exceptions when unsubscribing from ipc events (regression)", -> - configDirPath = temp.mkdirSync('atom-spec-environment') - fakeDocument = { - addEventListener: -> - removeEventListener: -> - head: document.createElement('head') - body: document.createElement('body') - } - atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate}) - atomEnvironment.initialize({window, document: fakeDocument}) - spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve()) - spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve()) - spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve()) - waitsForPromise -> - atomEnvironment.startEditorWindow() - runs -> - atomEnvironment.unloadEditorWindow() - atomEnvironment.destroy() - - describe "::whenShellEnvironmentLoaded()", -> - [atomEnvironment, envLoaded, spy] = [] - - beforeEach -> - resolve = null - promise = new Promise (r) -> resolve = r - envLoaded = -> - resolve() - waitsForPromise -> promise - atomEnvironment = new AtomEnvironment - applicationDelegate: atom.applicationDelegate - updateProcessEnv: -> promise - atomEnvironment.initialize({window, document}) - spy = jasmine.createSpy() - - afterEach -> - atomEnvironment.destroy() - - it "is triggered once the shell environment is loaded", -> - atomEnvironment.whenShellEnvironmentLoaded spy - atomEnvironment.updateProcessEnvAndTriggerHooks() - envLoaded() - runs -> expect(spy).toHaveBeenCalled() - - it "triggers the callback immediately if the shell environment is already loaded", -> - atomEnvironment.updateProcessEnvAndTriggerHooks() - envLoaded() - runs -> - atomEnvironment.whenShellEnvironmentLoaded spy - expect(spy).toHaveBeenCalled() - - describe "::openLocations(locations) (called via IPC from browser process)", -> - beforeEach -> - spyOn(atom.workspace, 'open') - atom.project.setPaths([]) - - describe "when there is no saved state", -> - beforeEach -> - spyOn(atom, "loadState").andReturn(Promise.resolve(null)) - - describe "when the opened path exists", -> - it "adds it to the project's paths", -> - pathToOpen = __filename - waitsForPromise -> atom.openLocations([{pathToOpen}]) - runs -> expect(atom.project.getPaths()[0]).toBe __dirname - - describe "then a second path is opened with forceAddToWindow", -> - it "adds the second path to the project's paths", -> - firstPathToOpen = __dirname - secondPathToOpen = path.resolve(__dirname, './fixtures') - waitsForPromise -> atom.openLocations([{pathToOpen: firstPathToOpen}]) - waitsForPromise -> atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}]) - runs -> expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen]) - - describe "when the opened path does not exist but its parent directory does", -> - it "adds the parent directory to the project paths", -> - pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') - waitsForPromise -> atom.openLocations([{pathToOpen}]) - runs -> expect(atom.project.getPaths()[0]).toBe __dirname - - describe "when the opened path is a file", -> - it "opens it in the workspace", -> - pathToOpen = __filename - waitsForPromise -> atom.openLocations([{pathToOpen}]) - runs -> expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename - - describe "when the opened path is a directory", -> - it "does not open it in the workspace", -> - pathToOpen = __dirname - waitsForPromise -> atom.openLocations([{pathToOpen}]) - runs -> expect(atom.workspace.open.callCount).toBe 0 - - describe "when the opened path is a uri", -> - it "adds it to the project's paths as is", -> - pathToOpen = 'remote://server:7644/some/dir/path' - spyOn(atom.project, 'addPath') - waitsForPromise -> atom.openLocations([{pathToOpen}]) - runs -> expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen) - - describe "when there is saved state for the relevant directories", -> - state = Symbol('savedState') - - beforeEach -> - spyOn(atom, "getStateKey").andCallFake (dirs) -> dirs.join(':') - spyOn(atom, "loadState").andCallFake (key) -> - if key is __dirname then Promise.resolve(state) else Promise.resolve(null) - spyOn(atom, "attemptRestoreProjectStateForPaths") - - describe "when there are no project folders", -> - it "attempts to restore the project state", -> - pathToOpen = __dirname - waitsForPromise -> atom.openLocations([{pathToOpen}]) - runs -> - expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], []) - expect(atom.project.getPaths()).toEqual([]) - - it "opens the specified files", -> - waitsForPromise -> atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}]) - runs -> - expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename]) - expect(atom.project.getPaths()).toEqual([]) - - - describe "when there are already project folders", -> - beforeEach -> - atom.project.setPaths([__dirname]) - - it "does not attempt to restore the project state, instead adding the project paths", -> - pathToOpen = path.join(__dirname, 'fixtures') - waitsForPromise -> atom.openLocations([{pathToOpen, forceAddToWindow: true}]) - runs -> - expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() - expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen]) - - it "opens the specified files", -> - pathToOpen = path.join(__dirname, 'fixtures') - fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt') - waitsForPromise -> atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}]) - runs -> - expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen]) - expect(atom.project.getPaths()).toEqual([__dirname]) - - describe "::updateAvailable(info) (called via IPC from browser process)", -> - subscription = null - - afterEach -> - subscription?.dispose() - - it "invokes onUpdateAvailable listeners", -> - return unless process.platform is 'darwin' # Test tied to electron autoUpdater, we use something else on Linux and Win32 - - atom.listenForUpdates() - - updateAvailableHandler = jasmine.createSpy("update-available-handler") - subscription = atom.onUpdateAvailable updateAvailableHandler - - autoUpdater = require('electron').remote.autoUpdater - autoUpdater.emit 'update-downloaded', null, "notes", "version" - - waitsFor -> - updateAvailableHandler.callCount > 0 - - runs -> - {releaseVersion} = updateAvailableHandler.mostRecentCall.args[0] - expect(releaseVersion).toBe 'version' - - describe "::getReleaseChannel()", -> - [version] = [] - beforeEach -> - spyOn(atom, 'getVersion').andCallFake -> version - - it "returns the correct channel based on the version number", -> - version = '1.5.6' - expect(atom.getReleaseChannel()).toBe 'stable' - - version = '1.5.0-beta10' - expect(atom.getReleaseChannel()).toBe 'beta' - - version = '1.7.0-dev-5340c91' - expect(atom.getReleaseChannel()).toBe 'dev' diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js new file mode 100644 index 000000000..3095b94f0 --- /dev/null +++ b/spec/atom-environment-spec.js @@ -0,0 +1,830 @@ +const _ = require('underscore-plus') +const path = require('path') +const temp = require('temp').track() +const AtomEnvironment = require('../src/atom-environment') +const StorageFolder = require('../src/storage-folder') + +describe('AtomEnvironment', () => { + afterEach(() => { + try { + temp.cleanupSync() + } catch (error) {} + }) + + describe('window sizing methods', () => { + describe('::getPosition and ::setPosition', () => { + let originalPosition = null + beforeEach(() => originalPosition = atom.getPosition()) + + afterEach(() => atom.setPosition(originalPosition.x, originalPosition.y)) + + it('sets the position of the window, and can retrieve the position just set', () => { + atom.setPosition(22, 45) + expect(atom.getPosition()).toEqual({x: 22, y: 45}) + }) + }) + + describe('::getSize and ::setSize', () => { + let originalSize = null + beforeEach(() => originalSize = atom.getSize()) + afterEach(() => atom.setSize(originalSize.width, originalSize.height)) + + it('sets the size of the window, and can retrieve the size just set', () => { + const newWidth = originalSize.width - 12 + const newHeight = originalSize.height - 23 + waitsForPromise(() => atom.setSize(newWidth, newHeight)) + runs(() => expect(atom.getSize()).toEqual({width: newWidth, height: newHeight})) + }) + }) + }) + + describe('.isReleasedVersion()', () => { + it('returns false if the version is a SHA and true otherwise', () => { + let version = '0.1.0' + spyOn(atom, 'getVersion').andCallFake(() => version) + expect(atom.isReleasedVersion()).toBe(true) + version = '36b5518' + expect(atom.isReleasedVersion()).toBe(false) + }) + }) + + describe('loading default config', () => { + it('loads the default core config schema', () => { + expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe(true) + expect(atom.config.get('core.followSymlinks')).toBe(true) + expect(atom.config.get('editor.showInvisibles')).toBe(false) + }) + }) + + describe('window onerror handler', () => { + let devToolsPromise = null + beforeEach(() => { + devToolsPromise = Promise.resolve() + spyOn(atom, 'openDevTools').andReturn(devToolsPromise) + spyOn(atom, 'executeJavaScriptInDevTools') + }) + + it('will open the dev tools when an error is triggered', () => { + try { + a + 1 + } catch (e) { + window.onerror.call(window, e.toString(), 'abc', 2, 3, e) + } + + waitsForPromise(() => devToolsPromise) + runs(() => { + expect(atom.openDevTools).toHaveBeenCalled() + expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled() + }) + }) + + describe('::onWillThrowError', () => { + let willThrowSpy = null + beforeEach(() => willThrowSpy = jasmine.createSpy()) + + it('is called when there is an error', () => { + let error = null + atom.onWillThrowError(willThrowSpy) + try { + a + 1 + } catch (e) { + error = e + window.onerror.call(window, e.toString(), 'abc', 2, 3, e) + } + + delete willThrowSpy.mostRecentCall.args[0].preventDefault + expect(willThrowSpy).toHaveBeenCalledWith({ + message: error.toString(), + url: 'abc', + line: 2, + column: 3, + originalError: error + }) + }) + + it('will not show the devtools when preventDefault() is called', () => { + willThrowSpy.andCallFake(errorObject => errorObject.preventDefault()) + atom.onWillThrowError(willThrowSpy) + + try { + a + 1 + } catch (e) { + window.onerror.call(window, e.toString(), 'abc', 2, 3, e) + } + + expect(willThrowSpy).toHaveBeenCalled() + expect(atom.openDevTools).not.toHaveBeenCalled() + expect(atom.executeJavaScriptInDevTools).not.toHaveBeenCalled() + }) + }) + + describe('::onDidThrowError', () => { + let didThrowSpy = null + beforeEach(() => didThrowSpy = jasmine.createSpy()) + + it('is called when there is an error', () => { + let error = null + atom.onDidThrowError(didThrowSpy) + try { + a + 1 + } catch (e) { + error = e + window.onerror.call(window, e.toString(), 'abc', 2, 3, e) + } + expect(didThrowSpy).toHaveBeenCalledWith({ + message: error.toString(), + url: 'abc', + line: 2, + column: 3, + originalError: error + }) + }) + }) + }) + + describe('.assert(condition, message, callback)', () => { + let errors = null + + beforeEach(() => { + errors = [] + spyOn(atom, 'isReleasedVersion').andReturn(true) + atom.onDidFailAssertion(error => errors.push(error)) + }) + + describe('if the condition is false', () => { + it('notifies onDidFailAssertion handlers with an error object based on the call site of the assertion', () => { + const result = atom.assert(false, 'a == b') + expect(result).toBe(false) + expect(errors.length).toBe(1) + expect(errors[0].message).toBe('Assertion failed: a == b') + expect(errors[0].stack).toContain('atom-environment-spec') + }) + + describe('if passed a callback function', () => { + it("calls the callback with the assertion failure's error object", () => { + let error = null + atom.assert(false, 'a == b', e => error = e) + expect(error).toBe(errors[0]) + }) + }) + + describe('if passed metadata', () => { + it("assigns the metadata on the assertion failure's error object", () => { + atom.assert(false, 'a == b', {foo: 'bar'}) + expect(errors[0].metadata).toEqual({foo: 'bar'}) + }) + }) + + describe('when Atom has been built from source', () => { + it('throws an error', () => { + atom.isReleasedVersion.andReturn(false) + expect(() => atom.assert(false, 'testing')).toThrow('Assertion failed: testing') + }) + }) + }) + + describe('if the condition is true', () => { + it('does nothing', () => { + const result = atom.assert(true, 'a == b') + expect(result).toBe(true) + expect(errors).toEqual([]) + }) + }) + }) + + describe('saving and loading', () => { + beforeEach(() => atom.enablePersistence = true) + + afterEach(() => atom.enablePersistence = false) + + it('selects the state based on the current project paths', () => { + jasmine.useRealClock() + + const [dir1, dir2] = [temp.mkdirSync('dir1-'), temp.mkdirSync('dir2-')] + + const loadSettings = _.extend(atom.getLoadSettings(), { + initialPaths: [dir1], + windowState: null + } + ) + + spyOn(atom, 'getLoadSettings').andCallFake(() => loadSettings) + spyOn(atom, 'serialize').andReturn({stuff: 'cool'}) + + atom.project.setPaths([dir1, dir2]) + // State persistence will fail if other Atom instances are running + waitsForPromise(() => + atom.stateStore.connect().then(isConnected => expect(isConnected).toBe(true)) + ) + + waitsForPromise(() => + atom.saveState().then(() => + atom.loadState().then(state => expect(state).toBeFalsy()) + ) + ) + + waitsForPromise(() => { + loadSettings.initialPaths = [dir2, dir1] + return atom.loadState().then(state => expect(state).toEqual({stuff: 'cool'})) + }) + }) + + it("loads state from the storage folder when it can't be found in atom.stateStore", () => { + jasmine.useRealClock() + + const storageFolderState = {foo: 1, bar: 2} + const serializedState = {someState: 42} + const loadSettings = _.extend(atom.getLoadSettings(), {initialPaths: [temp.mkdirSync('project-directory')]}) + spyOn(atom, 'getLoadSettings').andReturn(loadSettings) + spyOn(atom, 'serialize').andReturn(serializedState) + spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync('config-directory'))) + atom.project.setPaths(atom.getLoadSettings().initialPaths) + + waitsForPromise(() => atom.stateStore.connect()) + + runs(() => atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState)) + + waitsForPromise(() => atom.loadState().then(state => expect(state).toEqual(storageFolderState))) + + waitsForPromise(() => atom.saveState()) + + waitsForPromise(() => atom.loadState().then(state => expect(state).toEqual(serializedState))) + }) + + it('saves state when the CPU is idle after a keydown or mousedown event', () => { + const atomEnv = new AtomEnvironment({ + applicationDelegate: global.atom.applicationDelegate + }) + const idleCallbacks = [] + atomEnv.initialize({ + window: { + requestIdleCallback (callback) { idleCallbacks.push(callback) }, + addEventListener () {}, + removeEventListener () {} + }, + document: document.implementation.createHTMLDocument() + }) + + spyOn(atomEnv, 'saveState') + + const keydown = new KeyboardEvent('keydown') + atomEnv.document.dispatchEvent(keydown) + advanceClock(atomEnv.saveStateDebounceInterval) + idleCallbacks.shift()() + expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false}) + expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true}) + + atomEnv.saveState.reset() + const mousedown = new MouseEvent('mousedown') + atomEnv.document.dispatchEvent(mousedown) + advanceClock(atomEnv.saveStateDebounceInterval) + idleCallbacks.shift()() + expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false}) + expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true}) + + atomEnv.destroy() + }) + + it('ignores mousedown/keydown events happening after calling unloadEditorWindow', () => { + const atomEnv = new AtomEnvironment({ + applicationDelegate: global.atom.applicationDelegate + }) + const idleCallbacks = [] + atomEnv.initialize({ + window: { + requestIdleCallback (callback) { idleCallbacks.push(callback) }, + addEventListener () {}, + removeEventListener () {} + }, + document: document.implementation.createHTMLDocument() + }) + + spyOn(atomEnv, 'saveState') + + let mousedown = new MouseEvent('mousedown') + atomEnv.document.dispatchEvent(mousedown) + atomEnv.unloadEditorWindow() + expect(atomEnv.saveState).not.toHaveBeenCalled() + + advanceClock(atomEnv.saveStateDebounceInterval) + idleCallbacks.shift()() + expect(atomEnv.saveState).not.toHaveBeenCalled() + + mousedown = new MouseEvent('mousedown') + atomEnv.document.dispatchEvent(mousedown) + advanceClock(atomEnv.saveStateDebounceInterval) + idleCallbacks.shift()() + expect(atomEnv.saveState).not.toHaveBeenCalled() + + atomEnv.destroy() + }) + + it('serializes the project state with all the options supplied in saveState', () => { + spyOn(atom.project, 'serialize').andReturn({foo: 42}) + + waitsForPromise(() => atom.saveState({anyOption: 'any option'})) + runs(() => { + expect(atom.project.serialize.calls.length).toBe(1) + expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'}) + }) + }) + + it('serializes the text editor registry', () => { + let editor = null + + waitsForPromise(() => atom.workspace.open('sample.js').then(e => editor = e)) + + waitsForPromise(() => { + atom.textEditors.setGrammarOverride(editor, 'text.plain') + + const atom2 = new AtomEnvironment({ + applicationDelegate: atom.applicationDelegate, + window: document.createElement('div'), + document: Object.assign( + document.createElement('div'), + { + body: document.createElement('div'), + head: document.createElement('div') + } + ) + }) + atom2.initialize({document, window}) + return atom2.deserialize(atom.serialize()).then(() => { + expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain') + return atom2.destroy() + }) + }) + }) + + describe('deserialization failures', () => { + it('propagates project state restoration failures', () => { + spyOn(atom.project, 'deserialize').andCallFake(() => { + const err = new Error('deserialization failure') + err.missingProjectPaths = ['/foo'] + return Promise.reject(err) + }) + spyOn(atom.notifications, 'addError') + + waitsForPromise(() => atom.deserialize({project: 'should work'})) + runs(() => { + expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open project directory', { + description: 'Project directory `/foo` is no longer on disk.' + }) + }) + }) + + it('accumulates and reports two errors with one notification', () => { + spyOn(atom.project, 'deserialize').andCallFake(() => { + const err = new Error('deserialization failure') + err.missingProjectPaths = ['/foo', '/wat'] + return Promise.reject(err) + }) + spyOn(atom.notifications, 'addError') + + waitsForPromise(() => atom.deserialize({project: 'should work'})) + runs(() => { + expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 2 project directories', { + description: 'Project directories `/foo` and `/wat` are no longer on disk.' + }) + }) + }) + + it('accumulates and reports three+ errors with one notification', () => { + spyOn(atom.project, 'deserialize').andCallFake(() => { + const err = new Error('deserialization failure') + err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things'] + return Promise.reject(err) + }) + spyOn(atom.notifications, 'addError') + + waitsForPromise(() => atom.deserialize({project: 'should work'})) + runs(() => + expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 4 project directories', + {description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'})) + }) + }) + }) + + describe('openInitialEmptyEditorIfNecessary', () => { + describe('when there are no paths set', () => { + beforeEach(() => spyOn(atom, 'getLoadSettings').andReturn({initialPaths: []})) + + it('opens an empty buffer', () => { + spyOn(atom.workspace, 'open') + atom.openInitialEmptyEditorIfNecessary() + expect(atom.workspace.open).toHaveBeenCalledWith(null) + }) + + describe('when there is already a buffer open', () => { + beforeEach(() => waitsForPromise(() => atom.workspace.open())) + + it('does not open an empty buffer', () => { + spyOn(atom.workspace, 'open') + atom.openInitialEmptyEditorIfNecessary() + expect(atom.workspace.open).not.toHaveBeenCalled() + }) + }) + }) + + describe('when the project has a path', () => { + beforeEach(() => { + spyOn(atom, 'getLoadSettings').andReturn({initialPaths: ['something']}) + spyOn(atom.workspace, 'open') + }) + + it('does not open an empty buffer', () => { + atom.openInitialEmptyEditorIfNecessary() + expect(atom.workspace.open).not.toHaveBeenCalled() + }) + }) + }) + + describe('adding a project folder', () => { + it('does nothing if the user dismisses the file picker', () => { + const initialPaths = atom.project.getPaths() + const tempDirectory = temp.mkdirSync('a-new-directory') + spyOn(atom, 'pickFolder').andCallFake(callback => callback(null)) + atom.addProjectFolder() + expect(atom.project.getPaths()).toEqual(initialPaths) + }) + + describe('when there is no saved state for the added folders', () => { + beforeEach(() => { + spyOn(atom, 'loadState').andReturn(Promise.resolve(null)) + spyOn(atom, 'attemptRestoreProjectStateForPaths') + }) + + it('adds the selected folder to the project', () => { + const initialPaths = atom.project.setPaths([]) + const tempDirectory = temp.mkdirSync('a-new-directory') + spyOn(atom, 'pickFolder').andCallFake(callback => callback([tempDirectory])) + waitsForPromise(() => atom.addProjectFolder()) + runs(() => { + expect(atom.project.getPaths()).toEqual([tempDirectory]) + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + }) + }) + }) + + describe('when there is saved state for the relevant directories', () => { + const state = Symbol('savedState') + + beforeEach(() => { + spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':')) + spyOn(atom, 'loadState').andCallFake(function (key) { + if (key === __dirname) { return Promise.resolve(state) } else { return Promise.resolve(null) } + }) + spyOn(atom, 'attemptRestoreProjectStateForPaths') + spyOn(atom, 'pickFolder').andCallFake(callback => callback([__dirname])) + atom.project.setPaths([]) + }) + + describe('when there are no project folders', () => { + it('attempts to restore the project state', () => { + waitsForPromise(() => atom.addProjectFolder()) + runs(() => { + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname]) + expect(atom.project.getPaths()).toEqual([]) + }) + }) + }) + + describe('when there are already project folders', () => { + const openedPath = path.join(__dirname, 'fixtures') + beforeEach(() => atom.project.setPaths([openedPath])) + + it('does not attempt to restore the project state, instead adding the project paths', () => { + waitsForPromise(() => atom.addProjectFolder()) + runs(() => { + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + expect(atom.project.getPaths()).toEqual([openedPath, __dirname]) + }) + }) + }) + }) + }) + + describe('attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)', () => { + describe('when the window is clean (empty or has only unnamed, unmodified buffers)', () => { + beforeEach(() => + // Unnamed, unmodified buffer doesn't count toward "clean"-ness + waitsForPromise(() => atom.workspace.open()) + ) + + it('automatically restores the saved state into the current environment', () => { + const state = Symbol() + spyOn(atom.workspace, 'open') + spyOn(atom, 'restoreStateIntoThisEnvironment') + + atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) + expect(atom.restoreStateIntoThisEnvironment).toHaveBeenCalledWith(state) + expect(atom.workspace.open.callCount).toBe(1) + expect(atom.workspace.open).toHaveBeenCalledWith(__filename) + }) + + describe('when a dock has a non-text editor', () => { + it("doesn't prompt the user to restore state", () => { + const dock = atom.workspace.getLeftDock() + dock.getActivePane().addItem({ + getTitle () { return 'title' }, + element: document.createElement('div') + }) + const state = Symbol() + spyOn(atom, 'confirm') + atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) + expect(atom.confirm).not.toHaveBeenCalled() + }) + }) + }) + + describe('when the window is dirty', () => { + let editor = null + + beforeEach(() => + waitsForPromise(() => atom.workspace.open().then(function (e) { + editor = e + editor.setText('new editor') + }) + ) + ) + + describe('when a dock has a modified editor', () => { + it('prompts the user to restore the state', () => { + const dock = atom.workspace.getLeftDock() + dock.getActivePane().addItem(editor) + spyOn(atom, 'confirm').andReturn(1) + spyOn(atom.project, 'addPath') + spyOn(atom.workspace, 'open') + const state = Symbol() + atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) + expect(atom.confirm).toHaveBeenCalled() + }) + }) + + it('prompts the user to restore the state in a new window, discarding it and adding folder to current window', () => { + spyOn(atom, 'confirm').andReturn(1) + spyOn(atom.project, 'addPath') + spyOn(atom.workspace, 'open') + const state = Symbol() + + atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) + expect(atom.confirm).toHaveBeenCalled() + expect(atom.project.addPath.callCount).toBe(1) + expect(atom.project.addPath).toHaveBeenCalledWith(__dirname) + expect(atom.workspace.open.callCount).toBe(1) + expect(atom.workspace.open).toHaveBeenCalledWith(__filename) + }) + + it('prompts the user to restore the state in a new window, opening a new window', () => { + spyOn(atom, 'confirm').andReturn(0) + spyOn(atom, 'open') + const state = Symbol() + + atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) + expect(atom.confirm).toHaveBeenCalled() + expect(atom.open).toHaveBeenCalledWith({ + pathsToOpen: [__dirname, __filename], + newWindow: true, + devMode: atom.inDevMode(), + safeMode: atom.inSafeMode() + }) + }) + }) + }) + + describe('::unloadEditorWindow()', () => { + it('saves the BlobStore so it can be loaded after reload', () => { + const configDirPath = temp.mkdirSync('atom-spec-environment') + const fakeBlobStore = jasmine.createSpyObj('blob store', ['save']) + const atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true}) + atomEnvironment.initialize({configDirPath, blobStore: fakeBlobStore, window, document}) + + atomEnvironment.unloadEditorWindow() + + expect(fakeBlobStore.save).toHaveBeenCalled() + + atomEnvironment.destroy() + }) + }) + + describe('::destroy()', () => { + it('does not throw exceptions when unsubscribing from ipc events (regression)', () => { + const configDirPath = temp.mkdirSync('atom-spec-environment') + const fakeDocument = { + addEventListener () {}, + removeEventListener () {}, + head: document.createElement('head'), + body: document.createElement('body') + } + const atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate}) + atomEnvironment.initialize({window, document: fakeDocument}) + spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve()) + spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve()) + spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve()) + waitsForPromise(() => atomEnvironment.startEditorWindow()) + runs(() => { + atomEnvironment.unloadEditorWindow() + atomEnvironment.destroy() + }) + }) + }) + + describe('::whenShellEnvironmentLoaded()', () => { + let atomEnvironment, envLoaded, spy + + beforeEach(() => { + let resolve = null + const promise = new Promise(function (r) { resolve = r }) + envLoaded = () => { + resolve() + waitsForPromise(() => promise) + } + atomEnvironment = new AtomEnvironment({ + applicationDelegate: atom.applicationDelegate, + updateProcessEnv () { return promise } + }) + atomEnvironment.initialize({window, document}) + spy = jasmine.createSpy() + }) + + afterEach(() => atomEnvironment.destroy()) + + it('is triggered once the shell environment is loaded', () => { + atomEnvironment.whenShellEnvironmentLoaded(spy) + atomEnvironment.updateProcessEnvAndTriggerHooks() + envLoaded() + runs(() => expect(spy).toHaveBeenCalled()) + }) + + it('triggers the callback immediately if the shell environment is already loaded', () => { + atomEnvironment.updateProcessEnvAndTriggerHooks() + envLoaded() + runs(() => { + atomEnvironment.whenShellEnvironmentLoaded(spy) + expect(spy).toHaveBeenCalled() + }) + }) + }) + + describe('::openLocations(locations) (called via IPC from browser process)', () => { + beforeEach(() => { + spyOn(atom.workspace, 'open') + atom.project.setPaths([]) + }) + + describe('when there is no saved state', () => { + beforeEach(() => spyOn(atom, 'loadState').andReturn(Promise.resolve(null))) + + describe('when the opened path exists', () => { + it("adds it to the project's paths", () => { + const pathToOpen = __filename + waitsForPromise(() => atom.openLocations([{pathToOpen}])) + runs(() => expect(atom.project.getPaths()[0]).toBe(__dirname)) + }) + + describe('then a second path is opened with forceAddToWindow', () => { + it("adds the second path to the project's paths", () => { + const firstPathToOpen = __dirname + const secondPathToOpen = path.resolve(__dirname, './fixtures') + waitsForPromise(() => atom.openLocations([{pathToOpen: firstPathToOpen}])) + waitsForPromise(() => atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])) + runs(() => expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen])) + }) + }) + }) + + describe('when the opened path does not exist but its parent directory does', () => { + it('adds the parent directory to the project paths', () => { + const pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') + waitsForPromise(() => atom.openLocations([{pathToOpen}])) + runs(() => expect(atom.project.getPaths()[0]).toBe(__dirname)) + }) + }) + + describe('when the opened path is a file', () => { + it('opens it in the workspace', () => { + const pathToOpen = __filename + waitsForPromise(() => atom.openLocations([{pathToOpen}])) + runs(() => expect(atom.workspace.open.mostRecentCall.args[0]).toBe(__filename)) + }) + }) + + describe('when the opened path is a directory', () => { + it('does not open it in the workspace', () => { + const pathToOpen = __dirname + waitsForPromise(() => atom.openLocations([{pathToOpen}])) + runs(() => expect(atom.workspace.open.callCount).toBe(0)) + }) + }) + + describe('when the opened path is a uri', () => { + it("adds it to the project's paths as is", () => { + const pathToOpen = 'remote://server:7644/some/dir/path' + spyOn(atom.project, 'addPath') + waitsForPromise(() => atom.openLocations([{pathToOpen}])) + runs(() => expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen)) + }) + }) + }) + + describe('when there is saved state for the relevant directories', () => { + const state = Symbol('savedState') + + beforeEach(() => { + spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':')) + spyOn(atom, 'loadState').andCallFake(function (key) { + if (key === __dirname) { return Promise.resolve(state) } else { return Promise.resolve(null) } + }) + spyOn(atom, 'attemptRestoreProjectStateForPaths') + }) + + describe('when there are no project folders', () => { + it('attempts to restore the project state', () => { + const pathToOpen = __dirname + waitsForPromise(() => atom.openLocations([{pathToOpen}])) + runs(() => { + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], []) + expect(atom.project.getPaths()).toEqual([]) + }) + }) + + it('opens the specified files', () => { + waitsForPromise(() => atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}])) + runs(() => { + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename]) + expect(atom.project.getPaths()).toEqual([]) + }) + }) + }) + + describe('when there are already project folders', () => { + beforeEach(() => atom.project.setPaths([__dirname])) + + it('does not attempt to restore the project state, instead adding the project paths', () => { + const pathToOpen = path.join(__dirname, 'fixtures') + waitsForPromise(() => atom.openLocations([{pathToOpen, forceAddToWindow: true}])) + runs(() => { + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen]) + }) + }) + + it('opens the specified files', () => { + const pathToOpen = path.join(__dirname, 'fixtures') + const fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt') + waitsForPromise(() => atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}])) + runs(() => { + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen]) + expect(atom.project.getPaths()).toEqual([__dirname]) + }) + }) + }) + }) + }) + + describe('::updateAvailable(info) (called via IPC from browser process)', () => { + let subscription + + afterEach(() => { + if (subscription) subscription.dispose() + }) + + it('invokes onUpdateAvailable listeners', () => { + if (process.platform !== 'darwin') return // Test tied to electron autoUpdater, we use something else on Linux and Win32 + + atom.listenForUpdates() + + const updateAvailableHandler = jasmine.createSpy('update-available-handler') + subscription = atom.onUpdateAvailable(updateAvailableHandler) + + const { autoUpdater } = require('electron').remote + autoUpdater.emit('update-downloaded', null, 'notes', 'version') + + waitsFor(() => updateAvailableHandler.callCount > 0) + + runs(() => { + const {releaseVersion} = updateAvailableHandler.mostRecentCall.args[0] + expect(releaseVersion).toBe('version') + }) + }) + }) + + describe('::getReleaseChannel()', () => { + let version + + beforeEach(() => { + spyOn(atom, 'getVersion').andCallFake(() => version) + }) + + it('returns the correct channel based on the version number', () => { + version = '1.5.6' + expect(atom.getReleaseChannel()).toBe('stable') + + version = '1.5.0-beta10' + expect(atom.getReleaseChannel()).toBe('beta') + + version = '1.7.0-dev-5340c91' + expect(atom.getReleaseChannel()).toBe('dev') + }) + }) +}) From 786f8b6a93ffdf7443490b82641939488cdeb0b0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 6 Nov 2017 17:51:49 -0800 Subject: [PATCH 286/301] Use await instead of waitsForPromise in atom-environment-spec --- spec/atom-environment-spec.js | 333 +++++++++++++++------------------- 1 file changed, 146 insertions(+), 187 deletions(-) diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js index 3095b94f0..b8d7e309a 100644 --- a/spec/atom-environment-spec.js +++ b/spec/atom-environment-spec.js @@ -1,3 +1,4 @@ +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') const _ = require('underscore-plus') const path = require('path') const temp = require('temp').track() @@ -29,11 +30,11 @@ describe('AtomEnvironment', () => { beforeEach(() => originalSize = atom.getSize()) afterEach(() => atom.setSize(originalSize.width, originalSize.height)) - it('sets the size of the window, and can retrieve the size just set', () => { + it('sets the size of the window, and can retrieve the size just set', async () => { const newWidth = originalSize.width - 12 const newHeight = originalSize.height - 23 - waitsForPromise(() => atom.setSize(newWidth, newHeight)) - runs(() => expect(atom.getSize()).toEqual({width: newWidth, height: newHeight})) + await atom.setSize(newWidth, newHeight) + expect(atom.getSize()).toEqual({width: newWidth, height: newHeight}) }) }) }) @@ -64,23 +65,24 @@ describe('AtomEnvironment', () => { spyOn(atom, 'executeJavaScriptInDevTools') }) - it('will open the dev tools when an error is triggered', () => { + it('will open the dev tools when an error is triggered', async () => { try { a + 1 } catch (e) { window.onerror.call(window, e.toString(), 'abc', 2, 3, e) } - waitsForPromise(() => devToolsPromise) - runs(() => { - expect(atom.openDevTools).toHaveBeenCalled() - expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled() - }) + await devToolsPromise + expect(atom.openDevTools).toHaveBeenCalled() + expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled() }) describe('::onWillThrowError', () => { let willThrowSpy = null - beforeEach(() => willThrowSpy = jasmine.createSpy()) + + beforeEach(() => { + willThrowSpy = jasmine.createSpy() + }) it('is called when there is an error', () => { let error = null @@ -197,39 +199,32 @@ describe('AtomEnvironment', () => { afterEach(() => atom.enablePersistence = false) - it('selects the state based on the current project paths', () => { + it('selects the state based on the current project paths', async () => { jasmine.useRealClock() const [dir1, dir2] = [temp.mkdirSync('dir1-'), temp.mkdirSync('dir2-')] - const loadSettings = _.extend(atom.getLoadSettings(), { + const loadSettings = Object.assign(atom.getLoadSettings(), { initialPaths: [dir1], windowState: null - } - ) + }) spyOn(atom, 'getLoadSettings').andCallFake(() => loadSettings) spyOn(atom, 'serialize').andReturn({stuff: 'cool'}) atom.project.setPaths([dir1, dir2]) + // State persistence will fail if other Atom instances are running - waitsForPromise(() => - atom.stateStore.connect().then(isConnected => expect(isConnected).toBe(true)) - ) + expect(await atom.stateStore.connect()).toBe(true) - waitsForPromise(() => - atom.saveState().then(() => - atom.loadState().then(state => expect(state).toBeFalsy()) - ) - ) + await atom.saveState() + expect(await atom.loadState()).toBeFalsy() - waitsForPromise(() => { - loadSettings.initialPaths = [dir2, dir1] - return atom.loadState().then(state => expect(state).toEqual({stuff: 'cool'})) - }) + loadSettings.initialPaths = [dir2, dir1] + expect(await atom.loadState()).toEqual({stuff: 'cool'}) }) - it("loads state from the storage folder when it can't be found in atom.stateStore", () => { + it("loads state from the storage folder when it can't be found in atom.stateStore", async () => { jasmine.useRealClock() const storageFolderState = {foo: 1, bar: 2} @@ -240,15 +235,12 @@ describe('AtomEnvironment', () => { spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync('config-directory'))) atom.project.setPaths(atom.getLoadSettings().initialPaths) - waitsForPromise(() => atom.stateStore.connect()) + await atom.stateStore.connect() + atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState) + expect(await atom.loadState()).toEqual(storageFolderState) - runs(() => atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState)) - - waitsForPromise(() => atom.loadState().then(state => expect(state).toEqual(storageFolderState))) - - waitsForPromise(() => atom.saveState()) - - waitsForPromise(() => atom.loadState().then(state => expect(state).toEqual(serializedState))) + await atom.saveState() + expect(await atom.loadState()).toEqual(serializedState) }) it('saves state when the CPU is idle after a keydown or mousedown event', () => { @@ -319,45 +311,38 @@ describe('AtomEnvironment', () => { atomEnv.destroy() }) - it('serializes the project state with all the options supplied in saveState', () => { + it('serializes the project state with all the options supplied in saveState', async () => { spyOn(atom.project, 'serialize').andReturn({foo: 42}) - waitsForPromise(() => atom.saveState({anyOption: 'any option'})) - runs(() => { - expect(atom.project.serialize.calls.length).toBe(1) - expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'}) - }) + await atom.saveState({anyOption: 'any option'}) + expect(atom.project.serialize.calls.length).toBe(1) + expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'}) }) - it('serializes the text editor registry', () => { - let editor = null + it('serializes the text editor registry', async () => { + const editor = await atom.workspace.open('sample.js') + atom.textEditors.setGrammarOverride(editor, 'text.plain') - waitsForPromise(() => atom.workspace.open('sample.js').then(e => editor = e)) - - waitsForPromise(() => { - atom.textEditors.setGrammarOverride(editor, 'text.plain') - - const atom2 = new AtomEnvironment({ - applicationDelegate: atom.applicationDelegate, - window: document.createElement('div'), - document: Object.assign( - document.createElement('div'), - { - body: document.createElement('div'), - head: document.createElement('div') - } - ) - }) - atom2.initialize({document, window}) - return atom2.deserialize(atom.serialize()).then(() => { - expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain') - return atom2.destroy() - }) + const atom2 = new AtomEnvironment({ + applicationDelegate: atom.applicationDelegate, + window: document.createElement('div'), + document: Object.assign( + document.createElement('div'), + { + body: document.createElement('div'), + head: document.createElement('div') + } + ) }) + atom2.initialize({document, window}) + + await atom2.deserialize(atom.serialize()) + expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain') + atom2.destroy() }) describe('deserialization failures', () => { - it('propagates project state restoration failures', () => { + it('propagates project state restoration failures', async () => { spyOn(atom.project, 'deserialize').andCallFake(() => { const err = new Error('deserialization failure') err.missingProjectPaths = ['/foo'] @@ -365,15 +350,13 @@ describe('AtomEnvironment', () => { }) spyOn(atom.notifications, 'addError') - waitsForPromise(() => atom.deserialize({project: 'should work'})) - runs(() => { - expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open project directory', { - description: 'Project directory `/foo` is no longer on disk.' - }) + await atom.deserialize({project: 'should work'}) + expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open project directory', { + description: 'Project directory `/foo` is no longer on disk.' }) }) - it('accumulates and reports two errors with one notification', () => { + it('accumulates and reports two errors with one notification', async () => { spyOn(atom.project, 'deserialize').andCallFake(() => { const err = new Error('deserialization failure') err.missingProjectPaths = ['/foo', '/wat'] @@ -381,15 +364,13 @@ describe('AtomEnvironment', () => { }) spyOn(atom.notifications, 'addError') - waitsForPromise(() => atom.deserialize({project: 'should work'})) - runs(() => { - expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 2 project directories', { - description: 'Project directories `/foo` and `/wat` are no longer on disk.' - }) + await atom.deserialize({project: 'should work'}) + expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 2 project directories', { + description: 'Project directories `/foo` and `/wat` are no longer on disk.' }) }) - it('accumulates and reports three+ errors with one notification', () => { + it('accumulates and reports three+ errors with one notification', async () => { spyOn(atom.project, 'deserialize').andCallFake(() => { const err = new Error('deserialization failure') err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things'] @@ -397,10 +378,10 @@ describe('AtomEnvironment', () => { }) spyOn(atom.notifications, 'addError') - waitsForPromise(() => atom.deserialize({project: 'should work'})) - runs(() => - expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 4 project directories', - {description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'})) + await atom.deserialize({project: 'should work'}) + expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 4 project directories', { + description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.' + }) }) }) }) @@ -416,7 +397,9 @@ describe('AtomEnvironment', () => { }) describe('when there is already a buffer open', () => { - beforeEach(() => waitsForPromise(() => atom.workspace.open())) + beforeEach(async () => { + await atom.workspace.open() + }) it('does not open an empty buffer', () => { spyOn(atom.workspace, 'open') @@ -454,15 +437,13 @@ describe('AtomEnvironment', () => { spyOn(atom, 'attemptRestoreProjectStateForPaths') }) - it('adds the selected folder to the project', () => { + it('adds the selected folder to the project', async () => { const initialPaths = atom.project.setPaths([]) const tempDirectory = temp.mkdirSync('a-new-directory') spyOn(atom, 'pickFolder').andCallFake(callback => callback([tempDirectory])) - waitsForPromise(() => atom.addProjectFolder()) - runs(() => { - expect(atom.project.getPaths()).toEqual([tempDirectory]) - expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() - }) + await atom.addProjectFolder() + expect(atom.project.getPaths()).toEqual([tempDirectory]) + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() }) }) @@ -471,34 +452,29 @@ describe('AtomEnvironment', () => { beforeEach(() => { spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':')) - spyOn(atom, 'loadState').andCallFake(function (key) { - if (key === __dirname) { return Promise.resolve(state) } else { return Promise.resolve(null) } - }) + spyOn(atom, 'loadState').andCallFake(async (key) => key === __dirname ? state : null) spyOn(atom, 'attemptRestoreProjectStateForPaths') spyOn(atom, 'pickFolder').andCallFake(callback => callback([__dirname])) atom.project.setPaths([]) }) describe('when there are no project folders', () => { - it('attempts to restore the project state', () => { - waitsForPromise(() => atom.addProjectFolder()) - runs(() => { - expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname]) - expect(atom.project.getPaths()).toEqual([]) - }) + it('attempts to restore the project state', async () => { + await atom.addProjectFolder() + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname]) + expect(atom.project.getPaths()).toEqual([]) }) }) describe('when there are already project folders', () => { const openedPath = path.join(__dirname, 'fixtures') + beforeEach(() => atom.project.setPaths([openedPath])) - it('does not attempt to restore the project state, instead adding the project paths', () => { - waitsForPromise(() => atom.addProjectFolder()) - runs(() => { - expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() - expect(atom.project.getPaths()).toEqual([openedPath, __dirname]) - }) + it('does not attempt to restore the project state, instead adding the project paths', async () => { + await atom.addProjectFolder() + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + expect(atom.project.getPaths()).toEqual([openedPath, __dirname]) }) }) }) @@ -506,10 +482,10 @@ describe('AtomEnvironment', () => { describe('attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)', () => { describe('when the window is clean (empty or has only unnamed, unmodified buffers)', () => { - beforeEach(() => + beforeEach(async () => { // Unnamed, unmodified buffer doesn't count toward "clean"-ness - waitsForPromise(() => atom.workspace.open()) - ) + await atom.workspace.open() + }) it('automatically restores the saved state into the current environment', () => { const state = Symbol() @@ -538,15 +514,12 @@ describe('AtomEnvironment', () => { }) describe('when the window is dirty', () => { - let editor = null + let editor - beforeEach(() => - waitsForPromise(() => atom.workspace.open().then(function (e) { - editor = e - editor.setText('new editor') - }) - ) - ) + beforeEach(async () => { + editor = await atom.workspace.open() + editor.setText('new editor') + }) describe('when a dock has a modified editor', () => { it('prompts the user to restore the state', () => { @@ -608,7 +581,7 @@ describe('AtomEnvironment', () => { }) describe('::destroy()', () => { - it('does not throw exceptions when unsubscribing from ipc events (regression)', () => { + it('does not throw exceptions when unsubscribing from ipc events (regression)', async () => { const configDirPath = temp.mkdirSync('atom-spec-environment') const fakeDocument = { addEventListener () {}, @@ -621,11 +594,9 @@ describe('AtomEnvironment', () => { spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve()) spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve()) spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve()) - waitsForPromise(() => atomEnvironment.startEditorWindow()) - runs(() => { - atomEnvironment.unloadEditorWindow() - atomEnvironment.destroy() - }) + await atomEnvironment.startEditorWindow() + atomEnvironment.unloadEditorWindow() + atomEnvironment.destroy() }) }) @@ -634,10 +605,10 @@ describe('AtomEnvironment', () => { beforeEach(() => { let resolve = null - const promise = new Promise(function (r) { resolve = r }) + const promise = new Promise((r) => { resolve = r }) envLoaded = () => { resolve() - waitsForPromise(() => promise) + promise } atomEnvironment = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate, @@ -649,20 +620,18 @@ describe('AtomEnvironment', () => { afterEach(() => atomEnvironment.destroy()) - it('is triggered once the shell environment is loaded', () => { + it('is triggered once the shell environment is loaded', async () => { atomEnvironment.whenShellEnvironmentLoaded(spy) atomEnvironment.updateProcessEnvAndTriggerHooks() - envLoaded() - runs(() => expect(spy).toHaveBeenCalled()) + await envLoaded() + expect(spy).toHaveBeenCalled() }) - it('triggers the callback immediately if the shell environment is already loaded', () => { + it('triggers the callback immediately if the shell environment is already loaded', async () => { atomEnvironment.updateProcessEnvAndTriggerHooks() - envLoaded() - runs(() => { - atomEnvironment.whenShellEnvironmentLoaded(spy) - expect(spy).toHaveBeenCalled() - }) + await envLoaded() + atomEnvironment.whenShellEnvironmentLoaded(spy) + expect(spy).toHaveBeenCalled() }) }) @@ -673,56 +642,58 @@ describe('AtomEnvironment', () => { }) describe('when there is no saved state', () => { - beforeEach(() => spyOn(atom, 'loadState').andReturn(Promise.resolve(null))) + beforeEach(() => { + spyOn(atom, 'loadState').andReturn(Promise.resolve(null)) + }) describe('when the opened path exists', () => { - it("adds it to the project's paths", () => { + it("adds it to the project's paths", async () => { const pathToOpen = __filename - waitsForPromise(() => atom.openLocations([{pathToOpen}])) - runs(() => expect(atom.project.getPaths()[0]).toBe(__dirname)) + await atom.openLocations([{pathToOpen}]) + expect(atom.project.getPaths()[0]).toBe(__dirname) }) describe('then a second path is opened with forceAddToWindow', () => { - it("adds the second path to the project's paths", () => { + it("adds the second path to the project's paths", async () => { const firstPathToOpen = __dirname const secondPathToOpen = path.resolve(__dirname, './fixtures') - waitsForPromise(() => atom.openLocations([{pathToOpen: firstPathToOpen}])) - waitsForPromise(() => atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])) - runs(() => expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen])) + await atom.openLocations([{pathToOpen: firstPathToOpen}]) + await atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}]) + expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen]) }) }) }) describe('when the opened path does not exist but its parent directory does', () => { - it('adds the parent directory to the project paths', () => { + it('adds the parent directory to the project paths', async () => { const pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt') - waitsForPromise(() => atom.openLocations([{pathToOpen}])) - runs(() => expect(atom.project.getPaths()[0]).toBe(__dirname)) + await atom.openLocations([{pathToOpen}]) + expect(atom.project.getPaths()[0]).toBe(__dirname) }) }) describe('when the opened path is a file', () => { - it('opens it in the workspace', () => { + it('opens it in the workspace', async () => { const pathToOpen = __filename - waitsForPromise(() => atom.openLocations([{pathToOpen}])) - runs(() => expect(atom.workspace.open.mostRecentCall.args[0]).toBe(__filename)) + await atom.openLocations([{pathToOpen}]) + expect(atom.workspace.open.mostRecentCall.args[0]).toBe(__filename) }) }) describe('when the opened path is a directory', () => { - it('does not open it in the workspace', () => { + it('does not open it in the workspace', async () => { const pathToOpen = __dirname - waitsForPromise(() => atom.openLocations([{pathToOpen}])) - runs(() => expect(atom.workspace.open.callCount).toBe(0)) + await atom.openLocations([{pathToOpen}]) + expect(atom.workspace.open.callCount).toBe(0) }) }) describe('when the opened path is a uri', () => { - it("adds it to the project's paths as is", () => { + it("adds it to the project's paths as is", async () => { const pathToOpen = 'remote://server:7644/some/dir/path' spyOn(atom.project, 'addPath') - waitsForPromise(() => atom.openLocations([{pathToOpen}])) - runs(() => expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen)) + await atom.openLocations([{pathToOpen}]) + expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen) }) }) }) @@ -739,44 +710,36 @@ describe('AtomEnvironment', () => { }) describe('when there are no project folders', () => { - it('attempts to restore the project state', () => { + it('attempts to restore the project state', async () => { const pathToOpen = __dirname - waitsForPromise(() => atom.openLocations([{pathToOpen}])) - runs(() => { - expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], []) - expect(atom.project.getPaths()).toEqual([]) - }) + await atom.openLocations([{pathToOpen}]) + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], []) + expect(atom.project.getPaths()).toEqual([]) }) - it('opens the specified files', () => { - waitsForPromise(() => atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}])) - runs(() => { - expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename]) - expect(atom.project.getPaths()).toEqual([]) - }) + it('opens the specified files', async () => { + await atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}]) + expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename]) + expect(atom.project.getPaths()).toEqual([]) }) }) describe('when there are already project folders', () => { beforeEach(() => atom.project.setPaths([__dirname])) - it('does not attempt to restore the project state, instead adding the project paths', () => { + it('does not attempt to restore the project state, instead adding the project paths', async () => { const pathToOpen = path.join(__dirname, 'fixtures') - waitsForPromise(() => atom.openLocations([{pathToOpen, forceAddToWindow: true}])) - runs(() => { - expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() - expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen]) - }) + await atom.openLocations([{pathToOpen, forceAddToWindow: true}]) + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled() + expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen]) }) - it('opens the specified files', () => { + it('opens the specified files', async () => { const pathToOpen = path.join(__dirname, 'fixtures') const fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt') - waitsForPromise(() => atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}])) - runs(() => { - expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen]) - expect(atom.project.getPaths()).toEqual([__dirname]) - }) + await atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}]) + expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen]) + expect(atom.project.getPaths()).toEqual([__dirname]) }) }) }) @@ -789,23 +752,19 @@ describe('AtomEnvironment', () => { if (subscription) subscription.dispose() }) - it('invokes onUpdateAvailable listeners', () => { + it('invokes onUpdateAvailable listeners', async () => { if (process.platform !== 'darwin') return // Test tied to electron autoUpdater, we use something else on Linux and Win32 + const updateAvailablePromise = new Promise(resolve => { + subscription = atom.onUpdateAvailable(resolve) + }) + atom.listenForUpdates() - - const updateAvailableHandler = jasmine.createSpy('update-available-handler') - subscription = atom.onUpdateAvailable(updateAvailableHandler) - - const { autoUpdater } = require('electron').remote + const {autoUpdater} = require('electron').remote autoUpdater.emit('update-downloaded', null, 'notes', 'version') - waitsFor(() => updateAvailableHandler.callCount > 0) - - runs(() => { - const {releaseVersion} = updateAvailableHandler.mostRecentCall.args[0] - expect(releaseVersion).toBe('version') - }) + const {releaseVersion} = await updateAvailablePromise + expect(releaseVersion).toBe('version') }) }) From 843f91c8d2b5d436da2de5a64fbed42f1757a61b Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Tue, 7 Nov 2017 17:02:25 -0700 Subject: [PATCH 287/301] :arrow_up: text-buffer@13.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e87d07cb0..05ceaf931 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.8.1", + "text-buffer": "13.8.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 156002c4188db6afa69c39d59fff6bf9af974082 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 8 Nov 2017 07:15:53 -0800 Subject: [PATCH 288/301] :arrow_up: language-typescript --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05ceaf931..ea9ed93ad 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,7 @@ "language-text": "0.7.3", "language-todo": "0.29.3", "language-toml": "0.18.1", - "language-typescript": "0.2.2", + "language-typescript": "0.2.3", "language-xml": "0.35.2", "language-yaml": "0.31.1" }, From 7bae4e73241072ad5dc948ba6814c2b5dcba54af Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2017 11:37:16 -0800 Subject: [PATCH 289/301] Use dedent for multiline template strings in text-editor-spec --- spec/text-editor-spec.js | 111 +++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 84eea43ef..79b1e37b6 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -3321,13 +3321,13 @@ describe('TextEditor', () => { beforeEach(() => { editor.setSoftWrapped(true) editor.setEditorWidthInChars(80) - editor.setText(`\ -1 -2 -Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. -3 -4\ -`) + editor.setText(dedent ` + 1 + 2 + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. + 3 + 4 + `) }) it('moves the lines past the soft wrapped line', () => { @@ -5881,21 +5881,20 @@ Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh editor.duplicateLines() - expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe(`\ -\ if (items.length <= 1) return items; - if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - }\ -` - ) + expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe(dedent ` + if (items.length <= 1) return items; + if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + }\ + `.split('\n').map(l => ` ${l}`).join('\n')) expect(editor.getSelectedBufferRanges()).toEqual([[[3, 5], [3, 5]], [[9, 0], [14, 0]]]) // folds are also duplicated @@ -5911,42 +5910,40 @@ Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh editor.duplicateLines() - expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe(`\ -\ if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - }\ -` - ) + expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe(dedent` + if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + `.split('\n').map(l => ` ${l}`).join('\n')) expect(editor.getSelectedBufferRange()).toEqual([[8, 0], [8, 0]]) }) it('can duplicate the last line of the buffer', () => { editor.setSelectedBufferRange([[11, 0], [12, 2]]) editor.duplicateLines() - expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe(`\ -\ return sort(Array.apply(this, arguments)); -}; - return sort(Array.apply(this, arguments)); -};\ -` - ) + expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe(' ' + dedent ` + return sort(Array.apply(this, arguments)); + }; + return sort(Array.apply(this, arguments)); + }; + `.trim()) expect(editor.getSelectedBufferRange()).toEqual([[13, 0], [14, 2]]) }) it('only duplicates lines containing multiple selections once', () => { - editor.setText(`\ -aaaaaa -bbbbbb -cccccc -dddddd\ -`) + editor.setText(dedent ` + aaaaaa + bbbbbb + cccccc + dddddd + `) editor.setSelectedBufferRanges([ [[0, 1], [0, 2]], [[0, 3], [0, 4]], @@ -5955,15 +5952,15 @@ dddddd\ [[3, 3], [3, 4]] ]) editor.duplicateLines() - expect(editor.getText()).toBe(`\ -aaaaaa -aaaaaa -bbbbbb -cccccc -dddddd -cccccc -dddddd\ -`) + expect(editor.getText()).toBe(dedent ` + aaaaaa + aaaaaa + bbbbbb + cccccc + dddddd + cccccc + dddddd + `) expect(editor.getSelectedBufferRanges()).toEqual([ [[1, 1], [1, 2]], [[1, 3], [1, 4]], From ff8ecf1a4999294ed7e30ac77d66a17da892108c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2017 14:00:00 -0800 Subject: [PATCH 290/301] Fix errors when passing subword regex to native find methods --- package.json | 2 +- spec/text-editor-spec.js | 29 +++++++++++++++++++++ src/cursor.js | 55 +++++++++++++++++++++------------------- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index ea9ed93ad..89651c465 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.8.2", + "text-buffer": "13.8.3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 79b1e37b6..fa8406731 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -1077,6 +1077,20 @@ describe('TextEditor', () => { expect(editor.getCursorBufferPosition()).toEqual([0, 1]) }) + it('stops at camelCase boundaries with non-ascii characters', () => { + editor.setText(' gétÁrevìôüsWord\n') + editor.setCursorBufferPosition([0, 16]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 12]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + it('skips consecutive non-word characters', () => { editor.setText('e, => \n') editor.setCursorBufferPosition([0, 6]) @@ -1102,6 +1116,21 @@ describe('TextEditor', () => { expect(editor.getCursorBufferPosition()).toEqual([0, 2]) }) + it('skips consecutive uppercase non-ascii letters', () => { + editor.setText(' ÀÁÅDF \n') + editor.setCursorBufferPosition([0, 7]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.setText('ALPhA\n') + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + it('skips consecutive numbers', () => { editor.setText(' 88 \n') editor.setCursorBufferPosition([0, 4]) diff --git a/src/cursor.js b/src/cursor.js index 10bdef804..181eeb971 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -454,23 +454,25 @@ class Cursor extends Model { getPreviousWordBoundaryBufferPosition (options = {}) { const currentBufferPosition = this.getBufferPosition() const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row) - const scanRange = [[previousNonBlankRow || 0, 0], currentBufferPosition] + const scanRange = Range(Point(previousNonBlankRow || 0, 0), currentBufferPosition) - let beginningOfWordPosition - this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => { + const ranges = this.editor.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + scanRange + ) + + const range = ranges[ranges.length - 1] + if (range) { if (range.start.row < currentBufferPosition.row && currentBufferPosition.column > 0) { - // force it to stop at the beginning of each line - beginningOfWordPosition = new Point(currentBufferPosition.row, 0) - } else if (range.end.isLessThan(currentBufferPosition)) { - beginningOfWordPosition = range.end + return Point(currentBufferPosition.row, 0) + } else if (currentBufferPosition.isGreaterThan(range.end)) { + return Point.fromObject(range.end) } else { - beginningOfWordPosition = range.start + return Point.fromObject(range.start) } - - if (!beginningOfWordPosition.isEqual(currentBufferPosition)) stop() - }) - - return beginningOfWordPosition || currentBufferPosition + } else { + return currentBufferPosition + } } // Public: Returns buffer position of the next word boundary. It might be on @@ -481,23 +483,24 @@ class Cursor extends Model { // (default: {::wordRegExp}) getNextWordBoundaryBufferPosition (options = {}) { const currentBufferPosition = this.getBufferPosition() - const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()] + const scanRange = Range(currentBufferPosition, this.editor.getEofBufferPosition()) - let endOfWordPosition - this.editor.scanInBufferRange((options.wordRegex != null ? options.wordRegex : this.wordRegExp()), scanRange, function ({range, stop}) { + const range = this.editor.buffer.findInRangeSync( + options.wordRegex || this.wordRegExp(), + scanRange + ) + + if (range) { if (range.start.row > currentBufferPosition.row) { - // force it to stop at the beginning of each line - endOfWordPosition = new Point(range.start.row, 0) - } else if (range.start.isGreaterThan(currentBufferPosition)) { - endOfWordPosition = range.start + return Point(range.start.row, 0) + } else if (currentBufferPosition.isLessThan(range.start)) { + return Point.fromObject(range.start) } else { - endOfWordPosition = range.end + return Point.fromObject(range.end) } - - if (!endOfWordPosition.isEqual(currentBufferPosition)) stop() - }) - - return endOfWordPosition || currentBufferPosition + } else { + return currentBufferPosition + } } // Public: Retrieves the buffer position of where the current word starts. From fb74992454f9cfa1027523a3448036e07fc67f20 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 8 Nov 2017 17:44:32 -0500 Subject: [PATCH 291/301] Enhance test to catch bug reported in #16135 Enhance the fake jQuery object to more closely match a real jQuery object. With this change, the test fails, thus allowing us to reproduce the regression reported in #16135. --- spec/tooltip-manager-spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/tooltip-manager-spec.js b/spec/tooltip-manager-spec.js index 65587839f..3a6b56a1b 100644 --- a/spec/tooltip-manager-spec.js +++ b/spec/tooltip-manager-spec.js @@ -108,8 +108,12 @@ describe('TooltipManager', () => { const element2 = document.createElement('div') jasmine.attachToDOM(element2) - const fakeJqueryWrapper = [element, element2] - fakeJqueryWrapper.jquery = 'any-version' + const fakeJqueryWrapper = { + 0: element, + 1: element2, + length: 2, + jquery: 'any-version' + } const disposable = manager.add(fakeJqueryWrapper, {title: 'Title'}) hover(element, () => expect(document.body.querySelector('.tooltip')).toHaveText('Title')) From 0e82b8bb42ed9ca4a6a12497ffd0622fff0d45e8 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Wed, 8 Nov 2017 17:56:45 -0500 Subject: [PATCH 292/301] :bug: Fix #16135 --- src/tooltip-manager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js index 937f831d1..34f96775b 100644 --- a/src/tooltip-manager.js +++ b/src/tooltip-manager.js @@ -114,7 +114,9 @@ class TooltipManager { add (target, options) { if (target.jquery) { const disposable = new CompositeDisposable() - for (const element of target) { disposable.add(this.add(element, options)) } + for (let i = 0; i < target.length; i++) { + disposable.add(this.add(target[i], options)) + } return disposable } From bc774773f74557f50fc9b039ec90cdacf9412341 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2017 15:28:21 -0800 Subject: [PATCH 293/301] Convert AtomEnvironment to JS --- spec/atom-environment-spec.js | 23 +- src/atom-environment.coffee | 1168 ---------------------------- src/atom-environment.js | 1351 +++++++++++++++++++++++++++++++++ 3 files changed, 1353 insertions(+), 1189 deletions(-) delete mode 100644 src/atom-environment.coffee create mode 100644 src/atom-environment.js diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js index b8d7e309a..84b415eab 100644 --- a/spec/atom-environment-spec.js +++ b/spec/atom-environment-spec.js @@ -224,25 +224,6 @@ describe('AtomEnvironment', () => { expect(await atom.loadState()).toEqual({stuff: 'cool'}) }) - it("loads state from the storage folder when it can't be found in atom.stateStore", async () => { - jasmine.useRealClock() - - const storageFolderState = {foo: 1, bar: 2} - const serializedState = {someState: 42} - const loadSettings = _.extend(atom.getLoadSettings(), {initialPaths: [temp.mkdirSync('project-directory')]}) - spyOn(atom, 'getLoadSettings').andReturn(loadSettings) - spyOn(atom, 'serialize').andReturn(serializedState) - spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync('config-directory'))) - atom.project.setPaths(atom.getLoadSettings().initialPaths) - - await atom.stateStore.connect() - atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState) - expect(await atom.loadState()).toEqual(storageFolderState) - - await atom.saveState() - expect(await atom.loadState()).toEqual(serializedState) - }) - it('saves state when the CPU is idle after a keydown or mousedown event', () => { const atomEnv = new AtomEnvironment({ applicationDelegate: global.atom.applicationDelegate @@ -488,7 +469,7 @@ describe('AtomEnvironment', () => { }) it('automatically restores the saved state into the current environment', () => { - const state = Symbol() + const state = {} spyOn(atom.workspace, 'open') spyOn(atom, 'restoreStateIntoThisEnvironment') @@ -505,7 +486,7 @@ describe('AtomEnvironment', () => { getTitle () { return 'title' }, element: document.createElement('div') }) - const state = Symbol() + const state = {} spyOn(atom, 'confirm') atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename]) expect(atom.confirm).not.toHaveBeenCalled() diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee deleted file mode 100644 index 50b5d541e..000000000 --- a/src/atom-environment.coffee +++ /dev/null @@ -1,1168 +0,0 @@ -crypto = require 'crypto' -path = require 'path' -{ipcRenderer} = require 'electron' - -_ = require 'underscore-plus' -{deprecate} = require 'grim' -{CompositeDisposable, Disposable, Emitter} = require 'event-kit' -fs = require 'fs-plus' -{mapSourcePosition} = require '@atom/source-map-support' -Model = require './model' -WindowEventHandler = require './window-event-handler' -StateStore = require './state-store' -StorageFolder = require './storage-folder' -registerDefaultCommands = require './register-default-commands' -{updateProcessEnv} = require './update-process-env' -ConfigSchema = require './config-schema' - -DeserializerManager = require './deserializer-manager' -ViewRegistry = require './view-registry' -NotificationManager = require './notification-manager' -Config = require './config' -KeymapManager = require './keymap-extensions' -TooltipManager = require './tooltip-manager' -CommandRegistry = require './command-registry' -URIHandlerRegistry = require './uri-handler-registry' -GrammarRegistry = require './grammar-registry' -{HistoryManager, HistoryProject} = require './history-manager' -ReopenProjectMenuManager = require './reopen-project-menu-manager' -StyleManager = require './style-manager' -PackageManager = require './package-manager' -ThemeManager = require './theme-manager' -MenuManager = require './menu-manager' -ContextMenuManager = require './context-menu-manager' -CommandInstaller = require './command-installer' -CoreURIHandlers = require './core-uri-handlers' -ProtocolHandlerInstaller = require './protocol-handler-installer' -Project = require './project' -TitleBar = require './title-bar' -Workspace = require './workspace' -PanelContainer = require './panel-container' -Panel = require './panel' -PaneContainer = require './pane-container' -PaneAxis = require './pane-axis' -Pane = require './pane' -Dock = require './dock' -TextEditor = require './text-editor' -TextBuffer = require 'text-buffer' -Gutter = require './gutter' -TextEditorRegistry = require './text-editor-registry' -AutoUpdateManager = require './auto-update-manager' - -# Essential: Atom global for dealing with packages, themes, menus, and the window. -# -# An instance of this class is always available as the `atom` global. -module.exports = -class AtomEnvironment extends Model - @version: 1 # Increment this when the serialization format changes - - lastUncaughtError: null - - ### - Section: Properties - ### - - # Public: A {CommandRegistry} instance - commands: null - - # Public: A {Config} instance - config: null - - # Public: A {Clipboard} instance - clipboard: null - - # Public: A {ContextMenuManager} instance - contextMenu: null - - # Public: A {MenuManager} instance - menu: null - - # Public: A {KeymapManager} instance - keymaps: null - - # Public: A {TooltipManager} instance - tooltips: null - - # Public: A {NotificationManager} instance - notifications: null - - # Public: A {Project} instance - project: null - - # Public: A {GrammarRegistry} instance - grammars: null - - # Public: A {HistoryManager} instance - history: null - - # Public: A {PackageManager} instance - packages: null - - # Public: A {ThemeManager} instance - themes: null - - # Public: A {StyleManager} instance - styles: null - - # Public: A {DeserializerManager} instance - deserializers: null - - # Public: A {ViewRegistry} instance - views: null - - # Public: A {Workspace} instance - workspace: null - - # Public: A {TextEditorRegistry} instance - textEditors: null - - # Private: An {AutoUpdateManager} instance - autoUpdater: null - - saveStateDebounceInterval: 1000 - - ### - Section: Construction and Destruction - ### - - # Call .loadOrCreate instead - constructor: (params={}) -> - {@applicationDelegate, @clipboard, @enablePersistence, onlyLoadBaseStyleSheets, @updateProcessEnv} = params - - @nextProxyRequestId = 0 - @unloaded = false - @loadTime = null - @emitter = new Emitter - @disposables = new CompositeDisposable - @deserializers = new DeserializerManager(this) - @deserializeTimings = {} - @views = new ViewRegistry(this) - TextEditor.setScheduler(@views) - @notifications = new NotificationManager - @updateProcessEnv ?= updateProcessEnv # For testing - - @stateStore = new StateStore('AtomEnvironments', 1) - - @config = new Config({notificationManager: @notifications, @enablePersistence}) - @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} - - @keymaps = new KeymapManager({notificationManager: @notifications}) - @tooltips = new TooltipManager(keymapManager: @keymaps, viewRegistry: @views) - @commands = new CommandRegistry - @uriHandlerRegistry = new URIHandlerRegistry - @grammars = new GrammarRegistry({@config}) - @styles = new StyleManager() - @packages = new PackageManager({ - @config, styleManager: @styles, - commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications, - grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views, - uriHandlerRegistry: @uriHandlerRegistry - }) - @themes = new ThemeManager({ - packageManager: @packages, @config, styleManager: @styles, - notificationManager: @notifications, viewRegistry: @views - }) - @menu = new MenuManager({keymapManager: @keymaps, packageManager: @packages}) - @contextMenu = new ContextMenuManager({keymapManager: @keymaps}) - @packages.setMenuManager(@menu) - @packages.setContextMenuManager(@contextMenu) - @packages.setThemeManager(@themes) - - @project = new Project({notificationManager: @notifications, packageManager: @packages, @config, @applicationDelegate}) - @commandInstaller = new CommandInstaller(@applicationDelegate) - @protocolHandlerInstaller = new ProtocolHandlerInstaller() - - @textEditors = new TextEditorRegistry({ - @config, grammarRegistry: @grammars, assert: @assert.bind(this), - packageManager: @packages - }) - - @workspace = new Workspace({ - @config, @project, packageManager: @packages, grammarRegistry: @grammars, deserializerManager: @deserializers, - notificationManager: @notifications, @applicationDelegate, viewRegistry: @views, assert: @assert.bind(this), - textEditorRegistry: @textEditors, styleManager: @styles, @enablePersistence - }) - - @themes.workspace = @workspace - - @autoUpdater = new AutoUpdateManager({@applicationDelegate}) - - if @keymaps.canLoadBundledKeymapsFromMemory() - @keymaps.loadBundledKeymaps() - - @registerDefaultCommands() - @registerDefaultOpeners() - @registerDefaultDeserializers() - - @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate}) - - @history = new HistoryManager({@project, @commands, @stateStore}) - # Keep instances of HistoryManager in sync - @disposables.add @history.onDidChangeProjects (e) => - @applicationDelegate.didChangeHistoryManager() unless e.reloaded - - initialize: (params={}) -> - # This will force TextEditorElement to register the custom element, so that - # using `document.createElement('atom-text-editor')` works if it's called - # before opening a buffer. - require './text-editor-element' - - {@window, @document, @blobStore, @configDirPath, onlyLoadBaseStyleSheets} = params - {devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings() - - if clearWindowState - @getStorageFolder().clear() - @stateStore.clear() - - ConfigSchema.projectHome = { - type: 'string', - default: path.join(fs.getHomeDirectory(), 'github'), - description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' - } - @config.initialize({@configDirPath, resourcePath, projectHomeSchema: ConfigSchema.projectHome}) - - @menu.initialize({resourcePath}) - @contextMenu.initialize({resourcePath, devMode}) - - @keymaps.configDirPath = @configDirPath - @keymaps.resourcePath = resourcePath - @keymaps.devMode = devMode - unless @keymaps.canLoadBundledKeymapsFromMemory() - @keymaps.loadBundledKeymaps() - - @commands.attach(@window) - - @styles.initialize({@configDirPath}) - @packages.initialize({devMode, @configDirPath, resourcePath, safeMode}) - @themes.initialize({@configDirPath, resourcePath, safeMode, devMode}) - - @commandInstaller.initialize(@getVersion()) - @protocolHandlerInstaller.initialize(@config, @notifications) - @uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) - @autoUpdater.initialize() - - @config.load() - - @themes.loadBaseStylesheets() - @initialStyleElements = @styles.getSnapshot() - @themes.initialLoadComplete = true if onlyLoadBaseStyleSheets - @setBodyPlatformClass() - - @stylesElement = @styles.buildStylesElement() - @document.head.appendChild(@stylesElement) - - @keymaps.subscribeToFileReadFailure() - - @installUncaughtErrorHandler() - @attachSaveStateListeners() - @windowEventHandler.initialize(@window, @document) - - didChangeStyles = @didChangeStyles.bind(this) - @disposables.add(@styles.onDidAddStyleElement(didChangeStyles)) - @disposables.add(@styles.onDidUpdateStyleElement(didChangeStyles)) - @disposables.add(@styles.onDidRemoveStyleElement(didChangeStyles)) - - @observeAutoHideMenuBar() - - @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) - - preloadPackages: -> - @packages.preloadPackages() - - attachSaveStateListeners: -> - saveState = _.debounce((=> - @window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded - ), @saveStateDebounceInterval) - @document.addEventListener('mousedown', saveState, true) - @document.addEventListener('keydown', saveState, true) - @disposables.add new Disposable => - @document.removeEventListener('mousedown', saveState, true) - @document.removeEventListener('keydown', saveState, true) - - registerDefaultDeserializers: -> - @deserializers.add(Workspace) - @deserializers.add(PaneContainer) - @deserializers.add(PaneAxis) - @deserializers.add(Pane) - @deserializers.add(Dock) - @deserializers.add(Project) - @deserializers.add(TextEditor) - @deserializers.add(TextBuffer) - - registerDefaultCommands: -> - registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications, @project, @clipboard}) - - registerDefaultOpeners: -> - @workspace.addOpener (uri) => - switch uri - when 'atom://.atom/stylesheet' - @workspace.openTextFile(@styles.getUserStyleSheetPath()) - when 'atom://.atom/keymap' - @workspace.openTextFile(@keymaps.getUserKeymapPath()) - when 'atom://.atom/config' - @workspace.openTextFile(@config.getUserConfigPath()) - when 'atom://.atom/init-script' - @workspace.openTextFile(@getUserInitScriptPath()) - - registerDefaultTargetForKeymaps: -> - @keymaps.defaultTarget = @workspace.getElement() - - observeAutoHideMenuBar: -> - @disposables.add @config.onDidChange 'core.autoHideMenuBar', ({newValue}) => - @setAutoHideMenuBar(newValue) - @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar') - - reset: -> - @deserializers.clear() - @registerDefaultDeserializers() - - @config.clear() - @config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)} - - @keymaps.clear() - @keymaps.loadBundledKeymaps() - - @commands.clear() - @registerDefaultCommands() - - @styles.restoreSnapshot(@initialStyleElements) - - @menu.clear() - - @clipboard.reset() - - @notifications.clear() - - @contextMenu.clear() - - @packages.reset().then => - @workspace.reset(@packages) - @registerDefaultOpeners() - @project.reset(@packages) - @workspace.subscribeToEvents() - @grammars.clear() - @textEditors.clear() - @views.clear() - - destroy: -> - return if not @project - - @disposables.dispose() - @workspace?.destroy() - @workspace = null - @themes.workspace = null - @project?.destroy() - @project = null - @commands.clear() - @stylesElement.remove() - @config.unobserveUserConfig() - @autoUpdater.destroy() - @uriHandlerRegistry.destroy() - - @uninstallWindowEventHandler() - - ### - Section: Event Subscription - ### - - # Extended: Invoke the given callback whenever {::beep} is called. - # - # * `callback` {Function} to be called whenever {::beep} is called. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidBeep: (callback) -> - @emitter.on 'did-beep', callback - - # Extended: Invoke the given callback when there is an unhandled error, but - # before the devtools pop open - # - # * `callback` {Function} to be called whenever there is an unhandled error - # * `event` {Object} - # * `originalError` {Object} the original error object - # * `message` {String} the original error object - # * `url` {String} Url to the file where the error originated. - # * `line` {Number} - # * `column` {Number} - # * `preventDefault` {Function} call this to avoid popping up the dev tools. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillThrowError: (callback) -> - @emitter.on 'will-throw-error', callback - - # Extended: Invoke the given callback whenever there is an unhandled error. - # - # * `callback` {Function} to be called whenever there is an unhandled error - # * `event` {Object} - # * `originalError` {Object} the original error object - # * `message` {String} the original error object - # * `url` {String} Url to the file where the error originated. - # * `line` {Number} - # * `column` {Number} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidThrowError: (callback) -> - @emitter.on 'did-throw-error', callback - - # TODO: Make this part of the public API. We should make onDidThrowError - # match the interface by only yielding an exception object to the handler - # and deprecating the old behavior. - onDidFailAssertion: (callback) -> - @emitter.on 'did-fail-assertion', callback - - # Extended: Invoke the given callback as soon as the shell environment is - # loaded (or immediately if it was already loaded). - # - # * `callback` {Function} to be called whenever there is an unhandled error - whenShellEnvironmentLoaded: (callback) -> - if @shellEnvironmentLoaded - callback() - new Disposable() - else - @emitter.once 'loaded-shell-environment', callback - - ### - Section: Atom Details - ### - - # Public: Returns a {Boolean} that is `true` if the current window is in development mode. - inDevMode: -> - @devMode ?= @getLoadSettings().devMode - - # Public: Returns a {Boolean} that is `true` if the current window is in safe mode. - inSafeMode: -> - @safeMode ?= @getLoadSettings().safeMode - - # Public: Returns a {Boolean} that is `true` if the current window is running specs. - inSpecMode: -> - @specMode ?= @getLoadSettings().isSpec - - # Returns a {Boolean} indicating whether this the first time the window's been - # loaded. - isFirstLoad: -> - @firstLoad ?= @getLoadSettings().firstLoad - - # Public: Get the version of the Atom application. - # - # Returns the version text {String}. - getVersion: -> - @appVersion ?= @getLoadSettings().appVersion - - # Public: Gets the release channel of the Atom application. - # - # Returns the release channel as a {String}. Will return one of `dev`, `beta`, or `stable`. - getReleaseChannel: -> - version = @getVersion() - if version.indexOf('beta') > -1 - 'beta' - else if version.indexOf('dev') > -1 - 'dev' - else - 'stable' - - # Public: Returns a {Boolean} that is `true` if the current version is an official release. - isReleasedVersion: -> - not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix - - # Public: Get the time taken to completely load the current window. - # - # This time include things like loading and activating packages, creating - # DOM elements for the editor, and reading the config. - # - # Returns the {Number} of milliseconds taken to load the window or null - # if the window hasn't finished loading yet. - getWindowLoadTime: -> - @loadTime - - # Public: Get the load settings for the current window. - # - # Returns an {Object} containing all the load setting key/value pairs. - getLoadSettings: -> - @applicationDelegate.getWindowLoadSettings() - - ### - Section: Managing The Atom Window - ### - - # Essential: Open a new Atom window using the given options. - # - # Calling this method without an options parameter will open a prompt to pick - # a file/folder to open in the new window. - # - # * `params` An {Object} with the following keys: - # * `pathsToOpen` An {Array} of {String} paths to open. - # * `newWindow` A {Boolean}, true to always open a new window instead of - # reusing existing windows depending on the paths to open. - # * `devMode` A {Boolean}, true to open the window in development mode. - # Development mode loads the Atom source from the locally cloned - # repository and also loads all the packages in ~/.atom/dev/packages - # * `safeMode` A {Boolean}, true to open the window in safe mode. Safe - # mode prevents all packages installed to ~/.atom/packages from loading. - open: (params) -> - @applicationDelegate.open(params) - - # Extended: Prompt the user to select one or more folders. - # - # * `callback` A {Function} to call once the user has confirmed the selection. - # * `paths` An {Array} of {String} paths that the user selected, or `null` - # if the user dismissed the dialog. - pickFolder: (callback) -> - @applicationDelegate.pickFolder(callback) - - # Essential: Close the current window. - close: -> - @applicationDelegate.closeWindow() - - # Essential: Get the size of current window. - # - # Returns an {Object} in the format `{width: 1000, height: 700}` - getSize: -> - @applicationDelegate.getWindowSize() - - # Essential: Set the size of current window. - # - # * `width` The {Number} of pixels. - # * `height` The {Number} of pixels. - setSize: (width, height) -> - @applicationDelegate.setWindowSize(width, height) - - # Essential: Get the position of current window. - # - # Returns an {Object} in the format `{x: 10, y: 20}` - getPosition: -> - @applicationDelegate.getWindowPosition() - - # Essential: Set the position of current window. - # - # * `x` The {Number} of pixels. - # * `y` The {Number} of pixels. - setPosition: (x, y) -> - @applicationDelegate.setWindowPosition(x, y) - - # Extended: Get the current window - getCurrentWindow: -> - @applicationDelegate.getCurrentWindow() - - # Extended: Move current window to the center of the screen. - center: -> - @applicationDelegate.centerWindow() - - # Extended: Focus the current window. - focus: -> - @applicationDelegate.focusWindow() - @window.focus() - - # Extended: Show the current window. - show: -> - @applicationDelegate.showWindow() - - # Extended: Hide the current window. - hide: -> - @applicationDelegate.hideWindow() - - # Extended: Reload the current window. - reload: -> - @applicationDelegate.reloadWindow() - - # Extended: Relaunch the entire application. - restartApplication: -> - @applicationDelegate.restartApplication() - - # Extended: Returns a {Boolean} that is `true` if the current window is maximized. - isMaximized: -> - @applicationDelegate.isWindowMaximized() - - maximize: -> - @applicationDelegate.maximizeWindow() - - # Extended: Returns a {Boolean} that is `true` if the current window is in full screen mode. - isFullScreen: -> - @applicationDelegate.isWindowFullScreen() - - # Extended: Set the full screen state of the current window. - setFullScreen: (fullScreen=false) -> - @applicationDelegate.setWindowFullScreen(fullScreen) - - # Extended: Toggle the full screen state of the current window. - toggleFullScreen: -> - @setFullScreen(not @isFullScreen()) - - # Restore the window to its previous dimensions and show it. - # - # Restores the full screen and maximized state after the window has resized to - # prevent resize glitches. - displayWindow: -> - @restoreWindowDimensions().then => - steps = [ - @restoreWindowBackground(), - @show(), - @focus() - ] - steps.push(@setFullScreen(true)) if @windowDimensions?.fullScreen - steps.push(@maximize()) if @windowDimensions?.maximized and process.platform isnt 'darwin' - Promise.all(steps) - - # Get the dimensions of this window. - # - # Returns an {Object} with the following keys: - # * `x` The window's x-position {Number}. - # * `y` The window's y-position {Number}. - # * `width` The window's width {Number}. - # * `height` The window's height {Number}. - getWindowDimensions: -> - browserWindow = @getCurrentWindow() - [x, y] = browserWindow.getPosition() - [width, height] = browserWindow.getSize() - maximized = browserWindow.isMaximized() - {x, y, width, height, maximized} - - # Set the dimensions of the window. - # - # The window will be centered if either the x or y coordinate is not set - # in the dimensions parameter. If x or y are omitted the window will be - # centered. If height or width are omitted only the position will be changed. - # - # * `dimensions` An {Object} with the following keys: - # * `x` The new x coordinate. - # * `y` The new y coordinate. - # * `width` The new width. - # * `height` The new height. - setWindowDimensions: ({x, y, width, height}) -> - steps = [] - if width? and height? - steps.push(@setSize(width, height)) - if x? and y? - steps.push(@setPosition(x, y)) - else - steps.push(@center()) - Promise.all(steps) - - # Returns true if the dimensions are useable, false if they should be ignored. - # Work around for https://github.com/atom/atom-shell/issues/473 - isValidDimensions: ({x, y, width, height}={}) -> - width > 0 and height > 0 and x + width > 0 and y + height > 0 - - storeWindowDimensions: -> - @windowDimensions = @getWindowDimensions() - if @isValidDimensions(@windowDimensions) - localStorage.setItem("defaultWindowDimensions", JSON.stringify(@windowDimensions)) - - getDefaultWindowDimensions: -> - {windowDimensions} = @getLoadSettings() - return windowDimensions if windowDimensions? - - dimensions = null - try - dimensions = JSON.parse(localStorage.getItem("defaultWindowDimensions")) - catch error - console.warn "Error parsing default window dimensions", error - localStorage.removeItem("defaultWindowDimensions") - - if @isValidDimensions(dimensions) - dimensions - else - {width, height} = @applicationDelegate.getPrimaryDisplayWorkAreaSize() - {x: 0, y: 0, width: Math.min(1024, width), height} - - restoreWindowDimensions: -> - unless @windowDimensions? and @isValidDimensions(@windowDimensions) - @windowDimensions = @getDefaultWindowDimensions() - @setWindowDimensions(@windowDimensions).then => @windowDimensions - - restoreWindowBackground: -> - if backgroundColor = window.localStorage.getItem('atom:window-background-color') - @backgroundStylesheet = document.createElement('style') - @backgroundStylesheet.type = 'text/css' - @backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }' - document.head.appendChild(@backgroundStylesheet) - - storeWindowBackground: -> - return if @inSpecMode() - - backgroundColor = @window.getComputedStyle(@workspace.getElement())['background-color'] - @window.localStorage.setItem('atom:window-background-color', backgroundColor) - - # Call this method when establishing a real application window. - startEditorWindow: -> - @unloaded = false - - updateProcessEnvPromise = @updateProcessEnvAndTriggerHooks() - - loadStatePromise = @loadState().then (state) => - @windowDimensions = state?.windowDimensions - @displayWindow().then => - @commandInstaller.installAtomCommand false, (error) -> - console.warn error.message if error? - @commandInstaller.installApmCommand false, (error) -> - console.warn error.message if error? - - @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this))) - @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this))) - @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this))) - @disposables.add(@applicationDelegate.onURIMessage(@dispatchURIMessage.bind(this))) - @disposables.add @applicationDelegate.onDidRequestUnload => - @saveState({isUnloading: true}) - .catch(console.error) - .then => - @workspace?.confirmClose({ - windowCloseRequested: true, - projectHasPaths: @project.getPaths().length > 0 - }) - .then (closing) => - if closing - @packages.deactivatePackages().then -> closing - else - closing - - @listenForUpdates() - - @registerDefaultTargetForKeymaps() - - @packages.loadPackages() - - startTime = Date.now() - @deserialize(state).then => - @deserializeTimings.atom = Date.now() - startTime - - if process.platform is 'darwin' and @config.get('core.titleBar') is 'custom' - @workspace.addHeaderPanel({item: new TitleBar({@workspace, @themes, @applicationDelegate})}) - @document.body.classList.add('custom-title-bar') - if process.platform is 'darwin' and @config.get('core.titleBar') is 'custom-inset' - @workspace.addHeaderPanel({item: new TitleBar({@workspace, @themes, @applicationDelegate})}) - @document.body.classList.add('custom-inset-title-bar') - if process.platform is 'darwin' and @config.get('core.titleBar') is 'hidden' - @document.body.classList.add('hidden-title-bar') - - @document.body.appendChild(@workspace.getElement()) - @backgroundStylesheet?.remove() - - @watchProjectPaths() - - @packages.activate() - @keymaps.loadUserKeymap() - @requireUserInitScript() unless @getLoadSettings().safeMode - - @menu.update() - - @openInitialEmptyEditorIfNecessary() - - loadHistoryPromise = @history.loadState().then => - @reopenProjectMenuManager = new ReopenProjectMenuManager({ - @menu, @commands, @history, @config, - open: (paths) => @open(pathsToOpen: paths) - }) - @reopenProjectMenuManager.update() - - Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) - - serialize: (options) -> - version: @constructor.version - project: @project.serialize(options) - workspace: @workspace.serialize() - packageStates: @packages.serialize() - grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath} - fullScreen: @isFullScreen() - windowDimensions: @windowDimensions - textEditors: @textEditors.serialize() - - unloadEditorWindow: -> - return if not @project - - @storeWindowBackground() - @saveBlobStoreSync() - @unloaded = true - - saveBlobStoreSync: -> - if @enablePersistence - @blobStore.save() - - openInitialEmptyEditorIfNecessary: -> - return unless @config.get('core.openEmptyEditorOnStart') - if @getLoadSettings().initialPaths?.length is 0 and @workspace.getPaneItems().length is 0 - @workspace.open(null) - - installUncaughtErrorHandler: -> - @previousWindowErrorHandler = @window.onerror - @window.onerror = => - @lastUncaughtError = Array::slice.call(arguments) - [message, url, line, column, originalError] = @lastUncaughtError - - {line, column, source} = mapSourcePosition({source: url, line, column}) - - if url is '' - url = source - - eventObject = {message, url, line, column, originalError} - - openDevTools = true - eventObject.preventDefault = -> openDevTools = false - - @emitter.emit 'will-throw-error', eventObject - - if openDevTools - @openDevTools().then => @executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")') - - @emitter.emit 'did-throw-error', {message, url, line, column, originalError} - - uninstallUncaughtErrorHandler: -> - @window.onerror = @previousWindowErrorHandler - - installWindowEventHandler: -> - @windowEventHandler = new WindowEventHandler({atomEnvironment: this, @applicationDelegate}) - @windowEventHandler.initialize(@window, @document) - - uninstallWindowEventHandler: -> - @windowEventHandler?.unsubscribe() - @windowEventHandler = null - - didChangeStyles: (styleElement) -> - TextEditor.didUpdateStyles() - if styleElement.textContent.indexOf('scrollbar') >= 0 - TextEditor.didUpdateScrollbarStyles() - - updateProcessEnvAndTriggerHooks: -> - @updateProcessEnv(@getLoadSettings().env).then => - @shellEnvironmentLoaded = true - @emitter.emit('loaded-shell-environment') - @packages.triggerActivationHook('core:loaded-shell-environment') - - ### - Section: Messaging the User - ### - - # Essential: Visually and audibly trigger a beep. - beep: -> - @applicationDelegate.playBeepSound() if @config.get('core.audioBeep') - @emitter.emit 'did-beep' - - # Essential: A flexible way to open a dialog akin to an alert dialog. - # - # If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button - # the first button will be clicked unless a "Cancel" or "No" button is provided. - # - # ## Examples - # - # ```coffee - # atom.confirm - # message: 'How you feeling?' - # detailedMessage: 'Be honest.' - # buttons: - # Good: -> window.alert('good to hear') - # Bad: -> window.alert('bummer') - # ``` - # - # * `options` An {Object} with the following keys: - # * `message` The {String} message to display. - # * `detailedMessage` (optional) The {String} detailed message to display. - # * `buttons` (optional) Either an array of strings or an object where keys are - # button names and the values are callbacks to invoke when clicked. - # - # Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object. - confirm: (params={}) -> - @applicationDelegate.confirm(params) - - ### - Section: Managing the Dev Tools - ### - - # Extended: Open the dev tools for the current window. - # - # Returns a {Promise} that resolves when the DevTools have been opened. - openDevTools: -> - @applicationDelegate.openWindowDevTools() - - # Extended: Toggle the visibility of the dev tools for the current window. - # - # Returns a {Promise} that resolves when the DevTools have been opened or - # closed. - toggleDevTools: -> - @applicationDelegate.toggleWindowDevTools() - - # Extended: Execute code in dev tools. - executeJavaScriptInDevTools: (code) -> - @applicationDelegate.executeJavaScriptInWindowDevTools(code) - - ### - Section: Private - ### - - assert: (condition, message, callbackOrMetadata) -> - return true if condition - - error = new Error("Assertion failed: #{message}") - Error.captureStackTrace(error, @assert) - - if callbackOrMetadata? - if typeof callbackOrMetadata is 'function' - callbackOrMetadata?(error) - else - error.metadata = callbackOrMetadata - - @emitter.emit 'did-fail-assertion', error - unless @isReleasedVersion() - throw error - - false - - loadThemes: -> - @themes.load() - - # Notify the browser project of the window's current project path - watchProjectPaths: -> - @disposables.add @project.onDidChangePaths => - @applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths()) - - setDocumentEdited: (edited) -> - @applicationDelegate.setWindowDocumentEdited?(edited) - - setRepresentedFilename: (filename) -> - @applicationDelegate.setWindowRepresentedFilename?(filename) - - addProjectFolder: -> - @pickFolder (selectedPaths = []) => - @addToProject(selectedPaths) - - addToProject: (projectPaths) -> - @loadState(@getStateKey(projectPaths)).then (state) => - if state and @project.getPaths().length is 0 - @attemptRestoreProjectStateForPaths(state, projectPaths) - else - @project.addPath(folder) for folder in projectPaths - - attemptRestoreProjectStateForPaths: (state, projectPaths, filesToOpen = []) -> - center = @workspace.getCenter() - windowIsUnused = => - for container in @workspace.getPaneContainers() - for item in container.getPaneItems() - if item instanceof TextEditor - return false if item.getPath() or item.isModified() - else - return false if container is center - true - - if windowIsUnused() - @restoreStateIntoThisEnvironment(state) - Promise.all (@workspace.open(file) for file in filesToOpen) - else - nouns = if projectPaths.length is 1 then 'folder' else 'folders' - btn = @confirm - message: 'Previous automatically-saved project state detected' - detailedMessage: "There is previously saved state for the selected #{nouns}. " + - "Would you like to add the #{nouns} to this window, permanently discarding the saved state, " + - "or open the #{nouns} in a new window, restoring the saved state?" - buttons: [ - '&Open in new window and recover state' - '&Add to this window and discard state' - ] - if btn is 0 - @open - pathsToOpen: projectPaths.concat(filesToOpen) - newWindow: true - devMode: @inDevMode() - safeMode: @inSafeMode() - Promise.resolve(null) - else if btn is 1 - @project.addPath(selectedPath) for selectedPath in projectPaths - Promise.all (@workspace.open(file) for file in filesToOpen) - - restoreStateIntoThisEnvironment: (state) -> - state.fullScreen = @isFullScreen() - pane.destroy() for pane in @workspace.getPanes() - @deserialize(state) - - showSaveDialog: (callback) -> - callback(@showSaveDialogSync()) - - showSaveDialogSync: (options={}) -> - @applicationDelegate.showSaveDialog(options) - - saveState: (options, storageKey) -> - new Promise (resolve, reject) => - if @enablePersistence and @project - state = @serialize(options) - savePromise = - if storageKey ?= @getStateKey(@project?.getPaths()) - @stateStore.save(storageKey, state) - else - @applicationDelegate.setTemporaryWindowState(state) - savePromise.catch(reject).then(resolve) - else - resolve() - - loadState: (stateKey) -> - if @enablePersistence - if stateKey ?= @getStateKey(@getLoadSettings().initialPaths) - @stateStore.load(stateKey).then (state) => - if state - state - else - # TODO: remove this when every user has migrated to the IndexedDb state store. - @getStorageFolder().load(stateKey) - else - @applicationDelegate.getTemporaryWindowState() - else - Promise.resolve(null) - - deserialize: (state) -> - return Promise.resolve() unless state? - - if grammarOverridesByPath = state.grammars?.grammarOverridesByPath - @grammars.grammarOverridesByPath = grammarOverridesByPath - - @setFullScreen(state.fullScreen) - - missingProjectPaths = [] - - @packages.packageStates = state.packageStates ? {} - - startTime = Date.now() - if state.project? - projectPromise = @project.deserialize(state.project, @deserializers) - .catch (err) => - if err.missingProjectPaths? - missingProjectPaths.push(err.missingProjectPaths...) - else - @notifications.addError "Unable to deserialize project", description: err.message, stack: err.stack - else - projectPromise = Promise.resolve() - - projectPromise.then => - @deserializeTimings.project = Date.now() - startTime - - @textEditors.deserialize(state.textEditors) if state.textEditors - - startTime = Date.now() - @workspace.deserialize(state.workspace, @deserializers) if state.workspace? - @deserializeTimings.workspace = Date.now() - startTime - - if missingProjectPaths.length > 0 - count = if missingProjectPaths.length is 1 then '' else missingProjectPaths.length + ' ' - noun = if missingProjectPaths.length is 1 then 'directory' else 'directories' - toBe = if missingProjectPaths.length is 1 then 'is' else 'are' - escaped = missingProjectPaths.map (projectPath) -> "`#{projectPath}`" - group = switch escaped.length - when 1 then escaped[0] - when 2 then "#{escaped[0]} and #{escaped[1]}" - else escaped[..-2].join(", ") + ", and #{escaped[escaped.length - 1]}" - - @notifications.addError "Unable to open #{count}project #{noun}", - description: "Project #{noun} #{group} #{toBe} no longer on disk." - - getStateKey: (paths) -> - if paths?.length > 0 - sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex') - "editor-#{sha1}" - else - null - - getStorageFolder: -> - @storageFolder ?= new StorageFolder(@getConfigDirPath()) - - getConfigDirPath: -> - @configDirPath ?= process.env.ATOM_HOME - - getUserInitScriptPath: -> - initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee']) - initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee') - - requireUserInitScript: -> - if userInitScriptPath = @getUserInitScriptPath() - try - require(userInitScriptPath) if fs.isFileSync(userInitScriptPath) - catch error - @notifications.addError "Failed to load `#{userInitScriptPath}`", - detail: error.message - dismissable: true - - # TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead - onUpdateAvailable: (callback) -> - @emitter.on 'update-available', callback - - updateAvailable: (details) -> - @emitter.emit 'update-available', details - - listenForUpdates: -> - # listen for updates available locally (that have been successfully downloaded) - @disposables.add(@autoUpdater.onDidCompleteDownloadingUpdate(@updateAvailable.bind(this))) - - setBodyPlatformClass: -> - @document.body.classList.add("platform-#{process.platform}") - - setAutoHideMenuBar: (autoHide) -> - @applicationDelegate.setAutoHideWindowMenuBar(autoHide) - @applicationDelegate.setWindowMenuBarVisibility(not autoHide) - - dispatchApplicationMenuCommand: (command, arg) -> - activeElement = @document.activeElement - # Use the workspace element if body has focus - if activeElement is @document.body - activeElement = @workspace.getElement() - @commands.dispatch(activeElement, command, arg) - - dispatchContextMenuCommand: (command, args...) -> - @commands.dispatch(@contextMenu.activeElement, command, args) - - dispatchURIMessage: (uri) -> - if @packages.hasLoadedInitialPackages() - @uriHandlerRegistry.handleURI(uri) - else - sub = @packages.onDidLoadInitialPackages -> - sub.dispose() - @uriHandlerRegistry.handleURI(uri) - - openLocations: (locations) -> - needsProjectPaths = @project?.getPaths().length is 0 - - foldersToAddToProject = [] - fileLocationsToOpen = [] - - pushFolderToOpen = (folder) -> - if folder not in foldersToAddToProject - foldersToAddToProject.push(folder) - - for {pathToOpen, initialLine, initialColumn, forceAddToWindow} in locations - if pathToOpen? and (needsProjectPaths or forceAddToWindow) - if fs.existsSync(pathToOpen) - pushFolderToOpen @project.getDirectoryForProjectPath(pathToOpen).getPath() - else if fs.existsSync(path.dirname(pathToOpen)) - pushFolderToOpen @project.getDirectoryForProjectPath(path.dirname(pathToOpen)).getPath() - else - pushFolderToOpen @project.getDirectoryForProjectPath(pathToOpen).getPath() - - unless fs.isDirectorySync(pathToOpen) - fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn}) - - promise = Promise.resolve(null) - if foldersToAddToProject.length > 0 - promise = @loadState(@getStateKey(foldersToAddToProject)).then (state) => - if state and needsProjectPaths # only load state if this is the first path added to the project - files = (location.pathToOpen for location in fileLocationsToOpen) - @attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) - else - promises = [] - @project.addPath(folder) for folder in foldersToAddToProject - for {pathToOpen, initialLine, initialColumn} in fileLocationsToOpen - promises.push @workspace?.open(pathToOpen, {initialLine, initialColumn}) - Promise.all(promises) - else - promises = [] - for {pathToOpen, initialLine, initialColumn} in fileLocationsToOpen - promises.push @workspace?.open(pathToOpen, {initialLine, initialColumn}) - promise = Promise.all(promises) - - promise.then -> - ipcRenderer.send 'window-command', 'window:locations-opened' - - resolveProxy: (url) -> - return new Promise (resolve, reject) => - requestId = @nextProxyRequestId++ - disposable = @applicationDelegate.onDidResolveProxy (id, proxy) -> - if id is requestId - disposable.dispose() - resolve(proxy) - - @applicationDelegate.resolveProxy(requestId, url) - -# Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. -Promise.prototype.done = (callback) -> - deprecate("Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done") - @then(callback) diff --git a/src/atom-environment.js b/src/atom-environment.js new file mode 100644 index 000000000..1c2f1ebcf --- /dev/null +++ b/src/atom-environment.js @@ -0,0 +1,1351 @@ +const crypto = require('crypto') +const path = require('path') +const {ipcRenderer} = require('electron') + +const _ = require('underscore-plus') +const {deprecate} = require('grim') +const {CompositeDisposable, Disposable, Emitter} = require('event-kit') +const fs = require('fs-plus') +const {mapSourcePosition} = require('@atom/source-map-support') +const WindowEventHandler = require('./window-event-handler') +const StateStore = require('./state-store') +const StorageFolder = require('./storage-folder') +const registerDefaultCommands = require('./register-default-commands') +const {updateProcessEnv} = require('./update-process-env') +const ConfigSchema = require('./config-schema') + +const DeserializerManager = require('./deserializer-manager') +const ViewRegistry = require('./view-registry') +const NotificationManager = require('./notification-manager') +const Config = require('./config') +const KeymapManager = require('./keymap-extensions') +const TooltipManager = require('./tooltip-manager') +const CommandRegistry = require('./command-registry') +const URIHandlerRegistry = require('./uri-handler-registry') +const GrammarRegistry = require('./grammar-registry') +const {HistoryManager} = require('./history-manager') +const ReopenProjectMenuManager = require('./reopen-project-menu-manager') +const StyleManager = require('./style-manager') +const PackageManager = require('./package-manager') +const ThemeManager = require('./theme-manager') +const MenuManager = require('./menu-manager') +const ContextMenuManager = require('./context-menu-manager') +const CommandInstaller = require('./command-installer') +const CoreURIHandlers = require('./core-uri-handlers') +const ProtocolHandlerInstaller = require('./protocol-handler-installer') +const Project = require('./project') +const TitleBar = require('./title-bar') +const Workspace = require('./workspace') +const PaneContainer = require('./pane-container') +const PaneAxis = require('./pane-axis') +const Pane = require('./pane') +const Dock = require('./dock') +const TextEditor = require('./text-editor') +const TextBuffer = require('text-buffer') +const TextEditorRegistry = require('./text-editor-registry') +const AutoUpdateManager = require('./auto-update-manager') + +let nextId = 0 + +// Essential: Atom global for dealing with packages, themes, menus, and the window. +// +// An instance of this class is always available as the `atom` global. +class AtomEnvironment { + /* + Section: Construction and Destruction + */ + + // Call .loadOrCreate instead + constructor (params = {}) { + this.id = (params.id != null) ? params.id : nextId++ + this.clipboard = params.clipboard + this.updateProcessEnv = params.updateProcessEnv || updateProcessEnv + this.enablePersistence = params.enablePersistence + this.applicationDelegate = params.applicationDelegate + + this.nextProxyRequestId = 0 + this.unloaded = false + this.loadTime = null + this.emitter = new Emitter() + this.disposables = new CompositeDisposable() + this.deserializers = new DeserializerManager(this) + this.deserializeTimings = {} + this.views = new ViewRegistry(this) + TextEditor.setScheduler(this.views) + this.notifications = new NotificationManager() + + this.stateStore = new StateStore('AtomEnvironments', 1) + + this.config = new Config({ + notificationManager: this.notifications, + enablePersistence: this.enablePersistence + }) + this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)}) + + this.keymaps = new KeymapManager({notificationManager: this.notifications}) + this.tooltips = new TooltipManager({keymapManager: this.keymaps, viewRegistry: this.views}) + this.commands = new CommandRegistry() + this.uriHandlerRegistry = new URIHandlerRegistry() + this.grammars = new GrammarRegistry({config: this.config}) + this.styles = new StyleManager() + this.packages = new PackageManager({ + config: this.config, + styleManager: this.styles, + commandRegistry: this.commands, + keymapManager: this.keymaps, + notificationManager: this.notifications, + grammarRegistry: this.grammars, + deserializerManager: this.deserializers, + viewRegistry: this.views, + uriHandlerRegistry: this.uriHandlerRegistry + }) + this.themes = new ThemeManager({ + packageManager: this.packages, + config: this.config, + styleManager: this.styles, + notificationManager: this.notifications, + viewRegistry: this.views + }) + this.menu = new MenuManager({keymapManager: this.keymaps, packageManager: this.packages}) + this.contextMenu = new ContextMenuManager({keymapManager: this.keymaps}) + this.packages.setMenuManager(this.menu) + this.packages.setContextMenuManager(this.contextMenu) + this.packages.setThemeManager(this.themes) + + this.project = new Project({notificationManager: this.notifications, packageManager: this.packages, config: this.config, applicationDelegate: this.applicationDelegate}) + this.commandInstaller = new CommandInstaller(this.applicationDelegate) + this.protocolHandlerInstaller = new ProtocolHandlerInstaller() + + this.textEditors = new TextEditorRegistry({ + config: this.config, + grammarRegistry: this.grammars, + assert: this.assert.bind(this), + packageManager: this.packages + }) + + this.workspace = new Workspace({ + config: this.config, + project: this.project, + packageManager: this.packages, + grammarRegistry: this.grammars, + deserializerManager: this.deserializers, + notificationManager: this.notifications, + applicationDelegate: this.applicationDelegate, + viewRegistry: this.views, + assert: this.assert.bind(this), + textEditorRegistry: this.textEditors, + styleManager: this.styles, + enablePersistence: this.enablePersistence + }) + + this.themes.workspace = this.workspace + + this.autoUpdater = new AutoUpdateManager({applicationDelegate: this.applicationDelegate}) + + if (this.keymaps.canLoadBundledKeymapsFromMemory()) { + this.keymaps.loadBundledKeymaps() + } + + this.registerDefaultCommands() + this.registerDefaultOpeners() + this.registerDefaultDeserializers() + + this.windowEventHandler = new WindowEventHandler({atomEnvironment: this, applicationDelegate: this.applicationDelegate}) + + this.history = new HistoryManager({project: this.project, commands: this.commands, stateStore: this.stateStore}) + // Keep instances of HistoryManager in sync + this.disposables.add(this.history.onDidChangeProjects(event => { + if (!event.reloaded) this.applicationDelegate.didChangeHistoryManager() + })) + } + + initialize (params = {}) { + // This will force TextEditorElement to register the custom element, so that + // using `document.createElement('atom-text-editor')` works if it's called + // before opening a buffer. + require('./text-editor-element') + + this.window = params.window + this.document = params.document + this.blobStore = params.blobStore + this.configDirPath = params.configDirPath + + const {devMode, safeMode, resourcePath, clearWindowState} = this.getLoadSettings() + + if (clearWindowState) { + this.getStorageFolder().clear() + this.stateStore.clear() + } + + ConfigSchema.projectHome = { + type: 'string', + default: path.join(fs.getHomeDirectory(), 'github'), + description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' + } + this.config.initialize({configDirPath: this.configDirPath, resourcePath, projectHomeSchema: ConfigSchema.projectHome}) + + this.menu.initialize({resourcePath}) + this.contextMenu.initialize({resourcePath, devMode}) + + this.keymaps.configDirPath = this.configDirPath + this.keymaps.resourcePath = resourcePath + this.keymaps.devMode = devMode + if (!this.keymaps.canLoadBundledKeymapsFromMemory()) { + this.keymaps.loadBundledKeymaps() + } + + this.commands.attach(this.window) + + this.styles.initialize({configDirPath: this.configDirPath}) + this.packages.initialize({devMode, configDirPath: this.configDirPath, resourcePath, safeMode}) + this.themes.initialize({configDirPath: this.configDirPath, resourcePath, safeMode, devMode}) + + this.commandInstaller.initialize(this.getVersion()) + this.protocolHandlerInstaller.initialize(this.config, this.notifications) + this.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this)) + this.autoUpdater.initialize() + + this.config.load() + + this.themes.loadBaseStylesheets() + this.initialStyleElements = this.styles.getSnapshot() + if (params.onlyLoadBaseStyleSheets) this.themes.initialLoadComplete = true + this.setBodyPlatformClass() + + this.stylesElement = this.styles.buildStylesElement() + this.document.head.appendChild(this.stylesElement) + + this.keymaps.subscribeToFileReadFailure() + + this.installUncaughtErrorHandler() + this.attachSaveStateListeners() + this.windowEventHandler.initialize(this.window, this.document) + + const didChangeStyles = this.didChangeStyles.bind(this) + this.disposables.add(this.styles.onDidAddStyleElement(didChangeStyles)) + this.disposables.add(this.styles.onDidUpdateStyleElement(didChangeStyles)) + this.disposables.add(this.styles.onDidRemoveStyleElement(didChangeStyles)) + + this.observeAutoHideMenuBar() + + this.disposables.add(this.applicationDelegate.onDidChangeHistoryManager(() => this.history.loadState())) + } + + preloadPackages () { + return this.packages.preloadPackages() + } + + attachSaveStateListeners () { + const saveState = _.debounce(() => { + this.window.requestIdleCallback(() => { + if (!this.unloaded) this.saveState({isUnloading: false}) + }) + }, this.saveStateDebounceInterval) + this.document.addEventListener('mousedown', saveState, true) + this.document.addEventListener('keydown', saveState, true) + this.disposables.add(new Disposable(() => { + this.document.removeEventListener('mousedown', saveState, true) + this.document.removeEventListener('keydown', saveState, true) + })) + } + + registerDefaultDeserializers () { + this.deserializers.add(Workspace) + this.deserializers.add(PaneContainer) + this.deserializers.add(PaneAxis) + this.deserializers.add(Pane) + this.deserializers.add(Dock) + this.deserializers.add(Project) + this.deserializers.add(TextEditor) + this.deserializers.add(TextBuffer) + } + + registerDefaultCommands () { + registerDefaultCommands({commandRegistry: this.commands, config: this.config, commandInstaller: this.commandInstaller, notificationManager: this.notifications, project: this.project, clipboard: this.clipboard}) + } + + registerDefaultOpeners () { + this.workspace.addOpener(uri => { + switch (uri) { + case 'atom://.atom/stylesheet': + return this.workspace.openTextFile(this.styles.getUserStyleSheetPath()) + case 'atom://.atom/keymap': + return this.workspace.openTextFile(this.keymaps.getUserKeymapPath()) + case 'atom://.atom/config': + return this.workspace.openTextFile(this.config.getUserConfigPath()) + case 'atom://.atom/init-script': + return this.workspace.openTextFile(this.getUserInitScriptPath()) + } + }) + } + + registerDefaultTargetForKeymaps () { + this.keymaps.defaultTarget = this.workspace.getElement() + } + + observeAutoHideMenuBar () { + this.disposables.add(this.config.onDidChange('core.autoHideMenuBar', ({newValue}) => { + this.setAutoHideMenuBar(newValue) + })) + if (this.config.get('core.autoHideMenuBar')) this.setAutoHideMenuBar(true) + } + + reset () { + this.deserializers.clear() + this.registerDefaultDeserializers() + + this.config.clear() + this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)}) + + this.keymaps.clear() + this.keymaps.loadBundledKeymaps() + + this.commands.clear() + this.registerDefaultCommands() + + this.styles.restoreSnapshot(this.initialStyleElements) + + this.menu.clear() + + this.clipboard.reset() + + this.notifications.clear() + + this.contextMenu.clear() + + return this.packages.reset().then(() => { + this.workspace.reset(this.packages) + this.registerDefaultOpeners() + this.project.reset(this.packages) + this.workspace.subscribeToEvents() + this.grammars.clear() + this.textEditors.clear() + this.views.clear() + }) + } + + destroy () { + if (!this.project) return + + this.disposables.dispose() + if (this.workspace) this.workspace.destroy() + this.workspace = null + this.themes.workspace = null + if (this.project) this.project.destroy() + this.project = null + this.commands.clear() + this.stylesElement.remove() + this.config.unobserveUserConfig() + this.autoUpdater.destroy() + this.uriHandlerRegistry.destroy() + + this.uninstallWindowEventHandler() + } + + /* + Section: Event Subscription + */ + + // Extended: Invoke the given callback whenever {::beep} is called. + // + // * `callback` {Function} to be called whenever {::beep} is called. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidBeep (callback) { + return this.emitter.on('did-beep', callback) + } + + // Extended: Invoke the given callback when there is an unhandled error, but + // before the devtools pop open + // + // * `callback` {Function} to be called whenever there is an unhandled error + // * `event` {Object} + // * `originalError` {Object} the original error object + // * `message` {String} the original error object + // * `url` {String} Url to the file where the error originated. + // * `line` {Number} + // * `column` {Number} + // * `preventDefault` {Function} call this to avoid popping up the dev tools. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillThrowError (callback) { + return this.emitter.on('will-throw-error', callback) + } + + // Extended: Invoke the given callback whenever there is an unhandled error. + // + // * `callback` {Function} to be called whenever there is an unhandled error + // * `event` {Object} + // * `originalError` {Object} the original error object + // * `message` {String} the original error object + // * `url` {String} Url to the file where the error originated. + // * `line` {Number} + // * `column` {Number} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidThrowError (callback) { + return this.emitter.on('did-throw-error', callback) + } + + // TODO: Make this part of the public API. We should make onDidThrowError + // match the interface by only yielding an exception object to the handler + // and deprecating the old behavior. + onDidFailAssertion (callback) { + return this.emitter.on('did-fail-assertion', callback) + } + + // Extended: Invoke the given callback as soon as the shell environment is + // loaded (or immediately if it was already loaded). + // + // * `callback` {Function} to be called whenever there is an unhandled error + whenShellEnvironmentLoaded (callback) { + if (this.shellEnvironmentLoaded) { + callback() + return new Disposable() + } else { + return this.emitter.once('loaded-shell-environment', callback) + } + } + + /* + Section: Atom Details + */ + + // Public: Returns a {Boolean} that is `true` if the current window is in development mode. + inDevMode () { + if (this.devMode == null) this.devMode = this.getLoadSettings().devMode + return this.devMode + } + + // Public: Returns a {Boolean} that is `true` if the current window is in safe mode. + inSafeMode () { + if (this.safeMode == null) this.safeMode = this.getLoadSettings().safeMode + return this.safeMode + } + + // Public: Returns a {Boolean} that is `true` if the current window is running specs. + inSpecMode () { + if (this.specMode == null) this.specMode = this.getLoadSettings().isSpec + return this.specMode + } + + // Returns a {Boolean} indicating whether this the first time the window's been + // loaded. + isFirstLoad () { + if (this.firstLoad == null) this.firstLoad = this.getLoadSettings().firstLoad + return this.firstLoad + } + + // Public: Get the version of the Atom application. + // + // Returns the version text {String}. + getVersion () { + if (this.appVersion == null) this.appVersion = this.getLoadSettings().appVersion + return this.appVersion + } + + // Public: Gets the release channel of the Atom application. + // + // Returns the release channel as a {String}. Will return one of `dev`, `beta`, or `stable`. + getReleaseChannel () { + const version = this.getVersion() + if (version.includes('beta')) { + return 'beta' + } else if (version.includes('dev')) { + return 'dev' + } else { + return 'stable' + } + } + + // Public: Returns a {Boolean} that is `true` if the current version is an official release. + isReleasedVersion () { + return !/\w{7}/.test(this.getVersion()) // Check if the release is a 7-character SHA prefix + } + + // Public: Get the time taken to completely load the current window. + // + // This time include things like loading and activating packages, creating + // DOM elements for the editor, and reading the config. + // + // Returns the {Number} of milliseconds taken to load the window or null + // if the window hasn't finished loading yet. + getWindowLoadTime () { + return this.loadTime + } + + // Public: Get the load settings for the current window. + // + // Returns an {Object} containing all the load setting key/value pairs. + getLoadSettings () { + return this.applicationDelegate.getWindowLoadSettings() + } + + /* + Section: Managing The Atom Window + */ + + // Essential: Open a new Atom window using the given options. + // + // Calling this method without an options parameter will open a prompt to pick + // a file/folder to open in the new window. + // + // * `params` An {Object} with the following keys: + // * `pathsToOpen` An {Array} of {String} paths to open. + // * `newWindow` A {Boolean}, true to always open a new window instead of + // reusing existing windows depending on the paths to open. + // * `devMode` A {Boolean}, true to open the window in development mode. + // Development mode loads the Atom source from the locally cloned + // repository and also loads all the packages in ~/.atom/dev/packages + // * `safeMode` A {Boolean}, true to open the window in safe mode. Safe + // mode prevents all packages installed to ~/.atom/packages from loading. + open (params) { + return this.applicationDelegate.open(params) + } + + // Extended: Prompt the user to select one or more folders. + // + // * `callback` A {Function} to call once the user has confirmed the selection. + // * `paths` An {Array} of {String} paths that the user selected, or `null` + // if the user dismissed the dialog. + pickFolder (callback) { + return this.applicationDelegate.pickFolder(callback) + } + + // Essential: Close the current window. + close () { + return this.applicationDelegate.closeWindow() + } + + // Essential: Get the size of current window. + // + // Returns an {Object} in the format `{width: 1000, height: 700}` + getSize () { + return this.applicationDelegate.getWindowSize() + } + + // Essential: Set the size of current window. + // + // * `width` The {Number} of pixels. + // * `height` The {Number} of pixels. + setSize (width, height) { + return this.applicationDelegate.setWindowSize(width, height) + } + + // Essential: Get the position of current window. + // + // Returns an {Object} in the format `{x: 10, y: 20}` + getPosition () { + return this.applicationDelegate.getWindowPosition() + } + + // Essential: Set the position of current window. + // + // * `x` The {Number} of pixels. + // * `y` The {Number} of pixels. + setPosition (x, y) { + return this.applicationDelegate.setWindowPosition(x, y) + } + + // Extended: Get the current window + getCurrentWindow () { + return this.applicationDelegate.getCurrentWindow() + } + + // Extended: Move current window to the center of the screen. + center () { + return this.applicationDelegate.centerWindow() + } + + // Extended: Focus the current window. + focus () { + this.applicationDelegate.focusWindow() + return this.window.focus() + } + + // Extended: Show the current window. + show () { + return this.applicationDelegate.showWindow() + } + + // Extended: Hide the current window. + hide () { + return this.applicationDelegate.hideWindow() + } + + // Extended: Reload the current window. + reload () { + return this.applicationDelegate.reloadWindow() + } + + // Extended: Relaunch the entire application. + restartApplication () { + return this.applicationDelegate.restartApplication() + } + + // Extended: Returns a {Boolean} that is `true` if the current window is maximized. + isMaximized () { + return this.applicationDelegate.isWindowMaximized() + } + + maximize () { + return this.applicationDelegate.maximizeWindow() + } + + // Extended: Returns a {Boolean} that is `true` if the current window is in full screen mode. + isFullScreen () { + return this.applicationDelegate.isWindowFullScreen() + } + + // Extended: Set the full screen state of the current window. + setFullScreen (fullScreen = false) { + return this.applicationDelegate.setWindowFullScreen(fullScreen) + } + + // Extended: Toggle the full screen state of the current window. + toggleFullScreen () { + return this.setFullScreen(!this.isFullScreen()) + } + + // Restore the window to its previous dimensions and show it. + // + // Restores the full screen and maximized state after the window has resized to + // prevent resize glitches. + displayWindow () { + return this.restoreWindowDimensions().then(() => { + const steps = [ + this.restoreWindowBackground(), + this.show(), + this.focus() + ] + if (this.windowDimensions && this.windowDimensions.fullScreen) { + steps.push(this.setFullScreen(true)) + } + if (this.windowDimensions && this.windowDimensions.maximized && process.platform !== 'darwin') { + steps.push(this.maximize()) + } + return Promise.all(steps) + }) + } + + // Get the dimensions of this window. + // + // Returns an {Object} with the following keys: + // * `x` The window's x-position {Number}. + // * `y` The window's y-position {Number}. + // * `width` The window's width {Number}. + // * `height` The window's height {Number}. + getWindowDimensions () { + const browserWindow = this.getCurrentWindow() + const [x, y] = browserWindow.getPosition() + const [width, height] = browserWindow.getSize() + const maximized = browserWindow.isMaximized() + return {x, y, width, height, maximized} + } + + // Set the dimensions of the window. + // + // The window will be centered if either the x or y coordinate is not set + // in the dimensions parameter. If x or y are omitted the window will be + // centered. If height or width are omitted only the position will be changed. + // + // * `dimensions` An {Object} with the following keys: + // * `x` The new x coordinate. + // * `y` The new y coordinate. + // * `width` The new width. + // * `height` The new height. + setWindowDimensions ({x, y, width, height}) { + const steps = [] + if (width != null && height != null) { + steps.push(this.setSize(width, height)) + } + if (x != null && y != null) { + steps.push(this.setPosition(x, y)) + } else { + steps.push(this.center()) + } + return Promise.all(steps) + } + + // Returns true if the dimensions are useable, false if they should be ignored. + // Work around for https://github.com/atom/atom-shell/issues/473 + isValidDimensions ({x, y, width, height} = {}) { + return (width > 0) && (height > 0) && ((x + width) > 0) && ((y + height) > 0) + } + + storeWindowDimensions () { + this.windowDimensions = this.getWindowDimensions() + if (this.isValidDimensions(this.windowDimensions)) { + localStorage.setItem('defaultWindowDimensions', JSON.stringify(this.windowDimensions)) + } + } + + getDefaultWindowDimensions () { + const {windowDimensions} = this.getLoadSettings() + if (windowDimensions) return windowDimensions + + let dimensions + try { + dimensions = JSON.parse(localStorage.getItem('defaultWindowDimensions')) + } catch (error) { + console.warn('Error parsing default window dimensions', error) + localStorage.removeItem('defaultWindowDimensions') + } + + if (dimensions && this.isValidDimensions(dimensions)) { + return dimensions + } else { + const {width, height} = this.applicationDelegate.getPrimaryDisplayWorkAreaSize() + return {x: 0, y: 0, width: Math.min(1024, width), height} + } + } + + restoreWindowDimensions () { + if (!this.windowDimensions || !this.isValidDimensions(this.windowDimensions)) { + this.windowDimensions = this.getDefaultWindowDimensions() + } + return this.setWindowDimensions(this.windowDimensions).then(() => this.windowDimensions) + } + + restoreWindowBackground () { + const backgroundColor = window.localStorage.getItem('atom:window-background-color') + if (backgroundColor) { + this.backgroundStylesheet = document.createElement('style') + this.backgroundStylesheet.type = 'text/css' + this.backgroundStylesheet.innerText = `html, body { background: ${backgroundColor} !important; }` + document.head.appendChild(this.backgroundStylesheet) + } + } + + storeWindowBackground () { + if (this.inSpecMode()) return + + const backgroundColor = this.window.getComputedStyle(this.workspace.getElement())['background-color'] + this.window.localStorage.setItem('atom:window-background-color', backgroundColor) + } + + // Call this method when establishing a real application window. + startEditorWindow () { + this.unloaded = false + + const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks() + + const loadStatePromise = this.loadState().then(state => { + this.windowDimensions = state && state.windowDimensions + return this.displayWindow().then(() => { + this.commandInstaller.installAtomCommand(false, (error) => { + if (error) console.warn(error.message) + }) + this.commandInstaller.installApmCommand(false, (error) => { + if (error) console.warn(error.message) + }) + + this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this))) + this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this))) + this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this))) + this.disposables.add(this.applicationDelegate.onURIMessage(this.dispatchURIMessage.bind(this))) + this.disposables.add(this.applicationDelegate.onDidRequestUnload(() => { + return this.saveState({isUnloading: true}) + .catch(console.error) + .then(() => { + if (this.workspace) { + return this.workspace.confirmClose({ + windowCloseRequested: true, + projectHasPaths: this.project.getPaths().length > 0 + }) + } + }).then(closing => { + if (closing) { + return this.packages.deactivatePackages().then(() => closing) + } else { + return closing + } + }) + })) + + this.listenForUpdates() + + this.registerDefaultTargetForKeymaps() + + this.packages.loadPackages() + + const startTime = Date.now() + return this.deserialize(state).then(() => { + this.deserializeTimings.atom = Date.now() - startTime + + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom') { + this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) + this.document.body.classList.add('custom-title-bar') + } + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom-inset') { + this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) + this.document.body.classList.add('custom-inset-title-bar') + } + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'hidden') { + this.document.body.classList.add('hidden-title-bar') + } + + this.document.body.appendChild(this.workspace.getElement()) + if (this.backgroundStylesheet) this.backgroundStylesheet.remove() + + this.watchProjectPaths() + + this.packages.activate() + this.keymaps.loadUserKeymap() + if (!this.getLoadSettings().safeMode) this.requireUserInitScript() + + this.menu.update() + + return this.openInitialEmptyEditorIfNecessary() + }) + }) + }) + + const loadHistoryPromise = this.history.loadState().then(() => { + this.reopenProjectMenuManager = new ReopenProjectMenuManager({ + menu: this.menu, + commands: this.commands, + history: this.history, + config: this.config, + open: paths => this.open({pathsToOpen: paths}) + }) + return this.reopenProjectMenuManager.update() + }) + + return Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) + } + + serialize (options) { + return { + version: this.constructor.version, + project: this.project.serialize(options), + workspace: this.workspace.serialize(), + packageStates: this.packages.serialize(), + grammars: {grammarOverridesByPath: this.grammars.grammarOverridesByPath}, + fullScreen: this.isFullScreen(), + windowDimensions: this.windowDimensions, + textEditors: this.textEditors.serialize() + } + } + + unloadEditorWindow () { + if (!this.project) return + + this.storeWindowBackground() + this.saveBlobStoreSync() + this.unloaded = true + } + + saveBlobStoreSync () { + if (this.enablePersistence) { + this.blobStore.save() + } + } + + openInitialEmptyEditorIfNecessary () { + if (!this.config.get('core.openEmptyEditorOnStart')) return + const {initialPaths} = this.getLoadSettings() + if (initialPaths && initialPaths.length === 0 && this.workspace.getPaneItems().length === 0) { + return this.workspace.open(null) + } + } + + installUncaughtErrorHandler () { + this.previousWindowErrorHandler = this.window.onerror + this.window.onerror = (...args) => { + this.lastUncaughtError = args + let [message, url, line, column, originalError] = this.lastUncaughtError + + let source + ;({line, column, source} = mapSourcePosition({source: url, line, column})) + if (url === '') url = source + + const eventObject = {message, url, line, column, originalError} + + let openDevTools = true + eventObject.preventDefault = () => { openDevTools = false } + + this.emitter.emit('will-throw-error', eventObject) + + if (openDevTools) { + this.openDevTools().then(() => this.executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")')) + } + + this.emitter.emit('did-throw-error', {message, url, line, column, originalError}) + } + } + + uninstallUncaughtErrorHandler () { + this.window.onerror = this.previousWindowErrorHandler + } + + installWindowEventHandler () { + this.windowEventHandler = new WindowEventHandler({atomEnvironment: this, applicationDelegate: this.applicationDelegate}) + this.windowEventHandler.initialize(this.window, this.document) + } + + uninstallWindowEventHandler () { + if (this.windowEventHandler) { + this.windowEventHandler.unsubscribe() + } + this.windowEventHandler = null + } + + didChangeStyles (styleElement) { + TextEditor.didUpdateStyles() + if (styleElement.textContent.indexOf('scrollbar') >= 0) { + TextEditor.didUpdateScrollbarStyles() + } + } + + updateProcessEnvAndTriggerHooks () { + return this.updateProcessEnv(this.getLoadSettings().env).then(() => { + this.shellEnvironmentLoaded = true + this.emitter.emit('loaded-shell-environment') + this.packages.triggerActivationHook('core:loaded-shell-environment') + }) + } + + /* + Section: Messaging the User + */ + + // Essential: Visually and audibly trigger a beep. + beep () { + if (this.config.get('core.audioBeep')) this.applicationDelegate.playBeepSound() + this.emitter.emit('did-beep') + } + + // Essential: A flexible way to open a dialog akin to an alert dialog. + // + // If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button + // the first button will be clicked unless a "Cancel" or "No" button is provided. + // + // ## Examples + // + // ```coffee + // atom.confirm + // message: 'How you feeling?' + // detailedMessage: 'Be honest.' + // buttons: + // Good: -> window.alert('good to hear') + // Bad: -> window.alert('bummer') + // ``` + // + // * `options` An {Object} with the following keys: + // * `message` The {String} message to display. + // * `detailedMessage` (optional) The {String} detailed message to display. + // * `buttons` (optional) Either an array of strings or an object where keys are + // button names and the values are callbacks to invoke when clicked. + // + // Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object. + confirm (params = {}) { + return this.applicationDelegate.confirm(params) + } + + /* + Section: Managing the Dev Tools + */ + + // Extended: Open the dev tools for the current window. + // + // Returns a {Promise} that resolves when the DevTools have been opened. + openDevTools () { + return this.applicationDelegate.openWindowDevTools() + } + + // Extended: Toggle the visibility of the dev tools for the current window. + // + // Returns a {Promise} that resolves when the DevTools have been opened or + // closed. + toggleDevTools () { + return this.applicationDelegate.toggleWindowDevTools() + } + + // Extended: Execute code in dev tools. + executeJavaScriptInDevTools (code) { + return this.applicationDelegate.executeJavaScriptInWindowDevTools(code) + } + + /* + Section: Private + */ + + assert (condition, message, callbackOrMetadata) { + if (condition) return true + + const error = new Error(`Assertion failed: ${message}`) + Error.captureStackTrace(error, this.assert) + + if (callbackOrMetadata) { + if (typeof callbackOrMetadata === 'function') { + callbackOrMetadata(error) + } else { + error.metadata = callbackOrMetadata + } + } + + this.emitter.emit('did-fail-assertion', error) + if (!this.isReleasedVersion()) throw error + + return false + } + + loadThemes () { + return this.themes.load() + } + + // Notify the browser project of the window's current project path + watchProjectPaths () { + this.disposables.add(this.project.onDidChangePaths(() => { + this.applicationDelegate.setRepresentedDirectoryPaths(this.project.getPaths()) + })) + } + + setDocumentEdited (edited) { + if (typeof this.applicationDelegate.setWindowDocumentEdited === 'function') { + this.applicationDelegate.setWindowDocumentEdited(edited) + } + } + + setRepresentedFilename (filename) { + if (typeof this.applicationDelegate.setWindowRepresentedFilename === 'function') { + this.applicationDelegate.setWindowRepresentedFilename(filename) + } + } + + addProjectFolder () { + this.pickFolder((selectedPaths = []) => { + this.addToProject(selectedPaths) + }) + } + + addToProject (projectPaths) { + this.loadState(this.getStateKey(projectPaths)).then(state => { + if (state && (this.project.getPaths().length === 0)) { + this.attemptRestoreProjectStateForPaths(state, projectPaths) + } else { + projectPaths.map((folder) => this.project.addPath(folder)) + } + }) + } + + attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) { + const center = this.workspace.getCenter() + const windowIsUnused = () => { + for (let container of this.workspace.getPaneContainers()) { + for (let item of container.getPaneItems()) { + if (item instanceof TextEditor) { + if (item.getPath() || item.isModified()) return false + } else { + if (container === center) return false + } + } + } + return true + } + + if (windowIsUnused()) { + this.restoreStateIntoThisEnvironment(state) + return Promise.all(filesToOpen.map(file => this.workspace.open(file))) + } else { + const nouns = projectPaths.length === 1 ? 'folder' : 'folders' + const choice = this.confirm({ + message: 'Previous automatically-saved project state detected', + detailedMessage: `There is previously saved state for the selected ${nouns}. ` + + `Would you like to add the ${nouns} to this window, permanently discarding the saved state, ` + + `or open the ${nouns} in a new window, restoring the saved state?`, + buttons: [ + '&Open in new window and recover state', + '&Add to this window and discard state' + ]}) + if (choice === 0) { + this.open({ + pathsToOpen: projectPaths.concat(filesToOpen), + newWindow: true, + devMode: this.inDevMode(), + safeMode: this.inSafeMode() + }) + return Promise.resolve(null) + } else if (choice === 1) { + for (let selectedPath of projectPaths) { + this.project.addPath(selectedPath) + } + return Promise.all(filesToOpen.map(file => this.workspace.open(file))) + } + } + } + + restoreStateIntoThisEnvironment (state) { + state.fullScreen = this.isFullScreen() + for (let pane of this.workspace.getPanes()) { + pane.destroy() + } + return this.deserialize(state) + } + + showSaveDialog (callback) { + callback(this.showSaveDialogSync()) + } + + showSaveDialogSync (options = {}) { + this.applicationDelegate.showSaveDialog(options) + } + + saveState (options, storageKey) { + return new Promise((resolve, reject) => { + if (this.enablePersistence && this.project) { + const state = this.serialize(options) + if (!storageKey) storageKey = this.getStateKey(this.project && this.project.getPaths()) + const savePromise = storageKey + ? this.stateStore.save(storageKey, state) + : this.applicationDelegate.setTemporaryWindowState(state) + return savePromise.catch(reject).then(resolve) + } else { + return resolve() + } + }) + } + + loadState (stateKey) { + if (this.enablePersistence) { + if (!stateKey) stateKey = this.getStateKey(this.getLoadSettings().initialPaths) + if (stateKey) { + return this.stateStore.load(stateKey) + } else { + return this.applicationDelegate.getTemporaryWindowState() + } + } else { + return Promise.resolve(null) + } + } + + deserialize (state) { + if (!state) return Promise.resolve() + + const grammarOverridesByPath = state.grammars && state.grammars.grammarOverridesByPath + if (grammarOverridesByPath) { + this.grammars.grammarOverridesByPath = grammarOverridesByPath + } + + this.setFullScreen(state.fullScreen) + + const missingProjectPaths = [] + + this.packages.packageStates = state.packageStates || {} + + let projectPromise + let startTime = Date.now() + if (state.project) { + projectPromise = this.project.deserialize(state.project, this.deserializers) + .catch(err => { + if (err.missingProjectPaths) { + missingProjectPaths.push(...err.missingProjectPaths) + } else { + this.notifications.addError('Unable to deserialize project', { + description: err.message, + stack: err.stack + }) + } + }) + } else { + projectPromise = Promise.resolve() + } + + return projectPromise.then(() => { + this.deserializeTimings.project = Date.now() - startTime + + if (state.textEditors) this.textEditors.deserialize(state.textEditors) + + startTime = Date.now() + if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers) + this.deserializeTimings.workspace = Date.now() - startTime + + if (missingProjectPaths.length > 0) { + const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' ' + const noun = missingProjectPaths.length === 1 ? 'directory' : 'directories' + const toBe = missingProjectPaths.length === 1 ? 'is' : 'are' + const escaped = missingProjectPaths.map(projectPath => `\`${projectPath}\``) + let group + switch (escaped.length) { + case 1: + group = escaped[0] + break + case 2: + group = `${escaped[0]} and ${escaped[1]}` + break + default: + group = escaped.slice(0, -1).join(', ') + `, and ${escaped[escaped.length - 1]}` + } + + this.notifications.addError(`Unable to open ${count}project ${noun}`, { + description: `Project ${noun} ${group} ${toBe} no longer on disk.` + }) + } + }) + } + + getStateKey (paths) { + if (paths && paths.length > 0) { + const sha1 = crypto.createHash('sha1').update(paths.slice().sort().join('\n')).digest('hex') + return `editor-${sha1}` + } else { + return null + } + } + + getStorageFolder () { + if (!this.storageFolder) this.storageFolder = new StorageFolder(this.getConfigDirPath()) + return this.storageFolder + } + + getConfigDirPath () { + if (!this.configDirPath) this.configDirPath = process.env.ATOM_HOME + return this.configDirPath + } + + getUserInitScriptPath () { + const initScriptPath = fs.resolve(this.getConfigDirPath(), 'init', ['js', 'coffee']) + return initScriptPath || path.join(this.getConfigDirPath(), 'init.coffee') + } + + requireUserInitScript () { + const userInitScriptPath = this.getUserInitScriptPath() + if (userInitScriptPath) { + try { + if (fs.isFileSync(userInitScriptPath)) require(userInitScriptPath) + } catch (error) { + this.notifications.addError(`Failed to load \`${userInitScriptPath}\``, { + detail: error.message, + dismissable: true + }) + } + } + } + + // TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead + onUpdateAvailable (callback) { + return this.emitter.on('update-available', callback) + } + + updateAvailable (details) { + return this.emitter.emit('update-available', details) + } + + listenForUpdates () { + // listen for updates available locally (that have been successfully downloaded) + this.disposables.add(this.autoUpdater.onDidCompleteDownloadingUpdate(this.updateAvailable.bind(this))) + } + + setBodyPlatformClass () { + this.document.body.classList.add(`platform-${process.platform}`) + } + + setAutoHideMenuBar (autoHide) { + this.applicationDelegate.setAutoHideWindowMenuBar(autoHide) + this.applicationDelegate.setWindowMenuBarVisibility(!autoHide) + } + + dispatchApplicationMenuCommand (command, arg) { + let {activeElement} = this.document + // Use the workspace element if body has focus + if (activeElement === this.document.body) { + activeElement = this.workspace.getElement() + } + this.commands.dispatch(activeElement, command, arg) + } + + dispatchContextMenuCommand (command, ...args) { + this.commands.dispatch(this.contextMenu.activeElement, command, args) + } + + dispatchURIMessage (uri) { + if (this.packages.hasLoadedInitialPackages()) { + this.uriHandlerRegistry.handleURI(uri) + } else { + let subscription = this.packages.onDidLoadInitialPackages(() => { + subscription.dispose() + this.uriHandlerRegistry.handleURI(uri) + }) + } + } + + openLocations (locations) { + const needsProjectPaths = this.project && this.project.getPaths().length === 0 + const foldersToAddToProject = [] + const fileLocationsToOpen = [] + + function pushFolderToOpen (folder) { + if (!foldersToAddToProject.includes(folder)) { + foldersToAddToProject.push(folder) + } + } + + for (var {pathToOpen, initialLine, initialColumn, forceAddToWindow} of locations) { + if (pathToOpen && (needsProjectPaths || forceAddToWindow)) { + if (fs.existsSync(pathToOpen)) { + pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath()) + } else if (fs.existsSync(path.dirname(pathToOpen))) { + pushFolderToOpen(this.project.getDirectoryForProjectPath(path.dirname(pathToOpen)).getPath()) + } else { + pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath()) + } + } + + if (!fs.isDirectorySync(pathToOpen)) { + fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn}) + } + } + + let promise = Promise.resolve(null) + if (foldersToAddToProject.length > 0) { + promise = this.loadState(this.getStateKey(foldersToAddToProject)).then(state => { + if (state && needsProjectPaths) { // only load state if this is the first path added to the project + const files = (fileLocationsToOpen.map((location) => location.pathToOpen)) + return this.attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) + } else { + const promises = [] + for (let folder of foldersToAddToProject) { + this.project.addPath(folder) + } + for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { + promises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) + } + return Promise.all(promises) + } + }) + } else { + const promises = [] + for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { + promises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) + } + promise = Promise.all(promises) + } + + return promise.then(() => ipcRenderer.send('window-command', 'window:locations-opened')) + } + + resolveProxy (url) { + return new Promise((resolve, reject) => { + const requestId = this.nextProxyRequestId++ + const disposable = this.applicationDelegate.onDidResolveProxy((id, proxy) => { + if (id === requestId) { + disposable.dispose() + resolve(proxy) + } + }) + + return this.applicationDelegate.resolveProxy(requestId, url) + }) + } +} + +AtomEnvironment.version = 1 +AtomEnvironment.prototype.saveStateDebounceInterval = 1000 +module.exports = AtomEnvironment + +// Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. +Promise.prototype.done = function (callback) { + deprecate('Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done') + return this.then(callback) +} From 188142bac30e7dc34414b75922fbdffc316b64f8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2017 16:58:30 -0800 Subject: [PATCH 294/301] Suppress lint warning for Promise.prototype monkey patch --- src/atom-environment.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/atom-environment.js b/src/atom-environment.js index 1c2f1ebcf..c23d804c0 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -1344,8 +1344,12 @@ AtomEnvironment.version = 1 AtomEnvironment.prototype.saveStateDebounceInterval = 1000 module.exports = AtomEnvironment +/* eslint-disable */ + // Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. Promise.prototype.done = function (callback) { deprecate('Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done') return this.then(callback) } + +/* eslint-enable */ From fed595b49f07aaa259370f95e2080b3a7dfacd73 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Nov 2017 17:31:45 -0800 Subject: [PATCH 295/301] Use async/await in AtomEnvironment --- src/atom-environment.js | 368 +++++++++++++++++++--------------------- 1 file changed, 176 insertions(+), 192 deletions(-) diff --git a/src/atom-environment.js b/src/atom-environment.js index c23d804c0..663bb6c00 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -290,7 +290,7 @@ class AtomEnvironment { if (this.config.get('core.autoHideMenuBar')) this.setAutoHideMenuBar(true) } - reset () { + async reset () { this.deserializers.clear() this.registerDefaultDeserializers() @@ -313,15 +313,14 @@ class AtomEnvironment { this.contextMenu.clear() - return this.packages.reset().then(() => { - this.workspace.reset(this.packages) - this.registerDefaultOpeners() - this.project.reset(this.packages) - this.workspace.subscribeToEvents() - this.grammars.clear() - this.textEditors.clear() - this.views.clear() - }) + await this.packages.reset() + this.workspace.reset(this.packages) + this.registerDefaultOpeners() + this.project.reset(this.packages) + this.workspace.subscribeToEvents() + this.grammars.clear() + this.textEditors.clear() + this.views.clear() } destroy () { @@ -611,21 +610,20 @@ class AtomEnvironment { // // Restores the full screen and maximized state after the window has resized to // prevent resize glitches. - displayWindow () { - return this.restoreWindowDimensions().then(() => { - const steps = [ - this.restoreWindowBackground(), - this.show(), - this.focus() - ] - if (this.windowDimensions && this.windowDimensions.fullScreen) { - steps.push(this.setFullScreen(true)) - } - if (this.windowDimensions && this.windowDimensions.maximized && process.platform !== 'darwin') { - steps.push(this.maximize()) - } - return Promise.all(steps) - }) + async displayWindow () { + await this.restoreWindowDimensions() + const steps = [ + this.restoreWindowBackground(), + this.show(), + this.focus() + ] + if (this.windowDimensions && this.windowDimensions.fullScreen) { + steps.push(this.setFullScreen(true)) + } + if (this.windowDimensions && this.windowDimensions.maximized && process.platform !== 'darwin') { + steps.push(this.maximize()) + } + await Promise.all(steps) } // Get the dimensions of this window. @@ -700,11 +698,12 @@ class AtomEnvironment { } } - restoreWindowDimensions () { + async restoreWindowDimensions () { if (!this.windowDimensions || !this.isValidDimensions(this.windowDimensions)) { this.windowDimensions = this.getDefaultWindowDimensions() } - return this.setWindowDimensions(this.windowDimensions).then(() => this.windowDimensions) + await this.setWindowDimensions(this.windowDimensions) + return this.windowDimensions } restoreWindowBackground () { @@ -730,75 +729,70 @@ class AtomEnvironment { const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks() - const loadStatePromise = this.loadState().then(state => { + const loadStatePromise = this.loadState().then(async state => { this.windowDimensions = state && state.windowDimensions - return this.displayWindow().then(() => { - this.commandInstaller.installAtomCommand(false, (error) => { - if (error) console.warn(error.message) - }) - this.commandInstaller.installApmCommand(false, (error) => { - if (error) console.warn(error.message) - }) - - this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this))) - this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this))) - this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this))) - this.disposables.add(this.applicationDelegate.onURIMessage(this.dispatchURIMessage.bind(this))) - this.disposables.add(this.applicationDelegate.onDidRequestUnload(() => { - return this.saveState({isUnloading: true}) - .catch(console.error) - .then(() => { - if (this.workspace) { - return this.workspace.confirmClose({ - windowCloseRequested: true, - projectHasPaths: this.project.getPaths().length > 0 - }) - } - }).then(closing => { - if (closing) { - return this.packages.deactivatePackages().then(() => closing) - } else { - return closing - } - }) - })) - - this.listenForUpdates() - - this.registerDefaultTargetForKeymaps() - - this.packages.loadPackages() - - const startTime = Date.now() - return this.deserialize(state).then(() => { - this.deserializeTimings.atom = Date.now() - startTime - - if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom') { - this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) - this.document.body.classList.add('custom-title-bar') - } - if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom-inset') { - this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) - this.document.body.classList.add('custom-inset-title-bar') - } - if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'hidden') { - this.document.body.classList.add('hidden-title-bar') - } - - this.document.body.appendChild(this.workspace.getElement()) - if (this.backgroundStylesheet) this.backgroundStylesheet.remove() - - this.watchProjectPaths() - - this.packages.activate() - this.keymaps.loadUserKeymap() - if (!this.getLoadSettings().safeMode) this.requireUserInitScript() - - this.menu.update() - - return this.openInitialEmptyEditorIfNecessary() - }) + await this.displayWindow() + this.commandInstaller.installAtomCommand(false, (error) => { + if (error) console.warn(error.message) }) + this.commandInstaller.installApmCommand(false, (error) => { + if (error) console.warn(error.message) + }) + + this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this))) + this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this))) + this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this))) + this.disposables.add(this.applicationDelegate.onURIMessage(this.dispatchURIMessage.bind(this))) + this.disposables.add(this.applicationDelegate.onDidRequestUnload(async () => { + try { + await this.saveState({isUnloading: true}) + } catch (error) { + console.error(error) + } + + const closing = !this.workspace || await this.workspace.confirmClose({ + windowCloseRequested: true, + projectHasPaths: this.project.getPaths().length > 0 + }) + + if (closing) await this.packages.deactivatePackages() + return closing + })) + + this.listenForUpdates() + + this.registerDefaultTargetForKeymaps() + + this.packages.loadPackages() + + const startTime = Date.now() + await this.deserialize(state) + this.deserializeTimings.atom = Date.now() - startTime + + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom') { + this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) + this.document.body.classList.add('custom-title-bar') + } + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom-inset') { + this.workspace.addHeaderPanel({item: new TitleBar({workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate})}) + this.document.body.classList.add('custom-inset-title-bar') + } + if (process.platform === 'darwin' && this.config.get('core.titleBar') === 'hidden') { + this.document.body.classList.add('hidden-title-bar') + } + + this.document.body.appendChild(this.workspace.getElement()) + if (this.backgroundStylesheet) this.backgroundStylesheet.remove() + + this.watchProjectPaths() + + this.packages.activate() + this.keymaps.loadUserKeymap() + if (!this.getLoadSettings().safeMode) this.requireUserInitScript() + + this.menu.update() + + await this.openInitialEmptyEditorIfNecessary() }) const loadHistoryPromise = this.history.loadState().then(() => { @@ -809,7 +803,7 @@ class AtomEnvironment { config: this.config, open: paths => this.open({pathsToOpen: paths}) }) - return this.reopenProjectMenuManager.update() + this.reopenProjectMenuManager.update() }) return Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) @@ -852,13 +846,11 @@ class AtomEnvironment { installUncaughtErrorHandler () { this.previousWindowErrorHandler = this.window.onerror - this.window.onerror = (...args) => { - this.lastUncaughtError = args - let [message, url, line, column, originalError] = this.lastUncaughtError - - let source - ;({line, column, source} = mapSourcePosition({source: url, line, column})) - if (url === '') url = source + this.window.onerror = (message, url, line, column, originalError) => { + const mapping = mapSourcePosition({source: url, line, column}) + line = mapping.line + column = mapping.column + if (url === '') url = mapping.source const eventObject = {message, url, line, column, originalError} @@ -868,7 +860,9 @@ class AtomEnvironment { this.emitter.emit('will-throw-error', eventObject) if (openDevTools) { - this.openDevTools().then(() => this.executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")')) + this.openDevTools().then(() => + this.executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")') + ) } this.emitter.emit('did-throw-error', {message, url, line, column, originalError}) @@ -898,12 +892,11 @@ class AtomEnvironment { } } - updateProcessEnvAndTriggerHooks () { - return this.updateProcessEnv(this.getLoadSettings().env).then(() => { - this.shellEnvironmentLoaded = true - this.emitter.emit('loaded-shell-environment') - this.packages.triggerActivationHook('core:loaded-shell-environment') - }) + async updateProcessEnvAndTriggerHooks () { + await this.updateProcessEnv(this.getLoadSettings().env) + this.shellEnvironmentLoaded = true + this.emitter.emit('loaded-shell-environment') + this.packages.triggerActivationHook('core:loaded-shell-environment') } /* @@ -1020,14 +1013,13 @@ class AtomEnvironment { }) } - addToProject (projectPaths) { - this.loadState(this.getStateKey(projectPaths)).then(state => { - if (state && (this.project.getPaths().length === 0)) { - this.attemptRestoreProjectStateForPaths(state, projectPaths) - } else { - projectPaths.map((folder) => this.project.addPath(folder)) - } - }) + async addToProject (projectPaths) { + const state = await this.loadState(this.getStateKey(projectPaths)) + if (state && (this.project.getPaths().length === 0)) { + this.attemptRestoreProjectStateForPaths(state, projectPaths) + } else { + projectPaths.map((folder) => this.project.addPath(folder)) + } } attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) { @@ -1092,19 +1084,16 @@ class AtomEnvironment { this.applicationDelegate.showSaveDialog(options) } - saveState (options, storageKey) { - return new Promise((resolve, reject) => { - if (this.enablePersistence && this.project) { - const state = this.serialize(options) - if (!storageKey) storageKey = this.getStateKey(this.project && this.project.getPaths()) - const savePromise = storageKey - ? this.stateStore.save(storageKey, state) - : this.applicationDelegate.setTemporaryWindowState(state) - return savePromise.catch(reject).then(resolve) + async saveState (options, storageKey) { + if (this.enablePersistence && this.project) { + const state = this.serialize(options) + if (!storageKey) storageKey = this.getStateKey(this.project && this.project.getPaths()) + if (storageKey) { + await this.stateStore.save(storageKey, state) } else { - return resolve() + await this.applicationDelegate.setTemporaryWindowState(state) } - }) + } } loadState (stateKey) { @@ -1120,7 +1109,7 @@ class AtomEnvironment { } } - deserialize (state) { + async deserialize (state) { if (!state) return Promise.resolve() const grammarOverridesByPath = state.grammars && state.grammars.grammarOverridesByPath @@ -1134,55 +1123,51 @@ class AtomEnvironment { this.packages.packageStates = state.packageStates || {} - let projectPromise let startTime = Date.now() if (state.project) { - projectPromise = this.project.deserialize(state.project, this.deserializers) - .catch(err => { - if (err.missingProjectPaths) { - missingProjectPaths.push(...err.missingProjectPaths) - } else { - this.notifications.addError('Unable to deserialize project', { - description: err.message, - stack: err.stack - }) - } - }) - } else { - projectPromise = Promise.resolve() + try { + await this.project.deserialize(state.project, this.deserializers) + } catch (error) { + if (error.missingProjectPaths) { + missingProjectPaths.push(...error.missingProjectPaths) + } else { + this.notifications.addError('Unable to deserialize project', { + description: error.message, + stack: error.stack + }) + } + } } - return projectPromise.then(() => { - this.deserializeTimings.project = Date.now() - startTime + this.deserializeTimings.project = Date.now() - startTime - if (state.textEditors) this.textEditors.deserialize(state.textEditors) + if (state.textEditors) this.textEditors.deserialize(state.textEditors) - startTime = Date.now() - if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers) - this.deserializeTimings.workspace = Date.now() - startTime + startTime = Date.now() + if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers) + this.deserializeTimings.workspace = Date.now() - startTime - if (missingProjectPaths.length > 0) { - const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' ' - const noun = missingProjectPaths.length === 1 ? 'directory' : 'directories' - const toBe = missingProjectPaths.length === 1 ? 'is' : 'are' - const escaped = missingProjectPaths.map(projectPath => `\`${projectPath}\``) - let group - switch (escaped.length) { - case 1: - group = escaped[0] - break - case 2: - group = `${escaped[0]} and ${escaped[1]}` - break - default: - group = escaped.slice(0, -1).join(', ') + `, and ${escaped[escaped.length - 1]}` - } - - this.notifications.addError(`Unable to open ${count}project ${noun}`, { - description: `Project ${noun} ${group} ${toBe} no longer on disk.` - }) + if (missingProjectPaths.length > 0) { + const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' ' + const noun = missingProjectPaths.length === 1 ? 'directory' : 'directories' + const toBe = missingProjectPaths.length === 1 ? 'is' : 'are' + const escaped = missingProjectPaths.map(projectPath => `\`${projectPath}\``) + let group + switch (escaped.length) { + case 1: + group = escaped[0] + break + case 2: + group = `${escaped[0]} and ${escaped[1]}` + break + default: + group = escaped.slice(0, -1).join(', ') + `, and ${escaped[escaped.length - 1]}` } - }) + + this.notifications.addError(`Unable to open ${count}project ${noun}`, { + description: `Project ${noun} ${group} ${toBe} no longer on disk.` + }) + } } getStateKey (paths) { @@ -1270,7 +1255,7 @@ class AtomEnvironment { } } - openLocations (locations) { + async openLocations (locations) { const needsProjectPaths = this.project && this.project.getPaths().length === 0 const foldersToAddToProject = [] const fileLocationsToOpen = [] @@ -1297,32 +1282,31 @@ class AtomEnvironment { } } - let promise = Promise.resolve(null) + let restoredState = false if (foldersToAddToProject.length > 0) { - promise = this.loadState(this.getStateKey(foldersToAddToProject)).then(state => { - if (state && needsProjectPaths) { // only load state if this is the first path added to the project - const files = (fileLocationsToOpen.map((location) => location.pathToOpen)) - return this.attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) - } else { - const promises = [] - for (let folder of foldersToAddToProject) { - this.project.addPath(folder) - } - for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { - promises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) - } - return Promise.all(promises) + const state = await this.loadState(this.getStateKey(foldersToAddToProject)) + + // only restore state if this is the first path added to the project + if (state && needsProjectPaths) { + const files = fileLocationsToOpen.map((location) => location.pathToOpen) + await this.attemptRestoreProjectStateForPaths(state, foldersToAddToProject, files) + restoredState = true + } else { + for (let folder of foldersToAddToProject) { + this.project.addPath(folder) } - }) - } else { - const promises = [] - for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { - promises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) } - promise = Promise.all(promises) } - return promise.then(() => ipcRenderer.send('window-command', 'window:locations-opened')) + if (!restoredState) { + const fileOpenPromises = [] + for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) { + fileOpenPromises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn})) + } + await Promise.all(fileOpenPromises) + } + + ipcRenderer.send('window-command', 'window:locations-opened') } resolveProxy (url) { From 0673866a399e944a859b7db4f20a5fa81f2487e2 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Thu, 9 Nov 2017 10:32:29 -0800 Subject: [PATCH 296/301] Point Atom Core and build documentation to new Flight Manual section --- CONTRIBUTING.md | 6 +- README.md | 6 +- docs/build-instructions/linux.md | 130 ----------------------------- docs/build-instructions/macOS.md | 29 ------- docs/build-instructions/windows.md | 90 -------------------- 5 files changed, 8 insertions(+), 253 deletions(-) delete mode 100644 docs/build-instructions/linux.md delete mode 100644 docs/build-instructions/macOS.md delete mode 100644 docs/build-instructions/windows.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77c1889ac..0f0d2d5a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -199,7 +199,10 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F #### Local development -All packages can be developed locally. For instructions on how to do this, see [Contributing to Official Atom Packages][contributing-to-official-atom-packages] in the [Atom Flight Manual](http://flight-manual.atom.io). +Atom Core and all packages can be developed locally. For instructions on how to do this, see the following sections in the [Atom Flight Manual](http://flight-manual.atom.io): + +* [Hacking on Atom Core][hacking-on-atom-core] +* [Contributing to Official Atom Packages][contributing-to-official-atom-packages] ### Pull Requests @@ -492,3 +495,4 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and [beginner]:https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc [help-wanted]:https://github.com/issues?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc+-label%3Abeginner [contributing-to-official-atom-packages]:http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/ +[hacking-on-atom-core]: http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/ diff --git a/README.md b/README.md index ab6cd06a6..c29203ea0 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,10 @@ repeat these steps to upgrade to future releases. ## Building -* [Linux](./docs/build-instructions/linux.md) -* [macOS](./docs/build-instructions/macOS.md) * [FreeBSD](./docs/build-instructions/freebsd.md) -* [Windows](./docs/build-instructions/windows.md) +* [Linux](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-linux) +* [macOS](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-mac) +* [Windows](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-windows) ## License diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md deleted file mode 100644 index dee67d726..000000000 --- a/docs/build-instructions/linux.md +++ /dev/null @@ -1,130 +0,0 @@ -# Linux - -Ubuntu LTS 12.04 64-bit is the recommended platform. - -## Requirements - -* OS with 64-bit or 32-bit architecture -* C++11 toolchain -* Git -* Node.js 6.x or later (we recommend installing it via [nvm](https://github.com/creationix/nvm)) -* npm 3.10.x or later (run `npm install -g npm`) -* Ensure node-gyp uses python2 (run `npm config set python /usr/bin/python2 -g`, use `sudo` if you didn't install node via nvm) -* Development headers for [libsecret](https://wiki.gnome.org/Projects/Libsecret). - -For more details, scroll down to find how to setup a specific Linux distro. - -## Instructions - -```sh -git clone https://github.com/atom/atom.git -cd atom -script/build -``` - -To also install the newly built application, use `--create-debian-package` or `--create-rpm-package` and then install the generated package via the system package manager. - -### `script/build` Options - -* `--compress-artifacts`: zips the generated application as `out/atom-{arch}.tar.gz`. -* `--create-debian-package`: creates a .deb package as `out/atom-{arch}.deb` -* `--create-rpm-package`: creates a .rpm package as `out/atom-{arch}.rpm` -* `--install[=dir]`: installs the application in `${dir}`; `${dir}` defaults to `/usr/local`. - -### Ubuntu / Debian - -* Install GNOME headers and other basic prerequisites: - - ```sh - sudo apt-get install build-essential git libsecret-1-dev fakeroot rpm libx11-dev libxkbfile-dev - ``` - -* If `script/build` exits with an error, you may need to install a newer C++ compiler with C++11: - - ```sh - sudo add-apt-repository ppa:ubuntu-toolchain-r/test - sudo apt-get update - sudo apt-get install gcc-5 g++-5 - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 80 --slave /usr/bin/g++ g++ /usr/bin/g++-5 - sudo update-alternatives --config gcc # choose gcc-5 from the list - ``` - -### Fedora 22+ - -* `sudo dnf --assumeyes install make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools libX11-devel libxkbfile-devel` - -### Fedora 21 / CentOS / RHEL - -* `sudo yum install -y make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools` - -### Arch - -* `sudo pacman -S --needed gconf base-devel git nodejs npm libsecret python2 libx11 libxkbfile` -* `export PYTHON=/usr/bin/python2` before building Atom. - -### Slackware - -* `sbopkg -k -i node -i atom` - -### openSUSE - -* `sudo zypper install nodejs nodejs-devel make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools libX11-devel libxkbfile-devel` - - -## Troubleshooting - -### TypeError: Unable to watch path - -If you get following error with a big traceback right after Atom starts: - - ``` - TypeError: Unable to watch path - ``` - -you have to increase number of watched files by inotify. For testing if -this is the reason for this error you can issue - - ```sh - sudo sysctl fs.inotify.max_user_watches=32768 - ``` - -and restart Atom. If Atom now works fine, you can make this setting permanent: - - ```sh - echo 32768 | sudo tee -a /proc/sys/fs/inotify/max_user_watches - ``` - -See also [#2082](https://github.com/atom/atom/issues/2082). - -### /usr/bin/env: node: No such file or directory - -If you get this notice when attempting to run any script, you either do not have -Node.js installed, or node isn't identified as Node.js on your machine. If it's -the latter, this might be caused by installing Node.js via the distro package -manager and not nvm, so entering `sudo ln -s /usr/bin/nodejs /usr/bin/node` into -your terminal may fix the issue. On some variants (mostly Debian based distros) -you can use `update-alternatives` too: - -```sh -sudo update-alternatives --install /usr/bin/node node /usr/bin/nodejs 1 --slave /usr/bin/js js /usr/bin/nodejs -``` - -### AttributeError: 'module' object has no attribute 'script_main' - -If you get following error with a big traceback while building Atom: - - ``` - sys.exit(gyp.script_main()) AttributeError: 'module' object has no attribute 'script_main' gyp ERR! - ``` - -you need to uninstall the system version of gyp. - -On Fedora you would do the following: - -```sh -sudo yum remove gyp -``` - -### Linux build error reports in atom/atom -* Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Alinux&type=Issues) - to get a list of reports about build errors on Linux. diff --git a/docs/build-instructions/macOS.md b/docs/build-instructions/macOS.md deleted file mode 100644 index ae3ed9c84..000000000 --- a/docs/build-instructions/macOS.md +++ /dev/null @@ -1,29 +0,0 @@ -# macOS - -## Requirements - - * macOS 10.8 or later - * Node.js 6.x or later (we recommend installing it via [nvm](https://github.com/creationix/nvm)) - * npm 3.10.x or later (run `npm install -g npm`) - * Command Line Tools for [Xcode](https://developer.apple.com/xcode/downloads/) (run `xcode-select --install` to install) - -## Instructions - -```sh -git clone https://github.com/atom/atom.git -cd atom -script/build -``` - -To also install the newly built application, use `script/build --install`. - -### `script/build` Options - -* `--code-sign`: signs the application with the GitHub certificate specified in `$ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL`. -* `--compress-artifacts`: zips the generated application as `out/atom-mac.zip`. -* `--install[=dir]`: installs the application at `${dir}/Atom.app` for dev and stable versions or at `${dir}/Atom-Beta.app` for beta versions; `${dir}` defaults to `/Applications`. - -## Troubleshooting - -### macOS build error reports in atom/atom -* Use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Amac&type=Issues) to get a list of reports about build errors on macOS. diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md deleted file mode 100644 index a6c327ec8..000000000 --- a/docs/build-instructions/windows.md +++ /dev/null @@ -1,90 +0,0 @@ -# Windows - -## Requirements - -* Node.js 6.9.4 or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom) -* Python v2.7.x - * The python.exe must be available at `%SystemDrive%\Python27\python.exe`. If it is installed elsewhere create a symbolic link to the directory containing the python.exe using: `mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27` -* 7zip (7z.exe available from the command line) - for creating distribution zip files -* Visual Studio, either: - * [Visual C++ Build Tools 2015](http://landinghub.visualstudio.com/visual-cpp-build-tools) - * [Visual Studio 2013 Update 5](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express Edition or better) - * [Visual Studio 2015](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community Edition or better) - - Also ensure that: - * The default installation folder is chosen so the build tools can find it - * If using Visual Studio make sure Visual C++ support is selected/installed - * If using Visual C++ Build Tools make sure Windows 8 SDK is selected/installed - * A `git` command is in your path - * Set the `GYP_MSVS_VERSION` environment variable to the Visual Studio/Build Tools version (`2013` or `2015`) e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell (or set it in Windows advanced system settings). - -## Instructions - -You can run these commands using Command Prompt, PowerShell, Git Shell, or any other terminal. These instructions will assume the use of Command Prompt. - -``` -cd C:\ -git clone https://github.com/atom/atom.git -cd atom -script\build -``` - -To also install the newly built application, use `script\build --create-windows-installer` and launch the generated installers. - -### `script\build` Options -* `--code-sign`: signs the application with the GitHub certificate specified in `$WIN_P12KEY_URL`. -* `--compress-artifacts`: zips the generated application as `out\atom-windows.zip` (requires [7-Zip](http://www.7-zip.org)). -* `--create-windows-installer`: creates an `.msi`, an `.exe` and two `.nupkg` packages in the `out` directory. -* `--install[=dir]`: installs the application in `${dir}\Atom\app-dev`; `${dir}` defaults to `%LOCALAPPDATA%`. - -### Running tests - -In order to run tests from command line you need `apm`, available after you install Atom or after you build from source. If you installed it, run the following commands (assuming `C:\atom` is the root of your Atom repository): - -```bash -cd C:\atom -apm test -``` - -When building Atom from source, the `apm` command is not added to the system path by default. In this case, you can either add it yourself or explicitly list the complete path in previous commands. The default install location is `%LOCALAPPDATA%\Atom\app-dev\resources\cli\`. - -**NOTE**: Please keep in mind that there are still some tests that don't pass on Windows. - -## Troubleshooting - -### Common Errors -* `node is not recognized` - * If you just installed Node.js, you'll need to restart Command Prompt before the `node` command is available on your path. - -* `msbuild.exe failed with exit code: 1` - * If using **Visual Studio**, ensure you have the **Visual C++** component installed. Go into Add/Remove Programs, select Visual Studio, press Modify, and then check the Visual C++ box. - * If using **Visual C++ Build Tools**, ensure you have the **Windows 8 SDK** component installed. Go into Add/Remove Programs, select Visual C++ Build Tools, press Modify and then check the Windows 8 SDK box. - -* `script\build` stops with no error or warning shortly after displaying the versions of node, npm and Python - * Make sure that the path where you have checked out Atom does not include a space. For example, use `C:\atom` instead of `C:\my stuff\atom`. - * Try moving the repository to `C:\atom`. Most likely, the path is too long. See [issue #2200](https://github.com/atom/atom/issues/2200). - -* `error MSB4025: The project file could not be loaded. Invalid character in the given encoding.` - * This can occur because your home directory (`%USERPROFILE%`) has non-ASCII characters in it. This is a bug in [gyp](https://code.google.com/p/gyp/) - which is used to build native Node.js modules and there is no known workaround. - * https://github.com/TooTallNate/node-gyp/issues/297 - * https://code.google.com/p/gyp/issues/detail?id=393 - -* `'node_modules\.bin\npm' is not recognized as an internal or external command, operable program or batch file.` - * This occurs if the previous build left things in a bad state. Run `script\clean` and then `script\build` again. - -* `script\build` stops at installing runas with `Failed at the runas@x.y.z install script.` - * See the next item. - -* `error MSB8020: The build tools for Visual Studio 201? (Platform Toolset = 'v1?0') cannot be found.` - * Try setting the `GYP_MSVS_VERSION` environment variable to **2013** or **2015** depending on what version of Visual Studio/Build Tools is installed and then `script\clean` followed by `script\build` (re-open the Command Prompt if you set the variable using the GUI). - -* `'node-gyp' is not recognized as an internal or external command, operable program or batch file.` - * Try running `npm install -g node-gyp`, and run `script\build` again. - -* Other `node-gyp` errors on first build attempt, even though the right Node.js and Python versions are installed. - * Do try the build command one more time as experience shows it often works on second try in many cases. - -### Windows build error reports in atom/atom -* If all fails, use [this search](https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Awindows&type=Issues) to get a list of reports about build errors on Windows, and see if yours has already been reported. -* If it hasn't, please open a new issue with your Windows version, architecture (x86 or x64), and a screenshot of your build output, including the Node.js and Python versions. From 396b78f71d82bef0870a5fb0b0cf4bc5016729b8 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Thu, 9 Nov 2017 10:37:51 -0800 Subject: [PATCH 297/301] Add docs back with references to new location for bookmarks --- docs/build-instructions/linux.md | 1 + docs/build-instructions/macOS.md | 1 + docs/build-instructions/windows.md | 1 + 3 files changed, 3 insertions(+) create mode 100644 docs/build-instructions/linux.md create mode 100644 docs/build-instructions/macOS.md create mode 100644 docs/build-instructions/windows.md diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md new file mode 100644 index 000000000..3499f6ac9 --- /dev/null +++ b/docs/build-instructions/linux.md @@ -0,0 +1 @@ +See the [Hacking on Atom Core](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-linux) section in the [Atom Flight Manual](http://flight-manual.atom.io). diff --git a/docs/build-instructions/macOS.md b/docs/build-instructions/macOS.md new file mode 100644 index 000000000..3085d11f3 --- /dev/null +++ b/docs/build-instructions/macOS.md @@ -0,0 +1 @@ +See the [Hacking on Atom Core](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-mac) section in the [Atom Flight Manual](http://flight-manual.atom.io). diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md new file mode 100644 index 000000000..f75a07530 --- /dev/null +++ b/docs/build-instructions/windows.md @@ -0,0 +1 @@ +See the [Hacking on Atom Core](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-windows) section in the [Atom Flight Manual](http://flight-manual.atom.io). From e5ebdc08563706f1a731231a1ced9e4c6a5cd093 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 10 Nov 2017 15:05:30 -0700 Subject: [PATCH 298/301] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89651c465..611e6aaf0 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.7", "exception-reporting": "0.41.5", - "find-and-replace": "0.213.0", + "find-and-replace": "0.214.0", "fuzzy-finder": "1.7.3", "github": "0.8.2", "git-diff": "1.3.6", From 9f2a07448cf566898771862424e98f28e48b2084 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 11 Nov 2017 13:34:58 +0100 Subject: [PATCH 299/301] :arrow_up: tree-view@0.221.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 611e6aaf0..9a5c6c7cc 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.1", "tabs": "0.109.1", "timecop": "0.36.2", - "tree-view": "0.221.2", + "tree-view": "0.221.3", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.5", From 4321db050c57fad4e453c46048c4efb194a9b9aa Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 11 Nov 2017 15:55:30 +0100 Subject: [PATCH 300/301] :arrow_up: update-package-dependencies@0.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a5c6c7cc..95e817aab 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "tabs": "0.109.1", "timecop": "0.36.2", "tree-view": "0.221.3", - "update-package-dependencies": "0.12.0", + "update-package-dependencies": "0.13.0", "welcome": "0.36.5", "whitespace": "0.37.5", "wrap-guide": "0.40.2", From cb82a4201d87a0b4879f28ab1e74c347cd5cccf4 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Sat, 11 Nov 2017 17:33:52 -0700 Subject: [PATCH 301/301] :arrow_up: autocomplete-plus@2.37.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 95e817aab..3f94ad81e 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.5", "autocomplete-css": "0.17.4", "autocomplete-html": "0.8.3", - "autocomplete-plus": "2.37.2", + "autocomplete-plus": "2.37.3", "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", "autosave": "0.24.6",