From 3c14b8d77199a5c22702cf8e28ed6f3172c3129c Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 6 Apr 2016 17:04:16 -0700 Subject: [PATCH 001/448] 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/448] :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/448] :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 f16cbfbfa1e2e31e7da0fc9fd8a14b8e19a27522 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Fri, 26 May 2017 14:12:05 -0500 Subject: [PATCH 004/448] improve confirm documentation --- src/atom-environment.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index e97b14508..860fcaf0e 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -821,6 +821,9 @@ class AtomEnvironment extends Model @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 # @@ -839,7 +842,7 @@ class AtomEnvironment extends Model # * `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 was an array. + # 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) From 89d39a24d33b2c45e616d4df8b95527230bbb94b Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Mon, 26 Jun 2017 10:37:08 -0500 Subject: [PATCH 005/448] fix linting --- 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 860fcaf0e..5fa3b7846 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -821,7 +821,7 @@ class AtomEnvironment extends Model @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. # From 7c3fe7dba42a9d99a0a86877a43aa791dd6456d9 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 23 Aug 2017 17:43:47 -0700 Subject: [PATCH 006/448] Replace pathwatcher w/ bundled watcher to catch created & rename events --- src/config.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index f0628ffee..bb506f5f4 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -4,7 +4,7 @@ fs = require 'fs-plus' CSON = require 'season' path = require 'path' async = require 'async' -pathWatcher = require 'pathwatcher' +{watchPath} = require './path-watcher' { getValueAtKeyPath, setValueAtKeyPath, deleteValueAtKeyPath, pushKeyPath, splitKeyPath, @@ -880,8 +880,9 @@ class Config return if @shouldNotAccessFileSystem() try - @watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) => - @requestLoad() if eventType is 'change' and @watchSubscription? + @watchSubscription ?= watchPath @configFilePath, {}, (events) => + for {action} in events + @requestLoad() if action in ['created', 'modified', 'renamed'] and @watchSubscription? catch error @notifyFailure """ Unable to watch path: `#{path.basename(@configFilePath)}`. Make sure you have permissions to @@ -891,7 +892,7 @@ class Config """ unobserveUserConfig: -> - @watchSubscription?.close() + @watchSubscription?.dispose() @watchSubscription = null notifyFailure: (errorMessage, detail) -> From 1dc5dec816761861594a4a0f3e3810061a0a4c39 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 25 Aug 2017 19:58:51 -0700 Subject: [PATCH 007/448] WIP fix broken tests --- spec/config-spec.coffee | 27 +++++++++++++++++++++------ src/config.coffee | 17 +++++++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 552f44bf9..be8e8bdfd 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -899,12 +899,15 @@ describe "Config", -> previousSetTimeoutCallCount = setTimeout.callCount runs -> fs.writeFileSync(atom.config.configFilePath, data) - waitsFor "debounced config file load", -> - setTimeout.callCount > previousSetTimeoutCallCount + # waitsFor "debounced config file load", -> + # setTimeout.callCount > previousSetTimeoutCallCount + waitsFor "file written", -> + fs.readFileSync(atom.config.configFilePath, 'utf8') is data runs -> advanceClock(1000) beforeEach -> + console.log 'beforeEach' atom.config.setSchema 'foo', type: 'object' properties: @@ -930,16 +933,28 @@ describe "Config", -> scoped: true """ atom.config.loadUserConfig() - atom.config.observeUserConfig() - updatedHandler = jasmine.createSpy("updatedHandler") - atom.config.onDidChange updatedHandler + + console.log 'observeUserConfig promise', atom.config.observeUserConfig() + waitsForPromise -> atom.config.observeUserConfig() + + runs -> + updatedHandler = jasmine.createSpy("updatedHandler") + atom.config.onDidChange updatedHandler afterEach -> + # WHY IS THIS NOT RUNNING? + console.log 'afterEach' atom.config.unobserveUserConfig() fs.removeSync(dotAtomPath) describe "when the config file changes to contain valid cson", -> - it "updates the config data", -> + afterEach -> + # WHY IS THIS NOT RUNNING? + console.log 'afterEach' + atom.config.unobserveUserConfig() + fs.removeSync(dotAtomPath) + + fit "updates the config data", -> writeConfigFile("foo: { bar: 'quux', baz: 'bar'}") waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> diff --git a/src/config.coffee b/src/config.coffee index bb506f5f4..86352c189 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -849,6 +849,7 @@ class Config loadUserConfig: -> return if @shouldNotAccessFileSystem() + console.log 'loadUserConfig' try unless fs.existsSync(@configFilePath) fs.makeTreeSync(path.dirname(@configFilePath)) @@ -880,9 +881,14 @@ class Config return if @shouldNotAccessFileSystem() try - @watchSubscription ?= watchPath @configFilePath, {}, (events) => + console.trace 'create watch subscription', @watchSubscriptionPromise + @watchSubscriptionPromise ?= watchPath @configFilePath, {}, (events) => + console.log events for {action} in events - @requestLoad() if action in ['created', 'modified', 'renamed'] and @watchSubscription? + console.log action, @watchSubscriptionPromise? + if action in ['created', 'modified', 'renamed'] and @watchSubscriptionPromise? + console.warn 'request load' + @requestLoad() catch error @notifyFailure """ Unable to watch path: `#{path.basename(@configFilePath)}`. Make sure you have permissions to @@ -891,9 +897,12 @@ class Config [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path """ + @watchSubscriptionPromise + unobserveUserConfig: -> - @watchSubscription?.dispose() - @watchSubscription = null + @watchSubscriptionPromise?.then((watcher) => watcher?.dispose()) + @watchSubscriptionPromise = null + console.log 'unobserve' notifyFailure: (errorMessage, detail) -> @notificationManager?.addError(errorMessage, {detail, dismissable: true}) From b494d0fb9ee9caeb8cb70c4bb22bfd290b41353b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 25 Aug 2017 17:18:34 -0700 Subject: [PATCH 008/448] WIP don't overwrite config file if it exists Depends on https://github.com/atom/season/pull/22 --- src/config.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config.coffee b/src/config.coffee index 86352c189..389350ef9 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -851,6 +851,8 @@ class Config console.log 'loadUserConfig' try + # fs.makeTreeSync(path.dirname(@configFilePath)) + # CSON.writeFileSync(@configFilePath, {flag: 'x'}, {}) # fails if file exists unless fs.existsSync(@configFilePath) fs.makeTreeSync(path.dirname(@configFilePath)) CSON.writeFileSync(@configFilePath, {}) From a0766d9b69d3794154591a3cebf027c813729e78 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 25 Aug 2017 20:29:07 -0700 Subject: [PATCH 009/448] Ensure set/unset operations take place after user's config is loaded --- src/config.coffee | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/config.coffee b/src/config.coffee index 389350ef9..6c47caf88 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -399,6 +399,8 @@ class Config # Created during initialization, available as `atom.config` constructor: ({@notificationManager, @enablePersistence}={}) -> + @settingsLoaded = false + @pendingOperations = [] @clear() initialize: ({@configDirPath, @resourcePath, projectHomeSchema}) -> @@ -646,6 +648,10 @@ class Config # * `true` if the value was set. # * `false` if the value was not able to be coerced to the type specified in the setting's schema. set: -> + unless @settingsLoaded + @pendingOperations.push(() => @set.apply(@, arguments)) + return + [keyPath, value, options] = arguments scopeSelector = options?.scopeSelector source = options?.source @@ -677,6 +683,10 @@ class Config # * `scopeSelector` (optional) {String}. See {::set} # * `source` (optional) {String}. See {::set} unset: (keyPath, options) -> + unless @settingsLoaded + @pendingOperations.push(() => @unset.apply(@, arguments)) + return + {scopeSelector, source} = options ? {} source ?= @getUserConfigPath() @@ -944,7 +954,12 @@ class Config @transact => @settings = {} + @settingsLoaded = true @set(key, value, save: false) for key, value of newSettings + if @pendingOperations.length + op() for op in @pendingOperations + @debouncedSave() + @pendingOperations = [] return getRawValue: (keyPath, options) -> From 494cb7ea4b2f5e9e243971ef74c8521a689268fe Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Fri, 25 Aug 2017 20:32:17 -0700 Subject: [PATCH 010/448] WIP Add test for ensuring that set/unset operations take place after load --- spec/config-spec.coffee | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index be8e8bdfd..495ce031e 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -1756,3 +1756,34 @@ describe "Config", -> expect(atom.config.set('foo.bar.str_options', 'One')).toBe false expect(atom.config.get('foo.bar.str_options')).toEqual 'two' + describe "when .set/.unset is called prior to .loadUserConfig", -> + console.log 'this test' + beforeEach -> + fs.writeFileSync config.configFilePath, """ + '*': + foo: + bar: 'baz' + do: + ray: 'me' + """ + + it "ensures that all settings are loaded correctly", -> + console.log 'test start' + config.unset('foo.bar') + expect(config.save).not.toHaveBeenCalled() + config.set('foo.qux', 'boo') + expect(config.save).not.toHaveBeenCalled() + expect(config.get('foo.qux')).toBeUndefined() + expect(config.get('do.ray')).toBeUndefined() + + console.log 'loadUserConfig' + config.loadUserConfig() + + waitsFor -> config.get('foo.bar') is undefined + runs -> + expect(config.save).toHaveBeenCalled() + expect(config.get('foo.bar')).toBeUndefined() + expect(config.get('foo.qux')).toBe('boo') + expect(config.get('do.ray')).toBe('me') + + console.log 'end test' From 91bb1e12c7844587045c4c3c19a8bcbaacfd3461 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Sep 2017 15:11:49 +0200 Subject: [PATCH 011/448] Don't autoscroll when using the mouse to add, delete or move selections --- spec/text-editor-component-spec.js | 33 +++++++++++++++++++++--------- src/text-editor-component.js | 10 ++++----- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 311759bac..4ad478efd 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2706,6 +2706,8 @@ describe('TextEditorComponent', () => { clientY: clientTopForLine(component, 3) + lineHeight / 2 }) expect(editor.getCursorScreenPosition()).toEqual([3, 16]) + + expect(editor.testAutoscrollRequests).toEqual([]) }) it('selects words on double-click', () => { @@ -2714,6 +2716,7 @@ describe('TextEditorComponent', () => { component.didMouseDownOnContent({detail: 1, button: 0, clientX, clientY}) component.didMouseDownOnContent({detail: 2, button: 0, clientX, clientY}) expect(editor.getSelectedScreenRange()).toEqual([[1, 13], [1, 21]]) + expect(editor.testAutoscrollRequests).toEqual([]) }) it('selects lines on triple-click', () => { @@ -2723,6 +2726,7 @@ describe('TextEditorComponent', () => { component.didMouseDownOnContent({detail: 2, button: 0, clientX, clientY}) component.didMouseDownOnContent({detail: 3, button: 0, clientX, clientY}) expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [2, 0]]) + expect(editor.testAutoscrollRequests).toEqual([]) }) it('adds or removes cursors when holding cmd or ctrl when single-clicking', () => { @@ -2760,7 +2764,7 @@ describe('TextEditorComponent', () => { expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]) // cmd-clicking within a selection destroys it - editor.addSelectionForScreenRange([[2, 10], [2, 15]]) + editor.addSelectionForScreenRange([[2, 10], [2, 15]], {autoscroll: false}) expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 16], [1, 16]], [[2, 10], [2, 15]] @@ -2790,7 +2794,7 @@ describe('TextEditorComponent', () => { // ctrl-click adds cursors on platforms *other* than macOS component.props.platform = 'win32' - editor.setCursorScreenPosition([1, 4]) + editor.setCursorScreenPosition([1, 4], {autoscroll: false}) component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, @@ -2799,11 +2803,13 @@ describe('TextEditorComponent', () => { }) ) expect(editor.getCursorScreenPositions()).toEqual([[1, 4], [1, 16]]) + + expect(editor.testAutoscrollRequests).toEqual([]) }) it('adds word selections when holding cmd or ctrl when double-clicking', () => { const {component, editor} = buildComponent() - editor.addCursorAtScreenPosition([1, 16]) + editor.addCursorAtScreenPosition([1, 16], {autoscroll: false}) expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]) component.didMouseDownOnContent( @@ -2824,11 +2830,12 @@ describe('TextEditorComponent', () => { [[0, 0], [0, 0]], [[1, 13], [1, 21]] ]) + expect(editor.testAutoscrollRequests).toEqual([]) }) it('adds line selections when holding cmd or ctrl when triple-clicking', () => { const {component, editor} = buildComponent() - editor.addCursorAtScreenPosition([1, 16]) + editor.addCursorAtScreenPosition([1, 16], {autoscroll: false}) expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]) const {clientX, clientY} = clientPositionForCharacter(component, 1, 16) @@ -2840,12 +2847,13 @@ describe('TextEditorComponent', () => { [[0, 0], [0, 0]], [[1, 0], [2, 0]] ]) + expect(editor.testAutoscrollRequests).toEqual([]) }) it('expands the last selection on shift-click', () => { const {component, element, editor} = buildComponent() - editor.setCursorScreenPosition([2, 18]) + editor.setCursorScreenPosition([2, 18], {autoscroll: false}) component.didMouseDownOnContent(Object.assign({ detail: 1, button: 0, @@ -2862,8 +2870,8 @@ describe('TextEditorComponent', () => { // reorients word-wise selections to keep the word selected regardless of // where the subsequent shift-click occurs - editor.setCursorScreenPosition([2, 18]) - editor.getLastSelection().selectWord() + editor.setCursorScreenPosition([2, 18], {autoscroll: false}) + editor.getLastSelection().selectWord({autoscroll: false}) component.didMouseDownOnContent(Object.assign({ detail: 1, button: 0, @@ -2880,8 +2888,8 @@ describe('TextEditorComponent', () => { // reorients line-wise selections to keep the word selected regardless of // where the subsequent shift-click occurs - editor.setCursorScreenPosition([2, 18]) - editor.getLastSelection().selectLine() + editor.setCursorScreenPosition([2, 18], {autoscroll: false}) + editor.getLastSelection().selectLine(null, {autoscroll: false}) component.didMouseDownOnContent(Object.assign({ detail: 1, button: 0, @@ -2895,6 +2903,8 @@ describe('TextEditorComponent', () => { shiftKey: true }, clientPositionForCharacter(component, 3, 11))) expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [4, 0]]) + + expect(editor.testAutoscrollRequests).toEqual([]) }) it('expands the last selection on drag', () => { @@ -4250,7 +4260,10 @@ function buildEditor (params = {}) { for (const paramName of ['mini', 'autoHeight', 'autoWidth', 'lineNumberGutterVisible', 'showLineNumbers', 'placeholderText', 'softWrapped', 'scrollSensitivity']) { if (params[paramName] != null) editorParams[paramName] = params[paramName] } - return new TextEditor(editorParams) + const editor = new TextEditor(editorParams) + editor.testAutoscrollRequests = [] + editor.onDidRequestAutoscroll((request) => { editor.testAutoscrollRequests.push(request) }) + return editor } function buildComponent (params = {}) { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 61868228b..600a22fb0 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1761,22 +1761,22 @@ class TextEditorComponent { if (existingSelection) { if (model.hasMultipleCursors()) existingSelection.destroy() } else { - model.addCursorAtScreenPosition(screenPosition) + model.addCursorAtScreenPosition(screenPosition, {autoscroll: false}) } } else { if (shiftKey) { - model.selectToScreenPosition(screenPosition) + model.selectToScreenPosition(screenPosition, {autoscroll: false}) } else { - model.setCursorScreenPosition(screenPosition) + model.setCursorScreenPosition(screenPosition, {autoscroll: false}) } } break case 2: - if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition) + if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition, {autoscroll: false}) model.getLastSelection().selectWord({autoscroll: false}) break case 3: - if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition) + if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition, {autoscroll: false}) model.getLastSelection().selectLine(null, {autoscroll: false}) break } From 20ea98ad4149680b6d3e8c44b40bdeb05cd364a2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Sep 2017 18:02:48 +0200 Subject: [PATCH 012/448] Don't render block decorations located outside the visible range Previously, when trying to use block decorations on non-empty markers, Atom could sometimes throw an error if such markers ended or started at a position that was not currently rendered. In fact, even if we already restricted the decoration query to markers that intersected the visible row range, markers that were only partially visible would still be considered for rendering. If, depending on the `reversed` property, we decided to render the tail or head of the marker in question and this was outside the viewport, Atom would throw the aforementioned exception. This commit addresses the above issue by explicitly ignoring block decorations that are located on rows that are not yet rendered. --- spec/text-editor-component-spec.js | 25 +++++++++++++++++++++++++ src/text-editor-component.js | 8 +++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 311759bac..b34d3d766 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2388,6 +2388,31 @@ describe('TextEditorComponent', () => { ]) }) + 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) + expect(component.getRenderedStartRow()).toBe(0) + expect(component.getRenderedEndRow()).toBe(4) + + const marker1 = editor.markScreenRange([[3, 0], [5, 0]], {reversed: false}) + const item1 = document.createElement('div') + editor.decorateMarker(marker1, {type: 'block', item: item1}) + + const marker2 = editor.markScreenRange([[3, 0], [5, 0]], {reversed: true}) + const item2 = document.createElement('div') + editor.decorateMarker(marker2, {type: 'block', item: item2}) + + await component.getNextUpdatePromise() + expect(item1.parentElement).toBeNull() + expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3)) + + await setScrollTop(component, 4 * component.getLineHeight()) + expect(component.getRenderedStartRow()).toBe(4) + expect(component.getRenderedEndRow()).toBe(8) + expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 5)) + expect(item2.parentElement).toBeNull() + }) + it('measures block decorations correctly when they are added before the component width has been updated', async () => { { const {editor, component, element} = buildComponent({autoHeight: false, width: 500, attach: false}) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 61868228b..941bffc8d 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1153,9 +1153,11 @@ class TextEditorComponent { } addBlockDecorationToRender (decoration, screenRange, reversed) { - const screenPosition = reversed ? screenRange.start : screenRange.end - const tileStartRow = this.tileStartRowForRow(screenPosition.row) - const screenLine = this.renderedScreenLines[screenPosition.row - this.getRenderedStartRow()] + const {row} = reversed ? screenRange.start : screenRange.end + if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) return + + const tileStartRow = this.tileStartRowForRow(row) + const screenLine = this.renderedScreenLines[row - this.getRenderedStartRow()] let decorationsByScreenLine = this.decorationsToRender.blocks.get(tileStartRow) if (!decorationsByScreenLine) { From 15d988d4417ad5ab6ae033ae1aaf8b8338027a62 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Sep 2017 11:02:54 +0200 Subject: [PATCH 013/448] Use the Node version bundled in Electron to verify snapshot script Previously, we used to verify the snapshot script by running it in a new, empty context (similar to the one that `mksnapshot` creates when generating the startup blob). However, this context was being created using the Node version that `script/build` was executed with. Such version may not match the Node version shipped with Electron, and could thus cause the build script to report "false negatives" when verifying the snapshot script. For instance, running `script/build` with Node 4 would cause it to throw an error when encountering keywords like `async`/`await`, even if they're 100% supported in Electron 1.6.9. With this commit we are changing the snapshot verification code to use the Node version bundled in Electron in order to avoid the aforementioned mismatches. --- script/lib/generate-startup-snapshot.js | 15 ++++++++++++++- script/verify-snapshot-script | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100755 script/verify-snapshot-script diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 471bd1201..d9a5eb408 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -76,7 +76,20 @@ module.exports = function (packagedAppPath) { process.stdout.write('\n') console.log('Verifying if snapshot can be executed via `mksnapshot`') - vm.runInNewContext(snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true}) + const verifySnapshotScriptPath = path.join(CONFIG.repositoryRootPath, 'script', 'verify-snapshot-script') + let nodeBundledInElectronPath + if (process.platform === 'darwin') { + nodeBundledInElectronPath = path.join(packagedAppPath, 'Contents', 'MacOS', 'Atom') + } else if (process.platform === 'win32') { + nodeBundledInElectronPath = path.join(packagedAppPath, 'atom.exe') + } else { + nodeBundledInElectronPath = path.join(packagedAppPath, 'atom') + } + childProcess.execFileSync( + nodeBundledInElectronPath, + [verifySnapshotScriptPath, snapshotScriptPath], + {env: Object.assign(process.env, {ELECTRON_RUN_AS_NODE: 1})} + ) const generatedStartupBlobPath = path.join(CONFIG.buildOutputPath, 'snapshot_blob.bin') console.log(`Generating startup blob at "${generatedStartupBlobPath}"`) diff --git a/script/verify-snapshot-script b/script/verify-snapshot-script new file mode 100755 index 000000000..7fddbb1b9 --- /dev/null +++ b/script/verify-snapshot-script @@ -0,0 +1,6 @@ +#!/usr/bin/env node +const fs = require('fs') +const vm = require('vm') +const snapshotScriptPath = process.argv[2] +const snapshotScript = fs.readFileSync(snapshotScriptPath, 'utf8') +vm.runInNewContext(snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true}) From 2050f58a0e1b7021accd746fcb80e87cdebdd92e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Sep 2017 11:08:58 +0200 Subject: [PATCH 014/448] Don't override `process.env` variables --- script/lib/generate-startup-snapshot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index d9a5eb408..49893dde3 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -88,7 +88,7 @@ module.exports = function (packagedAppPath) { childProcess.execFileSync( nodeBundledInElectronPath, [verifySnapshotScriptPath, snapshotScriptPath], - {env: Object.assign(process.env, {ELECTRON_RUN_AS_NODE: 1})} + {env: Object.assign({}, process.env, {ELECTRON_RUN_AS_NODE: 1})} ) const generatedStartupBlobPath = path.join(CONFIG.buildOutputPath, 'snapshot_blob.bin') From 8c8d6f7ce465506d34c1e3ef50f2d6e3285fee4c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Sep 2017 11:28:19 +0200 Subject: [PATCH 015/448] :fire: Delete unused require --- script/lib/generate-startup-snapshot.js | 1 - 1 file changed, 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 49893dde3..bb635b4c1 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -3,7 +3,6 @@ const fs = require('fs') const path = require('path') const electronLink = require('electron-link') const CONFIG = require('../config') -const vm = require('vm') module.exports = function (packagedAppPath) { const snapshotScriptPath = path.join(CONFIG.buildOutputPath, 'startup.js') From a3e82a54d190f76463ec3157c78ad3904e6e722b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Sep 2017 14:52:01 +0200 Subject: [PATCH 016/448] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a297b552f..10b57076e 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.4", "exception-reporting": "0.41.4", - "find-and-replace": "0.212.0", + "find-and-replace": "0.212.1", "fuzzy-finder": "1.5.8", "github": "0.5.0", "git-diff": "1.3.6", From f00568bde5345c6288ad23baf91b8bb6a44303ba Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Wed, 6 Sep 2017 20:56:22 +0200 Subject: [PATCH 017/448] Update ScopeDescriptor links --- src/scope-descriptor.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scope-descriptor.coffee b/src/scope-descriptor.coffee index ca97a2a43..95539cc69 100644 --- a/src/scope-descriptor.coffee +++ b/src/scope-descriptor.coffee @@ -10,8 +10,8 @@ # # You should not need to create a `ScopeDescriptor` directly. # -# * {Editor::getRootScopeDescriptor} to get the language's descriptor. -# * {Editor::scopeDescriptorForBufferPosition} to get the descriptor at a +# * {TextEditor::getRootScopeDescriptor} to get the language's descriptor. +# * {TextEditor::scopeDescriptorForBufferPosition} to get the descriptor at a # specific position in the buffer. # * {Cursor::getScopeDescriptor} to get a cursor's descriptor based on position. # From 01d5822dd34a478a4c4e1ac418f35fa22a58fa31 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 20:47:21 -0700 Subject: [PATCH 018/448] :arrow_up: notifications --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10b57076e..798a8675e 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "link": "0.31.3", "markdown-preview": "0.159.13", "metrics": "1.2.6", - "notifications": "0.69.0", + "notifications": "0.69.1", "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.251.5", From 20420be7ae5485b7f27650fd68b2759e1487de95 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 20:48:59 -0700 Subject: [PATCH 019/448] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 798a8675e..88792b7ba 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.4", "exception-reporting": "0.41.4", - "find-and-replace": "0.212.1", + "find-and-replace": "0.212.2", "fuzzy-finder": "1.5.8", "github": "0.5.0", "git-diff": "1.3.6", From 45b0ce3d6d990cfebd0c705242098a3c697e4dc6 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 21:13:51 -0700 Subject: [PATCH 020/448] :arrow_up: status-bar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88792b7ba..d0fdb0ffc 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "settings-view": "0.251.5", "snippets": "1.1.4", "spell-check": "0.72.2", - "status-bar": "1.8.11", + "status-bar": "1.8.12", "styleguide": "0.49.6", "symbols-view": "0.117.1", "tabs": "0.107.1", From a53958e00742b24aa0ebf47e264c46094c5c6c9b Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 22:55:23 -0700 Subject: [PATCH 021/448] :arrow_up: about --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d0fdb0ffc..f377b631a 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "one-light-syntax": "1.8.0", "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", - "about": "1.7.6", + "about": "1.7.7", "archive-view": "0.63.3", "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", From 499ef160137c8f4ef554a625a0a205777b78fc3a Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Thu, 7 Sep 2017 14:48:35 +0300 Subject: [PATCH 022/448] Default re-used element class name to empty string --- src/text-editor-component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index c34a58dad..8bdca834f 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -4309,7 +4309,7 @@ class NodePool { } if (element) { - element.className = className + element.className = className || '' element.styleMap.forEach((value, key) => { if (!style || style[key] == null) element.style[key] = '' }) From 806b652da410a28e078d725050b6f33af66cb526 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Sep 2017 15:05:42 +0200 Subject: [PATCH 023/448] Flush scroll position to dummy scrollbar components on re-attach This prevents the dummy scrollbars from resetting their position to `0` when the editor element is moved elsewhere in the DOM (e.g. when splitting a pane item). --- spec/text-editor-component-spec.js | 67 ++++++++++++++++++------------ src/text-editor-component.js | 3 ++ 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index ac6a0cba4..e2cd0988b 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -378,35 +378,50 @@ describe('TextEditorComponent', () => { expect(horizontalScrollbar.style.visibility).toBe('') }) - it('updates the bottom/right of dummy scrollbars and client height/width measurements without forgetting the previous scroll top/left when scrollbar styles change', async () => { - const {component, element, editor} = buildComponent({height: 100, width: 100}) - expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10) - expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10) - setScrollTop(component, 20) - setScrollLeft(component, 10) - await component.getNextUpdatePromise() + describe('when scrollbar styles change or the editor element is detached and then reattached', () => { + it('updates the bottom/right of dummy scrollbars and client height/width measurements', async () => { + const {component, element, editor} = buildComponent({height: 100, width: 100}) + expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10) + expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10) + setScrollTop(component, 20) + setScrollLeft(component, 10) + await component.getNextUpdatePromise() - const style = document.createElement('style') - style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }' - jasmine.attachToDOM(style) + // Updating scrollbar styles. + const style = document.createElement('style') + style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }' + jasmine.attachToDOM(style) + TextEditor.didUpdateScrollbarStyles() + await component.getNextUpdatePromise() - TextEditor.didUpdateScrollbarStyles() - await component.getNextUpdatePromise() + expect(getHorizontalScrollbarHeight(component)).toBe(10) + expect(getVerticalScrollbarWidth(component)).toBe(10) + expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px') + expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px') + expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10) + expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20) + expect(component.getScrollContainerClientHeight()).toBe(100 - 10) + expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10) - expect(getHorizontalScrollbarHeight(component)).toBe(10) - expect(getVerticalScrollbarWidth(component)).toBe(10) - expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px') - expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px') - expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10) - expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20) - expect(component.getScrollContainerClientHeight()).toBe(100 - 10) - expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10) + // Detaching and re-attaching the editor element. + element.remove() + jasmine.attachToDOM(element) - // Ensure we don't throw an error trying to remeasure non-existent scrollbars for mini editors. - await editor.update({mini: true}) - TextEditor.didUpdateScrollbarStyles() - component.scheduleUpdate() - await component.getNextUpdatePromise() + expect(getHorizontalScrollbarHeight(component)).toBe(10) + expect(getVerticalScrollbarWidth(component)).toBe(10) + expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px') + expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px') + expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10) + expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20) + expect(component.getScrollContainerClientHeight()).toBe(100 - 10) + expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10) + + // Ensure we don't throw an error trying to remeasure non-existent scrollbars for mini editors. + await editor.update({mini: true}) + TextEditor.didUpdateScrollbarStyles() + component.scheduleUpdate() + await component.getNextUpdatePromise() + }) }) it('renders cursors within the visible row range', async () => { @@ -1142,7 +1157,7 @@ describe('TextEditorComponent', () => { expect(component.getScrollTopRow()).toBe(4) expect(component.getScrollTop()).toBe(Math.round(4 * component.getLineHeight())) - // Preserves the scrollTopRow when sdetached + // Preserves the scrollTopRow when detached element.remove() expect(component.getScrollTopRow()).toBe(4) expect(component.getScrollTop()).toBe(Math.round(4 * component.getLineHeight())) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index c34a58dad..257120e1f 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1403,6 +1403,9 @@ class TextEditorComponent { if (this.isVisible()) { this.didShow() + + if (this.refs.verticalScrollbar) this.refs.verticalScrollbar.flushScrollPosition() + if (this.refs.horizontalScrollbar) this.refs.horizontalScrollbar.flushScrollPosition() } else { this.didHide() } From c1981ffb44b704fce9a69d6ba2fd1ba8feed5c9c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 7 Sep 2017 17:52:04 +0200 Subject: [PATCH 024/448] Correctly remove block decorations whose markers have been destroyed In https://github.com/atom/atom/pull/15503 we mistakenly assumed `marker.isValid` accounted only for the validity of the marker. However, that method returns `false` also for markers that are valid but have been destroyed. As a result, the editor component was mistakenly not removing block decorations associated with such markers. With this commit we will rely on the local `wasValid` variable instead. If its value is `true`, it means that the block decoration has been accounted for in the `lineTopIndex` and must, as a result, be cleaned up in case the marker or the decoration gets destroyed. --- spec/text-editor-component-spec.js | 21 +++++++++++++++++++++ src/text-editor-component.js | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index e2cd0988b..d1ff883c9 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -2299,6 +2299,27 @@ describe('TextEditorComponent', () => { ]) }) + it('removes block decorations whose markers have been destroyed', async () => { + const {editor, component, element} = buildComponent({rowsPerTile: 3}) + const {marker} = createBlockDecorationAtScreenRow(editor, 2, {height: 5, position: 'before'}) + await component.getNextUpdatePromise() + assertLinesAreAlignedWithLineNumbers(component) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight() + 5}, + {tileStartRow: 3, height: 3 * component.getLineHeight()}, + {tileStartRow: 6, height: 3 * component.getLineHeight()} + ]) + + marker.destroy() + await component.getNextUpdatePromise() + assertLinesAreAlignedWithLineNumbers(component) + assertTilesAreSizedAndPositionedCorrectly(component, [ + {tileStartRow: 0, height: 3 * component.getLineHeight()}, + {tileStartRow: 3, height: 3 * component.getLineHeight()}, + {tileStartRow: 6, height: 3 * component.getLineHeight()} + ]) + }) + it('removes block decorations whose markers are invalidated, and adds them back when they become valid again', async () => { const editor = buildEditor({rowsPerTile: 3, autoHeight: false}) const {item, decoration, marker} = createBlockDecorationAtScreenRow(editor, 3, {height: 44, position: 'before', invalidate: 'touch'}) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 333486df8..c668ea8f6 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2506,7 +2506,7 @@ class TextEditorComponent { didUpdateDisposable.dispose() didDestroyDisposable.dispose() - if (marker.isValid()) { + if (wasValid) { this.blockDecorationsToMeasure.delete(decoration) this.heightsByBlockDecoration.delete(decoration) this.blockDecorationsByElement.delete(element) From 4d754d4232c29f7cdfa3be54ca8d35224ee88a41 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 7 Sep 2017 15:55:50 -0400 Subject: [PATCH 025/448] :arrow_up: github --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f377b631a..4583352d0 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.212.2", "fuzzy-finder": "1.5.8", - "github": "0.5.0", + "github": "0.5.1", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From b74321e087a50fc54e3acca9f0b97b43e9f13434 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 7 Sep 2017 14:26:12 -0700 Subject: [PATCH 026/448] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4583352d0..f4834a296 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.1", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.5", + "settings-view": "0.251.6", "snippets": "1.1.4", "spell-check": "0.72.2", "status-bar": "1.8.12", From 82e2939188639ffa5770d4d775cab89840f06d3e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 7 Sep 2017 17:29:02 -0400 Subject: [PATCH 027/448] :arrow_up: symbols-view@0.118.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4583352d0..3ac74015a 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "spell-check": "0.72.2", "status-bar": "1.8.12", "styleguide": "0.49.6", - "symbols-view": "0.117.1", + "symbols-view": "0.118.0", "tabs": "0.107.1", "timecop": "0.36.0", "tree-view": "0.217.8", From 18726c93d5c33aaea2d491d4881564837cdc206a Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 7 Sep 2017 19:47:12 -0700 Subject: [PATCH 028/448] :arrow_up: tree-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4834a296..71d775f11 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.117.1", "tabs": "0.107.1", "timecop": "0.36.0", - "tree-view": "0.217.8", + "tree-view": "0.217.9", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.2", From d986e65ad10926f6e486368733a24c98c67bb51a Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 7 Sep 2017 19:48:51 -0700 Subject: [PATCH 029/448] :arrow_up: status-bar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71d775f11..962513e10 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "settings-view": "0.251.6", "snippets": "1.1.4", "spell-check": "0.72.2", - "status-bar": "1.8.12", + "status-bar": "1.8.13", "styleguide": "0.49.6", "symbols-view": "0.117.1", "tabs": "0.107.1", From d66be967d080caf3581b62cc9042cf9312d2dc76 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 7 Sep 2017 19:57:07 -0700 Subject: [PATCH 030/448] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 962513e10..032f58307 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.1", - "autocomplete-plus": "2.35.8", + "autocomplete-plus": "2.35.9", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.3", From c4718e04454e7f0ba74bb0e807182aa2ab38b8c0 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 7 Sep 2017 19:58:41 -0700 Subject: [PATCH 031/448] :arrow_up: encoding-selector --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 032f58307..04019d8ed 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "dalek": "0.2.1", "deprecation-cop": "0.56.7", "dev-live-reload": "0.47.1", - "encoding-selector": "0.23.4", + "encoding-selector": "0.23.5", "exception-reporting": "0.41.4", "find-and-replace": "0.212.2", "fuzzy-finder": "1.5.8", From 032e22ebae57e760dea1422957d2bfcf45ef183f Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 7 Sep 2017 19:59:57 -0700 Subject: [PATCH 032/448] :arrow_up: deprecation-cop --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04019d8ed..66bb349ed 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "bracket-matcher": "0.87.3", "command-palette": "0.41.1", "dalek": "0.2.1", - "deprecation-cop": "0.56.7", + "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", "encoding-selector": "0.23.5", "exception-reporting": "0.41.4", From 8f7ad39df79a5be9573642de415f5d2d3fcd6bfd Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 7 Sep 2017 20:01:09 -0700 Subject: [PATCH 033/448] :arrow_up: tabs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66bb349ed..5d6e007e0 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.13", "styleguide": "0.49.6", "symbols-view": "0.117.1", - "tabs": "0.107.1", + "tabs": "0.107.2", "timecop": "0.36.0", "tree-view": "0.217.9", "update-package-dependencies": "0.12.0", From 87dbe4613aa9298c4bbbcae4d6720858d528af7a Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 8 Sep 2017 18:59:58 -0700 Subject: [PATCH 034/448] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f931e82f3..6074a4f7d 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "notifications": "0.69.1", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.6", + "settings-view": "0.251.7", "snippets": "1.1.4", "spell-check": "0.72.2", "status-bar": "1.8.13", From 69e631d5096b84a2b910713ec9bbf2ddf6a215f9 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 5 Sep 2017 16:12:12 -0700 Subject: [PATCH 035/448] Allow Promises to be returned by a package deactivate method --- benchmarks/benchmark-runner.js | 2 +- spec/spec-helper.coffee | 12 ++++++++---- src/atom-environment.coffee | 6 +++++- src/package-manager.js | 16 ++++++++-------- src/package.coffee | 13 ++++++++----- 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/benchmarks/benchmark-runner.js b/benchmarks/benchmark-runner.js index 30b23ffbf..56a37cfd4 100644 --- a/benchmarks/benchmark-runner.js +++ b/benchmarks/benchmark-runner.js @@ -65,7 +65,7 @@ export default async function ({test, benchmarkPaths}) { console.log(textualOutput) } - global.atom.reset() + await global.atom.reset() } } diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index eec8ce5fb..c20bfc827 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -108,10 +108,14 @@ beforeEach -> afterEach -> ensureNoDeprecatedFunctionCalls() ensureNoDeprecatedStylesheets() - atom.reset() - document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent - warnIfLeakingPathSubscriptions() - waits(0) # yield to ui thread to make screen update more frequently + + waitsForPromise -> + atom.reset() + + runs -> + document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent + warnIfLeakingPathSubscriptions() + waits(0) # yield to ui thread to make screen update more frequently warnIfLeakingPathSubscriptions = -> watchedPaths = pathwatcher.getWatchedPaths() diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index b37acddd1..7e425edcd 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -702,6 +702,11 @@ class AtomEnvironment extends Model windowCloseRequested: true, projectHasPaths: @project.getPaths().length > 0 }) + .then (closing) => + if closing + @packages.deactivatePackages().then -> closing + else + closing @listenForUpdates() @@ -758,7 +763,6 @@ class AtomEnvironment extends Model return if not @project @storeWindowBackground() - @packages.deactivatePackages() @saveBlobStoreSync() @unloaded = true diff --git a/src/package-manager.js b/src/package-manager.js index 73855ae37..7718c8618 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -77,9 +77,9 @@ module.exports = class PackageManager { this.themeManager = themeManager } - reset () { + async reset () { this.serviceHub.clear() - this.deactivatePackages() + await this.deactivatePackages() this.loadedPackages = {} this.preloadedPackages = {} this.packageStates = {} @@ -744,21 +744,21 @@ module.exports = class PackageManager { } // Deactivate all packages - deactivatePackages () { - this.config.transact(() => { - this.getLoadedPackages().forEach(pack => this.deactivatePackage(pack.name, true)) - }) + async deactivatePackages () { + await this.config.transactAsync(() => + Promise.all(this.getLoadedPackages().map(pack => this.deactivatePackage(pack.name, true))) + ) this.unobserveDisabledPackages() this.unobservePackagesWithKeymapsDisabled() } // Deactivate the package with the given name - deactivatePackage (name, suppressSerialization) { + async deactivatePackage (name, suppressSerialization) { const pack = this.getLoadedPackage(name) if (!suppressSerialization && this.isPackageActive(pack.name)) { this.serializePackage(pack) } - pack.deactivate() + await pack.deactivate() delete this.activePackages[pack.name] delete this.activatingPackages[pack.name] this.emitter.emit('did-deactivate-package', pack) diff --git a/src/package.coffee b/src/package.coffee index 039ccf9d3..556847bc9 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -506,15 +506,18 @@ class Package @configSchemaRegisteredOnActivate = false @deactivateResources() @deactivateKeymaps() + result = Promise.resolve() if @mainActivated try - @mainModule?.deactivate?() - @mainModule?.deactivateConfig?() - @mainActivated = false - @mainInitialized = false + result = Promise.resolve(@mainModule?.deactivate?()).then => + @mainModule?.deactivateConfig?() + @mainActivated = false + @mainInitialized = false catch e console.error "Error deactivating package '#{@name}'", e.stack - @emitter.emit 'did-deactivate' + result = result.then => + @emitter.emit 'did-deactivate' + result deactivateResources: -> grammar.deactivate() for grammar in @grammars From 9898f6b36c706c6d4634abe4e66108875acd481e Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 11:10:39 -0700 Subject: [PATCH 036/448] Ensure atom.global.reset returns a promise --- src/atom-environment.coffee | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 7e425edcd..fd9af9342 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -328,20 +328,14 @@ class AtomEnvironment extends Model @contextMenu.clear() - @packages.reset() - - @workspace.reset(@packages) - @registerDefaultOpeners() - - @project.reset(@packages) - - @workspace.subscribeToEvents() - - @grammars.clear() - - @textEditors.clear() - - @views.clear() + @packages.reset().then -> + @workspace.reset(@packages) + @registerDefaultOpeners() + @project.reset(@packages) + @workspace.subscribeToEvents() + @grammars.clear() + @textEditors.clear() + @views.clear() destroy: -> return if not @project From 0a2ff530ff7572e24650c246cd7b2ae051b58c78 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 14:15:26 -0700 Subject: [PATCH 037/448] Get more tests working after async --- spec/grammars-spec.coffee | 10 ++-- spec/language-mode-spec.coffee | 48 ++++++++++++------- spec/package-manager-spec.coffee | 79 ++++++++++++++++++++++++++------ spec/theme-manager-spec.coffee | 8 ++-- src/atom-environment.coffee | 2 +- src/theme-manager.coffee | 3 +- 6 files changed, 111 insertions(+), 39 deletions(-) diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index 7d4754397..7b70797ba 100644 --- a/spec/grammars-spec.coffee +++ b/spec/grammars-spec.coffee @@ -22,10 +22,12 @@ describe "the `grammars` global", -> atom.packages.activatePackage('language-git') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() - try - temp.cleanupSync() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() + try + temp.cleanupSync() describe ".selectGrammar(filePath)", -> it "always returns a grammar", -> diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index 4025472af..aca96d2cc 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -15,8 +15,10 @@ describe "LanguageMode", -> atom.packages.activatePackage('language-javascript') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() describe ".minIndentLevelForRowRange(startRow, endRow)", -> it "returns the minimum indent level for the given row range", -> @@ -175,8 +177,10 @@ describe "LanguageMode", -> atom.packages.activatePackage('language-coffee-script') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() describe ".toggleLineCommentsForBufferRows(start, end)", -> it "comments/uncomments lines in the given range", -> @@ -222,8 +226,10 @@ describe "LanguageMode", -> atom.packages.activatePackage('language-css') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() describe ".toggleLineCommentsForBufferRows(start, end)", -> it "comments/uncomments lines in the given range", -> @@ -274,8 +280,10 @@ describe "LanguageMode", -> atom.packages.activatePackage('language-css') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() describe "when commenting lines", -> it "only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`", -> @@ -294,8 +302,10 @@ describe "LanguageMode", -> atom.packages.activatePackage('language-xml') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() describe "when uncommenting lines", -> it "removes the leading whitespace from the comment end pattern match", -> @@ -313,8 +323,10 @@ describe "LanguageMode", -> atom.packages.activatePackage('language-javascript') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() it "maintains cursor buffer position when a folding/unfolding", -> editor.setCursorBufferPosition([5, 5]) @@ -403,8 +415,10 @@ describe "LanguageMode", -> atom.packages.activatePackage('language-javascript') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() describe ".unfoldAll()", -> it "unfolds every folded line", -> @@ -481,8 +495,10 @@ describe "LanguageMode", -> atom.packages.activatePackage('language-css') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() describe "suggestedIndentForBufferRow", -> it "does not return negative values (regression)", -> diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 88efff4ae..294a2bfac 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -56,8 +56,10 @@ describe "PackageManager", -> spyOn(atom.packages, 'loadAvailablePackage') afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() it "sets hasLoadedInitialPackages", -> expect(atom.packages.hasLoadedInitialPackages()).toBe false @@ -172,8 +174,10 @@ describe "PackageManager", -> model2 = {worksWithViewProvider2: true} afterEach -> - atom.packages.deactivatePackage('package-with-view-providers') - atom.packages.unloadPackage('package-with-view-providers') + waitsForPromise -> + atom.packages.deactivatePackage('package-with-view-providers') + runs -> + atom.packages.unloadPackage('package-with-view-providers') it "does not load the view providers immediately", -> pack = atom.packages.loadPackage("package-with-view-providers") @@ -641,7 +645,11 @@ describe "PackageManager", -> runs -> expect(mainModule.activate.callCount).toBe 1 + + waitsForPromise -> atom.packages.deactivatePackage('package-with-activation-hooks') + + runs -> promise = atom.packages.activatePackage('package-with-activation-hooks') atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() @@ -704,7 +712,9 @@ describe "PackageManager", -> expect(pack.mainModule.someNumber).not.toBe 77 pack.mainModule.someNumber = 77 atom.packages.serializePackage("package-with-serialization") + waitsForPromise -> atom.packages.deactivatePackage("package-with-serialization") + runs -> spyOn(pack.mainModule, 'activate').andCallThrough() waitsForPromise -> atom.packages.activatePackage("package-with-serialization") @@ -872,6 +882,7 @@ describe "PackageManager", -> expect(events.length).toBe(1) expect(events[0].type).toBe("user-command") + waitsForPromise -> atom.packages.deactivatePackage("package-with-keymaps") waitsForPromise -> @@ -1041,12 +1052,15 @@ describe "PackageManager", -> consumerModule.consumeFirstServiceV4.reset() consumerModule.consumeSecondService.reset() + waitsForPromise -> atom.packages.deactivatePackage("package-with-provided-services") + runs -> expect(firstServiceV3Disposed).toBe true expect(firstServiceV4Disposed).toBe true expect(secondServiceDisposed).toBe true + waitsForPromise -> atom.packages.deactivatePackage("package-with-consumed-services") waitsForPromise -> @@ -1112,8 +1126,11 @@ describe "PackageManager", -> runs -> spyOn(pack1.mainModule, 'deactivate') spyOn(pack2.mainModule, 'serialize') + + waitsForPromise -> atom.packages.deactivatePackages() + runs -> expect(pack1.mainModule.deactivate).toHaveBeenCalled() expect(pack2.mainModule.serialize).not.toHaveBeenCalled() @@ -1131,7 +1148,10 @@ describe "PackageManager", -> expect(atom.packages.isPackageActive("package-with-deactivate")).toBeTruthy() spyOn(pack.mainModule, 'deactivate').andCallThrough() + waitsForPromise -> atom.packages.deactivatePackage("package-with-deactivate") + + runs -> expect(pack.mainModule.deactivate).toHaveBeenCalled() expect(atom.packages.isPackageActive("package-with-module")).toBeFalsy() @@ -1145,26 +1165,38 @@ describe "PackageManager", -> expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeTruthy() spyOn(badPack.mainModule, 'deactivate').andCallThrough() + waitsForPromise -> atom.packages.deactivatePackage("package-that-throws-on-activate") + + runs -> expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy() it "absorbs exceptions that are thrown by the package module's deactivate method", -> spyOn(console, 'error') + thrownError = null waitsForPromise -> atom.packages.activatePackage("package-that-throws-on-deactivate") + waitsForPromise -> + try + atom.packages.deactivatePackage("package-that-throws-on-deactivate") + catch error + thrownError = error + runs -> - expect(-> atom.packages.deactivatePackage("package-that-throws-on-deactivate")).not.toThrow() + expect(thrownError).toBeNull() expect(console.error).toHaveBeenCalled() it "removes the package's grammars", -> waitsForPromise -> atom.packages.activatePackage('package-with-grammars') - runs -> + waitsForPromise -> atom.packages.deactivatePackage('package-with-grammars') + + runs -> expect(atom.grammars.selectGrammar('a.alot').name).toBe 'Null Grammar' expect(atom.grammars.selectGrammar('a.alittle').name).toBe 'Null Grammar' @@ -1172,8 +1204,10 @@ describe "PackageManager", -> waitsForPromise -> atom.packages.activatePackage('package-with-keymaps') - runs -> + waitsForPromise -> atom.packages.deactivatePackage('package-with-keymaps') + + runs -> expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: createTestElement('test-1'))).toHaveLength 0 expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: createTestElement('test-2'))).toHaveLength 0 @@ -1181,8 +1215,10 @@ describe "PackageManager", -> waitsForPromise -> atom.packages.activatePackage('package-with-styles') - runs -> + waitsForPromise -> atom.packages.deactivatePackage('package-with-styles') + + runs -> one = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/1.css") two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less") three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css") @@ -1196,17 +1232,26 @@ describe "PackageManager", -> runs -> expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBe '^a' + + waitsForPromise -> atom.packages.deactivatePackage("package-with-settings") + + runs -> expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBeUndefined() it "invokes ::onDidDeactivatePackage listeners with the deactivated package", -> + deactivatedPackage = null + waitsForPromise -> atom.packages.activatePackage("package-with-main") runs -> - deactivatedPackage = null atom.packages.onDidDeactivatePackage (pack) -> deactivatedPackage = pack + + waitsForPromise -> atom.packages.deactivatePackage("package-with-main") + + runs -> expect(deactivatedPackage.name).toBe "package-with-main" describe "::activate()", -> @@ -1220,10 +1265,11 @@ describe "PackageManager", -> expect(loadedPackages.length).toBeGreaterThan 0 afterEach -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() - - jasmine.restoreDeprecationsSnapshot() + waitsForPromise -> + atom.packages.deactivatePackages() + runs -> + atom.packages.unloadPackages() + jasmine.restoreDeprecationsSnapshot() it "sets hasActivatedInitialPackages", -> spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) @@ -1286,6 +1332,9 @@ describe "PackageManager", -> it "disables an enabled package", -> packageName = 'package-with-main' + pack = null + activatedPackages = null + waitsForPromise -> atom.packages.activatePackage(packageName) @@ -1295,7 +1344,11 @@ describe "PackageManager", -> pack = atom.packages.disablePackage(packageName) + waitsFor -> activatedPackages = atom.packages.getActivePackages() + activatedPackages.length is 0 + + runs -> expect(activatedPackages).not.toContain(pack) expect(atom.config.get('core.disabledPackages')).toContain packageName diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index 5d2912f5b..86237b71d 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -8,9 +8,11 @@ describe "atom.themes", -> spyOn(console, 'warn') afterEach -> - atom.themes.deactivateThemes() - try - temp.cleanupSync() + waitsForPromise -> + atom.themes.deactivateThemes() + runs -> + try + temp.cleanupSync() describe "theme getters and setters", -> beforeEach -> diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index fd9af9342..bd631435e 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -328,7 +328,7 @@ class AtomEnvironment extends Model @contextMenu.clear() - @packages.reset().then -> + @packages.reset().then => @workspace.reset(@packages) @registerDefaultOpeners() @project.reset(@packages) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 0f019dbf0..b6e36ba5b 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -287,8 +287,7 @@ class ThemeManager deactivateThemes: -> @removeActiveThemeClasses() @unwatchUserStylesheet() - @packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes() - null + Promise.all(@packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes()) isInitialLoadComplete: -> @initialLoadComplete From af66c5efaabf3d52e4ae7aa1d92d60e6d5c3810b Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 14:52:08 -0700 Subject: [PATCH 038/448] Fix two more tests --- spec/package-spec.coffee | 9 ++++++--- spec/text-editor-registry-spec.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index 5018d2eb4..0a6ee01f9 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -138,7 +138,8 @@ describe "Package", -> jasmine.attachToDOM(editorElement) afterEach -> - theme.deactivate() if theme? + waitsForPromise -> + theme.deactivate() if theme? describe "when the theme contains a single style file", -> it "loads and applies css", -> @@ -200,8 +201,10 @@ describe "Package", -> it "deactivated event fires on .deactivate()", -> theme.onDidDeactivate spy = jasmine.createSpy() - theme.deactivate() - expect(spy).toHaveBeenCalled() + waitsForPromise -> + theme.deactivate() + runs -> + expect(spy).toHaveBeenCalled() describe ".loadMetadata()", -> [packagePath, metadata] = [] diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js index 79d575a5f..2479bff9b 100644 --- a/spec/text-editor-registry-spec.js +++ b/spec/text-editor-registry-spec.js @@ -685,7 +685,7 @@ describe('TextEditorRegistry', function () { registry.setGrammarOverride(editor, 'source.c') registry.setGrammarOverride(editor2, 'source.js') - atom.packages.deactivatePackage('language-javascript') + await atom.packages.deactivatePackage('language-javascript') const editorCopy = TextEditor.deserialize(editor.serialize(), atom) const editor2Copy = TextEditor.deserialize(editor2.serialize(), atom) From 468f4a47a92292167cf4f5982af691095650c3de Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 15:28:58 -0700 Subject: [PATCH 039/448] Fix another two tests --- spec/package-manager-spec.coffee | 3 ++- src/theme-manager.coffee | 36 +++++++++++++++----------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 294a2bfac..d0051a452 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -1375,7 +1375,8 @@ describe "PackageManager", -> atom.themes.activateThemes() afterEach -> - atom.themes.deactivateThemes() + waitsForPromise -> + atom.themes.deactivateThemes() it "enables and disables a theme", -> packageName = 'theme-with-package-file' diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index b6e36ba5b..742b86ddd 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -262,27 +262,25 @@ class ThemeManager new Promise (resolve) => # @config.observe runs the callback once, then on subsequent changes. @config.observe 'core.themes', => - @deactivateThemes() + @deactivateThemes().then => + @warnForNonExistentThemes() + @refreshLessCache() # Update cache for packages in core.themes config - @warnForNonExistentThemes() + 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.") - @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() + 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() From 79fbef8e2463ecf61ec26d3a4c0e7eea249dcba9 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 6 Sep 2017 16:06:17 -0700 Subject: [PATCH 040/448] :shirt: --- spec/package-manager-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index d0051a452..9b7d46340 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -1181,7 +1181,7 @@ describe "PackageManager", -> waitsForPromise -> try - atom.packages.deactivatePackage("package-that-throws-on-deactivate") + atom.packages.deactivatePackage("package-that-throws-on-deactivate") catch error thrownError = error @@ -1375,8 +1375,8 @@ describe "PackageManager", -> atom.themes.activateThemes() afterEach -> - waitsForPromise -> - atom.themes.deactivateThemes() + waitsForPromise -> + atom.themes.deactivateThemes() it "enables and disables a theme", -> packageName = 'theme-with-package-file' From 524d483610baaa074c47769ef5ddfff62b26ca73 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 8 Sep 2017 11:20:23 -0700 Subject: [PATCH 041/448] Ensure non-async deactivate is run syncronously without await --- src/package-manager.js | 7 ++++++- src/package.coffee | 34 +++++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/package-manager.js b/src/package-manager.js index 7718c8618..0f4484a5b 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -758,7 +758,12 @@ module.exports = class PackageManager { if (!suppressSerialization && this.isPackageActive(pack.name)) { this.serializePackage(pack) } - await pack.deactivate() + + const deactivationResult = pack.deactivate() + if (deactivationResult && typeof deactivationResult.then === 'function') { + await deactivationResult; + } + delete this.activePackages[pack.name] delete this.activatingPackages[pack.name] this.emitter.emit('did-deactivate-package', pack) diff --git a/src/package.coffee b/src/package.coffee index 556847bc9..fdd89bc74 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -506,18 +506,30 @@ class Package @configSchemaRegisteredOnActivate = false @deactivateResources() @deactivateKeymaps() - result = Promise.resolve() - if @mainActivated - try - result = Promise.resolve(@mainModule?.deactivate?()).then => - @mainModule?.deactivateConfig?() - @mainActivated = false - @mainInitialized = false - catch e - console.error "Error deactivating package '#{@name}'", e.stack - result = result.then => + + unless @mainActivated @emitter.emit 'did-deactivate' - result + return + + try + deactivationResult = @mainModule?.deactivate?() + catch e + console.error "Error deactivating package '#{@name}'", e.stack + + # We support then-able async promises as well as sync ones from deactivate + if deactivationResult?.then is 'function' + deactivationResult.then => @afterDeactivation() + else + @afterDeactivation() + + afterDeactivation: -> + try + @mainModule?.deactivateConfig?() + catch e + console.error "Error deactivating package '#{@name}'", e.stack + @mainActivated = false + @mainInitialized = false + @emitter.emit 'did-deactivate' deactivateResources: -> grammar.deactivate() for grammar in @grammars From 199bab89934fec7198cce0ae39fdaa1ae7087803 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 8 Sep 2017 13:19:36 -0700 Subject: [PATCH 042/448] Fix a few more tests --- spec/package-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index 0a6ee01f9..e7bfd0249 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -139,7 +139,7 @@ describe "Package", -> afterEach -> waitsForPromise -> - theme.deactivate() if theme? + Promise.resolve(theme.deactivate()) if theme? describe "when the theme contains a single style file", -> it "loads and applies css", -> @@ -202,7 +202,7 @@ describe "Package", -> it "deactivated event fires on .deactivate()", -> theme.onDidDeactivate spy = jasmine.createSpy() waitsForPromise -> - theme.deactivate() + Promise.resolve(theme.deactivate()) runs -> expect(spy).toHaveBeenCalled() From dab150ad6caeaed269acd13696dfb71360d61fc6 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 8 Sep 2017 14:10:35 -0700 Subject: [PATCH 043/448] :tshirt: --- src/package-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package-manager.js b/src/package-manager.js index 0f4484a5b..da5c1261c 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -761,7 +761,7 @@ module.exports = class PackageManager { const deactivationResult = pack.deactivate() if (deactivationResult && typeof deactivationResult.then === 'function') { - await deactivationResult; + await deactivationResult } delete this.activePackages[pack.name] From a21480f441025bd1819768058c76f65e59b42a3f Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 8 Sep 2017 16:10:09 -0700 Subject: [PATCH 044/448] Better error handling on passing invalid packs --- src/package-manager.js | 4 ++++ src/theme-manager.coffee | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/package-manager.js b/src/package-manager.js index da5c1261c..b52e29cad 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -755,6 +755,10 @@ module.exports = class PackageManager { // Deactivate the package with the given name async deactivatePackage (name, suppressSerialization) { const pack = this.getLoadedPackage(name) + if (pack == null) { + return + } + if (!suppressSerialization && this.isPackageActive(pack.name)) { this.serializePackage(pack) } diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 742b86ddd..062e725c7 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -285,7 +285,8 @@ class ThemeManager deactivateThemes: -> @removeActiveThemeClasses() @unwatchUserStylesheet() - Promise.all(@packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes()) + results = @getActiveThemes().map((pack) => @packageManager.deactivatePackage(pack.name)) + Promise.all(results.filter((r) => typeof r?.then is 'function')) isInitialLoadComplete: -> @initialLoadComplete From a5535977527230594422a61e472b57f33bbc5506 Mon Sep 17 00:00:00 2001 From: simurai Date: Sat, 9 Sep 2017 16:08:23 +0900 Subject: [PATCH 045/448] :arrow_up: notifications@v0.69.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6074a4f7d..b3a368d61 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "link": "0.31.3", "markdown-preview": "0.159.13", "metrics": "1.2.6", - "notifications": "0.69.1", + "notifications": "0.69.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.251.7", From a157834d140d28387e324be38f00050d79068a10 Mon Sep 17 00:00:00 2001 From: simurai Date: Sat, 9 Sep 2017 18:42:26 +0900 Subject: [PATCH 046/448] :arrow_up: settings-view@v0.251.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b3a368d61..2482028bf 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.7", + "settings-view": "0.251.8", "snippets": "1.1.4", "spell-check": "0.72.2", "status-bar": "1.8.13", From 69eb2d541dd5cb43e8126209026e5f555541075b Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Sat, 9 Sep 2017 09:28:43 -0700 Subject: [PATCH 047/448] :t-shirt: --- src/theme-manager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 062e725c7..d6a67ee61 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -286,7 +286,7 @@ class ThemeManager @removeActiveThemeClasses() @unwatchUserStylesheet() results = @getActiveThemes().map((pack) => @packageManager.deactivatePackage(pack.name)) - Promise.all(results.filter((r) => typeof r?.then is 'function')) + Promise.all(results.filter((r) -> typeof r?.then is 'function')) isInitialLoadComplete: -> @initialLoadComplete From 5cbe7c8897e688cc104f79201533ac7234f2ae6e Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Thu, 7 Sep 2017 05:25:12 +0000 Subject: [PATCH 048/448] spelling: appearance --- spec/text-editor-component-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 311759bac..518f0e6e9 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1601,7 +1601,7 @@ describe('TextEditorComponent', () => { const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'a'}) decoration.flash('b', 10) - // Flash on initial appearence of highlight + // Flash on initial appearance of highlight await component.getNextUpdatePromise() const highlights = element.querySelectorAll('.highlight.a') expect(highlights.length).toBe(1) From a92178b876b244d6f04eb047acd55a4e650df39f Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 04:44:14 +0000 Subject: [PATCH 049/448] spelling: asynchronous --- 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 69bf36da0..2dfece46e 100644 --- a/src/path-watcher.js +++ b/src/path-watcher.js @@ -437,7 +437,7 @@ class PathWatcher { // await watcher.getStartPromise() // fs.writeFile(FILE, 'contents\n', err => { // // The watcher is listening and the event should be - // // received asyncronously + // // received asynchronously // } // }) // }) From 1b7f3cad92df22ac1a50025599976fc1782e51a6 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:31:32 +0000 Subject: [PATCH 050/448] spelling: beginning --- spec/text-editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index fc67881ab..080dd16e6 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -3517,7 +3517,7 @@ describe "TextEditor", -> 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 begginning of the line", -> + 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) {' From cbc4823cdee8f11d9ae479175a6a0ad0f39b0f85 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:32:25 +0000 Subject: [PATCH 051/448] spelling: combinations --- src/text-editor-component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 61868228b..694a50b50 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2745,7 +2745,7 @@ class TextEditorComponent { // but keeping this calculation simple ensures the number of tiles remains // fixed for a given editor height, which eliminates situations where a // tile is repeatedly added and removed during scrolling in certain - // comibinations of editor height and line height. + // combinations of editor height and line height. getVisibleTileCount () { if (this.derivedDimensionsCache.visibleTileCount == null) { const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() / this.getRowsPerTile() From 9623acf633cd4e18641cb236444c6a2de7298ccb Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:32:49 +0000 Subject: [PATCH 052/448] spelling: compatible --- src/default-directory-provider.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee index 44d5298dd..e55379092 100644 --- a/src/default-directory-provider.coffee +++ b/src/default-directory-provider.coffee @@ -13,7 +13,7 @@ class DefaultDirectoryProvider # # Returns: # * {Directory} if the given URI is compatible with this provider. - # * `null` if the given URI is not compatibile with this provider. + # * `null` if the given URI is not compatible with this provider. directoryForURISync: (uri) -> normalizedPath = @normalizePath(uri) {host} = url.parse(uri) @@ -39,7 +39,7 @@ class DefaultDirectoryProvider # # Returns a {Promise} that resolves to: # * {Directory} if the given URI is compatible with this provider. - # * `null` if the given URI is not compatibile with this provider. + # * `null` if the given URI is not compatible with this provider. directoryForURI: (uri) -> Promise.resolve(@directoryForURISync(uri)) From 2dc531b23c613bb555e9978198ca76ab088157af Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:33:12 +0000 Subject: [PATCH 053/448] spelling: contiguous --- spec/tokenized-buffer-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 07e7e80e6..444004e2b 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -376,7 +376,7 @@ describe "TokenizedBuffer", -> expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual [[0, 0], [0, 3]] describe "when the selector matches a run of multiple tokens at the position", -> - it "returns the range covered by all contigous tokens (within a single line)", -> + it "returns the range covered by all contiguous tokens (within a single line)", -> expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]] describe ".indentLevelForRow(row)", -> From fa249d5764ae055601cc148421c3805dea7a172e Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:34:15 +0000 Subject: [PATCH 054/448] spelling: cursor --- spec/text-editor-spec.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 080dd16e6..dbc764b32 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -620,7 +620,7 @@ describe "TextEditor", -> expect(editor.getCursorBufferPosition()).toEqual [0, 0] describe ".moveToBottom()", -> - it "moves the cusor to the bottom of the buffer", -> + it "moves the cursor to the bottom of the buffer", -> editor.setCursorScreenPosition [0, 0] editor.addCursorAtScreenPosition [1, 0] editor.moveToBottom() @@ -1397,7 +1397,7 @@ describe "TextEditor", -> expect(selection1.isReversed()).toBeTruthy() describe ".selectToTop()", -> - it "selects text from cusor position to the top of the buffer", -> + it "selects text from cursor position to the top of the buffer", -> editor.setCursorScreenPosition [11, 2] editor.addCursorAtScreenPosition [10, 0] editor.selectToTop() @@ -1407,7 +1407,7 @@ describe "TextEditor", -> expect(editor.getLastSelection().isReversed()).toBeTruthy() describe ".selectToBottom()", -> - it "selects text from cusor position to the bottom of the buffer", -> + it "selects text from cursor position to the bottom of the buffer", -> editor.setCursorScreenPosition [10, 0] editor.addCursorAtScreenPosition [9, 3] editor.selectToBottom() @@ -1422,7 +1422,7 @@ describe "TextEditor", -> expect(editor.getLastSelection().getBufferRange()).toEqual buffer.getRange() describe ".selectToBeginningOfLine()", -> - it "selects text from cusor position to beginning of line", -> + it "selects text from cursor position to beginning of line", -> editor.setCursorScreenPosition [12, 2] editor.addCursorAtScreenPosition [11, 3] @@ -1441,7 +1441,7 @@ describe "TextEditor", -> expect(selection2.isReversed()).toBeTruthy() describe ".selectToEndOfLine()", -> - it "selects text from cusor position to end of line", -> + it "selects text from cursor position to end of line", -> editor.setCursorScreenPosition [12, 0] editor.addCursorAtScreenPosition [11, 3] @@ -1483,7 +1483,7 @@ describe "TextEditor", -> expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [4, 0]] describe ".selectToBeginningOfWord()", -> - it "selects text from cusor position to beginning of word", -> + it "selects text from cursor position to beginning of word", -> editor.setCursorScreenPosition [0, 13] editor.addCursorAtScreenPosition [3, 49] @@ -1502,7 +1502,7 @@ describe "TextEditor", -> expect(selection2.isReversed()).toBeTruthy() describe ".selectToEndOfWord()", -> - it "selects text from cusor position to end of word", -> + it "selects text from cursor position to end of word", -> editor.setCursorScreenPosition [0, 4] editor.addCursorAtScreenPosition [3, 48] @@ -1521,7 +1521,7 @@ describe "TextEditor", -> expect(selection2.isReversed()).toBeFalsy() describe ".selectToBeginningOfNextWord()", -> - it "selects text from cusor position to beginning of next word", -> + it "selects text from cursor position to beginning of next word", -> editor.setCursorScreenPosition [0, 4] editor.addCursorAtScreenPosition [3, 48] From 6ce5f482039b2d0589bd357698cbab69819abe8c Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:34:43 +0000 Subject: [PATCH 055/448] spelling: doesn't --- spec/text-editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index dbc764b32..52da37147 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4992,7 +4992,7 @@ describe "TextEditor", -> editor.insertText('\n') expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - describe "when the line preceding the newline does't add a level of indentation", -> + 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') From ee87ac1604a7bb446a80bc788959b2778f06ff3a Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:35:29 +0000 Subject: [PATCH 056/448] spelling: falsy --- spec/workspace-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 476a4ba5b..43a04eba9 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -2773,7 +2773,7 @@ i = /test/; #FIXME\ }) }) - describe('when the core.allowPendingPaneItems option is falsey', () => { + describe('when the core.allowPendingPaneItems option is falsy', () => { it('does not open item with `pending: true` option as pending', () => { let pane = null atom.config.set('core.allowPendingPaneItems', false) From cb3fb74d6b5ca73b97d3553004e24bebc50dda05 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:36:12 +0000 Subject: [PATCH 057/448] spelling: horizontal --- src/text-editor-component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 694a50b50..f9b68f1e6 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -117,7 +117,7 @@ class TextEditorComponent { this.linesToMeasure = new Map() this.extraRenderedScreenLines = new Map() this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure - this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions + this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horizontal pixel positions this.blockDecorationsToMeasure = new Set() this.blockDecorationsByElement = new WeakMap() this.blockDecorationSentinel = document.createElement('div') From d425272725c5d087e3daa77cb80e10eec0e6e1df Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:36:32 +0000 Subject: [PATCH 058/448] spelling: indentation --- spec/text-editor-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 52da37147..ccfd10fd1 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -3704,7 +3704,7 @@ describe "TextEditor", -> 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 indentaion", -> + 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) @@ -3727,7 +3727,7 @@ describe "TextEditor", -> 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 indentaion", -> + 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") From faa39508e699320a268fe1e6eb90d91802fbfd7c Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:36:49 +0000 Subject: [PATCH 059/448] spelling: interpretation --- src/tooltip-manager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee index 03630c87f..1a9b6fe44 100644 --- a/src/tooltip-manager.coffee +++ b/src/tooltip-manager.coffee @@ -65,7 +65,7 @@ class TooltipManager # * `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 interpetation of the `title` 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 From ad003695b5ff292c1596421b8b92b21009c99d57 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:36:59 +0000 Subject: [PATCH 060/448] spelling: intersecting --- src/text-editor.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 54de91054..24c228f83 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2510,7 +2510,7 @@ class TextEditor extends Model # Essential: Select from the current cursor position to the given position in # buffer coordinates. # - # This method may merge selections that end up intesecting. + # This method may merge selections that end up intersecting. # # * `position` An instance of {Point}, with a given `row` and `column`. selectToBufferPosition: (position) -> @@ -2521,7 +2521,7 @@ class TextEditor extends Model # Essential: Select from the current cursor position to the given position in # screen coordinates. # - # This method may merge selections that end up intesecting. + # This method may merge selections that end up intersecting. # # * `position` An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position, options) -> @@ -2535,7 +2535,7 @@ class TextEditor extends Model # # * `rowCount` (optional) {Number} number of rows to select (default: 1) # - # This method may merge selections that end up intesecting. + # This method may merge selections that end up intersecting. selectUp: (rowCount) -> @expandSelectionsBackward (selection) -> selection.selectUp(rowCount) @@ -2544,7 +2544,7 @@ class TextEditor extends Model # # * `rowCount` (optional) {Number} number of rows to select (default: 1) # - # This method may merge selections that end up intesecting. + # This method may merge selections that end up intersecting. selectDown: (rowCount) -> @expandSelectionsForward (selection) -> selection.selectDown(rowCount) @@ -2553,7 +2553,7 @@ class TextEditor extends Model # # * `columnCount` (optional) {Number} number of columns to select (default: 1) # - # This method may merge selections that end up intesecting. + # This method may merge selections that end up intersecting. selectLeft: (columnCount) -> @expandSelectionsBackward (selection) -> selection.selectLeft(columnCount) @@ -2562,7 +2562,7 @@ class TextEditor extends Model # # * `columnCount` (optional) {Number} number of columns to select (default: 1) # - # This method may merge selections that end up intesecting. + # This method may merge selections that end up intersecting. selectRight: (columnCount) -> @expandSelectionsForward (selection) -> selection.selectRight(columnCount) @@ -2589,7 +2589,7 @@ class TextEditor extends Model # 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 intesecting. + # This method may merge selections that end up intersecting. selectToBeginningOfLine: -> @expandSelectionsBackward (selection) -> selection.selectToBeginningOfLine() From 54156a8f77bcf9336936e43d233a4d0a280511a7 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:37:36 +0000 Subject: [PATCH 061/448] spelling: javascript --- src/compile-cache.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/compile-cache.js b/src/compile-cache.js index 4209b30ab..a4f9ded1e 100644 --- a/src/compile-cache.js +++ b/src/compile-cache.js @@ -84,20 +84,20 @@ function compileFileAtPath (compiler, filePath, extension) { var sourceCode = fs.readFileSync(filePath, 'utf8') if (compiler.shouldCompile(sourceCode, filePath)) { var cachePath = compiler.getCachePath(sourceCode, filePath) - var compiledCode = readCachedJavascript(cachePath) + var compiledCode = readCachedJavaScript(cachePath) if (compiledCode != null) { cacheStats[extension].hits++ } else { cacheStats[extension].misses++ compiledCode = compiler.compile(sourceCode, filePath) - writeCachedJavascript(cachePath, compiledCode) + writeCachedJavaScript(cachePath, compiledCode) } return compiledCode } return sourceCode } -function readCachedJavascript (relativeCachePath) { +function readCachedJavaScript (relativeCachePath) { var cachePath = path.join(cacheDirectory, relativeCachePath) if (fs.isFileSync(cachePath)) { try { @@ -107,7 +107,7 @@ function readCachedJavascript (relativeCachePath) { return null } -function writeCachedJavascript (relativeCachePath, code) { +function writeCachedJavaScript (relativeCachePath, code) { var cachePath = path.join(cacheDirectory, relativeCachePath) fs.writeFileSync(cachePath, code, 'utf8') } @@ -153,7 +153,7 @@ exports.install = function (resourcesPath, nodeRequire) { if (!compiler) compiler = COMPILERS['.js'] try { - var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath)) + var fileData = readCachedJavaScript(compiler.getCachePath(sourceCode, filePath)) } catch (error) { console.warn('Error reading compiled file', error.stack) return null From ef946374b4e2c7c31a77c087cf8ddf4ca0c20f12 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:39:04 +0000 Subject: [PATCH 062/448] spelling: multiple --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 24c228f83..0eb0b1dee 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1223,7 +1223,7 @@ class TextEditor extends Model @autoIndentSelectedRows() if @shouldAutoIndent() @scrollToBufferPosition([newSelectionRanges[0].start.row, 0]) - # Move lines intersecting the most recent selection or muiltiple selections + # Move lines intersecting the most recent selection or multiple selections # down by one row in screen coordinates. moveLineDown: -> selections = @getSelectedBufferRanges() From 0e7739a026fdcacfdaa1082fdc0059948aa84c3c Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:39:32 +0000 Subject: [PATCH 063/448] spelling: package --- script/lib/transpile-packages-with-custom-transpiler-paths.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/lib/transpile-packages-with-custom-transpiler-paths.js b/script/lib/transpile-packages-with-custom-transpiler-paths.js index 558368905..7aaf1c319 100644 --- a/script/lib/transpile-packages-with-custom-transpiler-paths.js +++ b/script/lib/transpile-packages-with-custom-transpiler-paths.js @@ -25,7 +25,7 @@ module.exports = function () { const rootPackageBackup = backupNodeModules(rootPackagePath) const intermediatePackageBackup = backupNodeModules(intermediatePackagePath) - // Run `apm install` in the *root* pacakge's path, so we get devDeps w/o apm's weird caching + // Run `apm install` in the *root* package's path, so we get devDeps w/o apm's weird caching // Then copy this folder into the intermediate package's path so we can run the transpilation in-line. runApmInstall(rootPackagePath) if (fs.existsSync(intermediatePackageBackup.nodeModulesPath)) { From 6fc5ff851341d8395f37de2968fbeeee1e37249e Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:39:47 +0000 Subject: [PATCH 064/448] spelling: previous --- spec/text-editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index ccfd10fd1..7ef0d1731 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1364,7 +1364,7 @@ describe "TextEditor", -> expect(selections[0].getScreenRange()).toEqual [[3, 0], [10, 0]] describe ".selectToBeginningOfPreviousParagraph()", -> - it "selects from the cursor to the first line of the pevious paragraph", -> + 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]) From 27e021e22b750080955507c35492abd5142705a0 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:40:11 +0000 Subject: [PATCH 065/448] spelling: precedes --- spec/text-editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 7ef0d1731..e91746fbd 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4820,7 +4820,7 @@ describe "TextEditor", -> expect(buffer.lineForRow(6)).toBe(line7) expect(buffer.getLineCount()).toBe(count - 1) - describe "when the line being deleted preceeds a fold, and the command is undone", -> + 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() From 161a7f1788570b35e108b4c2040e724855bb6eda Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:40:41 +0000 Subject: [PATCH 066/448] spelling: perseveres --- spec/text-editor-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index e91746fbd..441368d3c 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -2258,7 +2258,7 @@ describe "TextEditor", -> describe "when the preceding row consists of folded code", -> - it "moves the line above the folded row and preseveres the correct folds", -> + 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 " };" From 00130a7589befcdf4d47563243bd25cd612b3b7f Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:40:51 +0000 Subject: [PATCH 067/448] spelling: programmatically --- spec/tooltip-manager-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee index 35e563dae..95182853e 100644 --- a/spec/tooltip-manager-spec.coffee +++ b/spec/tooltip-manager-spec.coffee @@ -204,7 +204,7 @@ describe "TooltipManager", -> disposable2.dispose() expect(manager.findTooltips(element).length).toBe(0) - it "lets us hide tooltips programatically", -> + it "lets us hide tooltips programmatically", -> disposable = manager.add element, title: "Title" hover element, -> expect(document.body.querySelector(".tooltip")).not.toBeNull() From 1b93fcf35ff9140954fdf31409e475c2c4ce15b5 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:41:06 +0000 Subject: [PATCH 068/448] spelling: recover --- spec/pane-container-element-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index b3953bddc..21c1d000a 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -172,7 +172,7 @@ describe "PaneContainerElement", -> lowerPane = leftPane.splitDown() expectPaneScale [lowerPane, 1], [leftPane, 1], [leftPane.getParent(), 0.5] - # dynamically close pane, the pane's flexscale will recorver to origin value + # dynamically close pane, the pane's flexscale will recover to origin value waitsForPromise -> lowerPane.close() runs -> expectPaneScale [leftPane, 0.5], [rightPane, 1.5] From 9ca5662bcbaca61ee85ac5635ed3fb8b03292396 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:41:19 +0000 Subject: [PATCH 069/448] spelling: recycle --- spec/text-editor-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 441368d3c..ec4a96359 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1800,7 +1800,7 @@ describe "TextEditor", -> editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[3, 3], [5, 5]]] - it "recyles existing selection instances", -> + it "recycles existing selection instances", -> selection = editor.getLastSelection() editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) @@ -1849,7 +1849,7 @@ describe "TextEditor", -> editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - it "recyles existing selection instances", -> + it "recycles existing selection instances", -> selection = editor.getLastSelection() editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]]) From 26d44a2ffd0265937ce292331b2b893d2026403e Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:41:30 +0000 Subject: [PATCH 070/448] spelling: registry --- src/native-watcher-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 798ff0619..a049d06ac 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -323,7 +323,7 @@ class RegistryWatcherNode { } } -// Private: A {RegisteryNode} traversal result that's returned when neither a directory, its children, nor its parents +// Private: A {RegistryNode} traversal result that's returned when neither a directory, its children, nor its parents // are present in the tree. class MissingResult { From e6f3aeeb2e63d47c1b220579109a44ad6107cc82 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:41:41 +0000 Subject: [PATCH 071/448] spelling: responsibility --- src/native-watcher-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index a049d06ac..100517920 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -239,7 +239,7 @@ class RegistryWatcherNode { this.childPaths.add(path.join(...childPathSegments)) } - // Private: Stop assuming responsbility for a previously assigned child path. If this node is + // Private: Stop assuming responsibility for a previously assigned child path. If this node is // removed, the named child path will no longer be allocated a {RegistryWatcherNode}. // // * `childPathSegments` the {Array} of path segments between this node's directory and the no longer From 3bf9be2a47510dca704fbb61f0cac597997da833 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:42:02 +0000 Subject: [PATCH 072/448] spelling: separated --- spec/reopen-project-menu-manager-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/reopen-project-menu-manager-spec.js b/spec/reopen-project-menu-manager-spec.js index e508b68ba..b11561c31 100644 --- a/spec/reopen-project-menu-manager-spec.js +++ b/spec/reopen-project-menu-manager-spec.js @@ -222,7 +222,7 @@ describe("ReopenProjectMenuManager", () => { expect(label).toBe('https://launch.pad/apollo/11') }) - it("returns a comma-seperated list of base names if there are multiple", () => { + it("returns a comma-separated list of base names if there are multiple", () => { const project = { paths: [ '/var/one', '/usr/bin/two', '/etc/mission/control/three' ] } const label = ReopenProjectMenuManager.createLabel(project) expect(label).toBe('one, two, three') From 4fbae9f4585609557a346e2c1aa466c85544db57 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:42:36 +0000 Subject: [PATCH 073/448] spelling: stylesheet --- src/theme-manager.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 0f019dbf0..e83ab5b80 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -127,7 +127,7 @@ class ThemeManager # Resolve and apply the stylesheet specified by the path. # - # This supports both CSS and Less stylsheets. + # 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. @@ -142,8 +142,8 @@ class ThemeManager throw new Error("Could not find a file at path '#{stylesheetPath}'") unwatchUserStylesheet: -> - @userStylsheetSubscriptions?.dispose() - @userStylsheetSubscriptions = null + @userStylesheetSubscriptions?.dispose() + @userStylesheetSubscriptions = null @userStylesheetFile = null @userStyleSheetDisposable?.dispose() @userStyleSheetDisposable = null @@ -156,11 +156,11 @@ class ThemeManager try @userStylesheetFile = new File(userStylesheetPath) - @userStylsheetSubscriptions = new CompositeDisposable() + @userStylesheetSubscriptions = new CompositeDisposable() reloadStylesheet = => @loadUserStylesheet() - @userStylsheetSubscriptions.add(@userStylesheetFile.onDidChange(reloadStylesheet)) - @userStylsheetSubscriptions.add(@userStylesheetFile.onDidRename(reloadStylesheet)) - @userStylsheetSubscriptions.add(@userStylesheetFile.onDidDelete(reloadStylesheet)) + @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 From 2ae8ec19d50bf089fca37fd8990c2c5642ebbf0f Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:43:07 +0000 Subject: [PATCH 074/448] spelling: successfully --- src/native-watcher-registry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 100517920..e63ac6cda 100644 --- a/src/native-watcher-registry.js +++ b/src/native-watcher-registry.js @@ -329,7 +329,7 @@ class MissingResult { // Private: Instantiate a new {MissingResult}. // - // * `lastParent` the final succesfully traversed {RegistryNode}. + // * `lastParent` the final successfully traversed {RegistryNode}. constructor (lastParent) { this.lastParent = lastParent } From dee7be7dd62f47410a6b2539830e9a3a006e3500 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:43:25 +0000 Subject: [PATCH 075/448] spelling: syntactic --- spec/language-mode-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee index 4025472af..1edc00a95 100644 --- a/spec/language-mode-spec.coffee +++ b/spec/language-mode-spec.coffee @@ -347,13 +347,13 @@ describe "LanguageMode", -> expect([fold.start.row, fold.end.row]).toEqual [1, 9] describe "when bufferRow can't be folded", -> - it "searches upward for the first row that begins a syntatic region containing the given buffer row (and folds it)", -> + it "searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)", -> languageMode.foldBufferRow(8) [fold] = languageMode.unfoldAll() expect([fold.start.row, fold.end.row]).toEqual [1, 9] describe "when the bufferRow is already folded", -> - it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", -> + it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", -> languageMode.foldBufferRow(2) expect(editor.isFoldedAtBufferRow(0)).toBe(false) expect(editor.isFoldedAtBufferRow(1)).toBe(true) @@ -369,7 +369,7 @@ describe "LanguageMode", -> expect([fold.start.row, fold.end.row]).toEqual [1, 3] describe "when the bufferRow is a single-line comment", -> - it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", -> + it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", -> buffer.insert([1, 0], " //this is a single line comment\n") languageMode.foldBufferRow(1) [fold] = languageMode.unfoldAll() From a5e3dfc802150b548ba56f3c090c977b6a683846 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:44:23 +0000 Subject: [PATCH 076/448] spelling: version --- script/lib/check-chromedriver-version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/lib/check-chromedriver-version.js b/script/lib/check-chromedriver-version.js index 6fd313fc7..1659f093c 100644 --- a/script/lib/check-chromedriver-version.js +++ b/script/lib/check-chromedriver-version.js @@ -9,7 +9,7 @@ module.exports = function () { const chromedriverVer = buildMetadata.dependencies['electron-chromedriver'] const mksnapshotVer = buildMetadata.dependencies['electron-mksnapshot'] - // Always use tilde on electron-chromedriver so that it can pick up the best patch vesion + // Always use tilde on electron-chromedriver so that it can pick up the best patch version if (!chromedriverVer.startsWith('~')) { throw new Error(`electron-chromedriver version in script/package.json should start with a tilde to match latest patch version.`) } From feb27a667668329bc83bcbd8d9f17c06414ef1cb Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:44:31 +0000 Subject: [PATCH 077/448] spelling: visible --- spec/panel-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/panel-spec.js b/spec/panel-spec.js index 7034cbec1..8df51a2fb 100644 --- a/spec/panel-spec.js +++ b/spec/panel-spec.js @@ -71,7 +71,7 @@ describe('Panel', () => { expect(spy).toHaveBeenCalledWith(false) }) - it('initially renders panel created with visibile: false', () => { + it('initially renders panel created with visible: false', () => { const panel = new Panel({visible: false, item: new TestPanelItem()}, atom.views) const element = panel.getElement() expect(element.style.display).toBe('none') @@ -91,7 +91,7 @@ describe('Panel', () => { }) describe('when a class name is specified', () => { - it('initially renders panel created with visibile: false', () => { + it('initially renders panel created with visible: false', () => { const panel = new Panel({className: 'some classes', item: new TestPanelItem()}, atom.views) const element = panel.getElement() From e39970cd1a8fb3291d98b27facc7377a5d45e6fd Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:44:39 +0000 Subject: [PATCH 078/448] spelling: visibility --- src/dock.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dock.js b/src/dock.js index 30284f884..7f2856800 100644 --- a/src/dock.js +++ b/src/dock.js @@ -119,7 +119,7 @@ module.exports = class Dock { this.setState({visible: false}) } - // Extended: Toggle the dock's visiblity without changing the {Workspace}'s + // Extended: Toggle the dock's visibility without changing the {Workspace}'s // active pane container. toggle () { const state = {visible: !this.state.visible} @@ -143,7 +143,7 @@ module.exports = class Dock { // frame to ensure the property is animated (or not) appropriately, however we luck out in this // case because the drag start always happens before the item is dragged into the toggle button. if (nextState.visible !== prevState.visible) { - // Never animate toggling visiblity... + // Never animate toggling visibility... nextState.shouldAnimate = false } else if (!nextState.visible && nextState.draggingItem && !prevState.draggingItem) { // ...but do animate if you start dragging while the panel is hidden. From c49c62aaa7d62e4a03a6dcbd1f5b5ffd7ca64f8e Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:44:44 +0000 Subject: [PATCH 079/448] spelling: visually --- src/config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index f0628ffee..bbcdad01f 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -221,7 +221,7 @@ ScopeDescriptor = require './scope-descriptor' # #### object / Grouping other types # # A config setting with the type `object` allows grouping a set of config -# settings. The group will be visualy separated and has its own group headline. +# settings. The group will be visually separated and has its own group headline. # The sub options must be listed under a `properties` key. # # ```coffee From 2947d6b81d0e239081db6f471096ddb4d4c90ece Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 10 Sep 2017 15:45:09 +0000 Subject: [PATCH 080/448] spelling: workspace --- src/workspace-element.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/workspace-element.js b/src/workspace-element.js index 3ba2c620f..bd0e1b971 100644 --- a/src/workspace-element.js +++ b/src/workspace-element.js @@ -78,10 +78,10 @@ class WorkspaceElement extends HTMLElement { this.project = project this.config = config this.styleManager = styleManager - if (this.viewRegistry == null) { throw new Error('Must pass a viewRegistry parameter when initializing WorskpaceElements') } - if (this.project == null) { throw new Error('Must pass a project parameter when initializing WorskpaceElements') } - if (this.config == null) { throw new Error('Must pass a config parameter when initializing WorskpaceElements') } - if (this.styleManager == null) { throw new Error('Must pass a styleManager parameter when initializing WorskpaceElements') } + if (this.viewRegistry == null) { throw new Error('Must pass a viewRegistry parameter when initializing WorkspaceElements') } + if (this.project == null) { throw new Error('Must pass a project parameter when initializing WorkspaceElements') } + if (this.config == null) { throw new Error('Must pass a config parameter when initializing WorkspaceElements') } + if (this.styleManager == null) { throw new Error('Must pass a styleManager parameter when initializing WorkspaceElements') } this.subscriptions = new CompositeDisposable( new Disposable(() => { From ea249777ddc1129b1bca64d165b85a5eb06e5985 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Sun, 10 Sep 2017 13:32:02 -0700 Subject: [PATCH 081/448] Make the atom.getReleaseChannel function public --- src/atom-environment.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index b37acddd1..a6558ab17 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -445,7 +445,9 @@ class AtomEnvironment extends Model getVersion: -> @appVersion ?= @getLoadSettings().appVersion - # Returns the release channel as a {String}. Will return one of `'dev', 'beta', 'stable'` + # 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 From 9e27c5989dd56cc6b1380838dc5b4e9a0e19b384 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Sun, 10 Sep 2017 19:53:53 -0700 Subject: [PATCH 082/448] :arrow_up: about@1.7.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2482028bf..65701c360 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "one-light-syntax": "1.8.0", "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", - "about": "1.7.7", + "about": "1.7.8", "archive-view": "0.63.3", "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", From 787b537f842f5926f566c2fb7efe5c70becf8cbc Mon Sep 17 00:00:00 2001 From: Hubot Date: Mon, 11 Sep 2017 00:13:52 -0500 Subject: [PATCH 083/448] 1.22.0-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65701c360..416bbf6c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.21.0-dev", + "version": "1.22.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { From a79ee746d144d05516d2430af3c1cbdb4f28050b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 11 Sep 2017 09:20:44 -0400 Subject: [PATCH 084/448] :arrow_up: season --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 80290b47a..f42f96c24 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "scandal": "^3.1.0", "scoped-property-store": "^0.17.0", "scrollbar-style": "^3.2", - "season": "^6.0.0", + "season": "^6.0.1", "semver": "^4.3.3", "service-hub": "^0.7.4", "sinon": "1.17.4", From 07c3c6ea2d17b2bb2921391d2cfb0ea27e8189f2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 11 Sep 2017 09:47:02 -0700 Subject: [PATCH 085/448] Use the Atom Beta executable to verify startup snapshots when building Beta --- script/lib/generate-startup-snapshot.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index bb635b4c1..7701b6a34 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -78,7 +78,8 @@ module.exports = function (packagedAppPath) { const verifySnapshotScriptPath = path.join(CONFIG.repositoryRootPath, 'script', 'verify-snapshot-script') let nodeBundledInElectronPath if (process.platform === 'darwin') { - nodeBundledInElectronPath = path.join(packagedAppPath, 'Contents', 'MacOS', 'Atom') + const executableName = CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom' + nodeBundledInElectronPath = path.join(packagedAppPath, 'Contents', 'MacOS', executableName) } else if (process.platform === 'win32') { nodeBundledInElectronPath = path.join(packagedAppPath, 'atom.exe') } else { From ce99d401c28cea3088c178169c47943aa006e38b Mon Sep 17 00:00:00 2001 From: Warren Powell Date: Fri, 19 May 2017 15:55:52 +1200 Subject: [PATCH 086/448] Enables MAX_SCREEN_LINE_LENGTH to now be set via a config option in editor settings. Defaults to 500 --- src/config-schema.js | 6 ++++++ src/text-editor-registry.js | 1 + 2 files changed, 7 insertions(+) diff --git a/src/config-schema.js b/src/config-schema.js index fb0164766..00fb8bbe3 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -409,6 +409,12 @@ const configSchema = { minimum: 1, description: 'Identifies the length of a line which is used when wrapping text with the `Soft Wrap At Preferred Line Length` setting enabled, in number of characters.' }, + maxScreenLineLength: { + type: 'integer', + default: 500, + minimum: 500, + description: 'Defines the maximum width of the editor window before soft wrapping is enforced, in number of characters.' + }, tabLength: { type: 'integer', default: 2, diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 72aa8b364..35be27fd1 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -18,6 +18,7 @@ const EDITOR_PARAMS_BY_SETTING_KEY = [ ['editor.softWrapHangingIndent', 'softWrapHangingIndentLength'], ['editor.softWrapAtPreferredLineLength', 'softWrapAtPreferredLineLength'], ['editor.preferredLineLength', 'preferredLineLength'], + ['editor.maxScreenLineLength', 'maxScreenLineLength'], ['editor.autoIndent', 'autoIndent'], ['editor.autoIndentOnPaste', 'autoIndentOnPaste'], ['editor.scrollPastEnd', 'scrollPastEnd'], From a73f68c0dd3cf4d244457262940f812bf1a3ecea Mon Sep 17 00:00:00 2001 From: Warren Powell Date: Wed, 24 May 2017 15:43:50 +1200 Subject: [PATCH 087/448] Spec test for setMaxScreenLineLength --- spec/text-editor-spec.coffee | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index ec4a96359..efe023a21 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5918,3 +5918,12 @@ describe "TextEditor", -> 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.maxScreenLineLength()).toBe 500 + editor.update({ + maxScreenLineLength: 1500 + }) + expect(editor.maxScreenLineLength()).toBe 1500 + From f2bb941a51cef395e5ff5a52933448d7c47e85ff Mon Sep 17 00:00:00 2001 From: wpowell-oss Date: Tue, 12 Sep 2017 13:51:57 +1200 Subject: [PATCH 088/448] Rebase to github/master --- spec/text-editor-spec.coffee | 9 --------- src/config-schema.js | 6 ------ src/text-editor-registry.js | 1 - 3 files changed, 16 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index efe023a21..ec4a96359 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5918,12 +5918,3 @@ describe "TextEditor", -> 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.maxScreenLineLength()).toBe 500 - editor.update({ - maxScreenLineLength: 1500 - }) - expect(editor.maxScreenLineLength()).toBe 1500 - diff --git a/src/config-schema.js b/src/config-schema.js index 00fb8bbe3..fb0164766 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -409,12 +409,6 @@ const configSchema = { minimum: 1, description: 'Identifies the length of a line which is used when wrapping text with the `Soft Wrap At Preferred Line Length` setting enabled, in number of characters.' }, - maxScreenLineLength: { - type: 'integer', - default: 500, - minimum: 500, - description: 'Defines the maximum width of the editor window before soft wrapping is enforced, in number of characters.' - }, tabLength: { type: 'integer', default: 2, diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 35be27fd1..72aa8b364 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -18,7 +18,6 @@ const EDITOR_PARAMS_BY_SETTING_KEY = [ ['editor.softWrapHangingIndent', 'softWrapHangingIndentLength'], ['editor.softWrapAtPreferredLineLength', 'softWrapAtPreferredLineLength'], ['editor.preferredLineLength', 'preferredLineLength'], - ['editor.maxScreenLineLength', 'maxScreenLineLength'], ['editor.autoIndent', 'autoIndent'], ['editor.autoIndentOnPaste', 'autoIndentOnPaste'], ['editor.scrollPastEnd', 'scrollPastEnd'], From e9cf61684489a953295fb5deb9cd52ffa36787b8 Mon Sep 17 00:00:00 2001 From: t9md Date: Tue, 12 Sep 2017 11:26:51 +0900 Subject: [PATCH 089/448] FIX "softWrapped" appear twice in field extraction --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 0eb0b1dee..68bd03abf 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -153,7 +153,7 @@ class TextEditor extends Model { @softTabs, @initialScrollTopRow, @initialScrollLeftColumn, initialLine, initialColumn, tabLength, - @softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation, + @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, @showLineNumbers, @largeFileMode, @assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @scrollSensitivity, @editorWidthInChars, @tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide, From 0b55d4e144a99ef63679fe4c7951e0fa4a957796 Mon Sep 17 00:00:00 2001 From: wpowell-oss Date: Tue, 12 Sep 2017 14:38:29 +1200 Subject: [PATCH 090/448] maxScreenLineLength rebased to github --- spec/text-editor-registry-spec.js | 15 +++++++++++++++ spec/text-editor-spec.coffee | 11 ++++++++++- src/config-schema.js | 6 ++++++ src/text-editor-registry.js | 1 + src/text-editor.coffee | 13 +++++++++---- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js index 2479bff9b..017ef1f1b 100644 --- a/spec/text-editor-registry-spec.js +++ b/spec/text-editor-registry-spec.js @@ -544,6 +544,21 @@ describe('TextEditorRegistry', function () { expect(editor.getSoftWrapColumn()).toBe(80) }) + it('allows for custom definition of maximum soft wrap based on config', async function () { + editor.update({ + softWrapped: false, + maxScreenLineLength: 1500, + }) + + expect(editor.getSoftWrapColumn()).toBe(1500) + + atom.config.set('editor.softWrap', false) + atom.config.set('editor.maxScreenLineLength', 500) + registry.maintainConfig(editor) + await initialPackageActivation + expect(editor.getSoftWrapColumn()).toBe(500) + }) + it('sets the preferred line length based on the config', async function () { editor.update({preferredLineLength: 80}) expect(editor.getPreferredLineLength()).toBe(80) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index ec4a96359..47b85bf1f 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -74,6 +74,7 @@ describe "TextEditor", -> 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()) describe "when the editor is constructed with the largeFileMode option set to true", -> it "loads the editor but doesn't tokenize", -> @@ -145,7 +146,7 @@ describe "TextEditor", -> returnedPromise = editor.update({ tabLength: 6, softTabs: false, softWrapped: true, editorWidthInChars: 40, showInvisibles: false, mini: false, lineNumberGutterVisible: false, scrollPastEnd: true, - autoHeight: false + autoHeight: false, maxScreenLineLength: 1000 }) expect(returnedPromise).toBe(element.component.getNextUpdatePromise()) @@ -5918,3 +5919,11 @@ describe "TextEditor", -> 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/src/config-schema.js b/src/config-schema.js index fb0164766..00fb8bbe3 100644 --- a/src/config-schema.js +++ b/src/config-schema.js @@ -409,6 +409,12 @@ const configSchema = { minimum: 1, description: 'Identifies the length of a line which is used when wrapping text with the `Soft Wrap At Preferred Line Length` setting enabled, in number of characters.' }, + maxScreenLineLength: { + type: 'integer', + default: 500, + minimum: 500, + description: 'Defines the maximum width of the editor window before soft wrapping is enforced, in number of characters.' + }, tabLength: { type: 'integer', default: 2, diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 72aa8b364..35be27fd1 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -18,6 +18,7 @@ const EDITOR_PARAMS_BY_SETTING_KEY = [ ['editor.softWrapHangingIndent', 'softWrapHangingIndentLength'], ['editor.softWrapAtPreferredLineLength', 'softWrapAtPreferredLineLength'], ['editor.preferredLineLength', 'preferredLineLength'], + ['editor.maxScreenLineLength', 'maxScreenLineLength'], ['editor.autoIndent', 'autoIndent'], ['editor.autoIndentOnPaste', 'autoIndentOnPaste'], ['editor.scrollPastEnd', 'scrollPastEnd'], diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 0eb0b1dee..f0b7f7f52 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -17,7 +17,6 @@ TextEditorElement = null {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' ZERO_WIDTH_NBSP = '\ufeff' -MAX_SCREEN_LINE_LENGTH = 500 # Essential: This class represents all essential editing state for a single # {TextBuffer}, including cursor and selection positions, folds, and soft wraps. @@ -158,7 +157,7 @@ class TextEditor extends Model @assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @scrollSensitivity, @editorWidthInChars, @tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide, @softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength, - @showCursorOnSelection + @showCursorOnSelection, @maxScreenLineLength } = params @assert ?= (condition) -> condition @@ -183,6 +182,7 @@ class TextEditor extends Model @softWrapped ?= false @softWrapAtPreferredLineLength ?= false @preferredLineLength ?= 80 + @maxScreenLineLength ?= 500 @showLineNumbers ?= true @buffer ?= new TextBuffer({ @@ -323,6 +323,11 @@ class TextEditor extends Model @preferredLineLength = value displayLayerParams.softWrapColumn = @getSoftWrapColumn() + when 'maxScreenLineLength' + if value isnt @maxScreenLineLength + @maxScreenLineLength = value + displayLayerParams.softWrapColumn = @getSoftWrapColumn() + when 'mini' if value isnt @mini @mini = value @@ -433,7 +438,7 @@ class TextEditor extends Model softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent @id, @softTabs, @softWrapped, @softWrapAtPreferredLineLength, - @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, + @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, @maxScreenLineLength, @registered, @invisibles, @showInvisibles, @showIndentGuide, @autoHeight, @autoWidth } @@ -3039,7 +3044,7 @@ class TextEditor extends Model else @getEditorWidthInChars() else - MAX_SCREEN_LINE_LENGTH + @maxScreenLineLength ### Section: Indentation From 046ff87f0167814ec08576a371b90bd42af969c9 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:12:38 -0400 Subject: [PATCH 091/448] Trick the specs into seeing the settings as loaded --- spec/config-spec.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 495ce031e..e1cd754d9 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -13,6 +13,7 @@ describe "Config", -> dotAtomPath = temp.path('atom-spec-config') atom.config.configDirPath = dotAtomPath atom.config.enablePersistence = true + atom.config.settingsLoaded = true atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson") afterEach -> From 21e220cd5fb1cd0250e6e34d9024e1dbf68b053f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:13:05 -0400 Subject: [PATCH 092/448] Stub the correct fs method --- spec/config-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index e1cd754d9..cb4b19113 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -878,7 +878,7 @@ describe "Config", -> beforeEach -> atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() - spyOn(fs, "existsSync").andCallFake -> + spyOn(fs, "makeTreeSync").andCallFake -> error = new Error() error.code = 'EPERM' throw error From fa0bd1e049fe39e752e4e8cc64ff91ba5aae2559 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:15:55 -0400 Subject: [PATCH 093/448] Use a real clock and artificial file mtimes to work with nsfw --- spec/config-spec.coffee | 57 +++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index cb4b19113..b33b9226b 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -896,19 +896,15 @@ describe "Config", -> describe ".observeUserConfig()", -> updatedHandler = null - writeConfigFile = (data) -> - previousSetTimeoutCallCount = setTimeout.callCount - runs -> - fs.writeFileSync(atom.config.configFilePath, data) - # waitsFor "debounced config file load", -> - # setTimeout.callCount > previousSetTimeoutCallCount - waitsFor "file written", -> - fs.readFileSync(atom.config.configFilePath, 'utf8') is data - runs -> - advanceClock(1000) + writeConfigFile = (data, secondsInFuture = 0) -> + fs.writeFileSync(atom.config.configFilePath, data) + + future = (Date.now() / 1000) + secondsInFuture + fs.utimesSync(atom.config.configFilePath, future, future) beforeEach -> - console.log 'beforeEach' + jasmine.useRealClock() + atom.config.setSchema 'foo', type: 'object' properties: @@ -924,7 +920,7 @@ describe "Config", -> default: 12 expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy() - fs.writeFileSync atom.config.configFilePath, """ + writeConfigFile """ '*': foo: bar: 'baz' @@ -955,9 +951,11 @@ describe "Config", -> atom.config.unobserveUserConfig() fs.removeSync(dotAtomPath) - fit "updates the config data", -> - writeConfigFile("foo: { bar: 'quux', baz: 'bar'}") + it "updates the config data", -> + writeConfigFile "foo: { bar: 'quux', baz: 'bar'}", 2 + waitsFor 'update event', -> updatedHandler.callCount > 0 + runs -> expect(atom.config.get('foo.bar')).toBe 'quux' expect(atom.config.get('foo.baz')).toBe 'bar' @@ -965,7 +963,7 @@ describe "Config", -> it "does not fire a change event for paths that did not change", -> atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy() - writeConfigFile("foo: { bar: 'baz', baz: 'ok'}") + writeConfigFile "foo: { bar: 'baz', baz: 'ok'}", 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> @@ -980,7 +978,8 @@ describe "Config", -> items: type: 'string' - writeConfigFile("foo: { bar: ['baz', 'ok']}") + updatedHandler.reset() + writeConfigFile "foo: { bar: ['baz', 'ok']}", 4 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> updatedHandler.reset() @@ -988,7 +987,7 @@ describe "Config", -> noChangeSpy = jasmine.createSpy() atom.config.onDidChange('foo.bar', noChangeSpy) - writeConfigFile("foo: { bar: ['baz', 'ok'], baz: 'another'}") + writeConfigFile "foo: { bar: ['baz', 'ok'], baz: 'another'}", 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> @@ -1005,7 +1004,7 @@ describe "Config", -> '*': foo: scoped: false - """ + """, 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> @@ -1023,7 +1022,7 @@ describe "Config", -> '.source.ruby': foo: scoped: true - """ + """, 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> @@ -1033,7 +1032,7 @@ describe "Config", -> describe "when the config file changes to omit a setting with a default", -> it "resets the setting back to the default", -> - writeConfigFile("foo: { baz: 'new'}") + writeConfigFile "foo: { baz: 'new'}", 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> expect(atom.config.get('foo.bar')).toBe 'def' @@ -1041,20 +1040,20 @@ describe "Config", -> describe "when the config file changes to be empty", -> beforeEach -> - writeConfigFile("") + updatedHandler.reset() + writeConfigFile "", 4 waitsFor 'update event', -> updatedHandler.callCount > 0 it "resets all settings back to the defaults", -> expect(updatedHandler.callCount).toBe 1 expect(atom.config.get('foo.bar')).toBe 'def' atom.config.set("hair", "blonde") # trigger a save - advanceClock(500) - expect(atom.config.save).toHaveBeenCalled() + waitsFor 'save', -> atom.config.save.callCount > 0 describe "when the config file subsequently changes again to contain configuration", -> beforeEach -> updatedHandler.reset() - writeConfigFile("foo: bar: 'newVal'") + writeConfigFile "foo: bar: 'newVal'", 2 waitsFor 'update event', -> updatedHandler.callCount > 0 it "sets the setting to the value specified in the config file", -> @@ -1064,24 +1063,25 @@ describe "Config", -> addErrorHandler = null beforeEach -> atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() - writeConfigFile("}}}") + writeConfigFile "}}}", 4 waitsFor "error to be logged", -> addErrorHandler.callCount > 0 it "logs a warning and does not update config data", -> expect(updatedHandler.callCount).toBe 0 expect(atom.config.get('foo.bar')).toBe 'baz' + atom.config.set("hair", "blonde") # trigger a save expect(atom.config.save).not.toHaveBeenCalled() describe "when the config file subsequently changes again to contain valid cson", -> beforeEach -> - writeConfigFile("foo: bar: 'newVal'") + updatedHandler.reset() + writeConfigFile "foo: bar: 'newVal'", 6 waitsFor 'update event', -> updatedHandler.callCount > 0 it "updates the config data and resumes saving", -> atom.config.set("hair", "blonde") - advanceClock(500) - expect(atom.config.save).toHaveBeenCalled() + waitsFor 'save', -> atom.config.save.callCount > 0 describe ".initializeConfigDirectory()", -> beforeEach -> @@ -1757,6 +1757,7 @@ describe "Config", -> expect(atom.config.set('foo.bar.str_options', 'One')).toBe false expect(atom.config.get('foo.bar.str_options')).toEqual 'two' + describe "when .set/.unset is called prior to .loadUserConfig", -> console.log 'this test' beforeEach -> From bf121eab722c47c93c3daa09a000309d60188d27 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:16:25 -0400 Subject: [PATCH 094/448] Remove some diagnostics --- spec/config-spec.coffee | 9 --------- 1 file changed, 9 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index b33b9226b..8088c96e3 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -931,7 +931,6 @@ describe "Config", -> """ atom.config.loadUserConfig() - console.log 'observeUserConfig promise', atom.config.observeUserConfig() waitsForPromise -> atom.config.observeUserConfig() runs -> @@ -939,17 +938,10 @@ describe "Config", -> atom.config.onDidChange updatedHandler afterEach -> - # WHY IS THIS NOT RUNNING? - console.log 'afterEach' atom.config.unobserveUserConfig() fs.removeSync(dotAtomPath) describe "when the config file changes to contain valid cson", -> - afterEach -> - # WHY IS THIS NOT RUNNING? - console.log 'afterEach' - atom.config.unobserveUserConfig() - fs.removeSync(dotAtomPath) it "updates the config data", -> writeConfigFile "foo: { bar: 'quux', baz: 'bar'}", 2 @@ -1759,7 +1751,6 @@ describe "Config", -> expect(atom.config.get('foo.bar.str_options')).toEqual 'two' describe "when .set/.unset is called prior to .loadUserConfig", -> - console.log 'this test' beforeEach -> fs.writeFileSync config.configFilePath, """ '*': From adb032adf11895a6475e9e4c9d15f128d3d14080 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:16:50 -0400 Subject: [PATCH 095/448] Some cosmetic coffeescript changes --- spec/config-spec.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 8088c96e3..1a90a25e7 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -934,7 +934,7 @@ describe "Config", -> waitsForPromise -> atom.config.observeUserConfig() runs -> - updatedHandler = jasmine.createSpy("updatedHandler") + updatedHandler = jasmine.createSpy "updatedHandler" atom.config.onDidChange updatedHandler afterEach -> @@ -953,7 +953,7 @@ describe "Config", -> expect(atom.config.get('foo.baz')).toBe 'bar' it "does not fire a change event for paths that did not change", -> - atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy() + atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy "unchanged" writeConfigFile "foo: { bar: 'baz', baz: 'ok'}", 2 waitsFor 'update event', -> updatedHandler.callCount > 0 @@ -976,7 +976,7 @@ describe "Config", -> runs -> updatedHandler.reset() it "does not fire a change event for paths that did not change", -> - noChangeSpy = jasmine.createSpy() + noChangeSpy = jasmine.createSpy "unchanged" atom.config.onDidChange('foo.bar', noChangeSpy) writeConfigFile "foo: { bar: ['baz', 'ok'], baz: 'another'}", 2 @@ -1004,7 +1004,7 @@ describe "Config", -> expect(atom.config.get('foo.scoped', scope: ['.source.ruby'])).toBe false it "does not fire a change event for paths that did not change", -> - noChangeSpy = jasmine.createSpy() + noChangeSpy = jasmine.createSpy "no change" atom.config.onDidChange('foo.scoped', scope: ['.source.ruby'], noChangeSpy) writeConfigFile """ @@ -1054,7 +1054,7 @@ describe "Config", -> describe "when the config file changes to contain invalid cson", -> addErrorHandler = null beforeEach -> - atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy "error handler" writeConfigFile "}}}", 4 waitsFor "error to be logged", -> addErrorHandler.callCount > 0 From e9588c8fae036e63ca825fbfa7811c9c3acaafd0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:18:09 -0400 Subject: [PATCH 096/448] Bring the new spec up to date --- spec/config-spec.coffee | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 1a90a25e7..14fbd64d0 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -1752,7 +1752,9 @@ describe "Config", -> describe "when .set/.unset is called prior to .loadUserConfig", -> beforeEach -> - fs.writeFileSync config.configFilePath, """ + atom.config.settingsLoaded = false + + fs.writeFileSync atom.config.configFilePath, """ '*': foo: bar: 'baz' @@ -1761,22 +1763,19 @@ describe "Config", -> """ it "ensures that all settings are loaded correctly", -> - console.log 'test start' - config.unset('foo.bar') - expect(config.save).not.toHaveBeenCalled() - config.set('foo.qux', 'boo') - expect(config.save).not.toHaveBeenCalled() - expect(config.get('foo.qux')).toBeUndefined() - expect(config.get('do.ray')).toBeUndefined() + atom.config.unset('foo.bar') + expect(atom.config.save).not.toHaveBeenCalled() + atom.config.set('foo.qux', 'boo') + expect(atom.config.save).not.toHaveBeenCalled() + expect(atom.config.get('foo.qux')).toBeUndefined() + expect(atom.config.get('do.ray')).toBeUndefined() - console.log 'loadUserConfig' - config.loadUserConfig() + atom.config.loadUserConfig() + advanceClock 100 + + waitsFor -> atom.config.save.callCount > 0 - waitsFor -> config.get('foo.bar') is undefined runs -> - expect(config.save).toHaveBeenCalled() - expect(config.get('foo.bar')).toBeUndefined() - expect(config.get('foo.qux')).toBe('boo') - expect(config.get('do.ray')).toBe('me') - - console.log 'end test' + expect(atom.config.get('foo.bar')).toBeUndefined() + expect(atom.config.get('foo.qux')).toBe('boo') + expect(atom.config.get('do.ray')).toBe('me') From 8b94ed95586c307f14a942509f3acd595b007a71 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:18:47 -0400 Subject: [PATCH 097/448] "arguments" is overwritten by closure arguments --- src/config.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 6c47caf88..e23724885 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -648,11 +648,12 @@ class Config # * `true` if the value was set. # * `false` if the value was not able to be coerced to the type specified in the setting's schema. set: -> + [keyPath, value, options] = arguments + unless @settingsLoaded - @pendingOperations.push(() => @set.apply(@, arguments)) + @pendingOperations.push () => @set.call(this, keyPath, value, options) return - [keyPath, value, options] = arguments scopeSelector = options?.scopeSelector source = options?.source shouldSave = options?.save ? true @@ -684,7 +685,7 @@ class Config # * `source` (optional) {String}. See {::set} unset: (keyPath, options) -> unless @settingsLoaded - @pendingOperations.push(() => @unset.apply(@, arguments)) + @pendingOperations.push () => @unset.call(this, keyPath, options) return {scopeSelector, source} = options ? {} From b45fb2e918d5c2b5a519bb743c078d8ad3821870 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:19:38 -0400 Subject: [PATCH 098/448] Use the wx flag to atomically create a config file if it doesn't exist --- src/config.coffee | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index e23724885..0d95ab2ef 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -862,11 +862,8 @@ class Config console.log 'loadUserConfig' try - # fs.makeTreeSync(path.dirname(@configFilePath)) - # CSON.writeFileSync(@configFilePath, {flag: 'x'}, {}) # fails if file exists - unless fs.existsSync(@configFilePath) - fs.makeTreeSync(path.dirname(@configFilePath)) - CSON.writeFileSync(@configFilePath, {}) + fs.makeTreeSync(path.dirname(@configFilePath)) + CSON.writeFileSync(@configFilePath, {}, {flag: 'wx'}) # fails if file exists catch error @configFileHasErrors = true @notifyFailure("Failed to initialize `#{path.basename(@configFilePath)}`", error.stack) From 85ca408b29d904269977e10d832a9d2740d3a284 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:20:14 -0400 Subject: [PATCH 099/448] Remove some console logging --- src/config.coffee | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 0d95ab2ef..e9e4a436e 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -860,7 +860,6 @@ class Config loadUserConfig: -> return if @shouldNotAccessFileSystem() - console.log 'loadUserConfig' try fs.makeTreeSync(path.dirname(@configFilePath)) CSON.writeFileSync(@configFilePath, {}, {flag: 'wx'}) # fails if file exists @@ -891,13 +890,9 @@ class Config return if @shouldNotAccessFileSystem() try - console.trace 'create watch subscription', @watchSubscriptionPromise @watchSubscriptionPromise ?= watchPath @configFilePath, {}, (events) => - console.log events for {action} in events - console.log action, @watchSubscriptionPromise? if action in ['created', 'modified', 'renamed'] and @watchSubscriptionPromise? - console.warn 'request load' @requestLoad() catch error @notifyFailure """ @@ -912,7 +907,6 @@ class Config unobserveUserConfig: -> @watchSubscriptionPromise?.then((watcher) => watcher?.dispose()) @watchSubscriptionPromise = null - console.log 'unobserve' notifyFailure: (errorMessage, detail) -> @notificationManager?.addError(errorMessage, {detail, dismissable: true}) From 8601a5df21237c576749797f55cae96d4e5a3c15 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:20:30 -0400 Subject: [PATCH 100/448] Only catch an expected EEXIST error --- src/config.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index e9e4a436e..2f0952e8a 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -864,9 +864,10 @@ class Config fs.makeTreeSync(path.dirname(@configFilePath)) CSON.writeFileSync(@configFilePath, {}, {flag: 'wx'}) # fails if file exists catch error - @configFileHasErrors = true - @notifyFailure("Failed to initialize `#{path.basename(@configFilePath)}`", error.stack) - return + if error.code isnt 'EEXIST' + @configFileHasErrors = true + @notifyFailure("Failed to initialize `#{path.basename(@configFilePath)}`", error.stack) + return try unless @savePending From 46d5ebb2f4b238b327f564fbc6d02ab95237f86c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:21:02 -0400 Subject: [PATCH 101/448] Only reset settings if the file is empty --- src/config.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 2f0952e8a..c7eea02c4 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -872,6 +872,11 @@ class Config try unless @savePending userConfig = CSON.readFileSync(@configFilePath) + userConfig = {} if userConfig is null + + unless isPlainObject(userConfig) + throw new Error("`#{path.basename(@configFilePath)}` must contain valid JSON or CSON") + @resetUserSettings(userConfig) @configFileHasErrors = false catch error @@ -930,11 +935,6 @@ class Config ### resetUserSettings: (newSettings) -> - unless isPlainObject(newSettings) - @settings = {} - @emitChangeEvent() - return - if newSettings.global? newSettings['*'] = newSettings.global delete newSettings.global From 4b7b513b9316aca4a058e5827315a00c1de96a98 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:21:47 -0400 Subject: [PATCH 102/448] The method is requestSave() --- src/config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index c7eea02c4..85de901db 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -951,7 +951,7 @@ class Config @set(key, value, save: false) for key, value of newSettings if @pendingOperations.length op() for op in @pendingOperations - @debouncedSave() + @requestSave() @pendingOperations = [] return From 18a0a5a8579cfc77a778f077564bb4015cb326e6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 10:33:46 -0400 Subject: [PATCH 103/448] .set operations should be immediately visible through .get --- spec/config-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 14fbd64d0..7beb74558 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -1750,7 +1750,7 @@ describe "Config", -> expect(atom.config.set('foo.bar.str_options', 'One')).toBe false expect(atom.config.get('foo.bar.str_options')).toEqual 'two' - describe "when .set/.unset is called prior to .loadUserConfig", -> + fdescribe "when .set/.unset is called prior to .loadUserConfig", -> beforeEach -> atom.config.settingsLoaded = false From e275a5ff7600f401f8b717952efa604300fa4aee Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 11:21:20 -0400 Subject: [PATCH 104/448] Ensure that .get calls before .requestLoad return .set properties --- spec/config-spec.coffee | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 7beb74558..dfc51b29f 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -1753,7 +1753,6 @@ describe "Config", -> fdescribe "when .set/.unset is called prior to .loadUserConfig", -> beforeEach -> atom.config.settingsLoaded = false - fs.writeFileSync atom.config.configFilePath, """ '*': foo: @@ -1762,20 +1761,23 @@ describe "Config", -> ray: 'me' """ - it "ensures that all settings are loaded correctly", -> - atom.config.unset('foo.bar') - expect(atom.config.save).not.toHaveBeenCalled() - atom.config.set('foo.qux', 'boo') - expect(atom.config.save).not.toHaveBeenCalled() - expect(atom.config.get('foo.qux')).toBeUndefined() + it "ensures that early set and unset calls are replayed after the config is loaded from disk", -> + atom.config.unset 'foo.bar' + atom.config.set 'foo.qux', 'boo' + + expect(atom.config.get('foo.bar')).toBeUndefined() + expect(atom.config.get('foo.qux')).toBe 'boo' expect(atom.config.get('do.ray')).toBeUndefined() - atom.config.loadUserConfig() advanceClock 100 + expect(atom.config.save).not.toHaveBeenCalled() + atom.config.loadUserConfig() + + advanceClock 100 waitsFor -> atom.config.save.callCount > 0 runs -> expect(atom.config.get('foo.bar')).toBeUndefined() - expect(atom.config.get('foo.qux')).toBe('boo') - expect(atom.config.get('do.ray')).toBe('me') + expect(atom.config.get('foo.qux')).toBe 'boo' + expect(atom.config.get('do.ray')).toBe 'me' From 28a42034822cdd03f1f87a2718f68d02e7e8ff5f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 11:22:40 -0400 Subject: [PATCH 105/448] Apply config changes before an initial load, but also replay them --- src/config.coffee | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 85de901db..4fb1c0e20 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -419,17 +419,21 @@ class Config @defaultSettings = {} @settings = {} @scopedSettingsStore = new ScopedPropertyStore + + @savePending = false @configFileHasErrors = false @transactDepth = 0 - @savePending = false - @requestLoad = _.debounce(@loadUserConfig, 100) + + @requestLoad = _.debounce => + @loadUserConfig() + , 100 + + debouncedSave = _.debounce => + @save() + , 100 @requestSave = => @savePending = true - debouncedSave.call(this) - save = => - @savePending = false - @save() - debouncedSave = _.debounce(save, 100) + debouncedSave() shouldNotAccessFileSystem: -> not @enablePersistence @@ -652,7 +656,6 @@ class Config unless @settingsLoaded @pendingOperations.push () => @set.call(this, keyPath, value, options) - return scopeSelector = options?.scopeSelector source = options?.source @@ -674,7 +677,8 @@ class Config else @setRawValue(keyPath, value) - @requestSave() if source is @getUserConfigPath() and shouldSave and not @configFileHasErrors + if source is @getUserConfigPath() and shouldSave and not @configFileHasErrors and @settingsLoaded + @requestSave() true # Essential: Restore the setting at `keyPath` to its default value. @@ -686,7 +690,6 @@ class Config unset: (keyPath, options) -> unless @settingsLoaded @pendingOperations.push () => @unset.call(this, keyPath, options) - return {scopeSelector, source} = options ? {} source ?= @getUserConfigPath() @@ -699,7 +702,7 @@ class Config setValueAtKeyPath(settings, keyPath, undefined) settings = withoutEmptyObjects(settings) @set(null, settings, {scopeSelector, source, priority: @priorityForSource(source)}) if settings? - @requestSave() + @requestSave() if source is @getUserConfigPath and not @configFileHasErrors and @settingsLoaded else @scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector) @emitChangeEvent() @@ -859,6 +862,7 @@ class Config loadUserConfig: -> return if @shouldNotAccessFileSystem() + return if @savePending try fs.makeTreeSync(path.dirname(@configFilePath)) @@ -870,15 +874,14 @@ class Config return try - unless @savePending - userConfig = CSON.readFileSync(@configFilePath) - userConfig = {} if userConfig is null + userConfig = CSON.readFileSync(@configFilePath) + userConfig = {} if userConfig is null - unless isPlainObject(userConfig) - throw new Error("`#{path.basename(@configFilePath)}` must contain valid JSON or CSON") + unless isPlainObject(userConfig) + throw new Error("`#{path.basename(@configFilePath)}` must contain valid JSON or CSON") - @resetUserSettings(userConfig) - @configFileHasErrors = false + @resetUserSettings(userConfig) + @configFileHasErrors = false catch error @configFileHasErrors = true message = "Failed to load `#{path.basename(@configFilePath)}`" @@ -918,6 +921,7 @@ class Config @notificationManager?.addError(errorMessage, {detail, dismissable: true}) save: -> + @savePending = false return if @shouldNotAccessFileSystem() allSettings = {'*': @settings} From 7737aec3ae00d005788b1693f839d778660e04a1 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 11:25:29 -0400 Subject: [PATCH 106/448] :fire: fdescribe --- spec/config-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index dfc51b29f..43a730ca8 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -1750,7 +1750,7 @@ describe "Config", -> expect(atom.config.set('foo.bar.str_options', 'One')).toBe false expect(atom.config.get('foo.bar.str_options')).toEqual 'two' - fdescribe "when .set/.unset is called prior to .loadUserConfig", -> + describe "when .set/.unset is called prior to .loadUserConfig", -> beforeEach -> atom.config.settingsLoaded = false fs.writeFileSync atom.config.configFilePath, """ From 19500d1b4087a484387577e046522d8f522236b0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 11:30:07 -0400 Subject: [PATCH 107/448] Functions are more useful if you call them --- src/config.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index 4fb1c0e20..2180bcc62 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -702,7 +702,8 @@ class Config setValueAtKeyPath(settings, keyPath, undefined) settings = withoutEmptyObjects(settings) @set(null, settings, {scopeSelector, source, priority: @priorityForSource(source)}) if settings? - @requestSave() if source is @getUserConfigPath and not @configFileHasErrors and @settingsLoaded + if source is @getUserConfigPath() and not @configFileHasErrors and @settingsLoaded + @requestSave() else @scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector) @emitChangeEvent() From 979f525e3beaeb12ed421d44aa589c6a7e864802 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 11:50:15 -0400 Subject: [PATCH 108/448] Work with spies a little more gracefully --- src/config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index 2180bcc62..1ad7fab47 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -429,6 +429,7 @@ class Config , 100 debouncedSave = _.debounce => + @savePending = false @save() , 100 @requestSave = => @@ -922,7 +923,6 @@ class Config @notificationManager?.addError(errorMessage, {detail, dismissable: true}) save: -> - @savePending = false return if @shouldNotAccessFileSystem() allSettings = {'*': @settings} From fa1ebd052992c1c16735db784e246d77c1050198 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 11:52:39 -0400 Subject: [PATCH 109/448] :shirt: lint lint lint --- src/config.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 1ad7fab47..ea4551652 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -656,7 +656,7 @@ class Config [keyPath, value, options] = arguments unless @settingsLoaded - @pendingOperations.push () => @set.call(this, keyPath, value, options) + @pendingOperations.push => @set.call(this, keyPath, value, options) scopeSelector = options?.scopeSelector source = options?.source @@ -690,7 +690,7 @@ class Config # * `source` (optional) {String}. See {::set} unset: (keyPath, options) -> unless @settingsLoaded - @pendingOperations.push () => @unset.call(this, keyPath, options) + @pendingOperations.push => @unset.call(this, keyPath, options) {scopeSelector, source} = options ? {} source ?= @getUserConfigPath() @@ -916,7 +916,7 @@ class Config @watchSubscriptionPromise unobserveUserConfig: -> - @watchSubscriptionPromise?.then((watcher) => watcher?.dispose()) + @watchSubscriptionPromise?.then (watcher) -> watcher?.dispose() @watchSubscriptionPromise = null notifyFailure: (errorMessage, detail) -> From 0c021f777e0fe2e6d5dc3b0cb1e1528e7f0caf13 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 14:38:49 -0400 Subject: [PATCH 110/448] Initialize the new flag and pendingOperations in clear() --- src/config.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index ea4551652..56b2808ca 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -399,8 +399,6 @@ class Config # Created during initialization, available as `atom.config` constructor: ({@notificationManager, @enablePersistence}={}) -> - @settingsLoaded = false - @pendingOperations = [] @clear() initialize: ({@configDirPath, @resourcePath, projectHomeSchema}) -> @@ -420,9 +418,11 @@ class Config @settings = {} @scopedSettingsStore = new ScopedPropertyStore + @settingsLoaded = false @savePending = false @configFileHasErrors = false @transactDepth = 0 + @pendingOperations = [] @requestLoad = _.debounce => @loadUserConfig() From ff8e21a20e9f38563df07196d3f1d270855c63a0 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 14:39:08 -0400 Subject: [PATCH 111/448] Let the debouncer debounce --- src/config.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 56b2808ca..a585a433a 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -956,9 +956,7 @@ class Config @set(key, value, save: false) for key, value of newSettings if @pendingOperations.length op() for op in @pendingOperations - @requestSave() @pendingOperations = [] - return getRawValue: (keyPath, options) -> unless options?.excludeSources?.indexOf(@getUserConfigPath()) >= 0 From fb4d7ee5e5c62de5492df66b756515b776f94f8e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 12 Sep 2017 14:39:17 -0400 Subject: [PATCH 112/448] Clean the pending operations too --- spec/config-spec.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 43a730ca8..bcf50c268 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -14,6 +14,7 @@ describe "Config", -> atom.config.configDirPath = dotAtomPath atom.config.enablePersistence = true atom.config.settingsLoaded = true + atom.config.pendingOperations = [] atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson") afterEach -> From 806306a005c3cfbdd8da1244d1f27d692a551107 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Sep 2017 11:43:54 -0700 Subject: [PATCH 113/448] :arrow_up: text-buffer for destroy-after-save fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 416bbf6c8..1f4ed4b72 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.1.14", + "text-buffer": "13.1.15-0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From bc70b0e45314ed09aa1ffffddc047bafa1221bcb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Sep 2017 12:26:09 -0700 Subject: [PATCH 114/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f4ed4b72..10c599353 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.1.15-0", + "text-buffer": "13.1.15", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 2374077a6e80c907afd67529d24af25d4b2129a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Sep 2017 15:04:37 -0700 Subject: [PATCH 115/448] Wait for async onWillDestroyItem handlers before destroying item --- package.json | 2 +- spec/pane-spec.js | 15 +++++++++++---- src/pane-container.js | 2 +- src/pane.coffee | 24 ++++++++++++------------ 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 1e6947554..08abc4e11 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "dedent": "^0.6.0", "devtron": "1.3.0", "etch": "^0.12.6", - "event-kit": "^2.3.0", + "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", "first-mate": "7.0.7", "focus-trap": "^2.3.0", diff --git a/spec/pane-spec.js b/spec/pane-spec.js index 41946a6c9..75df672b3 100644 --- a/spec/pane-spec.js +++ b/spec/pane-spec.js @@ -3,7 +3,7 @@ const {Emitter} = require('event-kit') const Grim = require('grim') const Pane = require('../src/pane') const PaneContainer = require('../src/pane-container') -const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers') +const {it, fit, ffit, fffit, beforeEach, timeoutPromise} = require('./async-spec-helpers') describe('Pane', () => { let confirm, showSaveDialog, deserializerDisposable @@ -491,14 +491,21 @@ describe('Pane', () => { expect(pane.getActiveItem()).toBeUndefined() }) - it('invokes ::onWillDestroyItem() observers before destroying the item', () => { + it('invokes ::onWillDestroyItem() observers before destroying the item', async () => { + jasmine.useRealClock() + + let handlerDidFinish = false const events = [] - pane.onWillDestroyItem(function (event) { + pane.onWillDestroyItem(async (event) => { expect(item2.isDestroyed()).toBe(false) events.push(event) + await timeoutPromise(50) + expect(item2.isDestroyed()).toBe(false) + handlerDidFinish = true }) - pane.destroyItem(item2) + await pane.destroyItem(item2) + expect(handlerDidFinish).toBe(true) expect(item2.isDestroyed()).toBe(true) expect(events).toEqual([{item: item2, index: 1}]) }) diff --git a/src/pane-container.js b/src/pane-container.js index d907aea65..25e57acc8 100644 --- a/src/pane-container.js +++ b/src/pane-container.js @@ -267,7 +267,7 @@ class PaneContainer { } willDestroyPaneItem (event) { - this.emitter.emit('will-destroy-pane-item', event) + return this.emitter.emitAsync('will-destroy-pane-item', event) } didDestroyPaneItem (event) { diff --git a/src/pane.coffee b/src/pane.coffee index 6ac3ef359..002b0a15e 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -624,18 +624,18 @@ class Pane if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?() return Promise.resolve(false) - @emitter.emit 'will-destroy-item', {item, index} - @container?.willDestroyPaneItem({item, index, pane: this}) - if force or not item?.shouldPromptToSave?() - @removeItem(item, false) - item.destroy?() - Promise.resolve(true) - else - @promptToSaveItem(item).then (result) => - if result - @removeItem(item, false) - item.destroy?() - result + @emitter.emitAsync('will-destroy-item', {item, index}).then => + @container?.willDestroyPaneItem({item, index, pane: this}) + if force or not item?.shouldPromptToSave?() + @removeItem(item, false) + item.destroy?() + true + else + @promptToSaveItem(item).then (result) => + if result + @removeItem(item, false) + item.destroy?() + result # Public: Destroy all items. destroyItems: -> From ed39ace4d9eefe880b0bb34679fe0c166a88ed97 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Sep 2017 15:14:28 -0700 Subject: [PATCH 116/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08abc4e11..0b886a26d 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.1.15", + "text-buffer": "13.2.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From cca04f85a3aa7ad452b8e9a203b779af0561fdc8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Sep 2017 15:15:28 -0700 Subject: [PATCH 117/448] :memo: Document async onWillDestroyItem behavior --- src/workspace.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/workspace.js b/src/workspace.js index d089421f3..80dfc47cb 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -820,7 +820,8 @@ module.exports = class Workspace extends Model { // Extended: Invoke the given callback when a pane item is about to be // destroyed, before the user is prompted to save it. // - // * `callback` {Function} to be called before pane items are destroyed. + // * `callback` {Function} to be called before pane items are destroyed. If this function returns + // a {Promise}, then the item will not be destroyed until the promise resolves. // * `event` {Object} with the following keys: // * `item` The item to be destroyed. // * `pane` {Pane} containing the item to be destroyed. From f786950d656323b128932962424779ba7675551a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 12 Sep 2017 16:03:10 -0700 Subject: [PATCH 118/448] Make .destroyItem take effect synchronously when possible This is ugly but prevents the breakage of community packages' tests. --- src/pane.coffee | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pane.coffee b/src/pane.coffee index 002b0a15e..64b9383f8 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -624,7 +624,7 @@ class Pane if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?() return Promise.resolve(false) - @emitter.emitAsync('will-destroy-item', {item, index}).then => + callback = => @container?.willDestroyPaneItem({item, index, pane: this}) if force or not item?.shouldPromptToSave?() @removeItem(item, false) @@ -637,6 +637,13 @@ class Pane item.destroy?() result + # In the case where there are no `onWillDestroyPaneItem` listeners, preserve the old behavior + # where `Pane.destroyItem` and callers such as `Pane.close` take effect synchronously. + if @emitter.listenerCountForEventName('will-destroy-item') is 0 + return Promise.resolve(callback()) + else + @emitter.emitAsync('will-destroy-item', {item, index}).then(callback) + # Public: Destroy all items. destroyItems: -> Promise.all( From 33c77c5be471e813586a01fbd40be07e7dbf9f18 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Sep 2017 09:29:56 -0700 Subject: [PATCH 119/448] :arrow_up: whitespace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0b886a26d..0e20d3b42 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "tree-view": "0.217.9", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", - "whitespace": "0.37.2", + "whitespace": "0.37.3", "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", From d7520625121cc3c096975b034be98629cff51569 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Sep 2017 13:01:14 -0700 Subject: [PATCH 120/448] :arrow_up: fs-admin --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e6947554..7f6ab9ba5 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "find-parent-dir": "^0.3.0", "first-mate": "7.0.7", "focus-trap": "^2.3.0", - "fs-admin": "^0.1.5", + "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", "fstream": "0.1.24", "fuzzaldrin": "^2.1", From 19e56b5b3bce049cc4649708403dfeee307c6a6d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 13 Sep 2017 13:31:33 -0700 Subject: [PATCH 121/448] Wait for async onWillDestroyPaneItem listeners --- spec/pane-spec.js | 22 +++++++++++++++------- src/pane.coffee | 13 ++++++++----- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/spec/pane-spec.js b/spec/pane-spec.js index 75df672b3..e448f992f 100644 --- a/spec/pane-spec.js +++ b/spec/pane-spec.js @@ -491,23 +491,31 @@ describe('Pane', () => { expect(pane.getActiveItem()).toBeUndefined() }) - it('invokes ::onWillDestroyItem() observers before destroying the item', async () => { + it('invokes ::onWillDestroyItem() and PaneContainer::onWillDestroyPaneItem observers before destroying the item', async () => { jasmine.useRealClock() - - let handlerDidFinish = false + pane.container = new PaneContainer({config: atom.config, confirm}) const events = [] + pane.onWillDestroyItem(async (event) => { expect(item2.isDestroyed()).toBe(false) - events.push(event) await timeoutPromise(50) expect(item2.isDestroyed()).toBe(false) - handlerDidFinish = true + events.push(['will-destroy-item', event]) + }) + + pane.container.onWillDestroyPaneItem(async (event) => { + expect(item2.isDestroyed()).toBe(false) + await timeoutPromise(50) + expect(item2.isDestroyed()).toBe(false) + events.push(['will-destroy-pane-item', event]) }) await pane.destroyItem(item2) - expect(handlerDidFinish).toBe(true) expect(item2.isDestroyed()).toBe(true) - expect(events).toEqual([{item: item2, index: 1}]) + expect(events).toEqual([ + ['will-destroy-item', {item: item2, index: 1}], + ['will-destroy-pane-item', {item: item2, index: 1, pane}] + ]) }) it('invokes ::onWillRemoveItem() observers', () => { diff --git a/src/pane.coffee b/src/pane.coffee index 64b9383f8..19a2df6d5 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -625,7 +625,6 @@ class Pane return Promise.resolve(false) callback = => - @container?.willDestroyPaneItem({item, index, pane: this}) if force or not item?.shouldPromptToSave?() @removeItem(item, false) item.destroy?() @@ -639,10 +638,14 @@ class Pane # In the case where there are no `onWillDestroyPaneItem` listeners, preserve the old behavior # where `Pane.destroyItem` and callers such as `Pane.close` take effect synchronously. - if @emitter.listenerCountForEventName('will-destroy-item') is 0 - return Promise.resolve(callback()) - else - @emitter.emitAsync('will-destroy-item', {item, index}).then(callback) + return Promise.resolve(callback()) unless ( + @emitter.listenerCountForEventName('will-destroy-item') or + @container?.emitter.listenerCountForEventName('will-destroy-pane-item') + ) + + @emitter.emitAsync('will-destroy-item', {item, index}) + .then => @container?.willDestroyPaneItem({item, index, pane: this}) + .then(callback) # Public: Destroy all items. destroyItems: -> From bd1ba8f02277cb6d59e99c197ac5bf26604b4e74 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Thu, 14 Sep 2017 08:38:27 +0200 Subject: [PATCH 122/448] :arrow_up: bracket-matcher@0.88.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2429c18d3..678d98c00 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "autosave": "0.24.3", "background-tips": "0.27.1", "bookmarks": "0.44.4", - "bracket-matcher": "0.87.3", + "bracket-matcher": "0.88.0", "command-palette": "0.41.1", "dalek": "0.2.1", "deprecation-cop": "0.56.9", From 6f9a11736259cb3ffda63848003c76b8ba836452 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 14 Sep 2017 08:20:07 -0400 Subject: [PATCH 123/448] :arrow_up: autocomplete-plus@2.35.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2429c18d3..f5e0af29c 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.1", - "autocomplete-plus": "2.35.9", + "autocomplete-plus": "2.35.10", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.3", From bf3e2e3ba182e28073f772683b9d94c65566200b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 14 Sep 2017 14:10:23 -0400 Subject: [PATCH 124/448] :arrow_up: apm --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index 8b8cf4c73..f6dfa8cde 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.5" + "atom-package-manager": "1.18.6" } } From 8b4c2a3e52fbd30fe478887832b0d0fd7b841106 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 14 Sep 2017 14:51:49 -0400 Subject: [PATCH 125/448] :arrow_up: apm --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index f6dfa8cde..e8d4321b1 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.6" + "atom-package-manager": "1.18.7" } } From a5dd3bb9b73478c7195fd51ed49a2f3e3100c704 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 14 Sep 2017 14:57:51 -0400 Subject: [PATCH 126/448] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84fb99829..7afdb2c29 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.5", "exception-reporting": "0.41.4", "find-and-replace": "0.212.2", - "fuzzy-finder": "1.5.8", + "fuzzy-finder": "1.6.0", "github": "0.5.1", "git-diff": "1.3.6", "go-to-line": "0.32.1", From dd84a7d2806da29c841bf3683b5b4b2c43bf1436 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 14 Sep 2017 21:34:35 +0200 Subject: [PATCH 127/448] :arrow_up: tree-view@0.218.0 Begone, __proto__! --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7afdb2c29..0ebacc2da 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "symbols-view": "0.118.0", "tabs": "0.107.2", "timecop": "0.36.0", - "tree-view": "0.217.9", + "tree-view": "0.218.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", "whitespace": "0.37.3", From 7a02e85331068ab92629e086c622007567852788 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 14 Sep 2017 21:48:12 +0200 Subject: [PATCH 128/448] :arrow_up: find-and-replace@0.212.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ebacc2da..c2216fdbe 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "dev-live-reload": "0.47.1", "encoding-selector": "0.23.5", "exception-reporting": "0.41.4", - "find-and-replace": "0.212.2", + "find-and-replace": "0.212.3", "fuzzy-finder": "1.6.0", "github": "0.5.1", "git-diff": "1.3.6", From 71c8df9d74cb6d6bd613da106be20ae29862e579 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 14 Sep 2017 13:07:05 -0700 Subject: [PATCH 129/448] :arrow_up: github@0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2216fdbe..495744cb7 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.0", - "github": "0.5.1", + "github": "0.6.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From 0e15a723dc8fd1086caf1b0a18f0c91ad700afb5 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 14 Sep 2017 14:37:33 -0700 Subject: [PATCH 130/448] :arrow_up: github@0.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 495744cb7..6d76ba422 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.0", - "github": "0.6.0", + "github": "0.6.1", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From 275089fdbe7af6005a21c755966eacf67bfe738f Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Thu, 14 Sep 2017 21:06:49 -0400 Subject: [PATCH 131/448] Restore the fs.existsSync guard --- src/config.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index 01cb732ca..b8bf8a76f 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -867,8 +867,9 @@ class Config return if @savePending try - fs.makeTreeSync(path.dirname(@configFilePath)) - CSON.writeFileSync(@configFilePath, {}, {flag: 'wx'}) # fails if file exists + unless fs.existsSync(@configFilePath) + fs.makeTreeSync(path.dirname(@configFilePath)) + CSON.writeFileSync(@configFilePath, {}, {flag: 'wx'}) # fails if file exists catch error if error.code isnt 'EEXIST' @configFileHasErrors = true From 3aa27a6256ee71581f177336f40cf0df1c785314 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 14 Sep 2017 23:45:37 -0700 Subject: [PATCH 132/448] :arrow_up: electron-winstaller --- script/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/package.json b/script/package.json index 59ca93375..6ff678be0 100644 --- a/script/package.json +++ b/script/package.json @@ -12,7 +12,7 @@ "electron-link": "0.1.1", "electron-mksnapshot": "~1.6", "electron-packager": "7.3.0", - "electron-winstaller": "2.6.2", + "electron-winstaller": "2.6.3", "fs-admin": "^0.1.5", "fs-extra": "0.30.0", "glob": "7.0.3", From 024ad73a52c81af2914ee432e578b76c120578e0 Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Fri, 15 Sep 2017 09:24:12 -0700 Subject: [PATCH 133/448] :arrow_up: atom-keymap@8.2.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d76ba422..e6758fafc 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.4", + "atom-keymap": "8.2.5", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", From 369c17f5993b5ce96ce6ea6b6fa4f1ddf3928e52 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 15 Sep 2017 12:08:12 -0700 Subject: [PATCH 134/448] :arrow_up: language-typescript --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e6758fafc..8bdb7b2df 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.1.0", + "language-typescript": "0.2.0", "language-xml": "0.35.2", "language-yaml": "0.30.2" }, From af095a649ea071e34e604f2f77b7a5a3dfdcecb3 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 15 Sep 2017 14:10:27 -0700 Subject: [PATCH 135/448] :arrow_up: language-typescript --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8bdb7b2df..4510c76a5 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.0", + "language-typescript": "0.2.1", "language-xml": "0.35.2", "language-yaml": "0.30.2" }, From 86b337edeb8f00a1f18acc4753af826b9df90514 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 15 Sep 2017 23:21:17 +0200 Subject: [PATCH 136/448] :arrow_up: language-css@0.42.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4510c76a5..821a61390 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "language-clojure": "0.22.4", "language-coffee-script": "0.49.0", "language-csharp": "0.14.2", - "language-css": "0.42.5", + "language-css": "0.42.6", "language-gfm": "0.90.1", "language-git": "0.19.1", "language-go": "0.44.2", From b269b208640aef6437cfe2331a20bc5197a9732a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 15 Sep 2017 16:19:39 -0600 Subject: [PATCH 137/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 821a61390..291208aff 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.2.1", + "text-buffer": "13.3.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 75af119bbbb97bf25327aeeed8d9715168fe45bd Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 16 Sep 2017 17:21:20 +0200 Subject: [PATCH 138/448] :arrow_up: language-yaml@0.31.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 291208aff..efc145bac 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "language-toml": "0.18.1", "language-typescript": "0.2.1", "language-xml": "0.35.2", - "language-yaml": "0.30.2" + "language-yaml": "0.31.0" }, "private": true, "scripts": { From d99147468dd28241adde9a1ed7c0b7b0d36621fa Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 16 Sep 2017 20:11:38 +0200 Subject: [PATCH 139/448] ...revert? --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index efc145bac..291208aff 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "language-toml": "0.18.1", "language-typescript": "0.2.1", "language-xml": "0.35.2", - "language-yaml": "0.31.0" + "language-yaml": "0.30.2" }, "private": true, "scripts": { From c17697ee0895bbf381e669dcef148ebded4a688d Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 16 Sep 2017 20:54:47 +0200 Subject: [PATCH 140/448] Unrevert the revert --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 291208aff..efc145bac 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "language-toml": "0.18.1", "language-typescript": "0.2.1", "language-xml": "0.35.2", - "language-yaml": "0.30.2" + "language-yaml": "0.31.0" }, "private": true, "scripts": { From 2b13d0b53e3c6831c4068e11f2d3fdfccacc08e8 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 16 Sep 2017 21:56:51 +0200 Subject: [PATCH 141/448] :arrow_up: encoding-selector@0.23.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index efc145bac..05c952d17 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "dalek": "0.2.1", "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", - "encoding-selector": "0.23.5", + "encoding-selector": "0.23.6", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", "fuzzy-finder": "1.6.0", From 69b1e2c4d89549692b1a365eec495539489f4cf2 Mon Sep 17 00:00:00 2001 From: Ben Russert Date: Sat, 16 Sep 2017 15:30:44 -0500 Subject: [PATCH 142/448] :memo: Fix cursor name letter casing [ci skip] --- src/cursor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cursor.coffee b/src/cursor.coffee index 74922ff51..fd53a14a4 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -37,7 +37,7 @@ class Cursor extends Model # * `newBufferPosition` {Point} # * `newScreenPosition` {Point} # * `textChanged` {Boolean} - # * `Cursor` {Cursor} that triggered the event + # * `cursor` {Cursor} that triggered the event # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangePosition: (callback) -> From 9ba632e223ce08b03145e8edb75b79d478245cd2 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 16 Sep 2017 23:50:16 +0200 Subject: [PATCH 143/448] Revert ":arrow_up: text-buffer" This reverts commit b269b208640aef6437cfe2331a20bc5197a9732a. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05c952d17..4a01294e8 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.3.0", + "text-buffer": "13.2.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 3fb699fa227a5ad57bc40437fb888af7878f802a Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 16 Sep 2017 17:39:41 -0400 Subject: [PATCH 144/448] :skull::coffee: Decaffeinate src/notification.coffee --- src/notification.coffee | 86 ----------------------------- src/notification.js | 118 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 86 deletions(-) delete mode 100644 src/notification.coffee create mode 100644 src/notification.js diff --git a/src/notification.coffee b/src/notification.coffee deleted file mode 100644 index d28bb88e8..000000000 --- a/src/notification.coffee +++ /dev/null @@ -1,86 +0,0 @@ -{Emitter} = require 'event-kit' -_ = require 'underscore-plus' - -# Public: A notification to the user containing a message and type. -module.exports = -class Notification - constructor: (@type, @message, @options={}) -> - @emitter = new Emitter - @timestamp = new Date() - @dismissed = true - @dismissed = false if @isDismissable() - @displayed = false - @validate() - - validate: -> - if typeof @message isnt 'string' - throw new Error("Notification must be created with string message: #{@message}") - - unless _.isObject(@options) and not _.isArray(@options) - throw new Error("Notification must be created with an options object: #{@options}") - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when the notification is dismissed. - # - # * `callback` {Function} to be called when the notification is dismissed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDismiss: (callback) -> - @emitter.on 'did-dismiss', callback - - # Public: Invoke the given callback when the notification is displayed. - # - # * `callback` {Function} to be called when the notification is displayed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDisplay: (callback) -> - @emitter.on 'did-display', callback - - getOptions: -> @options - - ### - Section: Methods - ### - - # Public: Returns the {String} type. - getType: -> @type - - # Public: Returns the {String} message. - getMessage: -> @message - - getTimestamp: -> @timestamp - - getDetail: -> @options.detail - - isEqual: (other) -> - @getMessage() is other.getMessage() \ - and @getType() is other.getType() \ - and @getDetail() is other.getDetail() - - # Extended: Dismisses the notification, removing it from the UI. Calling this programmatically - # will call all callbacks added via `onDidDismiss`. - dismiss: -> - return unless @isDismissable() and not @isDismissed() - @dismissed = true - @emitter.emit 'did-dismiss', this - - isDismissed: -> @dismissed - - isDismissable: -> !!@options.dismissable - - wasDisplayed: -> @displayed - - setDisplayed: (@displayed) -> - @emitter.emit 'did-display', this - - getIcon: -> - return @options.icon if @options.icon? - switch @type - when 'fatal' then 'bug' - when 'error' then 'flame' - when 'warning' then 'alert' - when 'info' then 'info' - when 'success' then 'check' diff --git a/src/notification.js b/src/notification.js new file mode 100644 index 000000000..320866d6b --- /dev/null +++ b/src/notification.js @@ -0,0 +1,118 @@ +const {Emitter} = require('event-kit') +const _ = require('underscore-plus') + +// Public: A notification to the user containing a message and type. +module.exports = +class Notification { + constructor (type, message, options = {}) { + this.type = type + this.message = message + this.options = options + this.emitter = new Emitter() + this.timestamp = new Date() + this.dismissed = true + if (this.isDismissable()) this.dismissed = false + this.displayed = false + this.validate() + } + + validate () { + if (typeof this.message !== 'string') { + throw new Error(`Notification must be created with string message: ${this.message}`) + } + + if (!_.isObject(this.options) || _.isArray(this.options)) { + throw new Error(`Notification must be created with an options object: ${this.options}`) + } + } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when the notification is dismissed. + // + // * `callback` {Function} to be called when the notification is dismissed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDismiss (callback) { + return this.emitter.on('did-dismiss', callback) + } + + // Public: Invoke the given callback when the notification is displayed. + // + // * `callback` {Function} to be called when the notification is displayed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDisplay (callback) { + return this.emitter.on('did-display', callback) + } + + getOptions () { + return this.options + } + + /* + Section: Methods + */ + + // Public: Returns the {String} type. + getType () { + return this.type + } + + // Public: Returns the {String} message. + getMessage () { + return this.message + } + + getTimestamp () { + return this.timestamp + } + + getDetail () { + return this.options.detail + } + + isEqual (other) { + return (this.getMessage() === other.getMessage()) && + (this.getType() === other.getType()) && + (this.getDetail() === other.getDetail()) + } + + // Extended: Dismisses the notification, removing it from the UI. Calling this + // programmatically will call all callbacks added via `onDidDismiss`. + dismiss () { + if (!this.isDismissable() || this.isDismissed()) return + this.dismissed = true + this.emitter.emit('did-dismiss', this) + } + + isDismissed () { + return this.dismissed + } + + isDismissable () { + return !!this.options.dismissable + } + + wasDisplayed () { + return this.displayed + } + + setDisplayed (displayed) { + this.displayed = displayed + this.emitter.emit('did-display', this) + } + + getIcon () { + if (this.options.icon != null) return this.options.icon + switch (this.type) { + case 'fatal': return 'bug' + case 'error': return 'flame' + case 'warning': return 'alert' + case 'info': return 'info' + case 'success': return 'check' + } + } +} From 3e21dd69231a4b47aab0f1ed82c630df0e9e01ad Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 16 Sep 2017 17:46:42 -0400 Subject: [PATCH 145/448] :skull::coffee: Decaffeinate src/notification-manager.coffee --- src/notification-manager.coffee | 183 ----------------------------- src/notification-manager.js | 196 ++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 183 deletions(-) delete mode 100644 src/notification-manager.coffee create mode 100644 src/notification-manager.js diff --git a/src/notification-manager.coffee b/src/notification-manager.coffee deleted file mode 100644 index 4beab82b9..000000000 --- a/src/notification-manager.coffee +++ /dev/null @@ -1,183 +0,0 @@ -{Emitter} = require 'event-kit' -Notification = require '../src/notification' - -# Public: A notification manager used to create {Notification}s to be shown -# to the user. -# -# An instance of this class is always available as the `atom.notifications` -# global. -module.exports = -class NotificationManager - constructor: -> - @notifications = [] - @emitter = new Emitter - - ### - Section: Events - ### - - # Public: Invoke the given callback after a notification has been added. - # - # * `callback` {Function} to be called after the notification is added. - # * `notification` The {Notification} that was added. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddNotification: (callback) -> - @emitter.on 'did-add-notification', callback - - ### - Section: Adding Notifications - ### - - # Public: Add a success notification. - # - # * `message` A {String} message - # * `options` (optional) An options {Object} with the following keys: - # * `buttons` (optional) An {Array} of {Object} where each {Object} has the - # following options: - # * `className` (optional) {String} a class name to add to the button's - # default class name (`btn btn-success`). - # * `onDidClick` (optional) {Function} callback to call when the button - # has been clicked. The context will be set to the - # {NotificationElement} instance. - # * `text` {String} inner text for the button - # * `description` (optional) A Markdown {String} containing a longer - # description about the notification. By default, this **will not** - # preserve newlines and whitespace when it is rendered. - # * `detail` (optional) A plain-text {String} containing additional details - # about the notification. By default, this **will** preserve newlines - # and whitespace when it is rendered. - # * `dismissable` (optional) A {Boolean} indicating whether this - # notification can be dismissed by the user. Defaults to `false`. - # * `icon` (optional) A {String} name of an icon from Octicons to display - # in the notification header. Defaults to `'check'`. - addSuccess: (message, options) -> - @addNotification(new Notification('success', message, options)) - - # Public: Add an informational notification. - # - # * `message` A {String} message - # * `options` (optional) An options {Object} with the following keys: - # * `buttons` (optional) An {Array} of {Object} where each {Object} has the - # following options: - # * `className` (optional) {String} a class name to add to the button's - # default class name (`btn btn-info`). - # * `onDidClick` (optional) {Function} callback to call when the button - # has been clicked. The context will be set to the - # {NotificationElement} instance. - # * `text` {String} inner text for the button - # * `description` (optional) A Markdown {String} containing a longer - # description about the notification. By default, this **will not** - # preserve newlines and whitespace when it is rendered. - # * `detail` (optional) A plain-text {String} containing additional details - # about the notification. By default, this **will** preserve newlines - # and whitespace when it is rendered. - # * `dismissable` (optional) A {Boolean} indicating whether this - # notification can be dismissed by the user. Defaults to `false`. - # * `icon` (optional) A {String} name of an icon from Octicons to display - # in the notification header. Defaults to `'info'`. - addInfo: (message, options) -> - @addNotification(new Notification('info', message, options)) - - # Public: Add a warning notification. - # - # * `message` A {String} message - # * `options` (optional) An options {Object} with the following keys: - # * `buttons` (optional) An {Array} of {Object} where each {Object} has the - # following options: - # * `className` (optional) {String} a class name to add to the button's - # default class name (`btn btn-warning`). - # * `onDidClick` (optional) {Function} callback to call when the button - # has been clicked. The context will be set to the - # {NotificationElement} instance. - # * `text` {String} inner text for the button - # * `description` (optional) A Markdown {String} containing a longer - # description about the notification. By default, this **will not** - # preserve newlines and whitespace when it is rendered. - # * `detail` (optional) A plain-text {String} containing additional details - # about the notification. By default, this **will** preserve newlines - # and whitespace when it is rendered. - # * `dismissable` (optional) A {Boolean} indicating whether this - # notification can be dismissed by the user. Defaults to `false`. - # * `icon` (optional) A {String} name of an icon from Octicons to display - # in the notification header. Defaults to `'alert'`. - addWarning: (message, options) -> - @addNotification(new Notification('warning', message, options)) - - # Public: Add an error notification. - # - # * `message` A {String} message - # * `options` (optional) An options {Object} with the following keys: - # * `buttons` (optional) An {Array} of {Object} where each {Object} has the - # following options: - # * `className` (optional) {String} a class name to add to the button's - # default class name (`btn btn-error`). - # * `onDidClick` (optional) {Function} callback to call when the button - # has been clicked. The context will be set to the - # {NotificationElement} instance. - # * `text` {String} inner text for the button - # * `description` (optional) A Markdown {String} containing a longer - # description about the notification. By default, this **will not** - # preserve newlines and whitespace when it is rendered. - # * `detail` (optional) A plain-text {String} containing additional details - # about the notification. By default, this **will** preserve newlines - # and whitespace when it is rendered. - # * `dismissable` (optional) A {Boolean} indicating whether this - # notification can be dismissed by the user. Defaults to `false`. - # * `icon` (optional) A {String} name of an icon from Octicons to display - # in the notification header. Defaults to `'flame'`. - # * `stack` (optional) A preformatted {String} with stack trace information - # describing the location of the error. - addError: (message, options) -> - @addNotification(new Notification('error', message, options)) - - # Public: Add a fatal error notification. - # - # * `message` A {String} message - # * `options` (optional) An options {Object} with the following keys: - # * `buttons` (optional) An {Array} of {Object} where each {Object} has the - # following options: - # * `className` (optional) {String} a class name to add to the button's - # default class name (`btn btn-error`). - # * `onDidClick` (optional) {Function} callback to call when the button - # has been clicked. The context will be set to the - # {NotificationElement} instance. - # * `text` {String} inner text for the button - # * `description` (optional) A Markdown {String} containing a longer - # description about the notification. By default, this **will not** - # preserve newlines and whitespace when it is rendered. - # * `detail` (optional) A plain-text {String} containing additional details - # about the notification. By default, this **will** preserve newlines - # and whitespace when it is rendered. - # * `dismissable` (optional) A {Boolean} indicating whether this - # notification can be dismissed by the user. Defaults to `false`. - # * `icon` (optional) A {String} name of an icon from Octicons to display - # in the notification header. Defaults to `'bug'`. - # * `stack` (optional) A preformatted {String} with stack trace information - # describing the location of the error. - addFatalError: (message, options) -> - @addNotification(new Notification('fatal', message, options)) - - add: (type, message, options) -> - @addNotification(new Notification(type, message, options)) - - addNotification: (notification) -> - @notifications.push(notification) - @emitter.emit('did-add-notification', notification) - notification - - ### - Section: Getting Notifications - ### - - # Public: Get all the notifications. - # - # Returns an {Array} of {Notification}s. - getNotifications: -> @notifications.slice() - - ### - Section: Managing Notifications - ### - - clear: -> - @notifications = [] diff --git a/src/notification-manager.js b/src/notification-manager.js new file mode 100644 index 000000000..b36f13e50 --- /dev/null +++ b/src/notification-manager.js @@ -0,0 +1,196 @@ +const {Emitter} = require('event-kit') +const Notification = require('../src/notification') + +// Public: A notification manager used to create {Notification}s to be shown +// to the user. +// +// An instance of this class is always available as the `atom.notifications` +// global. +module.exports = +class NotificationManager { + constructor () { + this.notifications = [] + this.emitter = new Emitter() + } + + /* + Section: Events + */ + + // Public: Invoke the given callback after a notification has been added. + // + // * `callback` {Function} to be called after the notification is added. + // * `notification` The {Notification} that was added. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddNotification (callback) { + return this.emitter.on('did-add-notification', callback) + } + + /* + Section: Adding Notifications + */ + + // Public: Add a success notification. + // + // * `message` A {String} message + // * `options` (optional) An options {Object} with the following keys: + // * `buttons` (optional) An {Array} of {Object} where each {Object} has + // the following options: + // * `className` (optional) {String} a class name to add to the button's + // default class name (`btn btn-success`). + // * `onDidClick` (optional) {Function} callback to call when the button + // has been clicked. The context will be set to the + // {NotificationElement} instance. + // * `text` {String} inner text for the button + // * `description` (optional) A Markdown {String} containing a longer + // description about the notification. By default, this **will not** + // preserve newlines and whitespace when it is rendered. + // * `detail` (optional) A plain-text {String} containing additional + // details about the notification. By default, this **will** preserve + // newlines and whitespace when it is rendered. + // * `dismissable` (optional) A {Boolean} indicating whether this + // notification can be dismissed by the user. Defaults to `false`. + // * `icon` (optional) A {String} name of an icon from Octicons to display + // in the notification header. Defaults to `'check'`. + addSuccess (message, options) { + return this.addNotification(new Notification('success', message, options)) + } + + // Public: Add an informational notification. + // + // * `message` A {String} message + // * `options` (optional) An options {Object} with the following keys: + // * `buttons` (optional) An {Array} of {Object} where each {Object} has + // the following options: + // * `className` (optional) {String} a class name to add to the button's + // default class name (`btn btn-info`). + // * `onDidClick` (optional) {Function} callback to call when the button + // has been clicked. The context will be set to the + // {NotificationElement} instance. + // * `text` {String} inner text for the button + // * `description` (optional) A Markdown {String} containing a longer + // description about the notification. By default, this **will not** + // preserve newlines and whitespace when it is rendered. + // * `detail` (optional) A plain-text {String} containing additional + // details about the notification. By default, this **will** preserve + // newlines and whitespace when it is rendered. + // * `dismissable` (optional) A {Boolean} indicating whether this + // notification can be dismissed by the user. Defaults to `false`. + // * `icon` (optional) A {String} name of an icon from Octicons to display + // in the notification header. Defaults to `'info'`. + addInfo (message, options) { + return this.addNotification(new Notification('info', message, options)) + } + + // Public: Add a warning notification. + // + // * `message` A {String} message + // * `options` (optional) An options {Object} with the following keys: + // * `buttons` (optional) An {Array} of {Object} where each {Object} has + // the following options: + // * `className` (optional) {String} a class name to add to the button's + // default class name (`btn btn-warning`). + // * `onDidClick` (optional) {Function} callback to call when the button + // has been clicked. The context will be set to the + // {NotificationElement} instance. + // * `text` {String} inner text for the button + // * `description` (optional) A Markdown {String} containing a longer + // description about the notification. By default, this **will not** + // preserve newlines and whitespace when it is rendered. + // * `detail` (optional) A plain-text {String} containing additional + // details about the notification. By default, this **will** preserve + // newlines and whitespace when it is rendered. + // * `dismissable` (optional) A {Boolean} indicating whether this + // notification can be dismissed by the user. Defaults to `false`. + // * `icon` (optional) A {String} name of an icon from Octicons to display + // in the notification header. Defaults to `'alert'`. + addWarning (message, options) { + return this.addNotification(new Notification('warning', message, options)) + } + + // Public: Add an error notification. + // + // * `message` A {String} message + // * `options` (optional) An options {Object} with the following keys: + // * `buttons` (optional) An {Array} of {Object} where each {Object} has + // the following options: + // * `className` (optional) {String} a class name to add to the button's + // default class name (`btn btn-error`). + // * `onDidClick` (optional) {Function} callback to call when the button + // has been clicked. The context will be set to the + // {NotificationElement} instance. + // * `text` {String} inner text for the button + // * `description` (optional) A Markdown {String} containing a longer + // description about the notification. By default, this **will not** + // preserve newlines and whitespace when it is rendered. + // * `detail` (optional) A plain-text {String} containing additional + // details about the notification. By default, this **will** preserve + // newlines and whitespace when it is rendered. + // * `dismissable` (optional) A {Boolean} indicating whether this + // notification can be dismissed by the user. Defaults to `false`. + // * `icon` (optional) A {String} name of an icon from Octicons to display + // in the notification header. Defaults to `'flame'`. + // * `stack` (optional) A preformatted {String} with stack trace + // information describing the location of the error. + addError (message, options) { + return this.addNotification(new Notification('error', message, options)) + } + + // Public: Add a fatal error notification. + // + // * `message` A {String} message + // * `options` (optional) An options {Object} with the following keys: + // * `buttons` (optional) An {Array} of {Object} where each {Object} has + // the following options: + // * `className` (optional) {String} a class name to add to the button's + // default class name (`btn btn-error`). + // * `onDidClick` (optional) {Function} callback to call when the button + // has been clicked. The context will be set to the + // {NotificationElement} instance. + // * `text` {String} inner text for the button + // * `description` (optional) A Markdown {String} containing a longer + // description about the notification. By default, this **will not** + // preserve newlines and whitespace when it is rendered. + // * `detail` (optional) A plain-text {String} containing additional + // details about the notification. By default, this **will** preserve + // newlines and whitespace when it is rendered. + // * `dismissable` (optional) A {Boolean} indicating whether this + // notification can be dismissed by the user. Defaults to `false`. + // * `icon` (optional) A {String} name of an icon from Octicons to display + // in the notification header. Defaults to `'bug'`. + // * `stack` (optional) A preformatted {String} with stack trace + // information describing the location of the error. + addFatalError (message, options) { + return this.addNotification(new Notification('fatal', message, options)) + } + + add (type, message, options) { + return this.addNotification(new Notification(type, message, options)) + } + + addNotification (notification) { + this.notifications.push(notification) + this.emitter.emit('did-add-notification', notification) + return notification + } + + /* + Section: Getting Notifications + */ + + // Public: Get all the notifications. + // + // Returns an {Array} of {Notification}s. + getNotifications () { + return this.notifications.slice() + } + + /* + Section: Managing Notifications + */ + + clear () { + this.notifications = [] + } +} From 45c3770293287cdcbd5e566c0d3a7644288c90a5 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 16 Sep 2017 17:55:05 -0400 Subject: [PATCH 146/448] :skull::coffee: Decaffeinate spec/notification-spec.coffee --- spec/notification-spec.coffee | 60 ----------------------------- spec/notification-spec.js | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 60 deletions(-) delete mode 100644 spec/notification-spec.coffee create mode 100644 spec/notification-spec.js diff --git a/spec/notification-spec.coffee b/spec/notification-spec.coffee deleted file mode 100644 index 94f2123a3..000000000 --- a/spec/notification-spec.coffee +++ /dev/null @@ -1,60 +0,0 @@ -Notification = require '../src/notification' - -describe "Notification", -> - [notification] = [] - - it "throws an error when created with a non-string message", -> - expect(-> new Notification('error', null)).toThrow() - expect(-> new Notification('error', 3)).toThrow() - expect(-> new Notification('error', {})).toThrow() - expect(-> new Notification('error', false)).toThrow() - expect(-> new Notification('error', [])).toThrow() - - it "throws an error when created with non-object options", -> - expect(-> new Notification('error', 'message', 'foo')).toThrow() - expect(-> new Notification('error', 'message', 3)).toThrow() - expect(-> new Notification('error', 'message', false)).toThrow() - expect(-> new Notification('error', 'message', [])).toThrow() - - describe "::getTimestamp()", -> - it "returns a Date object", -> - notification = new Notification('error', 'message!') - expect(notification.getTimestamp() instanceof Date).toBe true - - describe "::getIcon()", -> - it "returns a default when no icon specified", -> - notification = new Notification('error', 'message!') - expect(notification.getIcon()).toBe 'flame' - - it "returns the icon specified", -> - notification = new Notification('error', 'message!', icon: 'my-icon') - expect(notification.getIcon()).toBe 'my-icon' - - describe "dismissing notifications", -> - describe "when the notfication is dismissable", -> - it "calls a callback when the notification is dismissed", -> - dismissedSpy = jasmine.createSpy() - notification = new Notification('error', 'message', dismissable: true) - notification.onDidDismiss dismissedSpy - - expect(notification.isDismissable()).toBe true - expect(notification.isDismissed()).toBe false - - notification.dismiss() - - expect(dismissedSpy).toHaveBeenCalled() - expect(notification.isDismissed()).toBe true - - describe "when the notfication is not dismissable", -> - it "does nothing when ::dismiss() is called", -> - dismissedSpy = jasmine.createSpy() - notification = new Notification('error', 'message') - notification.onDidDismiss dismissedSpy - - expect(notification.isDismissable()).toBe false - expect(notification.isDismissed()).toBe true - - notification.dismiss() - - expect(dismissedSpy).not.toHaveBeenCalled() - expect(notification.isDismissed()).toBe true diff --git a/spec/notification-spec.js b/spec/notification-spec.js new file mode 100644 index 000000000..4702cd13d --- /dev/null +++ b/spec/notification-spec.js @@ -0,0 +1,71 @@ +const Notification = require('../src/notification') + +describe('Notification', () => { + it('throws an error when created with a non-string message', () => { + expect(() => new Notification('error', null)).toThrow() + expect(() => new Notification('error', 3)).toThrow() + expect(() => new Notification('error', {})).toThrow() + expect(() => new Notification('error', false)).toThrow() + expect(() => new Notification('error', [])).toThrow() + }) + + it('throws an error when created with non-object options', () => { + expect(() => new Notification('error', 'message', 'foo')).toThrow() + expect(() => new Notification('error', 'message', 3)).toThrow() + expect(() => new Notification('error', 'message', false)).toThrow() + expect(() => new Notification('error', 'message', [])).toThrow() + }) + + describe('::getTimestamp()', () => + it('returns a Date object', () => { + const notification = new Notification('error', 'message!') + expect(notification.getTimestamp() instanceof Date).toBe(true) + }) + ) + + describe('::getIcon()', () => { + it('returns a default when no icon specified', () => { + const notification = new Notification('error', 'message!') + expect(notification.getIcon()).toBe('flame') + }) + + it('returns the icon specified', () => { + const notification = new Notification('error', 'message!', {icon: 'my-icon'}) + expect(notification.getIcon()).toBe('my-icon') + }) + }) + + describe('dismissing notifications', () => { + describe('when the notfication is dismissable', () => + it('calls a callback when the notification is dismissed', () => { + const dismissedSpy = jasmine.createSpy() + const notification = new Notification('error', 'message', {dismissable: true}) + notification.onDidDismiss(dismissedSpy) + + expect(notification.isDismissable()).toBe(true) + expect(notification.isDismissed()).toBe(false) + + notification.dismiss() + + expect(dismissedSpy).toHaveBeenCalled() + expect(notification.isDismissed()).toBe(true) + }) + ) + + describe('when the notfication is not dismissable', () => + it('does nothing when ::dismiss() is called', () => { + const dismissedSpy = jasmine.createSpy() + const notification = new Notification('error', 'message') + notification.onDidDismiss(dismissedSpy) + + expect(notification.isDismissable()).toBe(false) + expect(notification.isDismissed()).toBe(true) + + notification.dismiss() + + expect(dismissedSpy).not.toHaveBeenCalled() + expect(notification.isDismissed()).toBe(true) + }) + ) + }) +}) From 0054e3f796acccf04058643bb2f573d2b76b7264 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 16 Sep 2017 18:01:55 -0400 Subject: [PATCH 147/448] :skull::coffee: Decaffeinate spec/notification-manager-spec.coffee --- spec/notification-manager-spec.coffee | 57 ---------------------- spec/notification-manager-spec.js | 69 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 57 deletions(-) delete mode 100644 spec/notification-manager-spec.coffee create mode 100644 spec/notification-manager-spec.js diff --git a/spec/notification-manager-spec.coffee b/spec/notification-manager-spec.coffee deleted file mode 100644 index dfc16322c..000000000 --- a/spec/notification-manager-spec.coffee +++ /dev/null @@ -1,57 +0,0 @@ -NotificationManager = require '../src/notification-manager' - -describe "NotificationManager", -> - [manager] = [] - - beforeEach -> - manager = new NotificationManager - - describe "the atom global", -> - it "has a notifications instance", -> - expect(atom.notifications instanceof NotificationManager).toBe true - - describe "adding events", -> - addSpy = null - - beforeEach -> - addSpy = jasmine.createSpy() - manager.onDidAddNotification(addSpy) - - it "emits an event when a notification has been added", -> - manager.add('error', 'Some error!', icon: 'someIcon') - expect(addSpy).toHaveBeenCalled() - - notification = addSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'error' - expect(notification.getMessage()).toBe 'Some error!' - expect(notification.getIcon()).toBe 'someIcon' - - it "emits a fatal error ::addFatalError has been called", -> - manager.addFatalError('Some error!', icon: 'someIcon') - expect(addSpy).toHaveBeenCalled() - notification = addSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'fatal' - - it "emits an error ::addError has been called", -> - manager.addError('Some error!', icon: 'someIcon') - expect(addSpy).toHaveBeenCalled() - notification = addSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'error' - - it "emits a warning notification ::addWarning has been called", -> - manager.addWarning('Something!', icon: 'someIcon') - expect(addSpy).toHaveBeenCalled() - notification = addSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - - it "emits an info notification ::addInfo has been called", -> - manager.addInfo('Something!', icon: 'someIcon') - expect(addSpy).toHaveBeenCalled() - notification = addSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'info' - - it "emits a success notification ::addSuccess has been called", -> - manager.addSuccess('Something!', icon: 'someIcon') - expect(addSpy).toHaveBeenCalled() - notification = addSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'success' diff --git a/spec/notification-manager-spec.js b/spec/notification-manager-spec.js new file mode 100644 index 000000000..3f6a20b67 --- /dev/null +++ b/spec/notification-manager-spec.js @@ -0,0 +1,69 @@ +const NotificationManager = require('../src/notification-manager') + +describe('NotificationManager', () => { + let manager + + beforeEach(() => { + manager = new NotificationManager() + }) + + describe('the atom global', () => + it('has a notifications instance', () => { + expect(atom.notifications instanceof NotificationManager).toBe(true) + }) + ) + + describe('adding events', () => { + let addSpy + + beforeEach(() => { + addSpy = jasmine.createSpy() + manager.onDidAddNotification(addSpy) + }) + + it('emits an event when a notification has been added', () => { + manager.add('error', 'Some error!', {icon: 'someIcon'}) + expect(addSpy).toHaveBeenCalled() + + const notification = addSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('error') + expect(notification.getMessage()).toBe('Some error!') + expect(notification.getIcon()).toBe('someIcon') + }) + + it('emits a fatal error when ::addFatalError has been called', () => { + manager.addFatalError('Some error!', {icon: 'someIcon'}) + expect(addSpy).toHaveBeenCalled() + const notification = addSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('fatal') + }) + + it('emits an error when ::addError has been called', () => { + manager.addError('Some error!', {icon: 'someIcon'}) + expect(addSpy).toHaveBeenCalled() + const notification = addSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('error') + }) + + it('emits a warning notification when ::addWarning has been called', () => { + manager.addWarning('Something!', {icon: 'someIcon'}) + expect(addSpy).toHaveBeenCalled() + const notification = addSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + }) + + it('emits an info notification when ::addInfo has been called', () => { + manager.addInfo('Something!', {icon: 'someIcon'}) + expect(addSpy).toHaveBeenCalled() + const notification = addSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('info') + }) + + it('emits a success notification when ::addSuccess has been called', () => { + manager.addSuccess('Something!', {icon: 'someIcon'}) + expect(addSpy).toHaveBeenCalled() + const notification = addSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('success') + }) + }) +}) From 9d0ed6da52ca4c275772ee0b59de8befd91c4586 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 17 Sep 2017 08:25:05 -0400 Subject: [PATCH 148/448] =?UTF-8?q?=F0=9F=93=9D=20Document=20that=20Notfic?= =?UTF-8?q?ationManager=20add*=20methods=20return=20a=20Notification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parts of the Atom ecosystem rely on the fact that these methods return the Notification that was added. For example: https://github.com/atom/exception-reporting/blob/4e6866c3a9/lib/reporter.js#L159 Therefore, we should document that these methods are expected to return the Notification that was added. --- src/notification-manager.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/notification-manager.js b/src/notification-manager.js index b36f13e50..df5e5fb42 100644 --- a/src/notification-manager.js +++ b/src/notification-manager.js @@ -53,6 +53,8 @@ class NotificationManager { // notification can be dismissed by the user. Defaults to `false`. // * `icon` (optional) A {String} name of an icon from Octicons to display // in the notification header. Defaults to `'check'`. + // + // Returns the {Notification} that was added. addSuccess (message, options) { return this.addNotification(new Notification('success', message, options)) } @@ -79,6 +81,8 @@ class NotificationManager { // notification can be dismissed by the user. Defaults to `false`. // * `icon` (optional) A {String} name of an icon from Octicons to display // in the notification header. Defaults to `'info'`. + // + // Returns the {Notification} that was added. addInfo (message, options) { return this.addNotification(new Notification('info', message, options)) } @@ -105,6 +109,8 @@ class NotificationManager { // notification can be dismissed by the user. Defaults to `false`. // * `icon` (optional) A {String} name of an icon from Octicons to display // in the notification header. Defaults to `'alert'`. + // + // Returns the {Notification} that was added. addWarning (message, options) { return this.addNotification(new Notification('warning', message, options)) } @@ -133,6 +139,8 @@ class NotificationManager { // in the notification header. Defaults to `'flame'`. // * `stack` (optional) A preformatted {String} with stack trace // information describing the location of the error. + // + // Returns the {Notification} that was added. addError (message, options) { return this.addNotification(new Notification('error', message, options)) } @@ -161,6 +169,8 @@ class NotificationManager { // in the notification header. Defaults to `'bug'`. // * `stack` (optional) A preformatted {String} with stack trace // information describing the location of the error. + // + // Returns the {Notification} that was added. addFatalError (message, options) { return this.addNotification(new Notification('fatal', message, options)) } From f41b7aa5f4d70c45b53f9c254c347873e0c11bfe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 18 Sep 2017 10:04:48 +0200 Subject: [PATCH 149/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a01294e8..97100827e 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.2.1", + "text-buffer": "13.3.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 15ade42e92a25f840f10bbac82fa1f2593001da4 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 18 Sep 2017 10:05:44 +0200 Subject: [PATCH 150/448] :arrow_up: language-php@0.42.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97100827e..a4f1867a6 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "language-mustache": "0.14.1", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.41.0", + "language-php": "0.42.0", "language-property-list": "0.9.1", "language-python": "0.45.4", "language-ruby": "0.71.3", From 248bdfda25ef20431c22d61304d3535839172bac Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 10:03:29 -0400 Subject: [PATCH 151/448] :arrow_up: styleguide --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4f1867a6..bec2910e0 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "snippets": "1.1.4", "spell-check": "0.72.2", "status-bar": "1.8.13", - "styleguide": "0.49.6", + "styleguide": "0.49.7", "symbols-view": "0.118.0", "tabs": "0.107.2", "timecop": "0.36.0", From 1f25cda7c4bf98626e97aafca179314766968cc3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 10:49:16 -0700 Subject: [PATCH 152/448] :arrow_up: github@0.6.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bec2910e0..1b6fe7b4e 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.0", - "github": "0.6.1", + "github": "0.6.2", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", From 8ef9dabffa2c6bf34941d37143fa73d578ca515c Mon Sep 17 00:00:00 2001 From: David Reiss Date: Mon, 18 Sep 2017 12:08:10 -0700 Subject: [PATCH 153/448] Allow atom:// urls to be opened from the command line Some Atom extensions (like Nuclide) have functionality that can only be triggered by open-url events, which only work on Mac OS. With this change, `atom://` URLs can be passed on the command-line, and they will opened with the normal openUrl method on all platforms. This change doesn't set Atom up as the default handler for atom:// urls. That will require some platform-specific changes. --- src/main-process/parse-command-line.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 7531e609b..6c5349437 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -19,6 +19,8 @@ module.exports = function parseCommandLine (processArgs) { will be opened in that window. Otherwise, they will be opened in a new window. + Paths that start with \`atom://\` will be interpreted as URLs. + Environment Variables: ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode. @@ -76,7 +78,6 @@ module.exports = function parseCommandLine (processArgs) { const addToLastWindow = args['add'] const safeMode = args['safe'] - const pathsToOpen = args._ const benchmark = args['benchmark'] const benchmarkTest = args['benchmark-test'] const test = args['test'] @@ -100,11 +101,20 @@ 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 devMode = args['dev'] let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom') let resourcePath = null + for (const path of args._) { + if (path.startsWith('atom://')) { + urlsToOpen.push(path) + } else { + pathsToOpen.push(path) + } + } + if (args['resource-path']) { devMode = true devResourcePath = args['resource-path'] From e875f5447edfea1e911aa15ce44bc97d1ff817b8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 14 Sep 2017 09:17:40 -0700 Subject: [PATCH 154/448] Convert Pane to JS --- src/pane.coffee | 1006 ------------------------------------- src/pane.js | 1251 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1251 insertions(+), 1006 deletions(-) delete mode 100644 src/pane.coffee create mode 100644 src/pane.js diff --git a/src/pane.coffee b/src/pane.coffee deleted file mode 100644 index 19a2df6d5..000000000 --- a/src/pane.coffee +++ /dev/null @@ -1,1006 +0,0 @@ -Grim = require 'grim' -{find, compact, extend, last} = require 'underscore-plus' -{CompositeDisposable, Emitter} = require 'event-kit' -PaneAxis = require './pane-axis' -TextEditor = require './text-editor' -PaneElement = require './pane-element' - -nextInstanceId = 1 - -class SaveCancelledError extends Error - constructor: -> super - -# Extended: A container for presenting content in the center of the workspace. -# Panes can contain multiple items, one of which is *active* at a given time. -# The view corresponding to the active item is displayed in the interface. In -# the default configuration, tabs are also displayed for each item. -# -# Each pane may also contain one *pending* item. When a pending item is added -# to a pane, it will replace the currently pending item, if any, instead of -# simply being added. In the default configuration, the text in the tab for -# pending items is shown in italics. -module.exports = -class Pane - inspect: -> "Pane #{@id}" - - @deserialize: (state, {deserializers, applicationDelegate, config, notifications, views}) -> - {items, activeItemIndex, activeItemURI, activeItemUri} = state - activeItemURI ?= activeItemUri - items = items.map (itemState) -> deserializers.deserialize(itemState) - state.activeItem = items[activeItemIndex] - state.items = compact(items) - if activeItemURI? - state.activeItem ?= find state.items, (item) -> - if typeof item.getURI is 'function' - itemURI = item.getURI() - itemURI is activeItemURI - new Pane(extend(state, { - deserializerManager: deserializers, - notificationManager: notifications, - viewRegistry: views, - config, applicationDelegate - })) - - constructor: (params) -> - { - @id, @activeItem, @focused, @applicationDelegate, @notificationManager, @config, - @deserializerManager, @viewRegistry - } = params - - if @id? - nextInstanceId = Math.max(nextInstanceId, @id + 1) - else - @id = nextInstanceId++ - @emitter = new Emitter - @alive = true - @subscriptionsPerItem = new WeakMap - @items = [] - @itemStack = [] - @container = null - @activeItem ?= undefined - @focused ?= false - - @addItems(compact(params?.items ? [])) - @setActiveItem(@items[0]) unless @getActiveItem()? - @addItemsToStack(params?.itemStackIndices ? []) - @setFlexScale(params?.flexScale ? 1) - - getElement: -> - @element ?= new PaneElement().initialize(this, {views: @viewRegistry, @applicationDelegate}) - - serialize: -> - itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function')) - itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function') - activeItemIndex = itemsToBeSerialized.indexOf(@activeItem) - - { - deserializer: 'Pane', - id: @id, - items: itemsToBeSerialized.map((item) -> item.serialize()) - itemStackIndices: itemStackIndices - activeItemIndex: activeItemIndex - focused: @focused - flexScale: @flexScale - } - - getParent: -> @parent - - setParent: (@parent) -> @parent - - getContainer: -> @container - - setContainer: (container) -> - if container and container isnt @container - @container = container - container.didAddPane({pane: this}) - - # Private: Determine whether the given item is allowed to exist in this pane. - # - # * `item` the Item - # - # Returns a {Boolean}. - isItemAllowed: (item) -> - if (typeof item.getAllowedLocations isnt 'function') - true - else - item.getAllowedLocations().includes(@getContainer().getLocation()) - - setFlexScale: (@flexScale) -> - @emitter.emit 'did-change-flex-scale', @flexScale - @flexScale - - getFlexScale: -> @flexScale - - increaseSize: -> @setFlexScale(@getFlexScale() * 1.1) - - decreaseSize: -> @setFlexScale(@getFlexScale() / 1.1) - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when the pane resizes - # - # The callback will be invoked when pane's flexScale property changes. - # Use {::getFlexScale} to get the current value. - # - # * `callback` {Function} to be called when the pane is resized - # * `flexScale` {Number} representing the panes `flex-grow`; ability for a - # flex item to grow if necessary. - # - # Returns a {Disposable} on which '.dispose()' can be called to unsubscribe. - onDidChangeFlexScale: (callback) -> - @emitter.on 'did-change-flex-scale', callback - - # Public: Invoke the given callback with the current and future values of - # {::getFlexScale}. - # - # * `callback` {Function} to be called with the current and future values of - # the {::getFlexScale} property. - # * `flexScale` {Number} representing the panes `flex-grow`; ability for a - # flex item to grow if necessary. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeFlexScale: (callback) -> - callback(@flexScale) - @onDidChangeFlexScale(callback) - - # Public: Invoke the given callback when the pane is activated. - # - # The given callback will be invoked whenever {::activate} is called on the - # pane, even if it is already active at the time. - # - # * `callback` {Function} to be called when the pane is activated. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidActivate: (callback) -> - @emitter.on 'did-activate', callback - - # Public: Invoke the given callback before the pane is destroyed. - # - # * `callback` {Function} to be called before the pane is destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillDestroy: (callback) -> - @emitter.on 'will-destroy', callback - - # Public: Invoke the given callback when the pane is destroyed. - # - # * `callback` {Function} to be called when the pane is destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - # Public: Invoke the given callback when the value of the {::isActive} - # property changes. - # - # * `callback` {Function} to be called when the value of the {::isActive} - # property changes. - # * `active` {Boolean} indicating whether the pane is active. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActive: (callback) -> - @container.onDidChangeActivePane (activePane) => - callback(this is activePane) - - # Public: Invoke the given callback with the current and future values of the - # {::isActive} property. - # - # * `callback` {Function} to be called with the current and future values of - # the {::isActive} property. - # * `active` {Boolean} indicating whether the pane is active. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeActive: (callback) -> - callback(@isActive()) - @onDidChangeActive(callback) - - # Public: Invoke the given callback when an item is added to the pane. - # - # * `callback` {Function} to be called with when items are added. - # * `event` {Object} with the following keys: - # * `item` The added pane item. - # * `index` {Number} indicating where the item is located. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddItem: (callback) -> - @emitter.on 'did-add-item', callback - - # Public: Invoke the given callback when an item is removed from the pane. - # - # * `callback` {Function} to be called with when items are removed. - # * `event` {Object} with the following keys: - # * `item` The removed pane item. - # * `index` {Number} indicating where the item was located. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveItem: (callback) -> - @emitter.on 'did-remove-item', callback - - # Public: Invoke the given callback before an item is removed from the pane. - # - # * `callback` {Function} to be called with when items are removed. - # * `event` {Object} with the following keys: - # * `item` The pane item to be removed. - # * `index` {Number} indicating where the item is located. - onWillRemoveItem: (callback) -> - @emitter.on 'will-remove-item', callback - - # Public: Invoke the given callback when an item is moved within the pane. - # - # * `callback` {Function} to be called with when items are moved. - # * `event` {Object} with the following keys: - # * `item` The removed pane item. - # * `oldIndex` {Number} indicating where the item was located. - # * `newIndex` {Number} indicating where the item is now located. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidMoveItem: (callback) -> - @emitter.on 'did-move-item', callback - - # Public: Invoke the given callback with all current and future items. - # - # * `callback` {Function} to be called with current and future items. - # * `item` An item that is present in {::getItems} at the time of - # subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeItems: (callback) -> - callback(item) for item in @getItems() - @onDidAddItem ({item}) -> callback(item) - - # Public: Invoke the given callback when the value of {::getActiveItem} - # changes. - # - # * `callback` {Function} to be called with when the active item changes. - # * `activeItem` The current active item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActiveItem: (callback) -> - @emitter.on 'did-change-active-item', callback - - # Public: Invoke the given callback when {::activateNextRecentlyUsedItem} - # has been called, either initiating or continuing a forward MRU traversal of - # pane items. - # - # * `callback` {Function} to be called with when the active item changes. - # * `nextRecentlyUsedItem` The next MRU item, now being set active - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onChooseNextMRUItem: (callback) -> - @emitter.on 'choose-next-mru-item', callback - - # Public: Invoke the given callback when {::activatePreviousRecentlyUsedItem} - # has been called, either initiating or continuing a reverse MRU traversal of - # pane items. - # - # * `callback` {Function} to be called with when the active item changes. - # * `previousRecentlyUsedItem` The previous MRU item, now being set active - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onChooseLastMRUItem: (callback) -> - @emitter.on 'choose-last-mru-item', callback - - # Public: Invoke the given callback when {::moveActiveItemToTopOfStack} - # has been called, terminating an MRU traversal of pane items and moving the - # current active item to the top of the stack. Typically bound to a modifier - # (e.g. CTRL) key up event. - # - # * `callback` {Function} to be called with when the MRU traversal is done. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDoneChoosingMRUItem: (callback) -> - @emitter.on 'done-choosing-mru-item', callback - - # Public: Invoke the given callback with the current and future values of - # {::getActiveItem}. - # - # * `callback` {Function} to be called with the current and future active - # items. - # * `activeItem` The current active item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeActiveItem: (callback) -> - callback(@getActiveItem()) - @onDidChangeActiveItem(callback) - - # Public: Invoke the given callback before items are destroyed. - # - # * `callback` {Function} to be called before items are destroyed. - # * `event` {Object} with the following keys: - # * `item` The item that will be destroyed. - # * `index` The location of the item. - # - # Returns a {Disposable} on which `.dispose()` can be called to - # unsubscribe. - onWillDestroyItem: (callback) -> - @emitter.on 'will-destroy-item', callback - - # Called by the view layer to indicate that the pane has gained focus. - focus: -> - @focused = true - @activate() - - # Called by the view layer to indicate that the pane has lost focus. - blur: -> - @focused = false - true # if this is called from an event handler, don't cancel it - - isFocused: -> @focused - - getPanes: -> [this] - - unsubscribeFromItem: (item) -> - @subscriptionsPerItem.get(item)?.dispose() - @subscriptionsPerItem.delete(item) - - ### - Section: Items - ### - - # Public: Get the items in this pane. - # - # Returns an {Array} of items. - getItems: -> - @items.slice() - - # Public: Get the active pane item in this pane. - # - # Returns a pane item. - getActiveItem: -> @activeItem - - setActiveItem: (activeItem, options) -> - {modifyStack} = options if options? - unless activeItem is @activeItem - @addItemToStack(activeItem) unless modifyStack is false - @activeItem = activeItem - @emitter.emit 'did-change-active-item', @activeItem - @container?.didChangeActiveItemOnPane(this, @activeItem) - @activeItem - - # Build the itemStack after deserializing - addItemsToStack: (itemStackIndices) -> - if @items.length > 0 - if itemStackIndices.length is 0 or itemStackIndices.length isnt @items.length or itemStackIndices.indexOf(-1) >= 0 - itemStackIndices = (i for i in [0..@items.length-1]) - for itemIndex in itemStackIndices - @addItemToStack(@items[itemIndex]) - return - - # Add item (or move item) to the end of the itemStack - addItemToStack: (newItem) -> - return unless newItem? - index = @itemStack.indexOf(newItem) - @itemStack.splice(index, 1) unless index is -1 - @itemStack.push(newItem) - - # Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise. - getActiveEditor: -> - @activeItem if @activeItem instanceof TextEditor - - # Public: Return the item at the given index. - # - # * `index` {Number} - # - # Returns an item or `null` if no item exists at the given index. - itemAtIndex: (index) -> - @items[index] - - # Makes the next item in the itemStack active. - activateNextRecentlyUsedItem: -> - if @items.length > 1 - @itemStackIndex = @itemStack.length - 1 unless @itemStackIndex? - @itemStackIndex = @itemStack.length if @itemStackIndex is 0 - @itemStackIndex = @itemStackIndex - 1 - nextRecentlyUsedItem = @itemStack[@itemStackIndex] - @emitter.emit 'choose-next-mru-item', nextRecentlyUsedItem - @setActiveItem(nextRecentlyUsedItem, modifyStack: false) - - # Makes the previous item in the itemStack active. - activatePreviousRecentlyUsedItem: -> - if @items.length > 1 - if @itemStackIndex + 1 is @itemStack.length or not @itemStackIndex? - @itemStackIndex = -1 - @itemStackIndex = @itemStackIndex + 1 - previousRecentlyUsedItem = @itemStack[@itemStackIndex] - @emitter.emit 'choose-last-mru-item', previousRecentlyUsedItem - @setActiveItem(previousRecentlyUsedItem, modifyStack: false) - - # Moves the active item to the end of the itemStack once the ctrl key is lifted - moveActiveItemToTopOfStack: -> - delete @itemStackIndex - @addItemToStack(@activeItem) - @emitter.emit 'done-choosing-mru-item' - - # Public: Makes the next item active. - activateNextItem: -> - index = @getActiveItemIndex() - if index < @items.length - 1 - @activateItemAtIndex(index + 1) - else - @activateItemAtIndex(0) - - # Public: Makes the previous item active. - activatePreviousItem: -> - index = @getActiveItemIndex() - if index > 0 - @activateItemAtIndex(index - 1) - else - @activateItemAtIndex(@items.length - 1) - - activateLastItem: -> - @activateItemAtIndex(@items.length - 1) - - # Public: Move the active tab to the right. - moveItemRight: -> - index = @getActiveItemIndex() - rightItemIndex = index + 1 - @moveItem(@getActiveItem(), rightItemIndex) unless rightItemIndex > @items.length - 1 - - # Public: Move the active tab to the left - moveItemLeft: -> - index = @getActiveItemIndex() - leftItemIndex = index - 1 - @moveItem(@getActiveItem(), leftItemIndex) unless leftItemIndex < 0 - - # Public: Get the index of the active item. - # - # Returns a {Number}. - getActiveItemIndex: -> - @items.indexOf(@activeItem) - - # Public: Activate the item at the given index. - # - # * `index` {Number} - activateItemAtIndex: (index) -> - item = @itemAtIndex(index) or @getActiveItem() - @setActiveItem(item) - - # Public: Make the given item *active*, causing it to be displayed by - # the pane's view. - # - # * `item` The item to activate - # * `options` (optional) {Object} - # * `pending` (optional) {Boolean} indicating that the item should be added - # in a pending state if it does not yet exist in the pane. Existing pending - # items in a pane are replaced with new pending items when they are opened. - activateItem: (item, options={}) -> - if item? - if @getPendingItem() is @activeItem - index = @getActiveItemIndex() - else - index = @getActiveItemIndex() + 1 - @addItem(item, extend({}, options, {index: index})) - @setActiveItem(item) - - # Public: Add the given item to the pane. - # - # * `item` The item to add. It can be a model with an associated view or a - # view. - # * `options` (optional) {Object} - # * `index` (optional) {Number} indicating the index at which to add the item. - # If omitted, the item is added after the current active item. - # * `pending` (optional) {Boolean} indicating that the item should be - # added in a pending state. Existing pending items in a pane are replaced with - # new pending items when they are opened. - # - # Returns the added item. - addItem: (item, options={}) -> - # Backward compat with old API: - # addItem(item, index=@getActiveItemIndex() + 1) - if typeof options is "number" - Grim.deprecate("Pane::addItem(item, #{options}) is deprecated in favor of Pane::addItem(item, {index: #{options}})") - options = index: options - - index = options.index ? @getActiveItemIndex() + 1 - moved = options.moved ? false - pending = options.pending ? false - - throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object' - throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?() - - return if item in @items - - if typeof item.onDidDestroy is 'function' - itemSubscriptions = new CompositeDisposable - itemSubscriptions.add item.onDidDestroy => @removeItem(item, false) - if typeof item.onDidTerminatePendingState is "function" - itemSubscriptions.add item.onDidTerminatePendingState => - @clearPendingItem() if @getPendingItem() is item - @subscriptionsPerItem.set item, itemSubscriptions - - @items.splice(index, 0, item) - lastPendingItem = @getPendingItem() - replacingPendingItem = lastPendingItem? and not moved - @pendingItem = null if replacingPendingItem - @setPendingItem(item) if pending - - @emitter.emit 'did-add-item', {item, index, moved} - @container?.didAddPaneItem(item, this, index) unless moved - - @destroyItem(lastPendingItem) if replacingPendingItem - @setActiveItem(item) unless @getActiveItem()? - item - - setPendingItem: (item) => - if @pendingItem isnt item - mostRecentPendingItem = @pendingItem - @pendingItem = item - if mostRecentPendingItem? - @emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem - - getPendingItem: => - @pendingItem or null - - clearPendingItem: => - @setPendingItem(null) - - onItemDidTerminatePendingState: (callback) => - @emitter.on 'item-did-terminate-pending-state', callback - - # Public: Add the given items to the pane. - # - # * `items` An {Array} of items to add. Items can be views or models with - # associated views. Any objects that are already present in the pane's - # current items will not be added again. - # * `index` (optional) {Number} index at which to add the items. If omitted, - # the item is # added after the current active item. - # - # Returns an {Array} of added items. - addItems: (items, index=@getActiveItemIndex() + 1) -> - items = items.filter (item) => not (item in @items) - @addItem(item, {index: index + i}) for item, i in items - items - - removeItem: (item, moved) -> - index = @items.indexOf(item) - return if index is -1 - @pendingItem = null if @getPendingItem() is item - @removeItemFromStack(item) - @emitter.emit 'will-remove-item', {item, index, destroyed: not moved, moved} - @unsubscribeFromItem(item) - - if item is @activeItem - if @items.length is 1 - @setActiveItem(undefined) - else if index is 0 - @activateNextItem() - else - @activatePreviousItem() - @items.splice(index, 1) - @emitter.emit 'did-remove-item', {item, index, destroyed: not moved, moved} - @container?.didDestroyPaneItem({item, index, pane: this}) unless moved - @destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes') - - # Remove the given item from the itemStack. - # - # * `item` The item to remove. - # * `index` {Number} indicating the index to which to remove the item from the itemStack. - removeItemFromStack: (item) -> - index = @itemStack.indexOf(item) - @itemStack.splice(index, 1) unless index is -1 - - # Public: Move the given item to the given index. - # - # * `item` The item to move. - # * `index` {Number} indicating the index to which to move the item. - moveItem: (item, newIndex) -> - oldIndex = @items.indexOf(item) - @items.splice(oldIndex, 1) - @items.splice(newIndex, 0, item) - @emitter.emit 'did-move-item', {item, oldIndex, newIndex} - - # Public: Move the given item to the given index on another pane. - # - # * `item` The item to move. - # * `pane` {Pane} to which to move the item. - # * `index` {Number} indicating the index to which to move the item in the - # given pane. - moveItemToPane: (item, pane, index) -> - @removeItem(item, true) - pane.addItem(item, {index: index, moved: true}) - - # Public: Destroy the active item and activate the next item. - destroyActiveItem: -> - @destroyItem(@activeItem) - false - - # Public: Destroy the given item. - # - # If the item is active, the next item will be activated. If the item is the - # last item, the pane will be destroyed if the `core.destroyEmptyPanes` config - # setting is `true`. - # - # * `item` Item to destroy - # * `force` (optional) {Boolean} Destroy the item without prompting to save - # it, even if the item's `isPermanentDockItem` method returns true. - # - # Returns a {Promise} that resolves with a {Boolean} indicating whether or not - # the item was destroyed. - destroyItem: (item, force) -> - index = @items.indexOf(item) - if index isnt -1 - if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?() - return Promise.resolve(false) - - callback = => - if force or not item?.shouldPromptToSave?() - @removeItem(item, false) - item.destroy?() - true - else - @promptToSaveItem(item).then (result) => - if result - @removeItem(item, false) - item.destroy?() - result - - # In the case where there are no `onWillDestroyPaneItem` listeners, preserve the old behavior - # where `Pane.destroyItem` and callers such as `Pane.close` take effect synchronously. - return Promise.resolve(callback()) unless ( - @emitter.listenerCountForEventName('will-destroy-item') or - @container?.emitter.listenerCountForEventName('will-destroy-pane-item') - ) - - @emitter.emitAsync('will-destroy-item', {item, index}) - .then => @container?.willDestroyPaneItem({item, index, pane: this}) - .then(callback) - - # Public: Destroy all items. - destroyItems: -> - Promise.all( - @getItems().map((item) => @destroyItem(item)) - ) - - # Public: Destroy all items except for the active item. - destroyInactiveItems: -> - Promise.all( - @getItems() - .filter((item) => item isnt @activeItem) - .map((item) => @destroyItem(item)) - ) - - promptToSaveItem: (item, options={}) -> - return Promise.resolve(true) unless item.shouldPromptToSave?(options) - - if typeof item.getURI is 'function' - uri = item.getURI() - else if typeof item.getUri is 'function' - uri = item.getUri() - else - return Promise.resolve(true) - - saveDialog = (saveButtonText, saveFn, message) => - chosen = @applicationDelegate.confirm - message: message - detailedMessage: "Your changes will be lost if you close this item without saving." - buttons: [saveButtonText, "Cancel", "&Don't Save"] - switch chosen - when 0 - new Promise (resolve) -> - saveFn item, (error) -> - if error instanceof SaveCancelledError - resolve(false) - else - saveError(error).then(resolve) - when 1 - Promise.resolve(false) - when 2 - Promise.resolve(true) - - saveError = (error) => - if error - saveDialog("Save as", @saveItemAs, "'#{item.getTitle?() ? uri}' could not be saved.\nError: #{@getMessageForErrorCode(error.code)}") - else - Promise.resolve(true) - - saveDialog("Save", @saveItem, "'#{item.getTitle?() ? uri}' has changes, do you want to save them?") - - # Public: Save the active item. - saveActiveItem: (nextAction) -> - @saveItem(@getActiveItem(), nextAction) - - # Public: Prompt the user for a location and save the active item with the - # path they select. - # - # * `nextAction` (optional) {Function} which will be called after the item is - # successfully saved. - # - # Returns a {Promise} that resolves when the save is complete - saveActiveItemAs: (nextAction) -> - @saveItemAs(@getActiveItem(), nextAction) - - # Public: Save the given item. - # - # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called with no argument - # after the item is successfully saved, or with the error if it failed. - # The return value will be that of `nextAction` or `undefined` if it was not - # provided - # - # Returns a {Promise} that resolves when the save is complete - saveItem: (item, nextAction) => - if typeof item?.getURI is 'function' - itemURI = item.getURI() - else if typeof item?.getUri is 'function' - itemURI = item.getUri() - - if itemURI? - if item.save? - promisify -> item.save() - .then -> nextAction?() - .catch (error) => - if nextAction - nextAction(error) - else - @handleSaveError(error, item) - else - nextAction?() - else - @saveItemAs(item, nextAction) - - # Public: Prompt the user for a location and save the active item with the - # path they select. - # - # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called with no argument - # after the item is successfully saved, or with the error if it failed. - # The return value will be that of `nextAction` or `undefined` if it was not - # provided - saveItemAs: (item, nextAction) => - return unless item?.saveAs? - - saveOptions = item.getSaveDialogOptions?() ? {} - itemPath = item.getPath() - saveOptions.defaultPath ?= itemPath if itemPath - newItemPath = @applicationDelegate.showSaveDialog(saveOptions) - if newItemPath - promisify -> item.saveAs(newItemPath) - .then -> nextAction?() - .catch (error) => - if nextAction? - nextAction(error) - else - @handleSaveError(error, item) - else if nextAction? - nextAction(new SaveCancelledError('Save Cancelled')) - - # Public: Save all items. - saveItems: -> - for item in @getItems() - @saveItem(item) if item.isModified?() - return - - # Public: Return the first item that matches the given URI or undefined if - # none exists. - # - # * `uri` {String} containing a URI. - itemForURI: (uri) -> - find @items, (item) -> - if typeof item.getURI is 'function' - itemUri = item.getURI() - else if typeof item.getUri is 'function' - itemUri = item.getUri() - - itemUri is uri - - # Public: Activate the first item that matches the given URI. - # - # * `uri` {String} containing a URI. - # - # Returns a {Boolean} indicating whether an item matching the URI was found. - activateItemForURI: (uri) -> - if item = @itemForURI(uri) - @activateItem(item) - true - else - false - - copyActiveItem: -> - @activeItem?.copy?() - - ### - Section: Lifecycle - ### - - # Public: Determine whether the pane is active. - # - # Returns a {Boolean}. - isActive: -> - @container?.getActivePane() is this - - # Public: Makes this pane the *active* pane, causing it to gain focus. - activate: -> - throw new Error("Pane has been destroyed") if @isDestroyed() - @container?.didActivatePane(this) - @emitter.emit 'did-activate' - - # Public: Close the pane and destroy all its items. - # - # If this is the last pane, all the items will be destroyed but the pane - # itself will not be destroyed. - destroy: -> - if @container?.isAlive() and @container.getPanes().length is 1 - @destroyItems() - else - @emitter.emit 'will-destroy' - @alive = false - @container?.willDestroyPane(pane: this) - @container.activateNextPane() if @isActive() - @emitter.emit 'did-destroy' - @emitter.dispose() - item.destroy?() for item in @items.slice() - @container?.didDestroyPane(pane: this) - - isAlive: -> @alive - - # Public: Determine whether this pane has been destroyed. - # - # Returns a {Boolean}. - isDestroyed: -> not @isAlive() - - ### - Section: Splitting - ### - - # Public: Create a new pane to the left of this pane. - # - # * `params` (optional) {Object} with the following keys: - # * `items` (optional) {Array} of items to add to the new pane. - # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane - # - # Returns the new {Pane}. - splitLeft: (params) -> - @split('horizontal', 'before', params) - - # Public: Create a new pane to the right of this pane. - # - # * `params` (optional) {Object} with the following keys: - # * `items` (optional) {Array} of items to add to the new pane. - # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane - # - # Returns the new {Pane}. - splitRight: (params) -> - @split('horizontal', 'after', params) - - # Public: Creates a new pane above the receiver. - # - # * `params` (optional) {Object} with the following keys: - # * `items` (optional) {Array} of items to add to the new pane. - # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane - # - # Returns the new {Pane}. - splitUp: (params) -> - @split('vertical', 'before', params) - - # Public: Creates a new pane below the receiver. - # - # * `params` (optional) {Object} with the following keys: - # * `items` (optional) {Array} of items to add to the new pane. - # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane - # - # Returns the new {Pane}. - splitDown: (params) -> - @split('vertical', 'after', params) - - split: (orientation, side, params) -> - if params?.copyActiveItem - params.items ?= [] - params.items.push(@copyActiveItem()) - - if @parent.orientation isnt orientation - @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale}, @viewRegistry)) - @setFlexScale(1) - - newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config, @viewRegistry}, params)) - switch side - when 'before' then @parent.insertChildBefore(this, newPane) - when 'after' then @parent.insertChildAfter(this, newPane) - - @moveItemToPane(@activeItem, newPane) if params?.moveActiveItem and @activeItem - - newPane.activate() - newPane - - # If the parent is a horizontal axis, returns its first child if it is a pane; - # otherwise returns this pane. - findLeftmostSibling: -> - if @parent.orientation is 'horizontal' - [leftmostSibling] = @parent.children - if leftmostSibling instanceof PaneAxis - this - else - leftmostSibling - else - this - - findRightmostSibling: -> - if @parent.orientation is 'horizontal' - rightmostSibling = last(@parent.children) - if rightmostSibling instanceof PaneAxis - this - else - rightmostSibling - else - this - - # If the parent is a horizontal axis, returns its last child if it is a pane; - # otherwise returns a new pane created by splitting this pane rightward. - findOrCreateRightmostSibling: -> - rightmostSibling = @findRightmostSibling() - if rightmostSibling is this then @splitRight() else rightmostSibling - - # If the parent is a vertical axis, returns its first child if it is a pane; - # otherwise returns this pane. - findTopmostSibling: -> - if @parent.orientation is 'vertical' - [topmostSibling] = @parent.children - if topmostSibling instanceof PaneAxis - this - else - topmostSibling - else - this - - findBottommostSibling: -> - if @parent.orientation is 'vertical' - bottommostSibling = last(@parent.children) - if bottommostSibling instanceof PaneAxis - this - else - bottommostSibling - else - this - - # If the parent is a vertical axis, returns its last child if it is a pane; - # otherwise returns a new pane created by splitting this pane bottomward. - findOrCreateBottommostSibling: -> - bottommostSibling = @findBottommostSibling() - if bottommostSibling is this then @splitDown() else bottommostSibling - - # Private: Close the pane unless the user cancels the action via a dialog. - # - # Returns a {Promise} that resolves once the pane is either closed, or the - # closing has been cancelled. - close: -> - Promise.all(@getItems().map((item) => @promptToSaveItem(item))).then (results) => - @destroy() unless results.includes(false) - - handleSaveError: (error, item) -> - itemPath = error.path ? item?.getPath?() - addWarningWithPath = (message, options) => - message = "#{message} '#{itemPath}'" if itemPath - @notificationManager.addWarning(message, options) - - customMessage = @getMessageForErrorCode(error.code) - if customMessage? - addWarningWithPath("Unable to save file: #{customMessage}") - else if error.code is 'EISDIR' or error.message?.endsWith?('is a directory') - @notificationManager.addWarning("Unable to save file: #{error.message}") - else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST', 'ELOOP', 'EAGAIN'] - addWarningWithPath('Unable to save file', detail: error.message) - else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) - fileName = errorMatch[1] - @notificationManager.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to") - else - throw error - - getMessageForErrorCode: (errorCode) -> - switch errorCode - when 'EACCES' then 'Permission denied' - when 'ECONNRESET' then 'Connection reset' - when 'EINTR' then 'Interrupted system call' - when 'EIO' then 'I/O error writing file' - when 'ENOSPC' then 'No space left on device' - when 'ENOTSUP' then 'Operation not supported on socket' - when 'ENXIO' then 'No such device or address' - when 'EROFS' then 'Read-only file system' - when 'ESPIPE' then 'Invalid seek' - when 'ETIMEDOUT' then 'Connection timed out' - -promisify = (callback) -> - try - Promise.resolve(callback()) - catch error - Promise.reject(error) diff --git a/src/pane.js b/src/pane.js new file mode 100644 index 000000000..f56c1c88b --- /dev/null +++ b/src/pane.js @@ -0,0 +1,1251 @@ +const Grim = require('grim') +const {CompositeDisposable, Emitter} = require('event-kit') +const PaneAxis = require('./pane-axis') +const TextEditor = require('./text-editor') +const PaneElement = require('./pane-element') + +let nextInstanceId = 1 + +class SaveCancelledError extends Error {} + +// Extended: A container for presenting content in the center of the workspace. +// Panes can contain multiple items, one of which is *active* at a given time. +// The view corresponding to the active item is displayed in the interface. In +// the default configuration, tabs are also displayed for each item. +// +// Each pane may also contain one *pending* item. When a pending item is added +// to a pane, it will replace the currently pending item, if any, instead of +// simply being added. In the default configuration, the text in the tab for +// pending items is shown in italics. +module.exports = +class Pane { + inspect () { + return `Pane ${this.id}` + } + + static deserialize (state, {deserializers, applicationDelegate, config, notifications, views}) { + const {activeItemIndex} = state + const activeItemURI = state.activeItemURI || state.activeItemUri + + const items = [] + for (const itemState of state.items) { + const item = deserializers.deserialize(itemState) + if (item) items.push(item) + } + state.items = items + + state.activeItem = items[activeItemIndex] + if (!state.activeItem && activeItemURI) { + state.activeItem = state.items.find((item) => + typeof item.getURI === 'function' && item.getURI() === activeItemURI + ) + } + + return new Pane(Object.assign(state, { + deserializerManager: deserializers, + notificationManager: notifications, + viewRegistry: views, + config, + applicationDelegate + })) + } + + constructor (params = {}) { + this.setPendingItem = this.setPendingItem.bind(this) + this.getPendingItem = this.getPendingItem.bind(this) + this.clearPendingItem = this.clearPendingItem.bind(this) + this.onItemDidTerminatePendingState = this.onItemDidTerminatePendingState.bind(this) + this.saveItem = this.saveItem.bind(this) + this.saveItemAs = this.saveItemAs.bind(this) + + this.id = params.id + if (this.id != null) { + nextInstanceId = Math.max(nextInstanceId, this.id + 1) + } else { + this.id = nextInstanceId++ + } + + this.activeItem = params.activeItem + this.focused = params.focused != null ? params.focused : false + this.applicationDelegate = params.applicationDelegate + this.notificationManager = params.notificationManager + this.config = params.config + this.deserializerManager = params.deserializerManager + this.viewRegistry = params.viewRegistry + + this.emitter = new Emitter() + this.alive = true + this.subscriptionsPerItem = new WeakMap() + this.items = [] + this.itemStack = [] + this.container = null + + this.addItems((params.items || []).filter(item => item)) + if (!this.getActiveItem()) this.setActiveItem(this.items[0]) + this.addItemsToStack(params.itemStackIndices || []) + this.setFlexScale(params.flexScale || 1) + } + + getElement () { + if (!this.element) { + this.element = new PaneElement().initialize( + this, + {views: this.viewRegistry, applicationDelegate: this.applicationDelegate} + ) + } + return this.element + } + + serialize () { + const itemsToBeSerialized = this.items.filter(item => item && typeof item.serialize === 'function') + + const itemStackIndices = [] + for (const item of this.itemStack) { + if (typeof item.serialize === 'function') { + itemStackIndices.push(itemsToBeSerialized.indexOf(item)) + } + } + + const activeItemIndex = itemsToBeSerialized.indexOf(this.activeItem) + + return { + deserializer: 'Pane', + id: this.id, + items: itemsToBeSerialized.map(item => item.serialize()), + itemStackIndices, + activeItemIndex, + focused: this.focused, + flexScale: this.flexScale + } + } + + getParent () { return this.parent } + + setParent (parent) { + this.parent = parent + } + + getContainer () { return this.container } + + setContainer (container) { + if (container && container !== this.container) { + this.container = container + container.didAddPane({pane: this}) + } + } + + // Private: Determine whether the given item is allowed to exist in this pane. + // + // * `item` the Item + // + // Returns a {Boolean}. + isItemAllowed (item) { + if (typeof item.getAllowedLocations !== 'function') { + return true + } else { + return item.getAllowedLocations().includes(this.getContainer().getLocation()) + } + } + + setFlexScale (flexScale) { + this.flexScale = flexScale + this.emitter.emit('did-change-flex-scale', this.flexScale) + return this.flexScale + } + + getFlexScale () { return this.flexScale } + + increaseSize () { this.setFlexScale(this.getFlexScale() * 1.1) } + + decreaseSize () { this.setFlexScale(this.getFlexScale() / 1.1) } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when the pane resizes + // + // The callback will be invoked when pane's flexScale property changes. + // Use {::getFlexScale} to get the current value. + // + // * `callback` {Function} to be called when the pane is resized + // * `flexScale` {Number} representing the panes `flex-grow`; ability for a + // flex item to grow if necessary. + // + // Returns a {Disposable} on which '.dispose()' can be called to unsubscribe. + onDidChangeFlexScale (callback) { + return this.emitter.on('did-change-flex-scale', callback) + } + + // Public: Invoke the given callback with the current and future values of + // {::getFlexScale}. + // + // * `callback` {Function} to be called with the current and future values of + // the {::getFlexScale} property. + // * `flexScale` {Number} representing the panes `flex-grow`; ability for a + // flex item to grow if necessary. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeFlexScale (callback) { + callback(this.flexScale) + return this.onDidChangeFlexScale(callback) + } + + // Public: Invoke the given callback when the pane is activated. + // + // The given callback will be invoked whenever {::activate} is called on the + // pane, even if it is already active at the time. + // + // * `callback` {Function} to be called when the pane is activated. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidActivate (callback) { + return this.emitter.on('did-activate', callback) + } + + // Public: Invoke the given callback before the pane is destroyed. + // + // * `callback` {Function} to be called before the pane is destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillDestroy (callback) { + return this.emitter.on('will-destroy', callback) + } + + // Public: Invoke the given callback when the pane is destroyed. + // + // * `callback` {Function} to be called when the pane is destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + // Public: Invoke the given callback when the value of the {::isActive} + // property changes. + // + // * `callback` {Function} to be called when the value of the {::isActive} + // property changes. + // * `active` {Boolean} indicating whether the pane is active. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActive (callback) { + return this.container.onDidChangeActivePane(activePane => { + const isActive = this === activePane + callback(isActive) + }) + } + + // Public: Invoke the given callback with the current and future values of the + // {::isActive} property. + // + // * `callback` {Function} to be called with the current and future values of + // the {::isActive} property. + // * `active` {Boolean} indicating whether the pane is active. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActive (callback) { + callback(this.isActive()) + return this.onDidChangeActive(callback) + } + + // Public: Invoke the given callback when an item is added to the pane. + // + // * `callback` {Function} to be called with when items are added. + // * `event` {Object} with the following keys: + // * `item` The added pane item. + // * `index` {Number} indicating where the item is located. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddItem (callback) { + return this.emitter.on('did-add-item', callback) + } + + // Public: Invoke the given callback when an item is removed from the pane. + // + // * `callback` {Function} to be called with when items are removed. + // * `event` {Object} with the following keys: + // * `item` The removed pane item. + // * `index` {Number} indicating where the item was located. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveItem (callback) { + return this.emitter.on('did-remove-item', callback) + } + + // Public: Invoke the given callback before an item is removed from the pane. + // + // * `callback` {Function} to be called with when items are removed. + // * `event` {Object} with the following keys: + // * `item` The pane item to be removed. + // * `index` {Number} indicating where the item is located. + onWillRemoveItem (callback) { + return this.emitter.on('will-remove-item', callback) + } + + // Public: Invoke the given callback when an item is moved within the pane. + // + // * `callback` {Function} to be called with when items are moved. + // * `event` {Object} with the following keys: + // * `item` The removed pane item. + // * `oldIndex` {Number} indicating where the item was located. + // * `newIndex` {Number} indicating where the item is now located. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidMoveItem (callback) { + return this.emitter.on('did-move-item', callback) + } + + // Public: Invoke the given callback with all current and future items. + // + // * `callback` {Function} to be called with current and future items. + // * `item` An item that is present in {::getItems} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeItems (callback) { + for (let item of this.getItems()) { + callback(item) + } + return this.onDidAddItem(({item}) => callback(item)) + } + + // Public: Invoke the given callback when the value of {::getActiveItem} + // changes. + // + // * `callback` {Function} to be called with when the active item changes. + // * `activeItem` The current active item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActiveItem (callback) { + return this.emitter.on('did-change-active-item', callback) + } + + // Public: Invoke the given callback when {::activateNextRecentlyUsedItem} + // has been called, either initiating or continuing a forward MRU traversal of + // pane items. + // + // * `callback` {Function} to be called with when the active item changes. + // * `nextRecentlyUsedItem` The next MRU item, now being set active + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onChooseNextMRUItem (callback) { + return this.emitter.on('choose-next-mru-item', callback) + } + + // Public: Invoke the given callback when {::activatePreviousRecentlyUsedItem} + // has been called, either initiating or continuing a reverse MRU traversal of + // pane items. + // + // * `callback` {Function} to be called with when the active item changes. + // * `previousRecentlyUsedItem` The previous MRU item, now being set active + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onChooseLastMRUItem (callback) { + return this.emitter.on('choose-last-mru-item', callback) + } + + // Public: Invoke the given callback when {::moveActiveItemToTopOfStack} + // has been called, terminating an MRU traversal of pane items and moving the + // current active item to the top of the stack. Typically bound to a modifier + // (e.g. CTRL) key up event. + // + // * `callback` {Function} to be called with when the MRU traversal is done. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDoneChoosingMRUItem (callback) { + return this.emitter.on('done-choosing-mru-item', callback) + } + + // Public: Invoke the given callback with the current and future values of + // {::getActiveItem}. + // + // * `callback` {Function} to be called with the current and future active + // items. + // * `activeItem` The current active item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActiveItem (callback) { + callback(this.getActiveItem()) + return this.onDidChangeActiveItem(callback) + } + + // Public: Invoke the given callback before items are destroyed. + // + // * `callback` {Function} to be called before items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The item that will be destroyed. + // * `index` The location of the item. + // + // Returns a {Disposable} on which `.dispose()` can be called to + // unsubscribe. + onWillDestroyItem (callback) { + return this.emitter.on('will-destroy-item', callback) + } + + // Called by the view layer to indicate that the pane has gained focus. + focus () { + this.focused = true + return this.activate() + } + + // Called by the view layer to indicate that the pane has lost focus. + blur () { + this.focused = false + return true // if this is called from an event handler, don't cancel it + } + + isFocused () { return this.focused } + + getPanes () { return [this] } + + unsubscribeFromItem (item) { + const subscription = this.subscriptionsPerItem.get(item) + if (subscription) { + subscription.dispose() + this.subscriptionsPerItem.delete(item) + } + } + + /* + Section: Items + */ + + // Public: Get the items in this pane. + // + // Returns an {Array} of items. + getItems () { + return this.items.slice() + } + + // Public: Get the active pane item in this pane. + // + // Returns a pane item. + getActiveItem () { return this.activeItem } + + setActiveItem (activeItem, options) { + const modifyStack = options && options.modifyStack + if (activeItem !== this.activeItem) { + if (modifyStack !== false) this.addItemToStack(activeItem) + this.activeItem = activeItem + this.emitter.emit('did-change-active-item', this.activeItem) + if (this.container) this.container.didChangeActiveItemOnPane(this, this.activeItem) + } + return this.activeItem + } + + // Build the itemStack after deserializing + addItemsToStack (itemStackIndices) { + if (this.items.length > 0) { + if (itemStackIndices.length !== this.items.length || itemStackIndices.includes(-1)) { + itemStackIndices = this.items.map((item, i) => i) + } + + for (let itemIndex of itemStackIndices) { + this.addItemToStack(this.items[itemIndex]) + } + } + } + + // Add item (or move item) to the end of the itemStack + addItemToStack (newItem) { + if (newItem == null) { return } + const index = this.itemStack.indexOf(newItem) + if (index !== -1) this.itemStack.splice(index, 1) + return this.itemStack.push(newItem) + } + + // Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise. + getActiveEditor () { + if (this.activeItem instanceof TextEditor) return this.activeItem + } + + // Public: Return the item at the given index. + // + // * `index` {Number} + // + // Returns an item or `null` if no item exists at the given index. + itemAtIndex (index) { + return this.items[index] + } + + // Makes the next item in the itemStack active. + activateNextRecentlyUsedItem () { + if (this.items.length > 1) { + if (this.itemStackIndex == null) this.itemStackIndex = this.itemStack.length - 1 + if (this.itemStackIndex === 0) this.itemStackIndex = this.itemStack.length + this.itemStackIndex-- + const nextRecentlyUsedItem = this.itemStack[this.itemStackIndex] + this.emitter.emit('choose-next-mru-item', nextRecentlyUsedItem) + this.setActiveItem(nextRecentlyUsedItem, {modifyStack: false}) + } + } + + // Makes the previous item in the itemStack active. + activatePreviousRecentlyUsedItem () { + if (this.items.length > 1) { + if (this.itemStackIndex + 1 === this.itemStack.length || this.itemStackIndex == null) { + this.itemStackIndex = -1 + } + this.itemStackIndex++ + const previousRecentlyUsedItem = this.itemStack[this.itemStackIndex] + this.emitter.emit('choose-last-mru-item', previousRecentlyUsedItem) + this.setActiveItem(previousRecentlyUsedItem, {modifyStack: false}) + } + } + + // Moves the active item to the end of the itemStack once the ctrl key is lifted + moveActiveItemToTopOfStack () { + delete this.itemStackIndex + this.addItemToStack(this.activeItem) + this.emitter.emit('done-choosing-mru-item') + } + + // Public: Makes the next item active. + activateNextItem () { + const index = this.getActiveItemIndex() + if (index < (this.items.length - 1)) { + this.activateItemAtIndex(index + 1) + } else { + this.activateItemAtIndex(0) + } + } + + // Public: Makes the previous item active. + activatePreviousItem () { + const index = this.getActiveItemIndex() + if (index > 0) { + this.activateItemAtIndex(index - 1) + } else { + this.activateItemAtIndex(this.items.length - 1) + } + } + + activateLastItem () { + this.activateItemAtIndex(this.items.length - 1) + } + + // Public: Move the active tab to the right. + moveItemRight () { + const index = this.getActiveItemIndex() + const rightItemIndex = index + 1 + if (rightItemIndex <= this.items.length - 1) this.moveItem(this.getActiveItem(), rightItemIndex) + } + + // Public: Move the active tab to the left + moveItemLeft () { + const index = this.getActiveItemIndex() + const leftItemIndex = index - 1 + if (leftItemIndex >= 0) return this.moveItem(this.getActiveItem(), leftItemIndex) + } + + // Public: Get the index of the active item. + // + // Returns a {Number}. + getActiveItemIndex () { + return this.items.indexOf(this.activeItem) + } + + // Public: Activate the item at the given index. + // + // * `index` {Number} + activateItemAtIndex (index) { + const item = this.itemAtIndex(index) || this.getActiveItem() + return this.setActiveItem(item) + } + + // Public: Make the given item *active*, causing it to be displayed by + // the pane's view. + // + // * `item` The item to activate + // * `options` (optional) {Object} + // * `pending` (optional) {Boolean} indicating that the item should be added + // in a pending state if it does not yet exist in the pane. Existing pending + // items in a pane are replaced with new pending items when they are opened. + activateItem (item, options = {}) { + if (item) { + const index = (this.getPendingItem() === this.activeItem) + ? this.getActiveItemIndex() + : this.getActiveItemIndex() + 1 + this.addItem(item, Object.assign({}, options, {index})) + this.setActiveItem(item) + } + } + + // Public: Add the given item to the pane. + // + // * `item` The item to add. It can be a model with an associated view or a + // view. + // * `options` (optional) {Object} + // * `index` (optional) {Number} indicating the index at which to add the item. + // If omitted, the item is added after the current active item. + // * `pending` (optional) {Boolean} indicating that the item should be + // added in a pending state. Existing pending items in a pane are replaced with + // new pending items when they are opened. + // + // Returns the added item. + addItem (item, options = {}) { + // Backward compat with old API: + // addItem(item, index=@getActiveItemIndex() + 1) + if (typeof options === 'number') { + Grim.deprecate(`Pane::addItem(item, ${options}) is deprecated in favor of Pane::addItem(item, {index: ${options}})`) + options = {index: options} + } + + const index = options.index != null ? options.index : this.getActiveItemIndex() + 1 + const moved = options.moved != null ? options.moved : false + const pending = options.pending != null ? options.pending : false + + if (!item || typeof item !== 'object') { + throw new Error(`Pane items must be objects. Attempted to add item ${item}.`) + } + + if (typeof item.isDestroyed === 'function' && item.isDestroyed()) { + throw new Error(`Adding a pane item with URI '${typeof item.getURI === 'function' && item.getURI()}' that has already been destroyed`) + } + + if (this.items.includes(item)) return + + if (typeof item.onDidDestroy === 'function') { + const itemSubscriptions = new CompositeDisposable() + itemSubscriptions.add(item.onDidDestroy(() => this.removeItem(item, false))) + if (typeof item.onDidTerminatePendingState === 'function') { + itemSubscriptions.add(item.onDidTerminatePendingState(() => { + if (this.getPendingItem() === item) this.clearPendingItem() + })) + } + this.subscriptionsPerItem.set(item, itemSubscriptions) + } + + this.items.splice(index, 0, item) + const lastPendingItem = this.getPendingItem() + const replacingPendingItem = lastPendingItem != null && !moved + if (replacingPendingItem) this.pendingItem = null + if (pending) this.setPendingItem(item) + + this.emitter.emit('did-add-item', {item, index, moved}) + if (!moved) { + if (this.container) this.container.didAddPaneItem(item, this, index) + } + + if (replacingPendingItem) this.destroyItem(lastPendingItem) + if (!this.getActiveItem()) this.setActiveItem(item) + return item + } + + setPendingItem (item) { + if (this.pendingItem !== item) { + const mostRecentPendingItem = this.pendingItem + this.pendingItem = item + if (mostRecentPendingItem) { + this.emitter.emit('item-did-terminate-pending-state', mostRecentPendingItem) + } + } + } + + getPendingItem () { + return this.pendingItem || null + } + + clearPendingItem () { + this.setPendingItem(null) + } + + onItemDidTerminatePendingState (callback) { + return this.emitter.on('item-did-terminate-pending-state', callback) + } + + // Public: Add the given items to the pane. + // + // * `items` An {Array} of items to add. Items can be views or models with + // associated views. Any objects that are already present in the pane's + // current items will not be added again. + // * `index` (optional) {Number} index at which to add the items. If omitted, + // the item is # added after the current active item. + // + // Returns an {Array} of added items. + addItems (items, index = this.getActiveItemIndex() + 1) { + items = items.filter(item => !this.items.includes(item)) + for (let i = 0; i < items.length; i++) { + const item = items[i] + this.addItem(item, {index: index + i}) + } + return items + } + + removeItem (item, moved) { + const index = this.items.indexOf(item) + if (index === -1) return + if (this.getPendingItem() === item) this.pendingItem = null + this.removeItemFromStack(item) + this.emitter.emit('will-remove-item', {item, index, destroyed: !moved, moved}) + this.unsubscribeFromItem(item) + + if (item === this.activeItem) { + if (this.items.length === 1) { + this.setActiveItem(undefined) + } else if (index === 0) { + this.activateNextItem() + } else { + this.activatePreviousItem() + } + } + this.items.splice(index, 1) + this.emitter.emit('did-remove-item', {item, index, destroyed: !moved, moved}) + if (!moved && this.container) this.container.didDestroyPaneItem({item, index, pane: this}) + if (this.items.length === 0 && this.config.get('core.destroyEmptyPanes')) this.destroy() + } + + // Remove the given item from the itemStack. + // + // * `item` The item to remove. + // * `index` {Number} indicating the index to which to remove the item from the itemStack. + removeItemFromStack (item) { + const index = this.itemStack.indexOf(item) + if (index !== -1) this.itemStack.splice(index, 1) + } + + // Public: Move the given item to the given index. + // + // * `item` The item to move. + // * `index` {Number} indicating the index to which to move the item. + moveItem (item, newIndex) { + const oldIndex = this.items.indexOf(item) + this.items.splice(oldIndex, 1) + this.items.splice(newIndex, 0, item) + this.emitter.emit('did-move-item', {item, oldIndex, newIndex}) + } + + // Public: Move the given item to the given index on another pane. + // + // * `item` The item to move. + // * `pane` {Pane} to which to move the item. + // * `index` {Number} indicating the index to which to move the item in the + // given pane. + moveItemToPane (item, pane, index) { + this.removeItem(item, true) + return pane.addItem(item, {index, moved: true}) + } + + // Public: Destroy the active item and activate the next item. + destroyActiveItem () { + this.destroyItem(this.activeItem) + return false + } + + // Public: Destroy the given item. + // + // If the item is active, the next item will be activated. If the item is the + // last item, the pane will be destroyed if the `core.destroyEmptyPanes` config + // setting is `true`. + // + // * `item` Item to destroy + // * `force` (optional) {Boolean} Destroy the item without prompting to save + // it, even if the item's `isPermanentDockItem` method returns true. + // + // Returns a {Promise} that resolves with a {Boolean} indicating whether or not + // the item was destroyed. + async destroyItem (item, force) { + const index = this.items.indexOf(item) + if (index === -1) return false + + if (!force && + typeof item.isPermanentDockItem === 'function' && item.isPermanentDockItem() && + (!this.container || this.container.getLocation() !== 'center')) { + return false + } + + // In the case where there are no `onWillDestroyPaneItem` listeners, preserve the old behavior + // where `Pane.destroyItem` and callers such as `Pane.close` take effect synchronously. + if (this.emitter.listenerCountForEventName('will-destroy-item') > 0) { + await this.emitter.emitAsync('will-destroy-item', {item, index}) + } + if (this.container && this.container.emitter.listenerCountForEventName('will-destroy-pane-item') > 0) { + await this.container.willDestroyPaneItem({item, index, pane: this}) + } + + if (!force && typeof item.shouldPromptToSave === 'function' && item.shouldPromptToSave()) { + if (!await this.promptToSaveItem(item)) return false + } + this.removeItem(item, false) + if (typeof item.destroy === 'function') item.destroy() + return true + } + + // Public: Destroy all items. + destroyItems () { + return Promise.all( + this.getItems().map(item => this.destroyItem(item)) + ) + } + + // Public: Destroy all items except for the active item. + destroyInactiveItems () { + return Promise.all( + this.getItems() + .filter(item => item !== this.activeItem) + .map(item => this.destroyItem(item)) + ) + } + + promptToSaveItem (item, options = {}) { + if (typeof item.shouldPromptToSave !== 'function' || !item.shouldPromptToSave(options)) { + return Promise.resolve(true) + } + + let uri + if (typeof item.getURI === 'function') { + uri = item.getURI() + } else if (typeof item.getUri === 'function') { + uri = item.getUri() + } else { + return Promise.resolve(true) + } + + const title = (typeof item.getTitle === 'function' && item.getTitle()) || uri + + const saveDialog = (saveButtonText, saveFn, message) => { + const chosen = this.applicationDelegate.confirm({ + message, + detailedMessage: 'Your changes will be lost if you close this item without saving.', + buttons: [saveButtonText, 'Cancel', "&Don't Save"]} + ) + + switch (chosen) { + case 0: + return new Promise(resolve => { + return saveFn(item, error => { + if (error instanceof SaveCancelledError) { + resolve(false) + } else if (error) { + saveDialog( + 'Save as', + this.saveItemAs, + `'${title}' could not be saved.\nError: ${this.getMessageForErrorCode(error.code)}` + ).then(resolve) + } else { + resolve(true) + } + }) + }) + case 1: + return Promise.resolve(false) + case 2: + return Promise.resolve(true) + } + } + + return saveDialog( + 'Save', + this.saveItem, + `'${title}' has changes, do you want to save them?` + ) + } + + // Public: Save the active item. + saveActiveItem (nextAction) { + return this.saveItem(this.getActiveItem(), nextAction) + } + + // Public: Prompt the user for a location and save the active item with the + // path they select. + // + // * `nextAction` (optional) {Function} which will be called after the item is + // successfully saved. + // + // Returns a {Promise} that resolves when the save is complete + saveActiveItemAs (nextAction) { + return this.saveItemAs(this.getActiveItem(), nextAction) + } + + // Public: Save the given item. + // + // * `item` The item to save. + // * `nextAction` (optional) {Function} which will be called with no argument + // after the item is successfully saved, or with the error if it failed. + // The return value will be that of `nextAction` or `undefined` if it was not + // provided + // + // Returns a {Promise} that resolves when the save is complete + saveItem (item, nextAction) { + if (!item) return Promise.resolve() + + let itemURI + if (typeof item.getURI === 'function') { + itemURI = item.getURI() + } else if (typeof item.getUri === 'function') { + itemURI = item.getUri() + } + + if (itemURI != null) { + if (typeof item.save === 'function') { + return promisify(() => item.save()) + .then(() => { + if (nextAction) nextAction() + }) + .catch(error => { + if (nextAction) { + nextAction(error) + } else { + this.handleSaveError(error, item) + } + }) + } else if (nextAction) { + nextAction() + return Promise.resolve() + } + } else { + return this.saveItemAs(item, nextAction) + } + } + + // Public: Prompt the user for a location and save the active item with the + // path they select. + // + // * `item` The item to save. + // * `nextAction` (optional) {Function} which will be called with no argument + // after the item is successfully saved, or with the error if it failed. + // The return value will be that of `nextAction` or `undefined` if it was not + // provided + saveItemAs (item, nextAction) { + if (!item) return + if (typeof item.saveAs !== 'function') return + + const saveOptions = typeof item.getSaveDialogOptions === 'function' + ? item.getSaveDialogOptions() + : {} + + const itemPath = item.getPath() + if (itemPath && !saveOptions.defaultPath) saveOptions.defaultPath = itemPath + + const newItemPath = this.applicationDelegate.showSaveDialog(saveOptions) + if (newItemPath) { + return promisify(() => item.saveAs(newItemPath)) + .then(() => { + if (nextAction) nextAction() + }) + .catch(error => { + if (nextAction) { + nextAction(error) + } else { + this.handleSaveError(error, item) + } + }) + } else if (nextAction) { + return nextAction(new SaveCancelledError('Save Cancelled')) + } + } + + // Public: Save all items. + saveItems () { + for (let item of this.getItems()) { + if (typeof item.isModified === 'function' && item.isModified()) { + this.saveItem(item) + } + } + } + + // Public: Return the first item that matches the given URI or undefined if + // none exists. + // + // * `uri` {String} containing a URI. + itemForURI (uri) { + return this.items.find(item => { + if (typeof item.getURI === 'function') { + return item.getURI() === uri + } else if (typeof item.getUri === 'function') { + return item.getUri() === uri + } + }) + } + + // Public: Activate the first item that matches the given URI. + // + // * `uri` {String} containing a URI. + // + // Returns a {Boolean} indicating whether an item matching the URI was found. + activateItemForURI (uri) { + const item = this.itemForURI(uri) + if (item) { + this.activateItem(item) + return true + } else { + return false + } + } + + copyActiveItem () { + if (this.activeItem && typeof this.activeItem.copy === 'function') { + return this.activeItem.copy() + } + } + + /* + Section: Lifecycle + */ + + // Public: Determine whether the pane is active. + // + // Returns a {Boolean}. + isActive () { + return this.container && this.container.getActivePane() === this + } + + // Public: Makes this pane the *active* pane, causing it to gain focus. + activate () { + if (this.isDestroyed()) throw new Error('Pane has been destroyed') + if (this.container) this.container.didActivatePane(this) + this.emitter.emit('did-activate') + } + + // Public: Close the pane and destroy all its items. + // + // If this is the last pane, all the items will be destroyed but the pane + // itself will not be destroyed. + destroy () { + if (this.container && this.container.isAlive() && this.container.getPanes().length === 1) { + return this.destroyItems() + } + + this.emitter.emit('will-destroy') + this.alive = false + if (this.container) { + this.container.willDestroyPane({pane: this}) + if (this.isActive()) this.container.activateNextPane() + } + this.emitter.emit('did-destroy') + this.emitter.dispose() + for (let item of this.items.slice()) { + if (typeof item.destroy === 'function') item.destroy() + } + if (this.container) this.container.didDestroyPane({pane: this}) + } + + isAlive () { return this.alive } + + // Public: Determine whether this pane has been destroyed. + // + // Returns a {Boolean}. + isDestroyed () { return !this.isAlive() } + + /* + Section: Splitting + */ + + // Public: Create a new pane to the left of this pane. + // + // * `params` (optional) {Object} with the following keys: + // * `items` (optional) {Array} of items to add to the new pane. + // * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane + // + // Returns the new {Pane}. + splitLeft (params) { + return this.split('horizontal', 'before', params) + } + + // Public: Create a new pane to the right of this pane. + // + // * `params` (optional) {Object} with the following keys: + // * `items` (optional) {Array} of items to add to the new pane. + // * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane + // + // Returns the new {Pane}. + splitRight (params) { + return this.split('horizontal', 'after', params) + } + + // Public: Creates a new pane above the receiver. + // + // * `params` (optional) {Object} with the following keys: + // * `items` (optional) {Array} of items to add to the new pane. + // * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane + // + // Returns the new {Pane}. + splitUp (params) { + return this.split('vertical', 'before', params) + } + + // Public: Creates a new pane below the receiver. + // + // * `params` (optional) {Object} with the following keys: + // * `items` (optional) {Array} of items to add to the new pane. + // * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane + // + // Returns the new {Pane}. + splitDown (params) { + return this.split('vertical', 'after', params) + } + + split (orientation, side, params) { + if (params && params.copyActiveItem) { + if (!params.items) params.items = [] + params.items.push(this.copyActiveItem()) + } + + if (this.parent.orientation !== orientation) { + this.parent.replaceChild(this, new PaneAxis({ + container: this.container, + orientation, + children: [this], + flexScale: this.flexScale}, + this.viewRegistry + )) + this.setFlexScale(1) + } + + const newPane = new Pane(Object.assign({ + applicationDelegate: this.applicationDelegate, + notificationManager: this.notificationManager, + deserializerManager: this.deserializerManager, + config: this.config, + viewRegistry: this.viewRegistry + }, params)) + + switch (side) { + case 'before': this.parent.insertChildBefore(this, newPane); break + case 'after': this.parent.insertChildAfter(this, newPane); break + } + + if (params && params.moveActiveItem && this.activeItem) this.moveItemToPane(this.activeItem, newPane) + + newPane.activate() + return newPane + } + + // If the parent is a horizontal axis, returns its first child if it is a pane; + // otherwise returns this pane. + findLeftmostSibling () { + if (this.parent.orientation === 'horizontal') { + const [leftmostSibling] = this.parent.children + if (leftmostSibling instanceof PaneAxis) { + return this + } else { + return leftmostSibling + } + } else { + return this + } + } + + findRightmostSibling () { + if (this.parent.orientation === 'horizontal') { + const rightmostSibling = this.parent.children[this.parent.children.length - 1] + if (rightmostSibling instanceof PaneAxis) { + return this + } else { + return rightmostSibling + } + } else { + return this + } + } + + // If the parent is a horizontal axis, returns its last child if it is a pane; + // otherwise returns a new pane created by splitting this pane rightward. + findOrCreateRightmostSibling () { + const rightmostSibling = this.findRightmostSibling() + if (rightmostSibling === this) { + return this.splitRight() + } else { + return rightmostSibling + } + } + + // If the parent is a vertical axis, returns its first child if it is a pane; + // otherwise returns this pane. + findTopmostSibling () { + if (this.parent.orientation === 'vertical') { + const [topmostSibling] = this.parent.children + if (topmostSibling instanceof PaneAxis) { + return this + } else { + return topmostSibling + } + } else { + return this + } + } + + findBottommostSibling () { + if (this.parent.orientation === 'vertical') { + const bottommostSibling = this.parent.children[this.parent.children.length - 1] + if (bottommostSibling instanceof PaneAxis) { + return this + } else { + return bottommostSibling + } + } else { + return this + } + } + + // If the parent is a vertical axis, returns its last child if it is a pane; + // otherwise returns a new pane created by splitting this pane bottomward. + findOrCreateBottommostSibling () { + const bottommostSibling = this.findBottommostSibling() + if (bottommostSibling === this) { + return this.splitDown() + } else { + return bottommostSibling + } + } + + // Private: Close the pane unless the user cancels the action via a dialog. + // + // Returns a {Promise} that resolves once the pane is either closed, or the + // closing has been cancelled. + close () { + return Promise.all(this.getItems().map(item => this.promptToSaveItem(item))) + .then(results => { + if (!results.includes(false)) return this.destroy() + }) + } + + handleSaveError (error, item) { + const itemPath = error.path || (typeof item.getPath === 'function' && item.getPath()) + const addWarningWithPath = (message, options) => { + if (itemPath) message = `${message} '${itemPath}'` + this.notificationManager.addWarning(message, options) + } + + const customMessage = this.getMessageForErrorCode(error.code) + if (customMessage != null) { + addWarningWithPath(`Unable to save file: ${customMessage}`) + } else if (error.code === 'EISDIR' || (error.message && error.message.endsWith('is a directory'))) { + return this.notificationManager.addWarning(`Unable to save file: ${error.message}`) + } else if (['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST', 'ELOOP', 'EAGAIN'].includes(error.code)) { + addWarningWithPath('Unable to save file', {detail: error.message}) + } else { + const errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message) + if (errorMatch) { + const fileName = errorMatch[1] + this.notificationManager.addWarning(`Unable to save file: A directory in the path '${fileName}' could not be written to`) + } else { + throw error + } + } + } + + getMessageForErrorCode (errorCode) { + switch (errorCode) { + case 'EACCES': return 'Permission denied' + case 'ECONNRESET': return 'Connection reset' + case 'EINTR': return 'Interrupted system call' + case 'EIO': return 'I/O error writing file' + case 'ENOSPC': return 'No space left on device' + case 'ENOTSUP': return 'Operation not supported on socket' + case 'ENXIO': return 'No such device or address' + case 'EROFS': return 'Read-only file system' + case 'ESPIPE': return 'Invalid seek' + case 'ETIMEDOUT': return 'Connection timed out' + } + } +} + +function promisify (callback) { + try { + return Promise.resolve(callback()) + } catch (error) { + return Promise.reject(error) + } +} From 666adb2f0e66517fb585a75eded0cc347930607e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 18 Sep 2017 17:02:31 -0700 Subject: [PATCH 155/448] Convert pane-container-spec to JS --- spec/pane-container-spec.coffee | 409 --------------------------- spec/pane-container-spec.js | 472 ++++++++++++++++++++++++++++++++ 2 files changed, 472 insertions(+), 409 deletions(-) delete mode 100644 spec/pane-container-spec.coffee create mode 100644 spec/pane-container-spec.js diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee deleted file mode 100644 index 1fa113b29..000000000 --- a/spec/pane-container-spec.coffee +++ /dev/null @@ -1,409 +0,0 @@ -PaneContainer = require '../src/pane-container' -Pane = require '../src/pane' - -describe "PaneContainer", -> - [confirm, params] = [] - - beforeEach -> - confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0) - params = { - location: 'center', - config: atom.config, - deserializerManager: atom.deserializers - applicationDelegate: atom.applicationDelegate, - viewRegistry: atom.views - } - - describe "serialization", -> - [containerA, pane1A, pane2A, pane3A] = [] - - beforeEach -> - # This is a dummy item to prevent panes from being empty on deserialization - class Item - atom.deserializers.add(this) - @deserialize: -> new this - serialize: -> deserializer: 'Item' - - containerA = new PaneContainer(params) - pane1A = containerA.getActivePane() - pane1A.addItem(new Item) - pane2A = pane1A.splitRight(items: [new Item]) - pane3A = pane2A.splitDown(items: [new Item]) - pane3A.focus() - - it "preserves the focused pane across serialization", -> - expect(pane3A.focused).toBe true - - containerB = new PaneContainer(params) - containerB.deserialize(containerA.serialize(), atom.deserializers) - [pane1B, pane2B, pane3B] = containerB.getPanes() - expect(pane3B.focused).toBe true - - it "preserves the active pane across serialization, independent of focus", -> - pane3A.activate() - expect(containerA.getActivePane()).toBe pane3A - - containerB = new PaneContainer(params) - containerB.deserialize(containerA.serialize(), atom.deserializers) - [pane1B, pane2B, pane3B] = containerB.getPanes() - expect(containerB.getActivePane()).toBe pane3B - - it "makes the first pane active if no pane exists for the activePaneId", -> - pane3A.activate() - state = containerA.serialize() - state.activePaneId = -22 - containerB = new PaneContainer(params) - containerB.deserialize(state, atom.deserializers) - expect(containerB.getActivePane()).toBe containerB.getPanes()[0] - - describe "if there are empty panes after deserialization", -> - beforeEach -> - pane3A.getItems()[0].serialize = -> deserializer: 'Bogus' - - describe "if the 'core.destroyEmptyPanes' config option is false (the default)", -> - it "leaves the empty panes intact", -> - state = containerA.serialize() - containerB = new PaneContainer(params) - containerB.deserialize(state, atom.deserializers) - [leftPane, column] = containerB.getRoot().getChildren() - [topPane, bottomPane] = column.getChildren() - - expect(leftPane.getItems().length).toBe 1 - expect(topPane.getItems().length).toBe 1 - expect(bottomPane.getItems().length).toBe 0 - - describe "if the 'core.destroyEmptyPanes' config option is true", -> - it "removes empty panes on deserialization", -> - atom.config.set('core.destroyEmptyPanes', true) - - state = containerA.serialize() - containerB = new PaneContainer(params) - containerB.deserialize(state, atom.deserializers) - [leftPane, rightPane] = containerB.getRoot().getChildren() - - expect(leftPane.getItems().length).toBe 1 - expect(rightPane.getItems().length).toBe 1 - - it "does not allow the root pane to be destroyed", -> - container = new PaneContainer(params) - container.getRoot().destroy() - expect(container.getRoot()).toBeDefined() - expect(container.getRoot().isDestroyed()).toBe false - - describe "::getActivePane()", -> - [container, pane1, pane2] = [] - - beforeEach -> - container = new PaneContainer(params) - pane1 = container.getRoot() - - it "returns the first pane if no pane has been made active", -> - expect(container.getActivePane()).toBe pane1 - expect(pane1.isActive()).toBe true - - it "returns the most pane on which ::activate() was most recently called", -> - pane2 = pane1.splitRight() - pane2.activate() - expect(container.getActivePane()).toBe pane2 - expect(pane1.isActive()).toBe false - expect(pane2.isActive()).toBe true - pane1.activate() - expect(container.getActivePane()).toBe pane1 - expect(pane1.isActive()).toBe true - expect(pane2.isActive()).toBe false - - it "returns the next pane if the current active pane is destroyed", -> - pane2 = pane1.splitRight() - pane2.activate() - pane2.destroy() - expect(container.getActivePane()).toBe pane1 - expect(pane1.isActive()).toBe true - - describe "::onDidChangeActivePane()", -> - [container, pane1, pane2, observed] = [] - - beforeEach -> - container = new PaneContainer(params) - container.getRoot().addItems([new Object, new Object]) - container.getRoot().splitRight(items: [new Object, new Object]) - [pane1, pane2] = container.getPanes() - - observed = [] - container.onDidChangeActivePane (pane) -> observed.push(pane) - - it "invokes observers when the active pane changes", -> - pane1.activate() - pane2.activate() - expect(observed).toEqual [pane1, pane2] - - describe "::onDidChangeActivePaneItem()", -> - [container, pane1, pane2, observed] = [] - - beforeEach -> - container = new PaneContainer(params) - container.getRoot().addItems([new Object, new Object]) - container.getRoot().splitRight(items: [new Object, new Object]) - [pane1, pane2] = container.getPanes() - - observed = [] - container.onDidChangeActivePaneItem (item) -> observed.push(item) - - it "invokes observers when the active item of the active pane changes", -> - pane2.activateNextItem() - pane2.activateNextItem() - expect(observed).toEqual [pane2.itemAtIndex(1), pane2.itemAtIndex(0)] - - it "invokes observers when the active pane changes", -> - pane1.activate() - pane2.activate() - expect(observed).toEqual [pane1.itemAtIndex(0), pane2.itemAtIndex(0)] - - describe "::onDidStopChangingActivePaneItem()", -> - [container, pane1, pane2, observed] = [] - - beforeEach -> - container = new PaneContainer(params) - container.getRoot().addItems([new Object, new Object]) - container.getRoot().splitRight(items: [new Object, new Object]) - [pane1, pane2] = container.getPanes() - - observed = [] - container.onDidStopChangingActivePaneItem (item) -> observed.push(item) - - it "invokes observers once when the active item of the active pane changes", -> - pane2.activateNextItem() - pane2.activateNextItem() - expect(observed).toEqual [] - advanceClock 100 - expect(observed).toEqual [pane2.itemAtIndex(0)] - - it "invokes observers once when the active pane changes", -> - pane1.activate() - pane2.activate() - expect(observed).toEqual [] - advanceClock 100 - expect(observed).toEqual [pane2.itemAtIndex(0)] - - describe "::onDidActivatePane", -> - it "invokes observers when a pane is activated (even if it was already active)", -> - container = new PaneContainer(params) - container.getRoot().splitRight() - [pane1, pane2] = container.getPanes() - - activatedPanes = [] - container.onDidActivatePane (pane) -> activatedPanes.push(pane) - - pane1.activate() - pane1.activate() - pane2.activate() - pane2.activate() - expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2]) - - describe "::observePanes()", -> - it "invokes observers with all current and future panes", -> - container = new PaneContainer(params) - container.getRoot().splitRight() - [pane1, pane2] = container.getPanes() - - observed = [] - container.observePanes (pane) -> observed.push(pane) - - pane3 = pane2.splitDown() - pane4 = pane2.splitRight() - - expect(observed).toEqual [pane1, pane2, pane3, pane4] - - describe "::observePaneItems()", -> - it "invokes observers with all current and future pane items", -> - container = new PaneContainer(params) - container.getRoot().addItems([new Object, new Object]) - container.getRoot().splitRight(items: [new Object]) - [pane1, pane2] = container.getPanes() - observed = [] - container.observePaneItems (pane) -> observed.push(pane) - - pane3 = pane2.splitDown(items: [new Object]) - pane3.addItems([new Object, new Object]) - - expect(observed).toEqual container.getPaneItems() - - describe "::confirmClose()", -> - [container, pane1, pane2] = [] - - beforeEach -> - class TestItem - shouldPromptToSave: -> true - getURI: -> 'test' - - container = new PaneContainer(params) - container.getRoot().splitRight() - [pane1, pane2] = container.getPanes() - pane1.addItem(new TestItem) - pane2.addItem(new TestItem) - - it "returns true if the user saves all modified files when prompted", -> - confirm.andReturn(0) - waitsForPromise -> - container.confirmClose().then (saved) -> - expect(confirm).toHaveBeenCalled() - expect(saved).toBeTruthy() - - it "returns false if the user cancels saving any modified file", -> - confirm.andReturn(1) - waitsForPromise -> - container.confirmClose().then (saved) -> - expect(confirm).toHaveBeenCalled() - expect(saved).toBeFalsy() - - describe "::onDidAddPane(callback)", -> - it "invokes the given callback when panes are added", -> - container = new PaneContainer(params) - events = [] - container.onDidAddPane (event) -> - expect(event.pane in container.getPanes()).toBe true - events.push(event) - - pane1 = container.getActivePane() - pane2 = pane1.splitRight() - pane3 = pane2.splitDown() - - expect(events).toEqual [{pane: pane2}, {pane: pane3}] - - describe "::onWillDestroyPane(callback)", -> - it "invokes the given callback before panes or their items are destroyed", -> - class TestItem - constructor: -> @_isDestroyed = false - destroy: -> @_isDestroyed = true - isDestroyed: -> @_isDestroyed - - container = new PaneContainer(params) - events = [] - container.onWillDestroyPane (event) -> - itemsDestroyed = (item.isDestroyed() for item in event.pane.getItems()) - events.push([event, itemsDestroyed: itemsDestroyed]) - - pane1 = container.getActivePane() - pane2 = pane1.splitRight() - pane2.addItem(new TestItem) - - pane2.destroy() - - expect(events).toEqual [[{pane: pane2}, itemsDestroyed: [false]]] - - describe "::onDidDestroyPane(callback)", -> - it "invokes the given callback when panes are destroyed", -> - container = new PaneContainer(params) - events = [] - container.onDidDestroyPane (event) -> - expect(event.pane in container.getPanes()).toBe false - events.push(event) - - pane1 = container.getActivePane() - pane2 = pane1.splitRight() - pane3 = pane2.splitDown() - - pane2.destroy() - pane3.destroy() - - expect(events).toEqual [{pane: pane2}, {pane: pane3}] - - it "invokes the given callback when the container is destroyed", -> - container = new PaneContainer(params) - events = [] - container.onDidDestroyPane (event) -> - expect(event.pane in container.getPanes()).toBe false - events.push(event) - - pane1 = container.getActivePane() - pane2 = pane1.splitRight() - pane3 = pane2.splitDown() - - container.destroy() - - expect(events).toEqual [{pane: pane1}, {pane: pane2}, {pane: pane3}] - - describe "::onWillDestroyPaneItem() and ::onDidDestroyPaneItem", -> - it "invokes the given callbacks when an item will be destroyed on any pane", -> - container = new PaneContainer(params) - pane1 = container.getRoot() - item1 = new Object - item2 = new Object - item3 = new Object - - pane1.addItem(item1) - events = [] - container.onWillDestroyPaneItem (event) -> events.push(['will', event]) - container.onDidDestroyPaneItem (event) -> events.push(['did', event]) - pane2 = pane1.splitRight(items: [item2, item3]) - - pane1.destroyItem(item1) - pane2.destroyItem(item3) - pane2.destroyItem(item2) - - expect(events).toEqual [ - ['will', {item: item1, pane: pane1, index: 0}] - ['did', {item: item1, pane: pane1, index: 0}] - ['will', {item: item3, pane: pane2, index: 1}] - ['did', {item: item3, pane: pane2, index: 1}] - ['will', {item: item2, pane: pane2, index: 0}] - ['did', {item: item2, pane: pane2, index: 0}] - ] - - describe "::saveAll()", -> - it "saves all modified pane items", -> - container = new PaneContainer(params) - pane1 = container.getRoot() - pane2 = pane1.splitRight() - - item1 = { - saved: false - getURI: -> '' - isModified: -> true, - save: -> @saved = true - } - item2 = { - saved: false - getURI: -> '' - isModified: -> false, - save: -> @saved = true - } - item3 = { - saved: false - getURI: -> '' - isModified: -> true, - save: -> @saved = true - } - - pane1.addItem(item1) - pane1.addItem(item2) - pane1.addItem(item3) - - container.saveAll() - - expect(item1.saved).toBe true - expect(item2.saved).toBe false - expect(item3.saved).toBe true - - describe "::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)", -> - [container, pane1, pane2, item1] = [] - - beforeEach -> - class TestItem - constructor: (id) -> @id = id - copy: -> new TestItem(@id) - - container = new PaneContainer(params) - pane1 = container.getRoot() - item1 = new TestItem('1') - pane2 = pane1.splitRight(items: [item1]) - - describe "::::moveActiveItemToPane(destPane)", -> - it "moves active item to given pane and focuses it", -> - container.moveActiveItemToPane(pane1) - expect(pane1.getActiveItem()).toBe item1 - - describe "::::copyActiveItemToPane(destPane)", -> - it "copies active item to given pane and focuses it", -> - container.copyActiveItemToPane(pane1) - expect(container.paneForItem(item1)).toBe pane2 - expect(pane1.getActiveItem().id).toBe item1.id diff --git a/spec/pane-container-spec.js b/spec/pane-container-spec.js new file mode 100644 index 000000000..1918364f9 --- /dev/null +++ b/spec/pane-container-spec.js @@ -0,0 +1,472 @@ +const PaneContainer = require('../src/pane-container') +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') + +describe('PaneContainer', () => { + let confirm, params + + beforeEach(() => { + confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0) + params = { + location: 'center', + config: atom.config, + deserializerManager: atom.deserializers, + applicationDelegate: atom.applicationDelegate, + viewRegistry: atom.views + } + }) + + describe('serialization', () => { + let containerA, pane1A, pane2A, pane3A + + beforeEach(() => { + // This is a dummy item to prevent panes from being empty on deserialization + class Item { + static deserialize () { return new (this)() } + serialize () { return {deserializer: 'Item'} } + } + atom.deserializers.add(Item) + + containerA = new PaneContainer(params) + pane1A = containerA.getActivePane() + pane1A.addItem(new Item()) + pane2A = pane1A.splitRight({items: [new Item()]}) + pane3A = pane2A.splitDown({items: [new Item()]}) + pane3A.focus() + }) + + it('preserves the focused pane across serialization', () => { + expect(pane3A.focused).toBe(true) + + const containerB = new PaneContainer(params) + containerB.deserialize(containerA.serialize(), atom.deserializers) + const pane3B = containerB.getPanes()[2] + expect(pane3B.focused).toBe(true) + }) + + it('preserves the active pane across serialization, independent of focus', () => { + pane3A.activate() + expect(containerA.getActivePane()).toBe(pane3A) + + const containerB = new PaneContainer(params) + containerB.deserialize(containerA.serialize(), atom.deserializers) + const pane3B = containerB.getPanes()[2] + expect(containerB.getActivePane()).toBe(pane3B) + }) + + it('makes the first pane active if no pane exists for the activePaneId', () => { + pane3A.activate() + const state = containerA.serialize() + state.activePaneId = -22 + const containerB = new PaneContainer(params) + containerB.deserialize(state, atom.deserializers) + expect(containerB.getActivePane()).toBe(containerB.getPanes()[0]) + }) + + describe('if there are empty panes after deserialization', () => { + beforeEach(() => { + pane3A.getItems()[0].serialize = () => ({deserializer: 'Bogus'}) + }) + + describe("if the 'core.destroyEmptyPanes' config option is false (the default)", () => + it('leaves the empty panes intact', () => { + const state = containerA.serialize() + const containerB = new PaneContainer(params) + containerB.deserialize(state, atom.deserializers) + const [leftPane, column] = containerB.getRoot().getChildren() + const [topPane, bottomPane] = column.getChildren() + + expect(leftPane.getItems().length).toBe(1) + expect(topPane.getItems().length).toBe(1) + expect(bottomPane.getItems().length).toBe(0) + }) + ) + + describe("if the 'core.destroyEmptyPanes' config option is true", () => + it('removes empty panes on deserialization', () => { + atom.config.set('core.destroyEmptyPanes', true) + + const state = containerA.serialize() + const containerB = new PaneContainer(params) + containerB.deserialize(state, atom.deserializers) + const [leftPane, rightPane] = containerB.getRoot().getChildren() + + expect(leftPane.getItems().length).toBe(1) + expect(rightPane.getItems().length).toBe(1) + }) + ) + }) + }) + + it('does not allow the root pane to be destroyed', () => { + const container = new PaneContainer(params) + container.getRoot().destroy() + expect(container.getRoot()).toBeDefined() + expect(container.getRoot().isDestroyed()).toBe(false) + }) + + describe('::getActivePane()', () => { + let container, pane1, pane2 + + beforeEach(() => { + container = new PaneContainer(params) + pane1 = container.getRoot() + }) + + it('returns the first pane if no pane has been made active', () => { + expect(container.getActivePane()).toBe(pane1) + expect(pane1.isActive()).toBe(true) + }) + + it('returns the most pane on which ::activate() was most recently called', () => { + pane2 = pane1.splitRight() + pane2.activate() + expect(container.getActivePane()).toBe(pane2) + expect(pane1.isActive()).toBe(false) + expect(pane2.isActive()).toBe(true) + pane1.activate() + expect(container.getActivePane()).toBe(pane1) + expect(pane1.isActive()).toBe(true) + expect(pane2.isActive()).toBe(false) + }) + + it('returns the next pane if the current active pane is destroyed', () => { + pane2 = pane1.splitRight() + pane2.activate() + pane2.destroy() + expect(container.getActivePane()).toBe(pane1) + expect(pane1.isActive()).toBe(true) + }) + }) + + describe('::onDidChangeActivePane()', () => { + let container, pane1, pane2, observed + + beforeEach(() => { + container = new PaneContainer(params) + container.getRoot().addItems([{}, {}]) + container.getRoot().splitRight({items: [{}, {}]}); + [pane1, pane2] = container.getPanes() + + observed = [] + container.onDidChangeActivePane(pane => observed.push(pane)) + }) + + it('invokes observers when the active pane changes', () => { + pane1.activate() + pane2.activate() + expect(observed).toEqual([pane1, pane2]) + }) + }) + + describe('::onDidChangeActivePaneItem()', () => { + let container, pane1, pane2, observed + + beforeEach(() => { + container = new PaneContainer(params) + container.getRoot().addItems([{}, {}]) + container.getRoot().splitRight({items: [{}, {}]}); + [pane1, pane2] = container.getPanes() + + observed = [] + container.onDidChangeActivePaneItem(item => observed.push(item)) + }) + + it('invokes observers when the active item of the active pane changes', () => { + pane2.activateNextItem() + pane2.activateNextItem() + expect(observed).toEqual([pane2.itemAtIndex(1), pane2.itemAtIndex(0)]) + }) + + it('invokes observers when the active pane changes', () => { + pane1.activate() + pane2.activate() + expect(observed).toEqual([pane1.itemAtIndex(0), pane2.itemAtIndex(0)]) + }) + }) + + describe('::onDidStopChangingActivePaneItem()', () => { + let container, pane1, pane2, observed + + beforeEach(() => { + container = new PaneContainer(params) + container.getRoot().addItems([{}, {}]) + container.getRoot().splitRight({items: [{}, {}]}); + [pane1, pane2] = container.getPanes() + + observed = [] + container.onDidStopChangingActivePaneItem(item => observed.push(item)) + }) + + it('invokes observers once when the active item of the active pane changes', () => { + pane2.activateNextItem() + pane2.activateNextItem() + expect(observed).toEqual([]) + advanceClock(100) + expect(observed).toEqual([pane2.itemAtIndex(0)]) + }) + + it('invokes observers once when the active pane changes', () => { + pane1.activate() + pane2.activate() + expect(observed).toEqual([]) + advanceClock(100) + expect(observed).toEqual([pane2.itemAtIndex(0)]) + }) + }) + + describe('::onDidActivatePane', () => { + it('invokes observers when a pane is activated (even if it was already active)', () => { + const container = new PaneContainer(params) + container.getRoot().splitRight() + const [pane1, pane2] = container.getPanes() + + const activatedPanes = [] + container.onDidActivatePane(pane => activatedPanes.push(pane)) + + pane1.activate() + pane1.activate() + pane2.activate() + pane2.activate() + expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2]) + }) + }) + + describe('::observePanes()', () => { + it('invokes observers with all current and future panes', () => { + const container = new PaneContainer(params) + container.getRoot().splitRight() + const [pane1, pane2] = container.getPanes() + + const observed = [] + container.observePanes(pane => observed.push(pane)) + + const pane3 = pane2.splitDown() + const pane4 = pane2.splitRight() + + expect(observed).toEqual([pane1, pane2, pane3, pane4]) + }) + }) + + describe('::observePaneItems()', () => + it('invokes observers with all current and future pane items', () => { + const container = new PaneContainer(params) + container.getRoot().addItems([{}, {}]) + container.getRoot().splitRight({items: [{}]}) + const pane2 = container.getPanes()[1] + const observed = [] + container.observePaneItems(pane => observed.push(pane)) + + const pane3 = pane2.splitDown({items: [{}]}) + pane3.addItems([{}, {}]) + + expect(observed).toEqual(container.getPaneItems()) + }) + ) + + describe('::confirmClose()', () => { + let container, pane1, pane2 + + beforeEach(() => { + class TestItem { + shouldPromptToSave () { return true } + getURI () { return 'test' } + } + + container = new PaneContainer(params) + container.getRoot().splitRight(); + [pane1, pane2] = container.getPanes() + pane1.addItem(new TestItem()) + pane2.addItem(new TestItem()) + }) + + it('returns true if the user saves all modified files when prompted', async () => { + confirm.andReturn(0) + const saved = await container.confirmClose() + expect(confirm).toHaveBeenCalled() + expect(saved).toBeTruthy() + }) + + it('returns false if the user cancels saving any modified file', async () => { + confirm.andReturn(1) + const saved = await container.confirmClose() + expect(confirm).toHaveBeenCalled() + expect(saved).toBeFalsy() + }) + }) + + describe('::onDidAddPane(callback)', () => { + it('invokes the given callback when panes are added', () => { + const container = new PaneContainer(params) + const events = [] + container.onDidAddPane((event) => { + expect(container.getPanes().includes(event.pane)).toBe(true) + events.push(event) + }) + + const pane1 = container.getActivePane() + const pane2 = pane1.splitRight() + const pane3 = pane2.splitDown() + + expect(events).toEqual([{pane: pane2}, {pane: pane3}]) + }) + }) + + describe('::onWillDestroyPane(callback)', () => { + it('invokes the given callback before panes or their items are destroyed', () => { + class TestItem { + constructor () { this._isDestroyed = false } + destroy () { this._isDestroyed = true } + isDestroyed () { return this._isDestroyed } + } + + const container = new PaneContainer(params) + const events = [] + container.onWillDestroyPane((event) => { + const itemsDestroyed = event.pane.getItems().map((item) => item.isDestroyed()) + events.push([event, {itemsDestroyed}]) + }) + + const pane1 = container.getActivePane() + const pane2 = pane1.splitRight() + pane2.addItem(new TestItem()) + + pane2.destroy() + + expect(events).toEqual([[{pane: pane2}, {itemsDestroyed: [false]}]]) + }) + }) + + describe('::onDidDestroyPane(callback)', () => { + it('invokes the given callback when panes are destroyed', () => { + const container = new PaneContainer(params) + const events = [] + container.onDidDestroyPane((event) => { + expect(container.getPanes().includes(event.pane)).toBe(false) + events.push(event) + }) + + const pane1 = container.getActivePane() + const pane2 = pane1.splitRight() + const pane3 = pane2.splitDown() + + pane2.destroy() + pane3.destroy() + + expect(events).toEqual([{pane: pane2}, {pane: pane3}]) + }) + + it('invokes the given callback when the container is destroyed', () => { + const container = new PaneContainer(params) + const events = [] + container.onDidDestroyPane((event) => { + expect(container.getPanes().includes(event.pane)).toBe(false) + events.push(event) + }) + + const pane1 = container.getActivePane() + const pane2 = pane1.splitRight() + const pane3 = pane2.splitDown() + + container.destroy() + + expect(events).toEqual([{pane: pane1}, {pane: pane2}, {pane: pane3}]) + }) + }) + + describe('::onWillDestroyPaneItem() and ::onDidDestroyPaneItem', () => { + it('invokes the given callbacks when an item will be destroyed on any pane', async () => { + const container = new PaneContainer(params) + const pane1 = container.getRoot() + const item1 = {} + const item2 = {} + const item3 = {} + + pane1.addItem(item1) + const events = [] + container.onWillDestroyPaneItem(event => events.push(['will', event])) + container.onDidDestroyPaneItem(event => events.push(['did', event])) + const pane2 = pane1.splitRight({items: [item2, item3]}) + + await pane1.destroyItem(item1) + await pane2.destroyItem(item3) + await pane2.destroyItem(item2) + + expect(events).toEqual([ + ['will', {item: item1, pane: pane1, index: 0}], + ['did', {item: item1, pane: pane1, index: 0}], + ['will', {item: item3, pane: pane2, index: 1}], + ['did', {item: item3, pane: pane2, index: 1}], + ['will', {item: item2, pane: pane2, index: 0}], + ['did', {item: item2, pane: pane2, index: 0}] + ]) + }) + }) + + describe('::saveAll()', () => + it('saves all modified pane items', async () => { + const container = new PaneContainer(params) + const pane1 = container.getRoot() + pane1.splitRight() + + const item1 = { + saved: false, + getURI () { return '' }, + isModified () { return true }, + save () { this.saved = true } + } + const item2 = { + saved: false, + getURI () { return '' }, + isModified () { return false }, + save () { this.saved = true } + } + const item3 = { + saved: false, + getURI () { return '' }, + isModified () { return true }, + save () { this.saved = true } + } + + pane1.addItem(item1) + pane1.addItem(item2) + pane1.addItem(item3) + + container.saveAll() + + expect(item1.saved).toBe(true) + expect(item2.saved).toBe(false) + expect(item3.saved).toBe(true) + }) + ) + + describe('::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)', () => { + let container, pane1, pane2, item1 + + beforeEach(() => { + class TestItem { + constructor (id) { this.id = id } + copy () { return new TestItem(this.id) } + } + + container = new PaneContainer(params) + pane1 = container.getRoot() + item1 = new TestItem('1') + pane2 = pane1.splitRight({items: [item1]}) + }) + + describe('::::moveActiveItemToPane(destPane)', () => + it('moves active item to given pane and focuses it', () => { + container.moveActiveItemToPane(pane1) + expect(pane1.getActiveItem()).toBe(item1) + }) + ) + + describe('::::copyActiveItemToPane(destPane)', () => + it('copies active item to given pane and focuses it', () => { + container.copyActiveItemToPane(pane1) + expect(container.paneForItem(item1)).toBe(pane2) + expect(pane1.getActiveItem().id).toBe(item1.id) + }) + ) + }) +}) From bf4985e265470291f90ca507f3968a3e4fda70c2 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 18 Sep 2017 17:33:42 -0700 Subject: [PATCH 156/448] :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 157/448] 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 158/448] 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 159/448] 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 160/448] 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 161/448] 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 162/448] 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 163/448] 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 164/448] 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 165/448] 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 166/448] 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 cd974e41a537a2b9b8cb38b81de550559cc14589 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 19 Sep 2017 10:58:34 +0200 Subject: [PATCH 167/448] :arrow_up: line-ending-selector@0.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b6fe7b4e..4af711b98 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "image-view": "0.62.3", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", - "line-ending-selector": "0.7.3", + "line-ending-selector": "0.7.4", "link": "0.31.3", "markdown-preview": "0.159.13", "metrics": "1.2.6", From 51ed51568c0b3ec619705e43251b9cefa29c1a24 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 19 Sep 2017 09:40:26 -0700 Subject: [PATCH 168/448] :arrow_up: tabs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2429c18d3..a0921cdf6 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.13", "styleguide": "0.49.6", "symbols-view": "0.118.0", - "tabs": "0.107.2", + "tabs": "0.107.3", "timecop": "0.36.0", "tree-view": "0.217.9", "update-package-dependencies": "0.12.0", From 006ebc7443fc46c30a00fedcfe2257e0edb47c7a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 19 Sep 2017 13:07:01 -0400 Subject: [PATCH 169/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4af711b98..a66607229 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.3.1", + "text-buffer": "13.3.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 56926695ba359cd10eeba50ed349c8f1853d8160 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 19 Sep 2017 10:11:40 -0700 Subject: [PATCH 170/448] Return a promise from Pane.destroyActiveItem --- src/pane.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pane.js b/src/pane.js index f56c1c88b..0305b39dd 100644 --- a/src/pane.js +++ b/src/pane.js @@ -728,9 +728,10 @@ class Pane { } // Public: Destroy the active item and activate the next item. + // + // Returns a {Promise} that resolves when the item is destroyed. destroyActiveItem () { - this.destroyItem(this.activeItem) - return false + return this.destroyItem(this.activeItem) } // Public: Destroy the given item. From 6c14dfb0af78d4ca686932cb073f79b917236c52 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 19 Sep 2017 10:36:59 -0700 Subject: [PATCH 171/448] :arrow_up: autosave, whitespace --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a0921cdf6..7e153df63 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "autocomplete-plus": "2.35.9", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", - "autosave": "0.24.3", + "autosave": "0.24.4", "background-tips": "0.27.1", "bookmarks": "0.44.4", "bracket-matcher": "0.87.3", @@ -134,7 +134,7 @@ "tree-view": "0.217.9", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", - "whitespace": "0.37.3", + "whitespace": "0.37.4", "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", From 3c5d471ec7935a29a18c6e4c0239d4e6293c64c3 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 19 Sep 2017 10:41:53 -0700 Subject: [PATCH 172/448] 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 173/448] 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 0b5a79bee25a22ebec67052e9e3c9bfb8deb8c8a Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 19 Sep 2017 20:15:34 +0200 Subject: [PATCH 174/448] :arrow_up: language-coffee-script@0.49.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a66607229..bd70b4d8a 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.0", + "language-coffee-script": "0.49.1", "language-csharp": "0.14.2", "language-css": "0.42.6", "language-gfm": "0.90.1", From a3e98d54e3d24bbc5c4a67a135cfae2744805285 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Tue, 19 Sep 2017 20:28:50 +0200 Subject: [PATCH 175/448] :arrow_up: grammar-selector@0.49.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd9d1c3d0..35f55ced7 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "github": "0.6.2", "git-diff": "1.3.6", "go-to-line": "0.32.1", - "grammar-selector": "0.49.5", + "grammar-selector": "0.49.6", "image-view": "0.62.3", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", From ed423672efa4fdff5fc748381ce5f4126b36da35 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 19 Sep 2017 12:22:58 -0700 Subject: [PATCH 176/448] 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 1745b4be6b049d261ee183a4c4267a078a724864 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 13:42:10 -0500 Subject: [PATCH 177/448] change activeItemPath if item's path changes --- src/pane-element.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pane-element.coffee b/src/pane-element.coffee index c4866816a..a1a760348 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -79,6 +79,7 @@ class PaneElement extends HTMLElement activeItemChanged: (item) -> delete @dataset.activeItemName delete @dataset.activeItemPath + @changePathDisposable?.dispose?() return unless item? @@ -89,6 +90,12 @@ class PaneElement extends HTMLElement @dataset.activeItemName = path.basename(itemPath) @dataset.activeItemPath = itemPath + if item.onDidChangePath? + @changePathDisposable = item.onDidChangePath => + itemPath = item.getPath() + @dataset.activeItemName = path.basename(itemPath) + @dataset.activeItemPath = itemPath + unless @itemViews.contains(itemView) @itemViews.appendChild(itemView) From 8b67b7037c92b90eb3e99d99f76dc8ed80774d5a Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 14:07:34 -0500 Subject: [PATCH 178/448] add specs --- spec/pane-element-spec.coffee | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index ff7634734..86db4ffe7 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -113,6 +113,49 @@ describe "PaneElement", -> expect(paneElement.dataset.activeItemPath).toBeUndefined() expect(paneElement.dataset.activeItemName).toBeUndefined() + describe "when the path of the item changes", -> + [item1, item2] = [] + + beforeEach -> + item1 = document.createElement('div') + item1.path = '/foo/bar.txt' + item1.changePathCallbacks = [] + item1.setPath = (path) -> + this.path = path + callback() for callback in changePathCallbacks + item1.getPath = -> this.path + item1.onDidChangePath = (callback) -> this.changePathCallbacks.push(callback) + + item2 = document.createElement('div') + + pane.addItem(item1) + pane.addItem(item2) + + it "changes the file path and file name data attributes on the pane if the active item path is changed", -> + + expect(paneElement.dataset.activeItemPath).toBe '/foo/bar.txt' + expect(paneElement.dataset.activeItemName).toBe 'bar.txt' + + item1.setPath "/foo/bar1.txt" + + expect(paneElement.dataset.activeItemPath).toBe '/foo/bar1.txt' + expect(paneElement.dataset.activeItemName).toBe 'bar1.txt' + + pane.activateItem(item2) + + expect(paneElement.dataset.activeItemPath).toBeUndefined() + expect(paneElement.dataset.activeItemName).toBeUndefined() + + item1.setPath "/foo/bar2.txt" + + expect(paneElement.dataset.activeItemPath).toBeUndefined() + expect(paneElement.dataset.activeItemName).toBeUndefined() + + pane.activateItem(item1) + + expect(paneElement.dataset.activeItemPath).toBe '/foo/bar2.txt' + expect(paneElement.dataset.activeItemName).toBe 'bar2.txt' + describe "when an item is removed from the pane", -> describe "when the destroyed item is an element", -> it "removes the item from the itemViews div", -> From 90503f6d4a04d51ed744e53b99083e15ee9621eb Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 14:11:33 -0500 Subject: [PATCH 179/448] dispose changePathDisposable on destroy --- src/pane-element.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pane-element.coffee b/src/pane-element.coffee index a1a760348..d68b3b834 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -79,7 +79,7 @@ class PaneElement extends HTMLElement activeItemChanged: (item) -> delete @dataset.activeItemName delete @dataset.activeItemPath - @changePathDisposable?.dispose?() + @changePathDisposable?.dispose() return unless item? @@ -126,6 +126,7 @@ class PaneElement extends HTMLElement paneDestroyed: -> @subscriptions.dispose() + @changePathDisposable?.dispose() flexScaleChanged: (flexScale) -> @style.flexGrow = flexScale From abeebd51ef9a873867d2cf0c6ac148f2209b91d7 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 14:35:23 -0500 Subject: [PATCH 180/448] add test dispose --- spec/pane-element-spec.coffee | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index 86db4ffe7..831354936 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -121,10 +121,14 @@ describe "PaneElement", -> item1.path = '/foo/bar.txt' item1.changePathCallbacks = [] item1.setPath = (path) -> - this.path = path + @path = path callback() for callback in changePathCallbacks - item1.getPath = -> this.path - item1.onDidChangePath = (callback) -> this.changePathCallbacks.push(callback) + return + item1.getPath = -> @path + item1.onDidChangePath = (callback) -> + @changePathCallbacks.push(callback) + return dispose: => + @changePathCallbacks = @changePathCallbacks.filter (f) -> f isnt callback item2 = document.createElement('div') From 1e72a7f0e57a1d5c8630a2717a1d0ec916f8f194 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 15:06:44 -0500 Subject: [PATCH 181/448] fix tests --- spec/pane-element-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index 831354936..af34681a6 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -122,11 +122,11 @@ describe "PaneElement", -> item1.changePathCallbacks = [] item1.setPath = (path) -> @path = path - callback() for callback in changePathCallbacks + callback() for callback in @changePathCallbacks return item1.getPath = -> @path item1.onDidChangePath = (callback) -> - @changePathCallbacks.push(callback) + @changePathCallbacks.push callback return dispose: => @changePathCallbacks = @changePathCallbacks.filter (f) -> f isnt callback From d81f48b1b29ab751f78eed24b826fb10915288e2 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 19 Sep 2017 13:47:46 -0700 Subject: [PATCH 182/448] :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 2e57312380e81d1140d69c7631088936617b01fe Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 20 Sep 2017 10:49:56 -0400 Subject: [PATCH 183/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 35f55ced7..6f4702c10 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.3.2", + "text-buffer": "13.3.3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 156cd0295301e43d21980eed25adfb1d6a5e8bce Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 10:32:47 -0400 Subject: [PATCH 184/448] Optionally throw an error when attempting to open a non-existent dir --- spec/project-spec.coffee | 5 +++++ src/project.coffee | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 4ce84617a..bf31d0509 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -498,6 +498,11 @@ describe "Project", -> 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 diff --git a/src/project.coffee b/src/project.coffee index cad5f03ac..ef8681fe4 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -226,8 +226,18 @@ class Project extends Model # Public: Add a path to the project's list of root paths # # * `projectPath` {String} The path to the directory to add. - addPath: (projectPath, options) -> + # * `options` An optional {Object} that may contain the following keys: + # * `mustExist` If `true`, throw an Error if `projectPath` does not exist. + addPath: (projectPath, options = {}) -> directory = @getDirectoryForProjectPath(projectPath) + unless directory.existsSync() + if options.mustExist is true + err = new Error "Project directory #{directory} does not exist" + err.missingProjectPaths = [directory] + throw err + else + return + return unless directory.existsSync() for existingDirectory in @getDirectories() return if existingDirectory.getPath() is directory.getPath() @@ -248,7 +258,7 @@ class Project extends Model break if repo = provider.repositoryForDirectorySync?(directory) @repositories.push(repo ? null) - unless options?.emitEvent is false + unless options.emitEvent is false @emitter.emit 'did-change-paths', @getPaths() getDirectoryForProjectPath: (projectPath) -> From 3d51ed9766f27f815e93a9a2679cb454b42b6e12 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 10:56:11 -0400 Subject: [PATCH 185/448] Support {mustExist} option in setPaths --- spec/project-spec.coffee | 15 +++++++++++++-- src/project.coffee | 27 +++++++++++++++++++++------ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index bf31d0509..694e60c64 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -411,7 +411,7 @@ describe "Project", -> runs -> expect(repository.isDestroyed()).toBe(false) - describe ".setPaths(paths)", -> + describe ".setPaths(paths, options)", -> describe "when path is a file", -> it "sets its path to the files parent directory and updates the root directory", -> filePath = require.resolve('./fixtures/dir/a') @@ -448,6 +448,17 @@ describe "Project", -> 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([]) @@ -459,7 +470,7 @@ describe "Project", -> 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)", -> + describe ".addPath(path, options)", -> it "calls callbacks registered with ::onDidChangePaths", -> onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') atom.project.onDidChangePaths(onDidChangePathsSpy) diff --git a/src/project.coffee b/src/project.coffee index ef8681fe4..9015edf78 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -211,7 +211,10 @@ class Project extends Model # Public: Set the paths of the project's directories. # # * `projectPaths` {Array} of {String} paths. - setPaths: (projectPaths) -> + # * `options` An optional {Object} that may contain the following keys: + # * `mustExist` If `true`, throw an Error if any `projectPaths` do not exist. The existing `projectPaths` will + # still be added to the project. + setPaths: (projectPaths, options = {}) -> repository?.destroy() for repository in @repositories @rootDirectories = [] @repositories = [] @@ -219,26 +222,38 @@ class Project extends Model watcher.then((w) -> w.dispose()) for _, watcher in @watcherPromisesByPath @watcherPromisesByPath = {} - @addPath(projectPath, emitEvent: false) for projectPath in projectPaths + added = false + missingProjectPaths = [] + for projectPath in projectPaths + try + @addPath(projectPath, emitEvent: false, mustExist: true) + added = true + catch e + missingProjectPaths.push e.missingProjectPaths... - @emitter.emit 'did-change-paths', projectPaths + if added + @emitter.emit 'did-change-paths', projectPaths + + if options.mustExist is true and missingProjectPaths + 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 `projectPath` does not exist. + # * `mustExist` If `true`, throw an Error if the `projectPath` does not exist. addPath: (projectPath, options = {}) -> directory = @getDirectoryForProjectPath(projectPath) unless directory.existsSync() if options.mustExist is true err = new Error "Project directory #{directory} does not exist" - err.missingProjectPaths = [directory] + err.missingProjectPaths = [projectPath] throw err else return - return unless directory.existsSync() for existingDirectory in @getDirectories() return if existingDirectory.getPath() is directory.getPath() From 928a3e2854878861d274fdd4ea79da8133a84f39 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 11:20:51 -0400 Subject: [PATCH 186/448] Expect the deserialization promise to reject with inaccessible dirs --- spec/project-spec.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 694e60c64..d88cbbf02 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -25,11 +25,14 @@ describe "Project", -> 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 include unretained buffers in the serialized state", -> waitsForPromise -> From 48c1fb0f3c00c641a717d6674076177af5eff519 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 11:21:10 -0400 Subject: [PATCH 187/448] .deserialize() is async --- spec/project-spec.coffee | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index d88cbbf02..7e4aef71a 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -75,7 +75,11 @@ describe "Project", -> 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", -> @@ -90,7 +94,11 @@ describe "Project", -> 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 "serializes marker layers and history only if Atom is quitting", -> @@ -100,6 +108,7 @@ describe "Project", -> bufferA = null layerA = null markerA = null + notQuittingProject = null runs -> bufferA = atom.project.getBuffers()[0] @@ -109,9 +118,11 @@ describe "Project", -> waitsForPromise -> notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})).then -> - expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined() - expect(notQuittingProject.getBuffers()[0].undo()).toBe(false) + 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) waitsForPromise -> quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) From c205a07bc49d953f8085162e87ceb2d4aa79263d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 11:21:30 -0400 Subject: [PATCH 188/448] Require project directories to exist on deserialization --- src/project.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index 9015edf78..910107ea9 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -72,7 +72,7 @@ class Project extends Model bufferPromises.push(TextBuffer.deserialize(bufferState)) Promise.all(bufferPromises).then (@buffers) => @subscribeToBuffer(buffer) for buffer in @buffers - @setPaths(state.paths) + @setPaths(state.paths, mustExist: true) serialize: (options={}) -> deserializer: 'Project' @@ -234,7 +234,7 @@ class Project extends Model if added @emitter.emit 'did-change-paths', projectPaths - if options.mustExist is true and missingProjectPaths + 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 From bdc503a79598be16f48c90ef9c4860d5df7da761 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 11:22:10 -0400 Subject: [PATCH 189/448] Report deserialization errors from the project --- spec/atom-environment-spec.coffee | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 8a3e4e0fb..03d65a3d6 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -322,6 +322,44 @@ describe "AtomEnvironment", -> 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 -> From 22c0b6a4f331361190abbd36d2e0898840c6b947 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 11:23:10 -0400 Subject: [PATCH 190/448] Notify if one or more deserialized project directories no longer exist --- src/atom-environment.coffee | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index db2ddc735..2fccc1d9d 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -998,11 +998,18 @@ class AtomEnvironment extends Model @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", stack: err.stack else projectPromise = Promise.resolve() @@ -1015,6 +1022,19 @@ class AtomEnvironment extends Model @workspace.deserialize(state.workspace, @deserializers) if state.workspace? @deserializeTimings.workspace = Date.now() - startTime + if missingProjectPaths.length + 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') From 7d62f8b6ebd7e700109bad5c023ec6a96090db12 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 15:11:16 -0400 Subject: [PATCH 191/448] Test for omitting deleted files --- spec/project-spec.coffee | 42 +++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 7e4aef71a..7a29d2f70 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -16,9 +16,13 @@ describe "Project", -> describe "serialization", -> deserializedProject = null + notQuittingProject = null + quittingProject = null afterEach -> deserializedProject?.destroy() + notQuittingProject?.destroy() + quittingProject?.destroy() it "does not deserialize paths to non directories", -> deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) @@ -101,34 +105,50 @@ describe "Project", -> runs -> expect(deserializedProject.getBuffers().length).toBe 0 - it "serializes marker layers and history only if Atom is quitting", -> + 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('a') + 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 "serializes marker layers and history only if Atom is quitting", -> + waitsForPromise -> atom.workspace.open('a') bufferA = null layerA = null markerA = null - notQuittingProject = null runs -> bufferA = atom.project.getBuffers()[0] layerA = bufferA.addMarkerLayer(persistent: true) markerA = layerA.markPosition([0, 3]) bufferA.append('!') - - waitsForPromise -> notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})) + + 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) - - waitsForPromise -> quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - quittingProject.deserialize(atom.project.serialize({isUnloading: true})).then -> - expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined() - expect(quittingProject.getBuffers()[0].undo()).toBe(true) + + 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", -> From 1734aa77874ef3462f3ebc613e679389d6c7a9fa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 18 Sep 2017 15:11:36 -0400 Subject: [PATCH 192/448] Shuffle Project deserialization a little --- src/project.coffee | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index 910107ea9..36b38b5da 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -58,19 +58,34 @@ class Project extends Model ### deserialize: (state) -> + checkNotDirectory = (filePath) -> + new Promise (resolve, reject) -> + fs.isDirectory filePath, (isDir) -> + if isDir then reject() else resolve() + + checkAccess = (filePath) -> + new Promise (resolve, reject) -> + fs.open filePath, 'r', (err, fd) -> + return reject() if err? + fs.close fd, () -> resolve() + bufferPromises = [] for bufferState in state.buffers - continue if fs.isDirectorySync(bufferState.filePath) - if bufferState.filePath - try - fs.closeSync(fs.openSync(bufferState.filePath, 'r')) - catch error - continue unless error.code is 'ENOENT' - unless bufferState.shouldDestroyOnFileDelete? - bufferState.shouldDestroyOnFileDelete = -> - atom.config.get('core.closeDeletedFileTabs') - bufferPromises.push(TextBuffer.deserialize(bufferState)) - Promise.all(bufferPromises).then (@buffers) => + bufferState.shouldDestroyOnFileDelete ?= -> atom.config.get('core.closeDeletedFileTabs') + + promise = Promise.resolve() + if bufferState.filePath? + promise = promise.then () -> Promise.all([ + checkNotDirectory(bufferState.filePath), + checkAccess(bufferState.filePath) + ]) + promise = promise.then () -> TextBuffer.deserialize(bufferState) + promise = promise.catch (err) -> null + + bufferPromises.push promise + + Promise.all(bufferPromises).then (buffers) => + @buffers = buffers.filter(Boolean) @subscribeToBuffer(buffer) for buffer in @buffers @setPaths(state.paths, mustExist: true) From 868d9393ded233042e8cbdfc1a5c7908ecec28f6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 19 Sep 2017 09:21:10 -0400 Subject: [PATCH 193/448] Deserialize the exact serialized project paths, never a parent dir --- spec/project-spec.coffee | 26 +++++++++++++++++++++++--- src/project.coffee | 25 +++++++++++++++++++------ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 7a29d2f70..40646e686 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -24,7 +24,7 @@ describe "Project", -> notQuittingProject?.destroy() quittingProject?.destroy() - it "does not deserialize paths to non directories", -> + 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') @@ -38,6 +38,26 @@ describe "Project", -> 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, 'suprise!\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') @@ -69,7 +89,7 @@ describe "Project", -> deserializedProject.getBuffers()[0].destroy() expect(deserializedProject.getBuffers().length).toBe 0 - it "does not deserialize buffers when their path is a directory that exists", -> + it "does not deserialize buffers when their path is now a directory", -> pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt') waitsForPromise -> @@ -447,7 +467,7 @@ describe "Project", -> describe ".setPaths(paths, options)", -> describe "when path is a file", -> - it "sets its path to the files parent directory and updates the root directory", -> + 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) diff --git a/src/project.coffee b/src/project.coffee index 36b38b5da..b50a28301 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -227,8 +227,10 @@ class Project extends Model # # * `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. The existing `projectPaths` will - # still be added to the project. + # * `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 = [] @@ -241,10 +243,13 @@ class Project extends Model missingProjectPaths = [] for projectPath in projectPaths try - @addPath(projectPath, emitEvent: false, mustExist: true) + @addPath projectPath, emitEvent: false, mustExist: true, exact: true added = true catch e - missingProjectPaths.push e.missingProjectPaths... + if e.missingProjectPaths? + missingProjectPaths.push e.missingProjectPaths... + else + throw e if added @emitter.emit 'did-change-paths', projectPaths @@ -258,10 +263,18 @@ class Project extends Model # # * `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. + # * `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) - unless directory.existsSync() + + 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] From f28192bfbc89b05eed8b603a5007515113a83825 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 19 Sep 2017 09:22:18 -0400 Subject: [PATCH 194/448] Scope is hard --- src/project.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index b50a28301..486d003e3 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -69,8 +69,7 @@ class Project extends Model return reject() if err? fs.close fd, () -> resolve() - bufferPromises = [] - for bufferState in state.buffers + handleBufferState = (bufferState) -> bufferState.shouldDestroyOnFileDelete ?= -> atom.config.get('core.closeDeletedFileTabs') promise = Promise.resolve() @@ -81,13 +80,14 @@ class Project extends Model ]) promise = promise.then () -> TextBuffer.deserialize(bufferState) promise = promise.catch (err) -> null + promise - bufferPromises.push promise + 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, mustExist: true) + @setPaths(state.paths or [], mustExist: true) serialize: (options={}) -> deserializer: 'Project' From 41db57469993c5fb471dbb1e71fc50de5fab70fa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 19 Sep 2017 13:05:31 -0400 Subject: [PATCH 195/448] Use a fat arrow for @notifications --- 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 2fccc1d9d..0d81b2a15 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1005,7 +1005,7 @@ class AtomEnvironment extends Model startTime = Date.now() if state.project? projectPromise = @project.deserialize(state.project, @deserializers) - .catch (err) -> + .catch (err) => if err.missingProjectPaths? missingProjectPaths.push(err.missingProjectPaths...) else From 54ecff451040933edf8e0177bbec9f192fd11aab Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 19 Sep 2017 13:05:42 -0400 Subject: [PATCH 196/448] Provide the message as a .description --- 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 0d81b2a15..63e738371 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1009,7 +1009,7 @@ class AtomEnvironment extends Model if err.missingProjectPaths? missingProjectPaths.push(err.missingProjectPaths...) else - @notifications.addError "Unable to deserialize project", stack: err.stack + @notifications.addError "Unable to deserialize project", description: err.message, stack: err.stack else projectPromise = Promise.resolve() From 46fc5ffc8cbbe6479aac142ac00dc485645b67ad Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 19 Sep 2017 13:06:12 -0400 Subject: [PATCH 197/448] Better conditional --- 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 63e738371..bfc387348 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -1022,7 +1022,7 @@ class AtomEnvironment extends Model @workspace.deserialize(state.workspace, @deserializers) if state.workspace? @deserializeTimings.workspace = Date.now() - startTime - if missingProjectPaths.length + 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' From 0782f0f4d0d3b07d439e92795450d685fb8065bb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 19 Sep 2017 14:17:04 -0400 Subject: [PATCH 198/448] "Retire" buffer IDs when the buffer can't be deserialized --- spec/text-editor-spec.coffee | 9 +++++++++ src/project.coffee | 32 ++++++++++++-------------------- src/text-editor.coffee | 5 ++++- src/tokenized-buffer.coffee | 8 ++++++-- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 47b85bf1f..cb70d030c 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -76,6 +76,15 @@ describe "TextEditor", -> 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 diff --git a/src/project.coffee b/src/project.coffee index 486d003e3..7e5cf975e 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -30,6 +30,8 @@ class Project extends Model @repositoryProviders = [new GitRepositoryProvider(this, config)] @loadPromisesByPath = {} @watcherPromisesByPath = {} + @retiredBufferIDs = new Set() + @retiredBufferPaths = new Set() @consumeServices(packageManager) destroyed: -> @@ -58,29 +60,17 @@ class Project extends Model ### deserialize: (state) -> - checkNotDirectory = (filePath) -> - new Promise (resolve, reject) -> - fs.isDirectory filePath, (isDir) -> - if isDir then reject() else resolve() + @retiredBufferIDs = new Set() + @retiredBufferPaths = new Set() - checkAccess = (filePath) -> - new Promise (resolve, reject) -> - fs.open filePath, 'r', (err, fd) -> - return reject() if err? - fs.close fd, () -> resolve() - - handleBufferState = (bufferState) -> + handleBufferState = (bufferState) => bufferState.shouldDestroyOnFileDelete ?= -> atom.config.get('core.closeDeletedFileTabs') + bufferState.mustExist = true - promise = Promise.resolve() - if bufferState.filePath? - promise = promise.then () -> Promise.all([ - checkNotDirectory(bufferState.filePath), - checkAccess(bufferState.filePath) - ]) - promise = promise.then () -> TextBuffer.deserialize(bufferState) - promise = promise.catch (err) -> null - promise + TextBuffer.deserialize(bufferState).catch (err) => + @retiredBufferIDs.add(bufferState.id) + @retiredBufferPaths.add(bufferState.filePath) + null bufferPromises = (handleBufferState(bufferState) for bufferState in state.buffers) @@ -465,11 +455,13 @@ class Project extends Model # 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() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b97e63957..a84f6f631 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -128,7 +128,10 @@ class TextEditor extends Model state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer try - state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) + 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' diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 8fca6c06b..e4d954a59 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -23,11 +23,15 @@ class TokenizedBuffer extends Model changeCount: 0 @deserialize: (state, atomEnvironment) -> + buffer = null if state.bufferId - state.buffer = atomEnvironment.project.bufferForIdSync(state.bufferId) + buffer = atomEnvironment.project.bufferForIdSync(state.bufferId) else # TODO: remove this fallback after everyone transitions to the latest version. - state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) + buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) + return null unless buffer? + + state.buffer = buffer state.assert = atomEnvironment.assert new this(state) From 1a53ad5722854e98ba55304a4707cd0f8164d96c Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 19 Sep 2017 15:29:28 -0400 Subject: [PATCH 199/448] Deserialize TextBuffers that have never been saved to disk --- spec/project-spec.coffee | 20 ++++++++++++++++++++ src/project.coffee | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index 40646e686..e6d94817d 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -143,6 +143,26 @@ describe "Project", -> 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') diff --git a/src/project.coffee b/src/project.coffee index 7e5cf975e..b8fd56e32 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -65,7 +65,11 @@ class Project extends Model handleBufferState = (bufferState) => bufferState.shouldDestroyOnFileDelete ?= -> atom.config.get('core.closeDeletedFileTabs') - bufferState.mustExist = true + + # 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) From 99feb7f2643d1e3019c7ef6337d3ead9a6daf8b6 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 20 Sep 2017 08:34:46 -0400 Subject: [PATCH 200/448] Spelling --- spec/project-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee index e6d94817d..1f5eb54a4 100644 --- a/spec/project-spec.coffee +++ b/spec/project-spec.coffee @@ -47,7 +47,7 @@ describe "Project", -> state = atom.project.serialize() fs.rmdirSync(childPath) - fs.writeFileSync(childPath, 'suprise!\n') + fs.writeFileSync(childPath, 'surprise!\n') err = null waitsForPromise -> From 74268cba03dacb0b39b6a03bff227c50e5b086ab Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 20 Sep 2017 10:43:35 -0400 Subject: [PATCH 201/448] Reset retained IDs and paths on .reset() --- src/project.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/project.coffee b/src/project.coffee index b8fd56e32..e29f35160 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -49,6 +49,8 @@ class Project extends Model @buffers = [] @setPaths([]) @loadPromisesByPath = {} + @retiredBufferIDs = new Set() + @retiredBufferPaths = new Set() @consumeServices(packageManager) destroyUnretainedBuffers: -> From 181b5b88287498003f2c715fe564d1dad0842935 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 20 Sep 2017 10:43:57 -0400 Subject: [PATCH 202/448] Pass exact: option from setPaths() to addPath() --- src/project.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index e29f35160..61ccf6b79 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -83,7 +83,7 @@ class Project extends Model Promise.all(bufferPromises).then (buffers) => @buffers = buffers.filter(Boolean) @subscribeToBuffer(buffer) for buffer in @buffers - @setPaths(state.paths or [], mustExist: true) + @setPaths(state.paths or [], mustExist: true, exact: true) serialize: (options={}) -> deserializer: 'Project' @@ -239,7 +239,7 @@ class Project extends Model missingProjectPaths = [] for projectPath in projectPaths try - @addPath projectPath, emitEvent: false, mustExist: true, exact: true + @addPath projectPath, emitEvent: false, mustExist: true, exact: options.exact is true added = true catch e if e.missingProjectPaths? From 880188b4b997e5da28b135a2c5e6d5b94eabe70e Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 20 Sep 2017 10:52:33 -0400 Subject: [PATCH 203/448] :shirt: Unnecessary fat arrows --- spec/atom-environment-spec.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 03d65a3d6..f178bbb6c 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -325,7 +325,7 @@ describe "AtomEnvironment", -> describe "deserialization failures", -> it "propagates project state restoration failures", -> - spyOn(atom.project, 'deserialize').andCallFake => + spyOn(atom.project, 'deserialize').andCallFake -> err = new Error('deserialization failure') err.missingProjectPaths = ['/foo'] Promise.reject(err) @@ -337,7 +337,7 @@ describe "AtomEnvironment", -> {description: 'Project directory `/foo` is no longer on disk.'} it "accumulates and reports two errors with one notification", -> - spyOn(atom.project, 'deserialize').andCallFake => + spyOn(atom.project, 'deserialize').andCallFake -> err = new Error('deserialization failure') err.missingProjectPaths = ['/foo', '/wat'] Promise.reject(err) @@ -349,7 +349,7 @@ describe "AtomEnvironment", -> {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 => + spyOn(atom.project, 'deserialize').andCallFake -> err = new Error('deserialization failure') err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things'] Promise.reject(err) From 817c98619c1581001db717b25796195462844d10 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 20 Sep 2017 11:43:35 -0400 Subject: [PATCH 204/448] Always emit did-change-paths --- src/project.coffee | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/project.coffee b/src/project.coffee index 61ccf6b79..ab41f9eb3 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -235,20 +235,17 @@ class Project extends Model watcher.then((w) -> w.dispose()) for _, watcher in @watcherPromisesByPath @watcherPromisesByPath = {} - added = false missingProjectPaths = [] for projectPath in projectPaths try @addPath projectPath, emitEvent: false, mustExist: true, exact: options.exact is true - added = true catch e if e.missingProjectPaths? missingProjectPaths.push e.missingProjectPaths... else throw e - if added - @emitter.emit 'did-change-paths', projectPaths + @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" From ea6478583ec251f639365da7efdb669dd29df35b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Sep 2017 10:13:05 -0600 Subject: [PATCH 205/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f4702c10..dadadbc07 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.3.3", + "text-buffer": "13.3.4", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From ba7bbdcc38c62f2b1d0017ef21935a164950ecb4 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 20 Sep 2017 14:26:30 -0700 Subject: [PATCH 206/448] :arrow_up: settings-view@0.251.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dadadbc07..fa3a5bb37 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.8", + "settings-view": "0.251.9", "snippets": "1.1.4", "spell-check": "0.72.2", "status-bar": "1.8.13", From a80c82d6daa37e3c59d89e500dc6ef324d756f67 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Sep 2017 11:57:18 +0200 Subject: [PATCH 207/448] :arrow_up: autocomplete-html@0.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa3a5bb37..5e631ae55 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "archive-view": "0.63.3", "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", - "autocomplete-html": "0.8.1", + "autocomplete-html": "0.8.2", "autocomplete-plus": "2.35.10", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", From 6d2761fea77d1de96ecc52d277c96a9a00fa7f54 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Sep 2017 19:24:04 +0200 Subject: [PATCH 208/448] :arrow_up: language-html@0.48.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e631ae55..28afafeac 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.1", "language-git": "0.19.1", "language-go": "0.44.2", - "language-html": "0.47.7", + "language-html": "0.48.0", "language-hyperlink": "0.16.2", "language-java": "0.27.4", "language-javascript": "0.127.3", From 2c26a6c1708d0c0c79913b31c31a4f488d73edc1 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Sep 2017 20:16:13 +0200 Subject: [PATCH 209/448] :arrow_down: language-html@0.47.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 28afafeac..5e631ae55 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.1", "language-git": "0.19.1", "language-go": "0.44.2", - "language-html": "0.48.0", + "language-html": "0.47.7", "language-hyperlink": "0.16.2", "language-java": "0.27.4", "language-javascript": "0.127.3", From 99924bf1e3b36881a9ab7b2764952d5b7f8baa41 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Sep 2017 20:47:13 +0200 Subject: [PATCH 210/448] :arrow_up: language-html@0.48.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e631ae55..28afafeac 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.1", "language-git": "0.19.1", "language-go": "0.44.2", - "language-html": "0.47.7", + "language-html": "0.48.0", "language-hyperlink": "0.16.2", "language-java": "0.27.4", "language-javascript": "0.127.3", From 72939ee94725927b05d466ab5a8e0cb39554d7ea Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Sep 2017 20:47:37 +0200 Subject: [PATCH 211/448] :arrow_up: language-javascript@0.127.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 28afafeac..0999c2b90 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "language-html": "0.48.0", "language-hyperlink": "0.16.2", "language-java": "0.27.4", - "language-javascript": "0.127.3", + "language-javascript": "0.127.4", "language-json": "0.19.1", "language-less": "0.33.0", "language-make": "0.22.3", From 41614501315f5cc059b5ce1be5a37ec3d22874c4 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Sep 2017 20:57:08 +0200 Subject: [PATCH 212/448] :arrow_up: language-mustache@0.14.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0999c2b90..44e640eee 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "language-json": "0.19.1", "language-less": "0.33.0", "language-make": "0.22.3", - "language-mustache": "0.14.1", + "language-mustache": "0.14.2", "language-objective-c": "0.15.1", "language-perl": "0.37.0", "language-php": "0.42.0", From 129c0cc0e38aa34d44feaf71558e6e8e24f12243 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Sep 2017 20:57:15 +0200 Subject: [PATCH 213/448] Fix tokenized buffer spec --- spec/tokenized-buffer-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 444004e2b..2c2379810 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -345,7 +345,7 @@ describe "TokenizedBuffer", -> runs -> fullyTokenize(tokenizedBuffer) {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby", "meta.tag.block.any.html", "punctuation.definition.tag.begin.html"] + expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby", "meta.tag.block.div.html", "punctuation.definition.tag.begin.html"] describe ".tokenForPosition(position)", -> afterEach -> From 8e300643fa5c532976a4c1148bfd5d03d3c493b7 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Thu, 21 Sep 2017 22:04:49 +0200 Subject: [PATCH 214/448] :arrow_up: language-javascript@0.127.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44e640eee..ffc655280 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "language-html": "0.48.0", "language-hyperlink": "0.16.2", "language-java": "0.27.4", - "language-javascript": "0.127.4", + "language-javascript": "0.127.5", "language-json": "0.19.1", "language-less": "0.33.0", "language-make": "0.22.3", From 67254766d75a8e69d560665c258acdd27779b284 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 10:21:41 -0700 Subject: [PATCH 215/448] Convert TokenizedBuffer to JS --- src/tokenized-buffer.coffee | 455 ---------------------------- src/tokenized-buffer.js | 586 ++++++++++++++++++++++++++++++++++++ 2 files changed, 586 insertions(+), 455 deletions(-) delete mode 100644 src/tokenized-buffer.coffee create mode 100644 src/tokenized-buffer.js diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee deleted file mode 100644 index e4d954a59..000000000 --- a/src/tokenized-buffer.coffee +++ /dev/null @@ -1,455 +0,0 @@ -_ = require 'underscore-plus' -{CompositeDisposable, Emitter} = require 'event-kit' -{Point, Range} = require 'text-buffer' -Model = require './model' -TokenizedLine = require './tokenized-line' -TokenIterator = require './token-iterator' -ScopeDescriptor = require './scope-descriptor' -TokenizedBufferIterator = require './tokenized-buffer-iterator' -NullGrammar = require './null-grammar' -{toFirstMateScopeId} = require './first-mate-helpers' - -prefixedScopes = new Map() - -module.exports = -class TokenizedBuffer extends Model - grammar: null - buffer: null - tabLength: null - tokenizedLines: null - chunkSize: 50 - invalidRows: null - visible: false - changeCount: 0 - - @deserialize: (state, atomEnvironment) -> - buffer = null - if state.bufferId - buffer = atomEnvironment.project.bufferForIdSync(state.bufferId) - else - # TODO: remove this fallback after everyone transitions to the latest version. - buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) - return null unless buffer? - - state.buffer = buffer - state.assert = atomEnvironment.assert - new this(state) - - constructor: (params) -> - {grammar, @buffer, @tabLength, @largeFileMode, @assert} = params - - @emitter = new Emitter - @disposables = new CompositeDisposable - @tokenIterator = new TokenIterator(this) - - @disposables.add @buffer.registerTextDecorationLayer(this) - - @setGrammar(grammar ? NullGrammar) - - destroyed: -> - @disposables.dispose() - @tokenizedLines.length = 0 - - buildIterator: -> - new TokenizedBufferIterator(this) - - classNameForScopeId: (id) -> - scope = @grammar.scopeForId(toFirstMateScopeId(id)) - if scope - prefixedScope = prefixedScopes.get(scope) - if prefixedScope - prefixedScope - else - prefixedScope = "syntax--#{scope.replace(/\./g, ' syntax--')}" - prefixedScopes.set(scope, prefixedScope) - prefixedScope - else - null - - getInvalidatedRanges: -> - [] - - onDidInvalidateRange: (fn) -> - @emitter.on 'did-invalidate-range', fn - - serialize: -> - { - deserializer: 'TokenizedBuffer' - bufferPath: @buffer.getPath() - bufferId: @buffer.getId() - tabLength: @tabLength - largeFileMode: @largeFileMode - } - - observeGrammar: (callback) -> - callback(@grammar) - @onDidChangeGrammar(callback) - - onDidChangeGrammar: (callback) -> - @emitter.on 'did-change-grammar', callback - - onDidTokenize: (callback) -> - @emitter.on 'did-tokenize', callback - - setGrammar: (grammar) -> - return unless grammar? and grammar isnt @grammar - - @grammar = grammar - @rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName]) - - @grammarUpdateDisposable?.dispose() - @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines() - @disposables.add(@grammarUpdateDisposable) - - @retokenizeLines() - - @emitter.emit 'did-change-grammar', grammar - - getGrammarSelectionContent: -> - @buffer.getTextInRange([[0, 0], [10, 0]]) - - hasTokenForSelector: (selector) -> - for tokenizedLine in @tokenizedLines when tokenizedLine? - for token in tokenizedLine.tokens - return true if selector.matches(token.scopes) - false - - retokenizeLines: -> - return unless @alive - @fullyTokenized = false - @tokenizedLines = new Array(@buffer.getLineCount()) - @invalidRows = [] - if @largeFileMode or @grammar.name is 'Null Grammar' - @markTokenizationComplete() - else - @invalidateRow(0) - - setVisible: (@visible) -> - if @visible and @grammar.name isnt 'Null Grammar' and not @largeFileMode - @tokenizeInBackground() - - getTabLength: -> @tabLength - - setTabLength: (@tabLength) -> - - tokenizeInBackground: -> - return if not @visible or @pendingChunk or not @isAlive() - - @pendingChunk = true - _.defer => - @pendingChunk = false - @tokenizeNextChunk() if @isAlive() and @buffer.isAlive() - - tokenizeNextChunk: -> - rowsRemaining = @chunkSize - - while @firstInvalidRow()? and rowsRemaining > 0 - startRow = @invalidRows.shift() - lastRow = @getLastRow() - continue if startRow > lastRow - - row = startRow - loop - previousStack = @stackForRow(row) - @tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row)) - if --rowsRemaining is 0 - filledRegion = false - endRow = row - break - if row is lastRow or _.isEqual(@stackForRow(row), previousStack) - filledRegion = true - endRow = row - break - row++ - - @validateRow(endRow) - @invalidateRow(endRow + 1) unless filledRegion - - @emitter.emit 'did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0)) - - if @firstInvalidRow()? - @tokenizeInBackground() - else - @markTokenizationComplete() - - markTokenizationComplete: -> - unless @fullyTokenized - @emitter.emit 'did-tokenize' - @fullyTokenized = true - - firstInvalidRow: -> - @invalidRows[0] - - validateRow: (row) -> - @invalidRows.shift() while @invalidRows[0] <= row - return - - invalidateRow: (row) -> - @invalidRows.push(row) - @invalidRows.sort (a, b) -> a - b - @tokenizeInBackground() - - updateInvalidRows: (start, end, delta) -> - @invalidRows = @invalidRows.map (row) -> - if row < start - row - else if start <= row <= end - end + delta + 1 - else if row > end - row + delta - - bufferDidChange: (e) -> - @changeCount = @buffer.changeCount - - {oldRange, newRange} = e - start = oldRange.start.row - end = oldRange.end.row - delta = newRange.end.row - oldRange.end.row - oldLineCount = oldRange.end.row - oldRange.start.row + 1 - newLineCount = newRange.end.row - newRange.start.row + 1 - - @updateInvalidRows(start, end, delta) - previousEndStack = @stackForRow(end) # used in spill detection below - if @largeFileMode or @grammar.name is 'Null Grammar' - _.spliceWithArray(@tokenizedLines, start, oldLineCount, new Array(newLineCount)) - else - newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start)) - _.spliceWithArray(@tokenizedLines, start, oldLineCount, newTokenizedLines) - newEndStack = @stackForRow(end + delta) - if newEndStack and not _.isEqual(newEndStack, previousEndStack) - @invalidateRow(end + delta + 1) - - isFoldableAtRow: (row) -> - @isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row) - - # Returns a {Boolean} indicating whether the given buffer row starts - # a a foldable row range due to the code's indentation patterns. - isFoldableCodeAtRow: (row) -> - if 0 <= row <= @buffer.getLastRow() - nextRow = @buffer.nextNonBlankRow(row) - tokenizedLine = @tokenizedLines[row] - if @buffer.isRowBlank(row) or tokenizedLine?.isComment() or not nextRow? - false - else - @indentLevelForRow(nextRow) > @indentLevelForRow(row) - else - false - - isFoldableCommentAtRow: (row) -> - previousRow = row - 1 - nextRow = row + 1 - if nextRow > @buffer.getLastRow() - false - else - Boolean( - not (@tokenizedLines[previousRow]?.isComment()) and - @tokenizedLines[row]?.isComment() and - @tokenizedLines[nextRow]?.isComment() - ) - - buildTokenizedLinesForRows: (startRow, endRow, startingStack, startingopenScopes) -> - ruleStack = startingStack - openScopes = startingopenScopes - stopTokenizingAt = startRow + @chunkSize - tokenizedLines = for row in [startRow..endRow] by 1 - if (ruleStack or row is 0) and row < stopTokenizingAt - tokenizedLine = @buildTokenizedLineForRow(row, ruleStack, openScopes) - ruleStack = tokenizedLine.ruleStack - openScopes = @scopesFromTags(openScopes, tokenizedLine.tags) - else - tokenizedLine = undefined - tokenizedLine - - if endRow >= stopTokenizingAt - @invalidateRow(stopTokenizingAt) - @tokenizeInBackground() - - tokenizedLines - - buildTokenizedLineForRow: (row, ruleStack, openScopes) -> - @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes) - - buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> - lineEnding = @buffer.lineEndingForRow(row) - {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) - new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator, @grammar}) - - tokenizedLineForRow: (bufferRow) -> - if 0 <= bufferRow <= @buffer.getLastRow() - if tokenizedLine = @tokenizedLines[bufferRow] - tokenizedLine - else - text = @buffer.lineForRow(bufferRow) - lineEnding = @buffer.lineEndingForRow(bufferRow) - tags = [@grammar.startIdForScope(@grammar.scopeName), text.length, @grammar.endIdForScope(@grammar.scopeName)] - @tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator, @grammar}) - - tokenizedLinesForRows: (startRow, endRow) -> - for row in [startRow..endRow] by 1 - @tokenizedLineForRow(row) - - stackForRow: (bufferRow) -> - @tokenizedLines[bufferRow]?.ruleStack - - openScopesForRow: (bufferRow) -> - if precedingLine = @tokenizedLines[bufferRow - 1] - @scopesFromTags(precedingLine.openScopes, precedingLine.tags) - else - [] - - scopesFromTags: (startingScopes, tags) -> - scopes = startingScopes.slice() - for tag in tags when tag < 0 - if (tag % 2) is -1 - scopes.push(tag) - else - matchingStartTag = tag + 1 - loop - break if scopes.pop() is matchingStartTag - if scopes.length is 0 - @assert false, "Encountered an unmatched scope end tag.", (error) => - error.metadata = { - grammarScopeName: @grammar.scopeName - unmatchedEndTag: @grammar.scopeForId(tag) - } - path = require 'path' - error.privateMetadataDescription = "The contents of `#{path.basename(@buffer.getPath())}`" - error.privateMetadata = { - filePath: @buffer.getPath() - fileContents: @buffer.getText() - } - break - scopes - - indentLevelForRow: (bufferRow) -> - line = @buffer.lineForRow(bufferRow) - indentLevel = 0 - - if line is '' - nextRow = bufferRow + 1 - lineCount = @getLineCount() - while nextRow < lineCount - nextLine = @buffer.lineForRow(nextRow) - unless nextLine is '' - indentLevel = Math.ceil(@indentLevelForLine(nextLine)) - break - nextRow++ - - previousRow = bufferRow - 1 - while previousRow >= 0 - previousLine = @buffer.lineForRow(previousRow) - unless previousLine is '' - indentLevel = Math.max(Math.ceil(@indentLevelForLine(previousLine)), indentLevel) - break - previousRow-- - - indentLevel - else - @indentLevelForLine(line) - - indentLevelForLine: (line) -> - indentLength = 0 - for char in line - if char is '\t' - indentLength += @getTabLength() - (indentLength % @getTabLength()) - else if char is ' ' - indentLength++ - else - break - - indentLength / @getTabLength() - - scopeDescriptorForPosition: (position) -> - {row, column} = @buffer.clipPosition(Point.fromObject(position)) - - iterator = @tokenizedLineForRow(row).getTokenIterator() - while iterator.next() - if iterator.getBufferEnd() > column - scopes = iterator.getScopes() - break - - # rebuild scope of last token if we iterated off the end - unless scopes? - scopes = iterator.getScopes() - scopes.push(iterator.getScopeEnds().reverse()...) - - new ScopeDescriptor({scopes}) - - tokenForPosition: (position) -> - {row, column} = Point.fromObject(position) - @tokenizedLineForRow(row).tokenAtBufferColumn(column) - - tokenStartPositionForPosition: (position) -> - {row, column} = Point.fromObject(position) - column = @tokenizedLineForRow(row).tokenStartColumnForBufferColumn(column) - new Point(row, column) - - bufferRangeForScopeAtPosition: (selector, position) -> - position = Point.fromObject(position) - - {openScopes, tags} = @tokenizedLineForRow(position.row) - scopes = openScopes.map (tag) => @grammar.scopeForId(tag) - - startColumn = 0 - for tag, tokenIndex in tags - if tag < 0 - if tag % 2 is -1 - scopes.push(@grammar.scopeForId(tag)) - else - scopes.pop() - else - endColumn = startColumn + tag - if endColumn >= position.column - break - else - startColumn = endColumn - - - return unless selectorMatchesAnyScope(selector, scopes) - - startScopes = scopes.slice() - for startTokenIndex in [(tokenIndex - 1)..0] by -1 - tag = tags[startTokenIndex] - if tag < 0 - if tag % 2 is -1 - startScopes.pop() - else - startScopes.push(@grammar.scopeForId(tag)) - else - break unless selectorMatchesAnyScope(selector, startScopes) - startColumn -= tag - - endScopes = scopes.slice() - for endTokenIndex in [(tokenIndex + 1)...tags.length] by 1 - tag = tags[endTokenIndex] - if tag < 0 - if tag % 2 is -1 - endScopes.push(@grammar.scopeForId(tag)) - else - endScopes.pop() - else - break unless selectorMatchesAnyScope(selector, endScopes) - endColumn += tag - - new Range(new Point(position.row, startColumn), new Point(position.row, endColumn)) - - # Gets the row number of the last line. - # - # Returns a {Number}. - getLastRow: -> - @buffer.getLastRow() - - getLineCount: -> - @buffer.getLineCount() - - logLines: (start=0, end=@buffer.getLastRow()) -> - for row in [start..end] - line = @tokenizedLines[row].text - console.log row, line, line.length - return - -selectorMatchesAnyScope = (selector, scopes) -> - targetClasses = selector.replace(/^\./, '').split('.') - _.any scopes, (scope) -> - scopeClasses = scope.split('.') - _.isSubset(targetClasses, scopeClasses) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js new file mode 100644 index 000000000..80601d1f3 --- /dev/null +++ b/src/tokenized-buffer.js @@ -0,0 +1,586 @@ +const _ = require('underscore-plus') +const {CompositeDisposable, Emitter} = require('event-kit') +const {Point, Range} = require('text-buffer') +const Model = require('./model') +const TokenizedLine = require('./tokenized-line') +const TokenIterator = require('./token-iterator') +const ScopeDescriptor = require('./scope-descriptor') +const TokenizedBufferIterator = require('./tokenized-buffer-iterator') +const NullGrammar = require('./null-grammar') +const {toFirstMateScopeId} = require('./first-mate-helpers') + +let nextId = 0 +const prefixedScopes = new Map() + +module.exports = +class TokenizedBuffer { + static deserialize (state, atomEnvironment) { + const buffer = atomEnvironment.project.bufferForIdSync(state.bufferId) + if (!buffer) return null + + state.buffer = buffer + state.assert = atomEnvironment.assert + return new TokenizedBuffer(state) + } + + constructor (params) { + this.emitter = new Emitter() + this.disposables = new CompositeDisposable() + this.tokenIterator = new TokenIterator(this) + + this.alive = true + this.id = params.id != null ? params.id : nextId++ + this.buffer = params.buffer + this.tabLength = params.tabLength + this.largeFileMode = params.largeFileMode + this.assert = params.assert + + this.setGrammar(params.grammar || NullGrammar) + this.disposables.add(this.buffer.registerTextDecorationLayer(this)) + } + + destroy () { + if (!this.alive) return + this.alive = false + this.disposables.dispose() + this.tokenizedLines.length = 0 + } + + isAlive () { + return this.alive + } + + isDestroyed () { + return !this.alive + } + + buildIterator () { + return new TokenizedBufferIterator(this) + } + + classNameForScopeId (id) { + const scope = this.grammar.scopeForId(toFirstMateScopeId(id)) + if (scope) { + let prefixedScope = prefixedScopes.get(scope) + if (prefixedScope) { + return prefixedScope + } else { + prefixedScope = `syntax--${scope.replace(/\./g, ' syntax--')}` + prefixedScopes.set(scope, prefixedScope) + return prefixedScope + } + } else { + return null + } + } + + getInvalidatedRanges () { + return [] + } + + onDidInvalidateRange (fn) { + return this.emitter.on('did-invalidate-range', fn) + } + + serialize () { + return { + deserializer: 'TokenizedBuffer', + bufferPath: this.buffer.getPath(), + bufferId: this.buffer.getId(), + tabLength: this.tabLength, + largeFileMode: this.largeFileMode + } + } + + observeGrammar (callback) { + callback(this.grammar) + return this.onDidChangeGrammar(callback) + } + + onDidChangeGrammar (callback) { + return this.emitter.on('did-change-grammar', callback) + } + + onDidTokenize (callback) { + return this.emitter.on('did-tokenize', callback) + } + + setGrammar (grammar) { + if (!grammar || grammar === this.grammar) return + + this.grammar = grammar + this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]}) + + if (this.grammarUpdateDisposable) this.grammarUpdateDisposable.dispose() + this.grammarUpdateDisposable = this.grammar.onDidUpdate(() => this.retokenizeLines()) + this.disposables.add(this.grammarUpdateDisposable) + + this.retokenizeLines() + this.emitter.emit('did-change-grammar', grammar) + } + + getGrammarSelectionContent () { + return this.buffer.getTextInRange([[0, 0], [10, 0]]) + } + + hasTokenForSelector (selector) { + for (const tokenizedLine of this.tokenizedLines) { + if (tokenizedLine) { + for (let token of tokenizedLine.tokens) { + if (selector.matches(token.scopes)) return true + } + } + } + return false + } + + retokenizeLines () { + if (!this.alive) return + this.fullyTokenized = false + this.tokenizedLines = new Array(this.buffer.getLineCount()) + this.invalidRows = [] + if (this.largeFileMode || this.grammar.name === 'Null Grammar') { + this.markTokenizationComplete() + } else { + this.invalidateRow(0) + } + } + + setVisible (visible) { + this.visible = visible + if (this.visible && this.grammar.name !== 'Null Grammar' && !this.largeFileMode) { + this.tokenizeInBackground() + } + } + + getTabLength () { return this.tabLength } + + setTabLength (tabLength) { + this.tabLength = tabLength + } + + tokenizeInBackground () { + if (!this.visible || this.pendingChunk || !this.alive) return + + this.pendingChunk = true + _.defer(() => { + this.pendingChunk = false + if (this.isAlive() && this.buffer.isAlive()) this.tokenizeNextChunk() + }) + } + + tokenizeNextChunk () { + let rowsRemaining = this.chunkSize + + while (this.firstInvalidRow() != null && rowsRemaining > 0) { + var endRow, filledRegion + const startRow = this.invalidRows.shift() + const lastRow = this.getLastRow() + if (startRow > lastRow) continue + + let row = startRow + while (true) { + const previousStack = this.stackForRow(row) + this.tokenizedLines[row] = this.buildTokenizedLineForRow(row, this.stackForRow(row - 1), this.openScopesForRow(row)) + if (--rowsRemaining === 0) { + filledRegion = false + endRow = row + break + } + if (row === lastRow || _.isEqual(this.stackForRow(row), previousStack)) { + filledRegion = true + endRow = row + break + } + row++ + } + + this.validateRow(endRow) + if (!filledRegion) this.invalidateRow(endRow + 1) + + this.emitter.emit('did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0))) + } + + if (this.firstInvalidRow() != null) { + this.tokenizeInBackground() + } else { + this.markTokenizationComplete() + } + } + + markTokenizationComplete () { + if (!this.fullyTokenized) { + this.emitter.emit('did-tokenize') + } + this.fullyTokenized = true + } + + firstInvalidRow () { + return this.invalidRows[0] + } + + validateRow (row) { + while (this.invalidRows[0] <= row) this.invalidRows.shift() + } + + invalidateRow (row) { + this.invalidRows.push(row) + this.invalidRows.sort((a, b) => a - b) + this.tokenizeInBackground() + } + + updateInvalidRows (start, end, delta) { + this.invalidRows = this.invalidRows.map((row) => { + if (row < start) { + return row + } else if (start <= row && row <= end) { + return end + delta + 1 + } else if (row > end) { + return row + delta + } + }) + } + + bufferDidChange (e) { + this.changeCount = this.buffer.changeCount + + const {oldRange, newRange} = e + const start = oldRange.start.row + const end = oldRange.end.row + const delta = newRange.end.row - oldRange.end.row + const oldLineCount = (oldRange.end.row - oldRange.start.row) + 1 + const newLineCount = (newRange.end.row - newRange.start.row) + 1 + + this.updateInvalidRows(start, end, delta) + const previousEndStack = this.stackForRow(end) // used in spill detection below + if (this.largeFileMode || (this.grammar.name === 'Null Grammar')) { + _.spliceWithArray(this.tokenizedLines, start, oldLineCount, new Array(newLineCount)) + } else { + const newTokenizedLines = this.buildTokenizedLinesForRows(start, end + delta, this.stackForRow(start - 1), this.openScopesForRow(start)) + _.spliceWithArray(this.tokenizedLines, start, oldLineCount, newTokenizedLines) + const newEndStack = this.stackForRow(end + delta) + if (newEndStack && !_.isEqual(newEndStack, previousEndStack)) { + this.invalidateRow(end + delta + 1) + } + } + } + + isFoldableAtRow (row) { + return this.isFoldableCodeAtRow(row) || this.isFoldableCommentAtRow(row) + } + + // Returns a {Boolean} indicating whether the given buffer row starts + // a a foldable row range due to the code's indentation patterns. + isFoldableCodeAtRow (row) { + if (row >= 0 && row <= this.buffer.getLastRow()) { + const nextRow = this.buffer.nextNonBlankRow(row) + const tokenizedLine = this.tokenizedLines[row] + if (this.buffer.isRowBlank(row) || (tokenizedLine != null ? tokenizedLine.isComment() : undefined) || (nextRow == null)) { + return false + } else { + return this.indentLevelForRow(nextRow) > this.indentLevelForRow(row) + } + } else { + return false + } + } + + isFoldableCommentAtRow (row) { + const previousRow = row - 1 + const nextRow = row + 1 + if (nextRow > this.buffer.getLastRow()) { + return false + } else { + return Boolean( + !(this.tokenizedLines[previousRow] != null ? this.tokenizedLines[previousRow].isComment() : undefined) && + (this.tokenizedLines[row] != null ? this.tokenizedLines[row].isComment() : undefined) && + (this.tokenizedLines[nextRow] != null ? this.tokenizedLines[nextRow].isComment() : undefined) + ) + } + } + + buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { + let ruleStack = startingStack + let openScopes = startingopenScopes + const stopTokenizingAt = startRow + this.chunkSize + const tokenizedLines = [] + for (let row = startRow, end = endRow; row <= end; row++) { + let tokenizedLine + if ((ruleStack || (row === 0)) && row < stopTokenizingAt) { + tokenizedLine = this.buildTokenizedLineForRow(row, ruleStack, openScopes) + ruleStack = tokenizedLine.ruleStack + openScopes = this.scopesFromTags(openScopes, tokenizedLine.tags) + } + tokenizedLines.push(tokenizedLine) + } + + if (endRow >= stopTokenizingAt) { + this.invalidateRow(stopTokenizingAt) + this.tokenizeInBackground() + } + + return tokenizedLines + } + + buildTokenizedLineForRow (row, ruleStack, openScopes) { + return this.buildTokenizedLineForRowWithText(row, this.buffer.lineForRow(row), ruleStack, openScopes) + } + + buildTokenizedLineForRowWithText (row, text, currentRuleStack = this.stackForRow(row - 1), openScopes = this.openScopesForRow(row)) { + const lineEnding = this.buffer.lineEndingForRow(row); + const {tags, ruleStack} = this.grammar.tokenizeLine(text, currentRuleStack, row === 0, false) + return new TokenizedLine({ + openScopes, + text, + tags, + ruleStack, + lineEnding, + tokenIterator: this.tokenIterator, + grammar: this.grammar + }) + } + + tokenizedLineForRow (bufferRow) { + if (bufferRow >= 0 && bufferRow <= this.buffer.getLastRow()) { + const tokenizedLine = this.tokenizedLines[bufferRow] + if (tokenizedLine) { + return tokenizedLine + } else { + const text = this.buffer.lineForRow(bufferRow) + const lineEnding = this.buffer.lineEndingForRow(bufferRow) + const tags = [ + this.grammar.startIdForScope(this.grammar.scopeName), + text.length, + this.grammar.endIdForScope(this.grammar.scopeName) + ] + return this.tokenizedLines[bufferRow] = new TokenizedLine({ + openScopes: [], + text, + tags, + lineEnding, + tokenIterator: this.tokenIterator, + grammar: this.grammar + }) + } + } + } + + tokenizedLinesForRows (startRow, endRow) { + const result = [] + for (let row = startRow, end = endRow; row <= end; row++) { + result.push(this.tokenizedLineForRow(row)) + } + return result + } + + stackForRow (bufferRow) { + return this.tokenizedLines[bufferRow] && this.tokenizedLines[bufferRow].ruleStack + } + + openScopesForRow (bufferRow) { + const precedingLine = this.tokenizedLines[bufferRow - 1] + if (precedingLine) { + return this.scopesFromTags(precedingLine.openScopes, precedingLine.tags) + } else { + return [] + } + } + + scopesFromTags (startingScopes, tags) { + const scopes = startingScopes.slice() + for (const tag of tags) { + if (tag < 0) { + if (tag % 2 === -1) { + scopes.push(tag) + } else { + const matchingStartTag = tag + 1 + while (true) { + if (scopes.pop() === matchingStartTag) break + if (scopes.length === 0) { + this.assert(false, 'Encountered an unmatched scope end tag.', error => { + error.metadata = { + grammarScopeName: this.grammar.scopeName, + unmatchedEndTag: this.grammar.scopeForId(tag) + } + const path = require('path') + error.privateMetadataDescription = `The contents of \`${path.basename(this.buffer.getPath())}\`` + return error.privateMetadata = { + filePath: this.buffer.getPath(), + fileContents: this.buffer.getText() + } + }) + break + } + } + } + } + } + return scopes + } + + indentLevelForRow (bufferRow) { + const line = this.buffer.lineForRow(bufferRow) + let indentLevel = 0 + + if (line === '') { + let nextRow = bufferRow + 1 + const lineCount = this.getLineCount() + while (nextRow < lineCount) { + const nextLine = this.buffer.lineForRow(nextRow) + if (nextLine !== '') { + indentLevel = Math.ceil(this.indentLevelForLine(nextLine)) + break + } + nextRow++ + } + + let previousRow = bufferRow - 1 + while (previousRow >= 0) { + const previousLine = this.buffer.lineForRow(previousRow) + if (previousLine !== '') { + indentLevel = Math.max(Math.ceil(this.indentLevelForLine(previousLine)), indentLevel) + break + } + previousRow-- + } + + return indentLevel + } else { + return this.indentLevelForLine(line) + } + } + + indentLevelForLine (line, tabLength = this.tabLength) { + let indentLength = 0 + for (let i = 0, {length} = line; i < length; i++) { + const char = line[i] + if (char === '\t') { + indentLength += tabLength - (indentLength % tabLength) + } else if (char === ' ') { + indentLength++ + } else { + break + } + } + return indentLength / tabLength + } + + scopeDescriptorForPosition (position) { + let scopes + const {row, column} = this.buffer.clipPosition(Point.fromObject(position)) + + const iterator = this.tokenizedLineForRow(row).getTokenIterator() + while (iterator.next()) { + if (iterator.getBufferEnd() > column) { + scopes = iterator.getScopes() + break + } + } + + // rebuild scope of last token if we iterated off the end + if (!scopes) { + scopes = iterator.getScopes() + scopes.push(...iterator.getScopeEnds().reverse()) + } + + return new ScopeDescriptor({scopes}) + } + + tokenForPosition (position) { + const {row, column} = Point.fromObject(position) + return this.tokenizedLineForRow(row).tokenAtBufferColumn(column) + } + + tokenStartPositionForPosition (position) { + let {row, column} = Point.fromObject(position) + column = this.tokenizedLineForRow(row).tokenStartColumnForBufferColumn(column) + return new Point(row, column) + } + + bufferRangeForScopeAtPosition (selector, position) { + let endColumn, tag, tokenIndex + position = Point.fromObject(position) + + const {openScopes, tags} = this.tokenizedLineForRow(position.row) + const scopes = openScopes.map(tag => this.grammar.scopeForId(tag)) + + let startColumn = 0 + for (tokenIndex = 0; tokenIndex < tags.length; tokenIndex++) { + tag = tags[tokenIndex] + if (tag < 0) { + if ((tag % 2) === -1) { + scopes.push(this.grammar.scopeForId(tag)) + } else { + scopes.pop() + } + } else { + endColumn = startColumn + tag + if (endColumn >= position.column) { + break + } else { + startColumn = endColumn + } + } + } + + if (!selectorMatchesAnyScope(selector, scopes)) return + + const startScopes = scopes.slice() + for (let startTokenIndex = tokenIndex - 1; startTokenIndex >= 0; startTokenIndex--) { + tag = tags[startTokenIndex] + if (tag < 0) { + if ((tag % 2) === -1) { + startScopes.pop() + } else { + startScopes.push(this.grammar.scopeForId(tag)) + } + } else { + if (!selectorMatchesAnyScope(selector, startScopes)) { break } + startColumn -= tag + } + } + + const endScopes = scopes.slice() + for (let endTokenIndex = tokenIndex + 1, end = tags.length; endTokenIndex < end; endTokenIndex++) { + tag = tags[endTokenIndex] + if (tag < 0) { + if ((tag % 2) === -1) { + endScopes.push(this.grammar.scopeForId(tag)) + } else { + endScopes.pop() + } + } else { + if (!selectorMatchesAnyScope(selector, endScopes)) { break } + endColumn += tag + } + } + + return new Range(new Point(position.row, startColumn), new Point(position.row, endColumn)) + } + + // Gets the row number of the last line. + // + // Returns a {Number}. + getLastRow () { + return this.buffer.getLastRow() + } + + getLineCount () { + return this.buffer.getLineCount() + } + + logLines (start = 0, end = this.buffer.getLastRow()) { + for (let row = start; row <= end1; row++) { + const line = this.tokenizedLines[row].text + console.log(row, line, line.length) + } + } +} + +function selectorMatchesAnyScope (selector, scopes) { + const targetClasses = selector.replace(/^\./, '').split('.') + return scopes.some((scope) => { + const scopeClasses = scope.split('.') + return _.isSubset(targetClasses, scopeClasses) + }) +} From 1ca1c545ba43460ff575a838acf3d470e8db02f0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 13:59:04 -0700 Subject: [PATCH 216/448] Convert TokenizedBuffer spec to JS --- spec/tokenized-buffer-spec.coffee | 688 -------------------------- spec/tokenized-buffer-spec.js | 779 ++++++++++++++++++++++++++++++ 2 files changed, 779 insertions(+), 688 deletions(-) delete mode 100644 spec/tokenized-buffer-spec.coffee create mode 100644 spec/tokenized-buffer-spec.js diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee deleted file mode 100644 index 2c2379810..000000000 --- a/spec/tokenized-buffer-spec.coffee +++ /dev/null @@ -1,688 +0,0 @@ -NullGrammar = require '../src/null-grammar' -TokenizedBuffer = require '../src/tokenized-buffer' -{Point} = TextBuffer = require 'text-buffer' -_ = require 'underscore-plus' - -describe "TokenizedBuffer", -> - [tokenizedBuffer, buffer] = [] - - beforeEach -> - # enable async tokenization - TokenizedBuffer.prototype.chunkSize = 5 - jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - tokenizedBuffer?.destroy() - - startTokenizing = (tokenizedBuffer) -> - tokenizedBuffer.setVisible(true) - - fullyTokenize = (tokenizedBuffer) -> - tokenizedBuffer.setVisible(true) - advanceClock() while tokenizedBuffer.firstInvalidRow()? - - describe "serialization", -> - describe "when the underlying buffer has a path", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - it "deserializes it searching among the buffers in the current project", -> - tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2}) - tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom) - expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) - - describe "when the underlying buffer has no path", -> - beforeEach -> - buffer = atom.project.bufferForPathSync(null) - - it "deserializes it searching among the buffers in the current project", -> - tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2}) - tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom) - expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) - - describe "when the buffer is destroyed", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - - it "stops tokenization", -> - tokenizedBuffer.destroy() - spyOn(tokenizedBuffer, 'tokenizeNextChunk') - advanceClock() - expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() - - describe "when the buffer contains soft-tabs", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - - afterEach -> - tokenizedBuffer.destroy() - buffer.release() - - describe "on construction", -> - it "tokenizes lines chunk at a time in the background", -> - line0 = tokenizedBuffer.tokenizedLines[0] - expect(line0).toBeUndefined() - - line11 = tokenizedBuffer.tokenizedLines[11] - expect(line11).toBeUndefined() - - # tokenize chunk 1 - advanceClock() - expect(tokenizedBuffer.tokenizedLines[0].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[4].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() - - # tokenize chunk 2 - advanceClock() - expect(tokenizedBuffer.tokenizedLines[5].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[9].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined() - - # tokenize last chunk - advanceClock() - expect(tokenizedBuffer.tokenizedLines[10].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[12].ruleStack?).toBeTruthy() - - describe "when the buffer is partially tokenized", -> - beforeEach -> - # tokenize chunk 1 only - advanceClock() - - describe "when there is a buffer change inside the tokenized region", -> - describe "when lines are added", -> - it "pushes the invalid rows down", -> - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - buffer.insert([1, 0], '\n\n') - expect(tokenizedBuffer.firstInvalidRow()).toBe 7 - - describe "when lines are removed", -> - it "pulls the invalid rows up", -> - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - buffer.delete([[1, 0], [3, 0]]) - expect(tokenizedBuffer.firstInvalidRow()).toBe 2 - - describe "when the change invalidates all the lines before the current invalid region", -> - it "retokenizes the invalidated lines and continues into the valid region", -> - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - buffer.insert([2, 0], '/*') - expect(tokenizedBuffer.firstInvalidRow()).toBe 3 - advanceClock() - expect(tokenizedBuffer.firstInvalidRow()).toBe 8 - - describe "when there is a buffer change surrounding an invalid row", -> - it "pushes the invalid row to the end of the change", -> - buffer.setTextInRange([[4, 0], [6, 0]], "\n\n\n") - expect(tokenizedBuffer.firstInvalidRow()).toBe 8 - - describe "when there is a buffer change inside an invalid region", -> - it "does not attempt to tokenize the lines in the change, and preserves the existing invalid row", -> - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - buffer.setTextInRange([[6, 0], [7, 0]], "\n\n\n") - expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined() - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - - describe "when the buffer is fully tokenized", -> - beforeEach -> - fullyTokenize(tokenizedBuffer) - - describe "when there is a buffer change that is smaller than the chunk size", -> - describe "when lines are updated, but none are added or removed", -> - it "updates tokens to reflect the change", -> - buffer.setTextInRange([[0, 0], [2, 0]], "foo()\n7\n") - - expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']) - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']) - # line 2 is unchanged - expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js']) - - describe "when the change invalidates the tokenization of subsequent lines", -> - it "schedules the invalidated lines to be tokenized in the background", -> - buffer.insert([5, 30], '/* */') - buffer.insert([2, 0], '/*') - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js'] - - advanceClock() - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - - it "resumes highlighting with the state of the previous line", -> - buffer.insert([0, 0], '/*') - buffer.insert([5, 0], '*/') - - buffer.insert([1, 0], 'var ') - expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - - describe "when lines are both updated and removed", -> - it "updates tokens to reflect the change", -> - buffer.setTextInRange([[1, 0], [3, 0]], "foo()") - - # previous line 0 remains - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.type.var.js']) - - # previous line 3 should be combined with input to form line 1 - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) - - # lines below deleted regions should be shifted upward - expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js']) - expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) - expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']) - - describe "when the change invalidates the tokenization of subsequent lines", -> - it "schedules the invalidated lines to be tokenized in the background", -> - buffer.insert([5, 30], '/* */') - buffer.setTextInRange([[2, 0], [3, 0]], '/*') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'] - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js'] - - advanceClock() - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - - describe "when lines are both updated and inserted", -> - it "updates tokens to reflect the change", -> - buffer.setTextInRange([[1, 0], [2, 0]], "foo()\nbar()\nbaz()\nquux()") - - # previous line 0 remains - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual( value: 'var', scopes: ['source.js', 'storage.type.var.js']) - - # 3 new lines inserted - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual(value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual(value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - - # previous line 2 is joined with quux() on line 4 - expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual(value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js']) - - # previous line 3 is pushed down to become line 5 - expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) - - describe "when the change invalidates the tokenization of subsequent lines", -> - it "schedules the invalidated lines to be tokenized in the background", -> - buffer.insert([5, 30], '/* */') - buffer.insert([2, 0], '/*\nabcde\nabcder') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'] - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js'] - - advanceClock() # tokenize invalidated lines in background - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe ['source.js', 'comment.block.js'] - - describe "when there is an insertion that is larger than the chunk size", -> - it "tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background", -> - commentBlock = _.multiplyString("// a comment\n", tokenizedBuffer.chunkSize + 2) - buffer.insert([0, 0], commentBlock) - expect(tokenizedBuffer.tokenizedLines[0].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[4].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() - - advanceClock() - expect(tokenizedBuffer.tokenizedLines[5].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[6].ruleStack?).toBeTruthy() - - it "does not break out soft tabs across a scope boundary", -> - waitsForPromise -> - atom.packages.activatePackage('language-gfm') - - runs -> - tokenizedBuffer.setTabLength(4) - tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) - buffer.setText(' 0 - - expect(length).toBe 4 - - describe "when the buffer contains hard-tabs", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - - afterEach -> - tokenizedBuffer.destroy() - buffer.release() - - describe "when the buffer is fully tokenized", -> - beforeEach -> - fullyTokenize(tokenizedBuffer) - - describe "when the grammar is tokenized", -> - it "emits the `tokenized` event", -> - editor = null - tokenizedHandler = jasmine.createSpy("tokenized handler") - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - tokenizedBuffer = editor.tokenizedBuffer - tokenizedBuffer.onDidTokenize tokenizedHandler - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - - it "doesn't re-emit the `tokenized` event when it is re-tokenized", -> - editor = null - tokenizedHandler = jasmine.createSpy("tokenized handler") - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - tokenizedBuffer = editor.tokenizedBuffer - fullyTokenize(tokenizedBuffer) - - tokenizedBuffer.onDidTokenize tokenizedHandler - editor.getBuffer().insert([0, 0], "'") - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler).not.toHaveBeenCalled() - - describe "when the grammar is updated because a grammar it includes is activated", -> - it "re-emits the `tokenized` event", -> - editor = null - tokenizedBuffer = null - tokenizedHandler = jasmine.createSpy("tokenized handler") - - waitsForPromise -> - atom.workspace.open('coffee.coffee').then (o) -> editor = o - - runs -> - tokenizedBuffer = editor.tokenizedBuffer - tokenizedBuffer.onDidTokenize tokenizedHandler - fullyTokenize(tokenizedBuffer) - tokenizedHandler.reset() - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - - it "retokenizes the buffer", -> - waitsForPromise -> - atom.packages.activatePackage('language-ruby-on-rails') - - waitsForPromise -> - atom.packages.activatePackage('language-ruby') - - runs -> - buffer = atom.project.bufferForPathSync() - buffer.setText "
<%= User.find(2).full_name %>
" - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual value: "
", scopes: ["text.html.ruby"] - - waitsForPromise -> - atom.packages.activatePackage('language-html') - - runs -> - fullyTokenize(tokenizedBuffer) - {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby", "meta.tag.block.div.html", "punctuation.definition.tag.begin.html"] - - describe ".tokenForPosition(position)", -> - afterEach -> - tokenizedBuffer.destroy() - buffer.release() - - it "returns the correct token (regression)", -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual ["source.js"] - expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual ["source.js"] - expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual ["source.js", "storage.type.var.js"] - - describe ".bufferRangeForScopeAtPosition(selector, position)", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - describe "when the selector does not match the token at the position", -> - it "returns a falsy value", -> - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined() - - describe "when the selector matches a single token at the position", -> - it "returns the range covered by the token", -> - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual [[0, 0], [0, 3]] - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual [[0, 0], [0, 3]] - - describe "when the selector matches a run of multiple tokens at the position", -> - it "returns the range covered by all contiguous tokens (within a single line)", -> - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]] - - describe ".indentLevelForRow(row)", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - describe "when the line is non-empty", -> - it "has an indent level based on the leading whitespace on the line", -> - expect(tokenizedBuffer.indentLevelForRow(0)).toBe 0 - expect(tokenizedBuffer.indentLevelForRow(1)).toBe 1 - expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2 - buffer.insert([2, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2.5 - - describe "when the line is empty", -> - it "assumes the indentation level of the first non-empty line below or above if one exists", -> - buffer.insert([12, 0], ' ') - buffer.insert([12, Infinity], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(14)).toBe 2 - - buffer.insert([1, Infinity], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(3)).toBe 2 - - buffer.setText('\n\n\n') - expect(tokenizedBuffer.indentLevelForRow(1)).toBe 0 - - describe "when the changed lines are surrounded by whitespace-only lines", -> - it "updates the indentLevel of empty lines that precede the change", -> - expect(tokenizedBuffer.indentLevelForRow(12)).toBe 0 - - buffer.insert([12, 0], '\n') - buffer.insert([13, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(12)).toBe 1 - - it "updates empty line indent guides when the empty line is the last line", -> - buffer.insert([12, 2], '\n') - - # The newline and the tab need to be in two different operations to surface the bug - buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe 1 - - buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 - expect(tokenizedBuffer.tokenizedLines[14]).not.toBeDefined() - - it "updates the indentLevel of empty lines surrounding a change that inserts lines", -> - buffer.insert([7, 0], '\n\n') - buffer.insert([5, 0], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe 3 - expect(tokenizedBuffer.indentLevelForRow(6)).toBe 3 - expect(tokenizedBuffer.indentLevelForRow(9)).toBe 3 - expect(tokenizedBuffer.indentLevelForRow(10)).toBe 3 - expect(tokenizedBuffer.indentLevelForRow(11)).toBe 2 - - buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe 4 - expect(tokenizedBuffer.indentLevelForRow(6)).toBe 4 - expect(tokenizedBuffer.indentLevelForRow(11)).toBe 4 - expect(tokenizedBuffer.indentLevelForRow(12)).toBe 4 - expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 - - it "updates the indentLevel of empty lines surrounding a change that removes lines", -> - buffer.insert([7, 0], '\n\n') - buffer.insert([5, 0], '\n\n') - buffer.setTextInRange([[7, 0], [8, 65]], ' ok') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(6)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(7)).toBe 2 # new text - expect(tokenizedBuffer.indentLevelForRow(8)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(9)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(10)).toBe 2 # } - - describe "::isFoldableAtRow(row)", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - buffer.insert [10, 0], " // multi-line\n // comment\n // block\n" - buffer.insert [0, 0], "// multi-line\n// comment\n// block\n" - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - it "includes the first line of multi-line comments", -> - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent - expect(tokenizedBuffer.isFoldableAtRow(13)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(14)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(15)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(16)).toBe false - - buffer.insert([0, Infinity], '\n') - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe false - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent - - it "includes non-comment lines that precede an increase in indentation", -> - buffer.insert([2, 0], ' ') # commented lines preceding an indent aren't foldable - - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(4)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(5)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - buffer.insert([7, 0], ' ') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - buffer.insert([7, 0], " \n x\n") - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - buffer.insert([9, 0], " ") - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - describe "::tokenizedLineForRow(row)", -> - it "returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", -> - buffer = atom.project.bufferForPathSync('sample.js') - grammar = atom.grammars.grammarForScopeName('source.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) - line0 = buffer.lineForRow(0) - - jsScopeStartId = grammar.startIdForScope(grammar.scopeName) - jsScopeEndId = grammar.endIdForScope(grammar.scopeName) - startTokenizing(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) - expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId]) - advanceClock(1) - expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined() - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) - expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId]) - - nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName) - nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName) - tokenizedBuffer.setGrammar(NullGrammar) - startTokenizing(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) - expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId]) - advanceClock(1) - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) - expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId]) - - it "returns undefined if the requested row is outside the buffer range", -> - buffer = atom.project.bufferForPathSync('sample.js') - grammar = atom.grammars.grammarForScopeName('source.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined() - - describe "when the buffer is configured with the null grammar", -> - it "does not actually tokenize using the grammar", -> - spyOn(NullGrammar, 'tokenizeLine').andCallThrough() - buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') - buffer.setText('a\nb\nc') - tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2}) - tokenizeCallback = jasmine.createSpy('onDidTokenize') - tokenizedBuffer.onDidTokenize(tokenizeCallback) - - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() - expect(tokenizeCallback.callCount).toBe(0) - expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() - expect(tokenizeCallback.callCount).toBe(0) - expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - - describe "text decoration layer API", -> - describe "iterator", -> - it "iterates over the syntactic scope boundaries", -> - buffer = new TextBuffer(text: "var foo = 1 /*\nhello*/var bar = 2\n") - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName("source.js"), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - iterator = tokenizedBuffer.buildIterator() - iterator.seek(Point(0, 0)) - - expectedBoundaries = [ - {position: Point(0, 0), closeTags: [], openTags: ["syntax--source syntax--js", "syntax--storage syntax--type syntax--var syntax--js"]} - {position: Point(0, 3), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []} - {position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]} - {position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} - {position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]} - {position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []} - {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"]} - {position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"], openTags: []} - {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js"]} - {position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]} - {position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []} - {position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]} - {position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} - {position: Point(1, 17), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]} - {position: Point(1, 18), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []} - ] - - loop - boundary = { - position: iterator.getPosition(), - closeTags: iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)), - openTags: iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)) - } - - expect(boundary).toEqual(expectedBoundaries.shift()) - break unless iterator.moveToSuccessor() - - expect(iterator.seek(Point(0, 1)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js", - "syntax--storage syntax--type syntax--var syntax--js" - ]) - expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.seek(Point(0, 8)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js" - ]) - expect(iterator.getPosition()).toEqual(Point(0, 8)) - expect(iterator.seek(Point(1, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js", - "syntax--comment syntax--block syntax--js" - ]) - expect(iterator.getPosition()).toEqual(Point(1, 0)) - expect(iterator.seek(Point(1, 18)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js", - "syntax--constant syntax--numeric syntax--decimal syntax--js" - ]) - expect(iterator.getPosition()).toEqual(Point(1, 18)) - - expect(iterator.seek(Point(2, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js" - ]) - iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test) - - it "does not report columns beyond the length of the line", -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - buffer = new TextBuffer(text: "# hello\n# world") - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName("source.coffee"), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - iterator = tokenizedBuffer.buildIterator() - iterator.seek(Point(0, 0)) - iterator.moveToSuccessor() - iterator.moveToSuccessor() - expect(iterator.getPosition().column).toBe(7) - - iterator.moveToSuccessor() - expect(iterator.getPosition().column).toBe(0) - - iterator.seek(Point(0, 7)) - expect(iterator.getPosition().column).toBe(7) - - iterator.seek(Point(0, 8)) - expect(iterator.getPosition().column).toBe(7) - - 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\nend x\nx') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - iterator = tokenizedBuffer.buildIterator() - iterator.seek(Point(1, 0)) - - expect(iterator.getPosition()).toEqual([1, 0]) - expect(iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--blue syntax--broken'] - expect(iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--yellow syntax--broken'] diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js new file mode 100644 index 000000000..783f5545c --- /dev/null +++ b/spec/tokenized-buffer-spec.js @@ -0,0 +1,779 @@ +const NullGrammar = require('../src/null-grammar') +const TokenizedBuffer = require('../src/tokenized-buffer') +const TextBuffer = require('text-buffer') +const {Point} = TextBuffer +const _ = require('underscore-plus') + +describe('TokenizedBuffer', () => { + let tokenizedBuffer, buffer + + beforeEach(() => { + // enable async tokenization + TokenizedBuffer.prototype.chunkSize = 5 + jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') + + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + }) + + afterEach(() => tokenizedBuffer && tokenizedBuffer.destroy()) + + function startTokenizing (tokenizedBuffer) { + tokenizedBuffer.setVisible(true) + } + + function fullyTokenize (tokenizedBuffer) { + tokenizedBuffer.setVisible(true) + while (tokenizedBuffer.firstInvalidRow() != null) { + advanceClock() + } + } + + describe('serialization', () => { + describe('when the underlying buffer has a path', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + }) + + it('deserializes it searching among the buffers in the current project', () => { + const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2}) + const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom) + expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) + }) + }) + + describe('when the underlying buffer has no path', () => { + beforeEach(() => buffer = atom.project.bufferForPathSync(null)) + + it('deserializes it searching among the buffers in the current project', () => { + const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2}) + const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom) + expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) + }) + }) + }) + + describe('when the buffer is destroyed', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) + + it('stops tokenization', () => { + tokenizedBuffer.destroy() + spyOn(tokenizedBuffer, 'tokenizeNextChunk') + advanceClock() + expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() + }) + }) + + describe('when the buffer contains soft-tabs', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) + + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) + + describe('on construction', () => + it('tokenizes lines chunk at a time in the background', () => { + const line0 = tokenizedBuffer.tokenizedLines[0] + expect(line0).toBeUndefined() + + const line11 = tokenizedBuffer.tokenizedLines[11] + expect(line11).toBeUndefined() + + // tokenize chunk 1 + advanceClock() + expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() + + // tokenize chunk 2 + advanceClock() + expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined() + + // tokenize last chunk + advanceClock() + expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy() + }) + ) + + describe('when the buffer is partially tokenized', () => { + beforeEach(() => { + // tokenize chunk 1 only + advanceClock() + }) + + describe('when there is a buffer change inside the tokenized region', () => { + describe('when lines are added', () => { + it('pushes the invalid rows down', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.insert([1, 0], '\n\n') + expect(tokenizedBuffer.firstInvalidRow()).toBe(7) + }) + }) + + describe('when lines are removed', () => { + it('pulls the invalid rows up', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.delete([[1, 0], [3, 0]]) + expect(tokenizedBuffer.firstInvalidRow()).toBe(2) + }) + }) + + describe('when the change invalidates all the lines before the current invalid region', () => { + it('retokenizes the invalidated lines and continues into the valid region', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.insert([2, 0], '/*') + expect(tokenizedBuffer.firstInvalidRow()).toBe(3) + advanceClock() + expect(tokenizedBuffer.firstInvalidRow()).toBe(8) + }) + }) + }) + + describe('when there is a buffer change surrounding an invalid row', () => { + it('pushes the invalid row to the end of the change', () => { + buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n') + expect(tokenizedBuffer.firstInvalidRow()).toBe(8) + }) + }) + + describe('when there is a buffer change inside an invalid region', () => { + it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n') + expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined() + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + }) + }) + }) + + describe('when the buffer is fully tokenized', () => { + beforeEach(() => fullyTokenize(tokenizedBuffer)) + + describe('when there is a buffer change that is smaller than the chunk size', () => { + describe('when lines are updated, but none are added or removed', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n') + + expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']}) + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']}) + // line 2 is unchanged + expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + }) + + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.insert([2, 0], '/*') + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + it('resumes highlighting with the state of the previous line', () => { + buffer.insert([0, 0], '/*') + buffer.insert([5, 0], '*/') + + buffer.insert([1, 0], 'var ') + expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + describe('when lines are both updated and removed', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[1, 0], [3, 0]], 'foo()') + + // previous line 0 remains + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + + // previous line 3 should be combined with input to form line 1 + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + + // lines below deleted regions should be shifted upward + expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']}) + expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']}) + }) + }) + + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.setTextInRange([[2, 0], [3, 0]], '/*') + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + describe('when lines are both updated and inserted', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()') + + // previous line 0 remains + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + + // 3 new lines inserted + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual({value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual({value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + + // previous line 2 is joined with quux() on line 4 + expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + + // previous line 3 is pushed down to become line 5 + expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + }) + }) + + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.insert([2, 0], '/*\nabcde\nabcder') + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() // tokenize invalidated lines in background + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js']) + }) + }) + }) + + describe('when there is an insertion that is larger than the chunk size', () => + it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => { + const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2) + buffer.insert([0, 0], commentBlock) + expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() + + advanceClock() + expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy() + }) + ) + + it('does not break out soft tabs across a scope boundary', () => { + waitsForPromise(() => atom.packages.activatePackage('language-gfm')) + + runs(() => { + tokenizedBuffer.setTabLength(4) + tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) + buffer.setText(' 0) { length += tag } + } + + expect(length).toBe(4) + }) + }) + }) + }) + + describe('when the buffer contains hard-tabs', () => { + beforeEach(() => { + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + + runs(() => { + buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) + }) + + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) + + describe('when the buffer is fully tokenized', () => { + beforeEach(() => fullyTokenize(tokenizedBuffer)) + }) + }) + + describe('when the grammar is tokenized', () => { + it('emits the `tokenized` event', () => { + let editor = null + const tokenizedHandler = jasmine.createSpy('tokenized handler') + + waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) + + runs(() => { + ({ tokenizedBuffer } = editor) + tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) + }) + }) + + it("doesn't re-emit the `tokenized` event when it is re-tokenized", () => { + let editor = null + const tokenizedHandler = jasmine.createSpy('tokenized handler') + + waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) + + runs(() => { + ({ tokenizedBuffer } = editor) + fullyTokenize(tokenizedBuffer) + + tokenizedBuffer.onDidTokenize(tokenizedHandler) + editor.getBuffer().insert([0, 0], "'") + fullyTokenize(tokenizedBuffer) + expect(tokenizedHandler).not.toHaveBeenCalled() + }) + }) + }) + + describe('when the grammar is updated because a grammar it includes is activated', () => { + it('re-emits the `tokenized` event', () => { + let editor = null + tokenizedBuffer = null + const tokenizedHandler = jasmine.createSpy('tokenized handler') + + waitsForPromise(() => atom.workspace.open('coffee.coffee').then(o => editor = o)) + + runs(() => { + ({ tokenizedBuffer } = editor) + tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(tokenizedBuffer) + tokenizedHandler.reset() + }) + + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + + runs(() => { + fullyTokenize(tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) + }) + }) + + it('retokenizes the buffer', () => { + waitsForPromise(() => atom.packages.activatePackage('language-ruby-on-rails')) + + waitsForPromise(() => atom.packages.activatePackage('language-ruby')) + + runs(() => { + buffer = atom.project.bufferForPathSync() + buffer.setText("
<%= User.find(2).full_name %>
") + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + + const {tokens} = tokenizedBuffer.tokenizedLines[0] + expect(tokens[0]).toEqual({value: "
", scopes: ['text.html.ruby']}) + }) + + waitsForPromise(() => atom.packages.activatePackage('language-html')) + + runs(() => { + fullyTokenize(tokenizedBuffer) + const {tokens} = tokenizedBuffer.tokenizedLines[0] + expect(tokens[0]).toEqual({value: '<', scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html']}) + }) + }) + }) + + describe('.tokenForPosition(position)', () => { + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) + + it('returns the correct token (regression)', () => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual(['source.js']) + expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual(['source.js']) + expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual(['source.js', 'storage.type.var.js']) + }) + }) + + describe('.bufferRangeForScopeAtPosition(selector, position)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + describe('when the selector does not match the token at the position', () => + it('returns a falsy value', () => expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined()) + ) + + describe('when the selector matches a single token at the position', () => { + it('returns the range covered by the token', () => { + expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual([[0, 0], [0, 3]]) + expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when the selector matches a run of multiple tokens at the position', () => { + it('returns the range covered by all contiguous tokens (within a single line)', () => { + expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual([[1, 6], [1, 28]]) + }) + }) + }) + + describe('.indentLevelForRow(row)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + describe('when the line is non-empty', () => { + it('has an indent level based on the leading whitespace on the line', () => { + expect(tokenizedBuffer.indentLevelForRow(0)).toBe(0) + expect(tokenizedBuffer.indentLevelForRow(1)).toBe(1) + expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2) + buffer.insert([2, 0], ' ') + expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2.5) + }) + }) + + describe('when the line is empty', () => { + it('assumes the indentation level of the first non-empty line below or above if one exists', () => { + buffer.insert([12, 0], ' ') + buffer.insert([12, Infinity], '\n\n') + expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(14)).toBe(2) + + buffer.insert([1, Infinity], '\n\n') + expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(3)).toBe(2) + + buffer.setText('\n\n\n') + expect(tokenizedBuffer.indentLevelForRow(1)).toBe(0) + }) + }) + + describe('when the changed lines are surrounded by whitespace-only lines', () => { + it('updates the indentLevel of empty lines that precede the change', () => { + expect(tokenizedBuffer.indentLevelForRow(12)).toBe(0) + + buffer.insert([12, 0], '\n') + buffer.insert([13, 0], ' ') + expect(tokenizedBuffer.indentLevelForRow(12)).toBe(1) + }) + + it('updates empty line indent guides when the empty line is the last line', () => { + buffer.insert([12, 2], '\n') + + // The newline and the tab need to be in two different operations to surface the bug + buffer.insert([12, 0], ' ') + expect(tokenizedBuffer.indentLevelForRow(13)).toBe(1) + + buffer.insert([12, 0], ' ') + expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) + expect(tokenizedBuffer.tokenizedLines[14]).not.toBeDefined() + }) + + it('updates the indentLevel of empty lines surrounding a change that inserts lines', () => { + buffer.insert([7, 0], '\n\n') + buffer.insert([5, 0], '\n\n') + expect(tokenizedBuffer.indentLevelForRow(5)).toBe(3) + expect(tokenizedBuffer.indentLevelForRow(6)).toBe(3) + expect(tokenizedBuffer.indentLevelForRow(9)).toBe(3) + expect(tokenizedBuffer.indentLevelForRow(10)).toBe(3) + expect(tokenizedBuffer.indentLevelForRow(11)).toBe(2) + + buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four') + expect(tokenizedBuffer.indentLevelForRow(5)).toBe(4) + expect(tokenizedBuffer.indentLevelForRow(6)).toBe(4) + expect(tokenizedBuffer.indentLevelForRow(11)).toBe(4) + expect(tokenizedBuffer.indentLevelForRow(12)).toBe(4) + expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) + }) + + it('updates the indentLevel of empty lines surrounding a change that removes lines', () => { + buffer.insert([7, 0], '\n\n') + buffer.insert([5, 0], '\n\n') + buffer.setTextInRange([[7, 0], [8, 65]], ' ok') + expect(tokenizedBuffer.indentLevelForRow(5)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(6)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(7)).toBe(2) // new text + expect(tokenizedBuffer.indentLevelForRow(8)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(9)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(10)).toBe(2) + }) + }) + }) // } + + describe('::isFoldableAtRow(row)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') + buffer.insert([0, 0], '// multi-line\n// comment\n// block\n') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + it('includes the first line of multi-line comments', () => { + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent + expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false) + + buffer.insert([0, Infinity], '\n') + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + }) // because of indent + + it('includes non-comment lines that precede an increase in indentation', () => { + buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable + + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' \n x\n') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([9, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + }) + }) + + describe('::tokenizedLineForRow(row)', () => { + it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { + buffer = atom.project.bufferForPathSync('sample.js') + const grammar = atom.grammars.grammarForScopeName('source.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) + const line0 = buffer.lineForRow(0) + + const jsScopeStartId = grammar.startIdForScope(grammar.scopeName) + const jsScopeEndId = grammar.endIdForScope(grammar.scopeName) + startTokenizing(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) + expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId]) + advanceClock(1) + expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined() + expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) + expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId]) + + const nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName) + const nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName) + tokenizedBuffer.setGrammar(NullGrammar) + startTokenizing(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) + expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId]) + advanceClock(1) + expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) + expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId]) + }) + + it('returns undefined if the requested row is outside the buffer range', () => { + buffer = atom.project.bufferForPathSync('sample.js') + const grammar = atom.grammars.grammarForScopeName('source.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined() + }) + }) + + describe('when the buffer is configured with the null grammar', () => { + it('does not actually tokenize using the grammar', () => { + spyOn(NullGrammar, 'tokenizeLine').andCallThrough() + buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') + buffer.setText('a\nb\nc') + tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2}) + const tokenizeCallback = jasmine.createSpy('onDidTokenize') + tokenizedBuffer.onDidTokenize(tokenizeCallback) + + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() + expect(tokenizeCallback.callCount).toBe(0) + expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() + + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() + expect(tokenizeCallback.callCount).toBe(0) + expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() + }) + }) + + describe('text decoration layer API', () => { + describe('iterator', () => { + it('iterates over the syntactic scope boundaries', () => { + buffer = new TextBuffer({text: 'var foo = 1 /*\nhello*/var bar = 2\n'}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + + const iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(0, 0)) + + const expectedBoundaries = [ + {position: Point(0, 0), closeTags: [], openTags: ['syntax--source syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']}, + {position: Point(0, 3), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []}, + {position: Point(0, 8), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']}, + {position: Point(0, 9), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []}, + {position: Point(0, 10), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']}, + {position: Point(0, 11), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []}, + {position: Point(0, 12), closeTags: [], openTags: ['syntax--comment syntax--block syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js']}, + {position: Point(0, 14), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js'], openTags: []}, + {position: Point(1, 5), closeTags: [], openTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js']}, + {position: Point(1, 7), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js', 'syntax--comment syntax--block syntax--js'], openTags: ['syntax--storage syntax--type syntax--var syntax--js']}, + {position: Point(1, 10), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []}, + {position: Point(1, 15), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']}, + {position: Point(1, 16), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []}, + {position: Point(1, 17), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']}, + {position: Point(1, 18), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []} + ] + + while (true) { + const boundary = { + position: iterator.getPosition(), + closeTags: iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId)), + openTags: iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId)) + } + + expect(boundary).toEqual(expectedBoundaries.shift()) + if (!iterator.moveToSuccessor()) { break } + } + + expect(iterator.seek(Point(0, 1)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js', + 'syntax--storage syntax--type syntax--var syntax--js' + ]) + expect(iterator.getPosition()).toEqual(Point(0, 3)) + expect(iterator.seek(Point(0, 8)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js' + ]) + expect(iterator.getPosition()).toEqual(Point(0, 8)) + expect(iterator.seek(Point(1, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js', + 'syntax--comment syntax--block syntax--js' + ]) + expect(iterator.getPosition()).toEqual(Point(1, 0)) + expect(iterator.seek(Point(1, 18)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js', + 'syntax--constant syntax--numeric syntax--decimal syntax--js' + ]) + expect(iterator.getPosition()).toEqual(Point(1, 18)) + + expect(iterator.seek(Point(2, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js' + ]) + iterator.moveToSuccessor() + }) // ensure we don't infinitely loop (regression test) + + it('does not report columns beyond the length of the line', () => { + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + + runs(() => { + buffer = new TextBuffer({text: '# hello\n# world'}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + + const iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(0, 0)) + iterator.moveToSuccessor() + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(7) + + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(0) + + iterator.seek(Point(0, 7)) + expect(iterator.getPosition().column).toBe(7) + + iterator.seek(Point(0, 8)) + expect(iterator.getPosition().column).toBe(7) + }) + }) + + 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'} + ] + }) + + buffer = new TextBuffer({text: 'start x\nend x\nx'}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) + fullyTokenize(tokenizedBuffer) + + const iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(1, 0)) + + expect(iterator.getPosition()).toEqual([1, 0]) + expect(iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--blue syntax--broken']) + expect(iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--yellow syntax--broken']) + }) + }) + }) +}) From 15a57287510521c26a597ff2e1b2a46a340fc659 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 14:27:52 -0700 Subject: [PATCH 217/448] Use async/await in TokenizedBuffer test --- spec/tokenized-buffer-spec.js | 180 ++++++++++++++-------------------- 1 file changed, 76 insertions(+), 104 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 783f5545c..5eeceb34a 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -3,16 +3,17 @@ const TokenizedBuffer = require('../src/tokenized-buffer') const TextBuffer = require('text-buffer') const {Point} = TextBuffer const _ = require('underscore-plus') +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('TokenizedBuffer', () => { let tokenizedBuffer, buffer - beforeEach(() => { + beforeEach(async () => { // enable async tokenization TokenizedBuffer.prototype.chunkSize = 5 jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') - waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + await atom.packages.activatePackage('language-javascript') }) afterEach(() => tokenizedBuffer && tokenizedBuffer.destroy()) @@ -30,10 +31,9 @@ describe('TokenizedBuffer', () => { describe('serialization', () => { describe('when the underlying buffer has a path', () => { - beforeEach(() => { + beforeEach(async () => { buffer = atom.project.bufferForPathSync('sample.js') - - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + await atom.packages.activatePackage('language-coffee-script') }) it('deserializes it searching among the buffers in the current project', () => { @@ -280,35 +280,31 @@ describe('TokenizedBuffer', () => { }) ) - it('does not break out soft tabs across a scope boundary', () => { - waitsForPromise(() => atom.packages.activatePackage('language-gfm')) + it('does not break out soft tabs across a scope boundary', async () => { + await atom.packages.activatePackage('language-gfm') - runs(() => { - tokenizedBuffer.setTabLength(4) - tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) - buffer.setText(' 0) { length += tag } - } + let length = 0 + for (let tag of tokenizedBuffer.tokenizedLines[1].tags) { + if (tag > 0) length += tag + } - expect(length).toBe(4) - }) + expect(length).toBe(4) }) }) }) describe('when the buffer contains hard-tabs', () => { - beforeEach(() => { - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + beforeEach(async () => { + atom.packages.activatePackage('language-coffee-script') - runs(() => { - buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - }) + buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + startTokenizing(tokenizedBuffer) }) afterEach(() => { @@ -322,82 +318,60 @@ describe('TokenizedBuffer', () => { }) describe('when the grammar is tokenized', () => { - it('emits the `tokenized` event', () => { - let editor = null + it('emits the `tokenized` event', async () => { + const editor = await atom.workspace.open('sample.js') + const tokenizedHandler = jasmine.createSpy('tokenized handler') - - waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) - - runs(() => { - ({ tokenizedBuffer } = editor) - tokenizedBuffer.onDidTokenize(tokenizedHandler) - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - }) + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) }) - it("doesn't re-emit the `tokenized` event when it is re-tokenized", () => { - let editor = null + it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => { + const editor = await atom.workspace.open('sample.js') + fullyTokenize(editor.tokenizedBuffer) + const tokenizedHandler = jasmine.createSpy('tokenized handler') - - waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) - - runs(() => { - ({ tokenizedBuffer } = editor) - fullyTokenize(tokenizedBuffer) - - tokenizedBuffer.onDidTokenize(tokenizedHandler) - editor.getBuffer().insert([0, 0], "'") - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler).not.toHaveBeenCalled() - }) + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + editor.getBuffer().insert([0, 0], "'") + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler).not.toHaveBeenCalled() }) }) - describe('when the grammar is updated because a grammar it includes is activated', () => { - it('re-emits the `tokenized` event', () => { - let editor = null - tokenizedBuffer = null + describe('when the grammar is updated because a grammar it includes is activated', async () => { + it('re-emits the `tokenized` event', async () => { + const editor = await atom.workspace.open('coffee.coffee') + const tokenizedHandler = jasmine.createSpy('tokenized handler') + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(editor.tokenizedBuffer) + tokenizedHandler.reset() - waitsForPromise(() => atom.workspace.open('coffee.coffee').then(o => editor = o)) - - runs(() => { - ({ tokenizedBuffer } = editor) - tokenizedBuffer.onDidTokenize(tokenizedHandler) - fullyTokenize(tokenizedBuffer) - tokenizedHandler.reset() - }) - - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) - - runs(() => { - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - }) + await atom.packages.activatePackage('language-coffee-script') + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) }) - it('retokenizes the buffer', () => { - waitsForPromise(() => atom.packages.activatePackage('language-ruby-on-rails')) + it('retokenizes the buffer', async () => { + await atom.packages.activatePackage('language-ruby-on-rails') + await atom.packages.activatePackage('language-ruby') - waitsForPromise(() => atom.packages.activatePackage('language-ruby')) + buffer = atom.project.bufferForPathSync() + buffer.setText("
<%= User.find(2).full_name %>
") - runs(() => { - buffer = atom.project.bufferForPathSync() - buffer.setText("
<%= User.find(2).full_name %>
") - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - const {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual({value: "
", scopes: ['text.html.ruby']}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: "
", + scopes: ['text.html.ruby'] }) - waitsForPromise(() => atom.packages.activatePackage('language-html')) - - runs(() => { - fullyTokenize(tokenizedBuffer) - const {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual({value: '<', scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html']}) + await atom.packages.activatePackage('language-html') + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: '<', + scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html'] }) }) }) @@ -728,29 +702,27 @@ describe('TokenizedBuffer', () => { iterator.moveToSuccessor() }) // ensure we don't infinitely loop (regression test) - it('does not report columns beyond the length of the line', () => { - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + it('does not report columns beyond the length of the line', async () => { + await atom.packages.activatePackage('language-coffee-script') - runs(() => { - buffer = new TextBuffer({text: '# hello\n# world'}) - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) + buffer = new TextBuffer({text: '# hello\n# world'}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) - const iterator = tokenizedBuffer.buildIterator() - iterator.seek(Point(0, 0)) - iterator.moveToSuccessor() - iterator.moveToSuccessor() - expect(iterator.getPosition().column).toBe(7) + const iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(0, 0)) + iterator.moveToSuccessor() + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(7) - iterator.moveToSuccessor() - expect(iterator.getPosition().column).toBe(0) + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(0) - iterator.seek(Point(0, 7)) - expect(iterator.getPosition().column).toBe(7) + iterator.seek(Point(0, 7)) + expect(iterator.getPosition().column).toBe(7) - iterator.seek(Point(0, 8)) - expect(iterator.getPosition().column).toBe(7) - }) + iterator.seek(Point(0, 8)) + expect(iterator.getPosition().column).toBe(7) }) it('correctly terminates scopes at the beginning of the line (regression)', () => { From 4c2680e68a9d9ed1234b192bc3cdcdd7d8878132 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 14:31:13 -0700 Subject: [PATCH 218/448] Organize TokenizedBuffer test --- spec/tokenized-buffer-spec.js | 584 +++++++++++++++++----------------- 1 file changed, 293 insertions(+), 291 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 5eeceb34a..4f12ed69f 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -54,324 +54,350 @@ describe('TokenizedBuffer', () => { }) }) - describe('when the buffer is destroyed', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - }) - - it('stops tokenization', () => { - tokenizedBuffer.destroy() - spyOn(tokenizedBuffer, 'tokenizeNextChunk') - advanceClock() - expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() - }) - }) - - describe('when the buffer contains soft-tabs', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - }) - - afterEach(() => { - tokenizedBuffer.destroy() - buffer.release() - }) - - describe('on construction', () => - it('tokenizes lines chunk at a time in the background', () => { - const line0 = tokenizedBuffer.tokenizedLines[0] - expect(line0).toBeUndefined() - - const line11 = tokenizedBuffer.tokenizedLines[11] - expect(line11).toBeUndefined() - - // tokenize chunk 1 - advanceClock() - expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() - - // tokenize chunk 2 - advanceClock() - expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined() - - // tokenize last chunk - advanceClock() - expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy() - }) - ) - - describe('when the buffer is partially tokenized', () => { + describe('tokenizing', () => { + describe('when the buffer is destroyed', () => { beforeEach(() => { - // tokenize chunk 1 only - advanceClock() + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + startTokenizing(tokenizedBuffer) }) - describe('when there is a buffer change inside the tokenized region', () => { - describe('when lines are added', () => { - it('pushes the invalid rows down', () => { - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - buffer.insert([1, 0], '\n\n') - expect(tokenizedBuffer.firstInvalidRow()).toBe(7) + it('stops tokenization', () => { + tokenizedBuffer.destroy() + spyOn(tokenizedBuffer, 'tokenizeNextChunk') + advanceClock() + expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() + }) + }) + + describe('when the buffer contains soft-tabs', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) + + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) + + describe('on construction', () => + it('tokenizes lines chunk at a time in the background', () => { + const line0 = tokenizedBuffer.tokenizedLines[0] + expect(line0).toBeUndefined() + + const line11 = tokenizedBuffer.tokenizedLines[11] + expect(line11).toBeUndefined() + + // tokenize chunk 1 + advanceClock() + expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() + + // tokenize chunk 2 + advanceClock() + expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined() + + // tokenize last chunk + advanceClock() + expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy() + }) + ) + + describe('when the buffer is partially tokenized', () => { + beforeEach(() => { + // tokenize chunk 1 only + advanceClock() + }) + + describe('when there is a buffer change inside the tokenized region', () => { + describe('when lines are added', () => { + it('pushes the invalid rows down', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.insert([1, 0], '\n\n') + expect(tokenizedBuffer.firstInvalidRow()).toBe(7) + }) + }) + + describe('when lines are removed', () => { + it('pulls the invalid rows up', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.delete([[1, 0], [3, 0]]) + expect(tokenizedBuffer.firstInvalidRow()).toBe(2) + }) + }) + + describe('when the change invalidates all the lines before the current invalid region', () => { + it('retokenizes the invalidated lines and continues into the valid region', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.insert([2, 0], '/*') + expect(tokenizedBuffer.firstInvalidRow()).toBe(3) + advanceClock() + expect(tokenizedBuffer.firstInvalidRow()).toBe(8) + }) }) }) - describe('when lines are removed', () => { - it('pulls the invalid rows up', () => { - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - buffer.delete([[1, 0], [3, 0]]) - expect(tokenizedBuffer.firstInvalidRow()).toBe(2) - }) - }) - - describe('when the change invalidates all the lines before the current invalid region', () => { - it('retokenizes the invalidated lines and continues into the valid region', () => { - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - buffer.insert([2, 0], '/*') - expect(tokenizedBuffer.firstInvalidRow()).toBe(3) - advanceClock() + describe('when there is a buffer change surrounding an invalid row', () => { + it('pushes the invalid row to the end of the change', () => { + buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n') expect(tokenizedBuffer.firstInvalidRow()).toBe(8) }) }) - }) - describe('when there is a buffer change surrounding an invalid row', () => { - it('pushes the invalid row to the end of the change', () => { - buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n') - expect(tokenizedBuffer.firstInvalidRow()).toBe(8) + describe('when there is a buffer change inside an invalid region', () => { + it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n') + expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined() + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + }) }) }) - describe('when there is a buffer change inside an invalid region', () => { - it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => { - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n') - expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined() - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - }) - }) - }) + describe('when the buffer is fully tokenized', () => { + beforeEach(() => fullyTokenize(tokenizedBuffer)) - describe('when the buffer is fully tokenized', () => { - beforeEach(() => fullyTokenize(tokenizedBuffer)) + describe('when there is a buffer change that is smaller than the chunk size', () => { + describe('when lines are updated, but none are added or removed', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n') - describe('when there is a buffer change that is smaller than the chunk size', () => { - describe('when lines are updated, but none are added or removed', () => { - it('updates tokens to reflect the change', () => { - buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n') + expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']}) + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']}) + // line 2 is unchanged + expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + }) - expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']}) - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']}) - // line 2 is unchanged - expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.insert([2, 0], '/*') + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + it('resumes highlighting with the state of the previous line', () => { + buffer.insert([0, 0], '/*') + buffer.insert([5, 0], '*/') + + buffer.insert([1, 0], 'var ') + expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + describe('when lines are both updated and removed', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[1, 0], [3, 0]], 'foo()') + + // previous line 0 remains + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + + // previous line 3 should be combined with input to form line 1 + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + + // lines below deleted regions should be shifted upward + expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']}) + expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']}) + }) }) describe('when the change invalidates the tokenization of subsequent lines', () => { it('schedules the invalidated lines to be tokenized in the background', () => { buffer.insert([5, 30], '/* */') - buffer.insert([2, 0], '/*') + buffer.setTextInRange([[2, 0], [3, 0]], '/*') + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) advanceClock() expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) }) }) - it('resumes highlighting with the state of the previous line', () => { - buffer.insert([0, 0], '/*') - buffer.insert([5, 0], '*/') + describe('when lines are both updated and inserted', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()') - buffer.insert([1, 0], 'var ') - expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + // previous line 0 remains + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + + // 3 new lines inserted + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual({value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual({value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + + // previous line 2 is joined with quux() on line 4 + expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + + // previous line 3 is pushed down to become line 5 + expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + }) + }) + + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.insert([2, 0], '/*\nabcde\nabcder') + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() // tokenize invalidated lines in background + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js']) + }) }) }) - describe('when lines are both updated and removed', () => { - it('updates tokens to reflect the change', () => { - buffer.setTextInRange([[1, 0], [3, 0]], 'foo()') - - // previous line 0 remains - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']}) - - // previous line 3 should be combined with input to form line 1 - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) - expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) - - // lines below deleted regions should be shifted upward - expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']}) - expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) - expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']}) - }) - }) - - describe('when the change invalidates the tokenization of subsequent lines', () => { - it('schedules the invalidated lines to be tokenized in the background', () => { - buffer.insert([5, 30], '/* */') - buffer.setTextInRange([[2, 0], [3, 0]], '/*') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) + describe('when there is an insertion that is larger than the chunk size', () => + it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => { + const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2) + buffer.insert([0, 0], commentBlock) + expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() advanceClock() - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy() }) - }) + ) - describe('when lines are both updated and inserted', () => { - it('updates tokens to reflect the change', () => { - buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()') + it('does not break out soft tabs across a scope boundary', async () => { + await atom.packages.activatePackage('language-gfm') - // previous line 0 remains - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + tokenizedBuffer.setTabLength(4) + tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) + buffer.setText(' 0) length += tag + } - // previous line 2 is joined with quux() on line 4 - expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) - expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) - - // previous line 3 is pushed down to become line 5 - expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) - }) - }) - - describe('when the change invalidates the tokenization of subsequent lines', () => { - it('schedules the invalidated lines to be tokenized in the background', () => { - buffer.insert([5, 30], '/* */') - buffer.insert([2, 0], '/*\nabcde\nabcder') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js']) - - advanceClock() // tokenize invalidated lines in background - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js']) - }) + expect(length).toBe(4) }) }) + }) - describe('when there is an insertion that is larger than the chunk size', () => - it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => { - const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2) - buffer.insert([0, 0], commentBlock) - expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() + describe('when the buffer contains hard-tabs', () => { + beforeEach(async () => { + atom.packages.activatePackage('language-coffee-script') - advanceClock() - expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy() - }) - ) + buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) - it('does not break out soft tabs across a scope boundary', async () => { - await atom.packages.activatePackage('language-gfm') + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) - tokenizedBuffer.setTabLength(4) - tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) - buffer.setText(' { + beforeEach(() => fullyTokenize(tokenizedBuffer)) + }) + }) + + describe('when tokenization completes', () => { + it('emits the `tokenized` event', async () => { + const editor = await atom.workspace.open('sample.js') + + const tokenizedHandler = jasmine.createSpy('tokenized handler') + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) + }) + + it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => { + const editor = await atom.workspace.open('sample.js') + fullyTokenize(editor.tokenizedBuffer) + + const tokenizedHandler = jasmine.createSpy('tokenized handler') + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + editor.getBuffer().insert([0, 0], "'") + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler).not.toHaveBeenCalled() + }) + }) + + describe('when the grammar is updated because a grammar it includes is activated', async () => { + it('re-emits the `tokenized` event', async () => { + const editor = await atom.workspace.open('coffee.coffee') + + const tokenizedHandler = jasmine.createSpy('tokenized handler') + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(editor.tokenizedBuffer) + tokenizedHandler.reset() + + await atom.packages.activatePackage('language-coffee-script') + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) + }) + + it('retokenizes the buffer', async () => { + await atom.packages.activatePackage('language-ruby-on-rails') + await atom.packages.activatePackage('language-ruby') + + buffer = atom.project.bufferForPathSync() + buffer.setText("
<%= User.find(2).full_name %>
") + + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: "
", + scopes: ['text.html.ruby'] + }) - let length = 0 - for (let tag of tokenizedBuffer.tokenizedLines[1].tags) { - if (tag > 0) length += tag - } - - expect(length).toBe(4) + await atom.packages.activatePackage('language-html') + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: '<', + scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html'] + }) }) }) - }) - describe('when the buffer contains hard-tabs', () => { - beforeEach(async () => { - atom.packages.activatePackage('language-coffee-script') + describe('when the buffer is configured with the null grammar', () => { + it('does not actually tokenize using the grammar', () => { + spyOn(NullGrammar, 'tokenizeLine').andCallThrough() + buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') + buffer.setText('a\nb\nc') + tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2}) + const tokenizeCallback = jasmine.createSpy('onDidTokenize') + tokenizedBuffer.onDidTokenize(tokenizeCallback) - buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - }) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() + expect(tokenizeCallback.callCount).toBe(0) + expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - afterEach(() => { - tokenizedBuffer.destroy() - buffer.release() - }) - - describe('when the buffer is fully tokenized', () => { - beforeEach(() => fullyTokenize(tokenizedBuffer)) - }) - }) - - describe('when the grammar is tokenized', () => { - it('emits the `tokenized` event', async () => { - const editor = await atom.workspace.open('sample.js') - - const tokenizedHandler = jasmine.createSpy('tokenized handler') - editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) - fullyTokenize(editor.tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - }) - - it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => { - const editor = await atom.workspace.open('sample.js') - fullyTokenize(editor.tokenizedBuffer) - - const tokenizedHandler = jasmine.createSpy('tokenized handler') - editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) - editor.getBuffer().insert([0, 0], "'") - fullyTokenize(editor.tokenizedBuffer) - expect(tokenizedHandler).not.toHaveBeenCalled() - }) - }) - - describe('when the grammar is updated because a grammar it includes is activated', async () => { - it('re-emits the `tokenized` event', async () => { - const editor = await atom.workspace.open('coffee.coffee') - - const tokenizedHandler = jasmine.createSpy('tokenized handler') - editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) - fullyTokenize(editor.tokenizedBuffer) - tokenizedHandler.reset() - - await atom.packages.activatePackage('language-coffee-script') - fullyTokenize(editor.tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - }) - - it('retokenizes the buffer', async () => { - await atom.packages.activatePackage('language-ruby-on-rails') - await atom.packages.activatePackage('language-ruby') - - buffer = atom.project.bufferForPathSync() - buffer.setText("
<%= User.find(2).full_name %>
") - - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ - value: "
", - scopes: ['text.html.ruby'] - }) - - await atom.packages.activatePackage('language-html') - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ - value: '<', - scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html'] + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() + expect(tokenizeCallback.callCount).toBe(0) + expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() }) }) }) @@ -502,7 +528,7 @@ describe('TokenizedBuffer', () => { }) }) // } - describe('::isFoldableAtRow(row)', () => { + describe('.isFoldableAtRow(row)', () => { beforeEach(() => { buffer = atom.project.bufferForPathSync('sample.js') buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') @@ -574,7 +600,7 @@ describe('TokenizedBuffer', () => { }) }) - describe('::tokenizedLineForRow(row)', () => { + describe('.tokenizedLineForRow(row)', () => { it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { buffer = atom.project.bufferForPathSync('sample.js') const grammar = atom.grammars.grammarForScopeName('source.js') @@ -613,30 +639,6 @@ describe('TokenizedBuffer', () => { }) }) - describe('when the buffer is configured with the null grammar', () => { - it('does not actually tokenize using the grammar', () => { - spyOn(NullGrammar, 'tokenizeLine').andCallThrough() - buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') - buffer.setText('a\nb\nc') - tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2}) - const tokenizeCallback = jasmine.createSpy('onDidTokenize') - tokenizedBuffer.onDidTokenize(tokenizeCallback) - - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() - expect(tokenizeCallback.callCount).toBe(0) - expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() - expect(tokenizeCallback.callCount).toBe(0) - expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - }) - }) - describe('text decoration layer API', () => { describe('iterator', () => { it('iterates over the syntactic scope boundaries', () => { From 7ac071d62269812b37e59daf29680597440a88e8 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 21 Sep 2017 14:32:23 -0700 Subject: [PATCH 219/448] 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 220/448] 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 221/448] 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 222/448] 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 58035e46822d4c265a0704031565113505c56f9d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 15:15:53 -0700 Subject: [PATCH 223/448] :shirt: --- src/tokenized-buffer.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 80601d1f3..fbb9de77f 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -1,7 +1,6 @@ const _ = require('underscore-plus') const {CompositeDisposable, Emitter} = require('event-kit') const {Point, Range} = require('text-buffer') -const Model = require('./model') const TokenizedLine = require('./tokenized-line') const TokenIterator = require('./token-iterator') const ScopeDescriptor = require('./scope-descriptor') @@ -275,7 +274,7 @@ class TokenizedBuffer { if (row >= 0 && row <= this.buffer.getLastRow()) { const nextRow = this.buffer.nextNonBlankRow(row) const tokenizedLine = this.tokenizedLines[row] - if (this.buffer.isRowBlank(row) || (tokenizedLine != null ? tokenizedLine.isComment() : undefined) || (nextRow == null)) { + if (this.buffer.isRowBlank(row) || (tokenizedLine && tokenizedLine.isComment()) || nextRow == null) { return false } else { return this.indentLevelForRow(nextRow) > this.indentLevelForRow(row) @@ -288,15 +287,11 @@ class TokenizedBuffer { isFoldableCommentAtRow (row) { const previousRow = row - 1 const nextRow = row + 1 - if (nextRow > this.buffer.getLastRow()) { - return false - } else { - return Boolean( - !(this.tokenizedLines[previousRow] != null ? this.tokenizedLines[previousRow].isComment() : undefined) && - (this.tokenizedLines[row] != null ? this.tokenizedLines[row].isComment() : undefined) && - (this.tokenizedLines[nextRow] != null ? this.tokenizedLines[nextRow].isComment() : undefined) - ) - } + return ( + (!this.tokenizedLines[previousRow] || !this.tokenizedLines[previousRow].isComment()) && + (this.tokenizedLines[row] && this.tokenizedLines[row].isComment()) && + (this.tokenizedLines[nextRow] && this.tokenizedLines[nextRow].isComment()) + ) } buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { @@ -327,7 +322,7 @@ class TokenizedBuffer { } buildTokenizedLineForRowWithText (row, text, currentRuleStack = this.stackForRow(row - 1), openScopes = this.openScopesForRow(row)) { - const lineEnding = this.buffer.lineEndingForRow(row); + const lineEnding = this.buffer.lineEndingForRow(row) const {tags, ruleStack} = this.grammar.tokenizeLine(text, currentRuleStack, row === 0, false) return new TokenizedLine({ openScopes, @@ -353,7 +348,7 @@ class TokenizedBuffer { text.length, this.grammar.endIdForScope(this.grammar.scopeName) ] - return this.tokenizedLines[bufferRow] = new TokenizedLine({ + this.tokenizedLines[bufferRow] = new TokenizedLine({ openScopes: [], text, tags, @@ -361,6 +356,7 @@ class TokenizedBuffer { tokenIterator: this.tokenIterator, grammar: this.grammar }) + return this.tokenizedLines[bufferRow] } } } @@ -404,7 +400,7 @@ class TokenizedBuffer { } const path = require('path') error.privateMetadataDescription = `The contents of \`${path.basename(this.buffer.getPath())}\`` - return error.privateMetadata = { + error.privateMetadata = { filePath: this.buffer.getPath(), fileContents: this.buffer.getText() } @@ -570,7 +566,7 @@ class TokenizedBuffer { } logLines (start = 0, end = this.buffer.getLastRow()) { - for (let row = start; row <= end1; row++) { + for (let row = start; row <= end; row++) { const line = this.tokenizedLines[row].text console.log(row, line, line.length) } From a93ebf24658d8af770e6a5a235216ffeecabaf69 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Thu, 21 Sep 2017 15:33:40 -0700 Subject: [PATCH 224/448] 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 b1a3460ad9f2abd1f6e1bc26479c46edd5f2f0fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 15:52:07 -0700 Subject: [PATCH 225/448] Fix scope name in TokenizedBuffer test --- spec/tokenized-buffer-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 4f12ed69f..a8e01f798 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -372,7 +372,7 @@ describe('TokenizedBuffer', () => { fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: '<', - scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html'] + scopes: ['text.html.ruby', 'meta.tag.block.div.html', 'punctuation.definition.tag.begin.html'] }) }) }) From 6c1356cae317313b1dad479fde8d5df3b169ad65 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 12:04:51 -0700 Subject: [PATCH 226/448] Move folding logic from LanguageMode to TokenizedBuffer * Restate the folding logic to not *use* the TextEditor, but instead to *return* ranges which can be folded by the editor. * Convert the LanguageMode spec to JS --- spec/language-mode-spec.coffee | 506 ----------------------------- spec/language-mode-spec.js | 503 ++++++++++++++++++++++++++++ spec/text-editor-component-spec.js | 6 +- spec/tokenized-buffer-spec.js | 311 +++++++++++++----- src/language-mode.coffee | 147 +-------- src/text-editor.coffee | 40 ++- src/tokenized-buffer.js | 142 ++++++-- 7 files changed, 902 insertions(+), 753 deletions(-) delete mode 100644 spec/language-mode-spec.coffee create mode 100644 spec/language-mode-spec.js diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee deleted file mode 100644 index 68d0f7b09..000000000 --- a/spec/language-mode-spec.coffee +++ /dev/null @@ -1,506 +0,0 @@ -describe "LanguageMode", -> - [editor, buffer, languageMode] = [] - - afterEach -> - editor.destroy() - - describe "javascript", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".minIndentLevelForRowRange(startRow, endRow)", -> - it "returns the minimum indent level for the given row range", -> - expect(languageMode.minIndentLevelForRowRange(4, 7)).toBe 2 - expect(languageMode.minIndentLevelForRowRange(5, 7)).toBe 2 - expect(languageMode.minIndentLevelForRowRange(5, 6)).toBe 3 - expect(languageMode.minIndentLevelForRowRange(9, 11)).toBe 1 - expect(languageMode.minIndentLevelForRowRange(10, 10)).toBe 0 - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.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 " // }" - - languageMode.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;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "\t// var i;" - - buffer.setText('var i;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "// var i;" - - buffer.setText(' var i;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe " // var i;" - - buffer.setText(' ') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe " // " - - buffer.setText(' a\n \n b') - languageMode.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;') - languageMode.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe ' ' - expect(buffer.lineForRow(1)).toBe ' var i;' - - describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable region starting at the given row", -> - expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 12] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 9] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull() - expect(languageMode.rowRangeForCodeFoldAtBufferRow(4)).toEqual [4, 7] - - describe ".rowRangeForCommentAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable comment starting at the given row", -> - buffer.setText("//this is a multi line comment\n//another line") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 1] - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 1] - - buffer.setText("//this is a multi line comment\n//another line\n//and one more") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 2] - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 2] - - buffer.setText("//this is a multi line comment\n\n//with an empty line") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(2)).toBeUndefined() - - buffer.setText("//this is a single line comment\n") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined() - - buffer.setText("//this is a single line comment") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - - describe ".suggestedIndentForBufferRow", -> - it "bases indentation off of the previous non-blank line", -> - expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0 - expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3 - expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1 - - it "does not take invisibles into account", -> - editor.update({showInvisibles: true}) - expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0 - expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3 - expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1 - - describe "rowRangeForParagraphAtBufferRow", -> - describe "with code and comments", -> - beforeEach -> - buffer.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) { - return item; - } - - }; - ''' - - it "will limit paragraph range to comments", -> - range = languageMode.rowRangeForParagraphAtBufferRow(0) - expect(range).toEqual [[0, 0], [0, 29]] - - range = languageMode.rowRangeForParagraphAtBufferRow(10) - expect(range).toEqual [[10, 0], [10, 14]] - range = languageMode.rowRangeForParagraphAtBufferRow(11) - expect(range).toBeFalsy() - range = languageMode.rowRangeForParagraphAtBufferRow(12) - expect(range).toEqual [[12, 0], [13, 10]] - - range = languageMode.rowRangeForParagraphAtBufferRow(14) - expect(range).toEqual [[14, 0], [14, 32]] - - range = languageMode.rowRangeForParagraphAtBufferRow(15) - expect(range).toEqual [[15, 0], [15, 26]] - - range = languageMode.rowRangeForParagraphAtBufferRow(18) - expect(range).toEqual [[17, 0], [19, 3]] - - describe "coffeescript", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.toggleLineCommentsForBufferRows(4, 6) - expect(buffer.lineForRow(4)).toBe " # pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " # left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - - languageMode.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 lines when empty line", -> - languageMode.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 " # " - - languageMode.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 "fold suggestion", -> - describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable region starting at the given row", -> - expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 20] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 17] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull() - expect(languageMode.rowRangeForCodeFoldAtBufferRow(19)).toEqual [19, 20] - - describe "css", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('css.css', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.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;" - - languageMode.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;" - - languageMode.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%;*/") - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe " width: 110%;" - - it "uncomments lines with trailing whitespace", -> - buffer.setTextInRange([[2, 0], [2, Infinity]], "/*width: 110%;*/ ") - languageMode.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%;*/ ") - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe " width: 110%; " - - describe "less", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.less', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-less') - - waitsForPromise -> - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "when commenting lines", -> - it "only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`", -> - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "// @color: #4D926F;" - - describe "xml", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.xml', autoIndent: false).then (o) -> - editor = o - editor.setText("") - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-xml') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "when uncommenting lines", -> - it "removes the leading whitespace from the comment end pattern match", -> - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "test" - - describe "folding", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - it "maintains cursor buffer position when a folding/unfolding", -> - editor.setCursorBufferPosition([5, 5]) - languageMode.foldAll() - expect(editor.getCursorBufferPosition()).toEqual([5, 5]) - - describe ".unfoldAll()", -> - it "unfolds every folded line", -> - initialScreenLineCount = editor.getScreenLineCount() - languageMode.foldBufferRow(0) - languageMode.foldBufferRow(1) - expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount - languageMode.unfoldAll() - expect(editor.getScreenLineCount()).toBe initialScreenLineCount - - describe ".foldAll()", -> - it "folds every foldable line", -> - languageMode.foldAll() - - [fold1, fold2, fold3] = languageMode.unfoldAll() - expect([fold1.start.row, fold1.end.row]).toEqual [0, 12] - expect([fold2.start.row, fold2.end.row]).toEqual [1, 9] - expect([fold3.start.row, fold3.end.row]).toEqual [4, 7] - - describe ".foldBufferRow(bufferRow)", -> - describe "when bufferRow can be folded", -> - it "creates a fold based on the syntactic region starting at the given row", -> - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 9] - - describe "when bufferRow can't be folded", -> - it "searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)", -> - languageMode.foldBufferRow(8) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 9] - - describe "when the bufferRow is already folded", -> - it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", -> - languageMode.foldBufferRow(2) - expect(editor.isFoldedAtBufferRow(0)).toBe(false) - expect(editor.isFoldedAtBufferRow(1)).toBe(true) - - languageMode.foldBufferRow(1) - expect(editor.isFoldedAtBufferRow(0)).toBe(true) - - describe "when the bufferRow is in a multi-line comment", -> - it "searches upward and downward for surrounding comment lines and folds them as a single fold", -> - buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment") - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 3] - - describe "when the bufferRow is a single-line comment", -> - it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", -> - buffer.insert([1, 0], " //this is a single line comment\n") - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [0, 13] - - describe ".foldAllAtIndentLevel(indentLevel)", -> - it "folds blocks of text at the given indentation level", -> - languageMode.foldAllAtIndentLevel(0) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter - expect(editor.getLastScreenRow()).toBe 0 - - languageMode.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 - - languageMode.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 - - describe "folding with comments", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-comments.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".unfoldAll()", -> - it "unfolds every folded line", -> - initialScreenLineCount = editor.getScreenLineCount() - languageMode.foldBufferRow(0) - languageMode.foldBufferRow(5) - expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount - languageMode.unfoldAll() - expect(editor.getScreenLineCount()).toBe initialScreenLineCount - - describe ".foldAll()", -> - it "folds every foldable line", -> - languageMode.foldAll() - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 8 - expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30] - expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4] - expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27] - expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8] - expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16] - expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20] - expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22] - expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25] - - describe ".foldAllAtIndentLevel()", -> - it "folds every foldable range at a given indentLevel", -> - languageMode.foldAllAtIndentLevel(2) - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 5 - expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8] - expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16] - expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20] - expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22] - expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25] - - it "does not fold anything but the indentLevel", -> - languageMode.foldAllAtIndentLevel(0) - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 1 - expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30] - - describe ".isFoldableAtBufferRow(bufferRow)", -> - it "returns true if the line starts a multi-line comment", -> - expect(languageMode.isFoldableAtBufferRow(1)).toBe true - expect(languageMode.isFoldableAtBufferRow(6)).toBe true - expect(languageMode.isFoldableAtBufferRow(8)).toBe false - expect(languageMode.isFoldableAtBufferRow(11)).toBe true - expect(languageMode.isFoldableAtBufferRow(15)).toBe false - expect(languageMode.isFoldableAtBufferRow(17)).toBe true - expect(languageMode.isFoldableAtBufferRow(21)).toBe true - expect(languageMode.isFoldableAtBufferRow(24)).toBe true - expect(languageMode.isFoldableAtBufferRow(28)).toBe false - - it "returns true for lines that end with a comment and are followed by an indented line", -> - expect(languageMode.isFoldableAtBufferRow(5)).toBe true - - it "does not return true for a line in the middle of a comment that's followed by an indented line", -> - expect(languageMode.isFoldableAtBufferRow(7)).toBe false - editor.buffer.insert([8, 0], ' ') - expect(languageMode.isFoldableAtBufferRow(7)).toBe false - - describe "css", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('css.css', autoIndent: true).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-source') - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "suggestedIndentForBufferRow", -> - it "does not return negative values (regression)", -> - editor.setText('.test {\npadding: 0;\n}') - expect(editor.suggestedIndentForBufferRow(2)).toBe 0 diff --git a/spec/language-mode-spec.js b/spec/language-mode-spec.js new file mode 100644 index 000000000..34f341bfc --- /dev/null +++ b/spec/language-mode-spec.js @@ -0,0 +1,503 @@ +const dedent = require('dedent') +const {Point, Range} = require('text-buffer') +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') + +describe('LanguageMode', () => { + let editor + + afterEach(() => { + editor.destroy() + }) + + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + 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('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { + it('returns the start/end rows of the foldable region starting at the given row', () => { + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]]) + }) + }) + + describe('.suggestedIndentForBufferRow', () => { + it('bases indentation off of the previous non-blank line', () => { + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + + it('does not take invisibles into account', () => { + editor.update({showInvisibles: true}) + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + }) + + describe('rowRangeForParagraphAtBufferRow', () => { + describe('with code and comments', () => { + beforeEach(() => + 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; + } + + }; + `) + ) + + it('will limit paragraph range to comments', () => { + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(0)).toEqual([[0, 0], [0, 29]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(1)).toEqual([[1, 0], [1, 33]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(2)).toEqual([[2, 0], [2, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(3)).toBeFalsy() + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(4)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(5)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(6)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(7)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(8)).toEqual([[8, 0], [8, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(9)).toBeFalsy() + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(10)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(11)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(12)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(14)).toEqual([[14, 0], [14, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(15)).toEqual([[15, 0], [15, 26]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(18)).toEqual([[17, 0], [19, 3]]) + }) + }) + }) + }) + + describe('coffeescript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) + await atom.packages.activatePackage('language-coffee-script') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + 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 lines when empty line', () => { + 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('fold suggestion', () => { + describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { + it('returns the start/end rows of the foldable region starting at the given row', () => { + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]]) + }) + }) + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: false}) + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + 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('less', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.less', {autoIndent: false}) + await atom.packages.activatePackage('language-less') + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('when commenting lines', () => { + it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + }) + }) + }) + + describe('xml', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.xml', {autoIndent: false}) + editor.setText('') + await atom.packages.activatePackage('language-xml') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('when uncommenting lines', () => { + it('removes the leading whitespace from the comment end pattern match', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('test') + }) + }) + }) + + describe('folding', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + it('maintains cursor buffer position when a folding/unfolding', () => { + editor.setCursorBufferPosition([5, 5]) + editor.foldAll() + expect(editor.getCursorBufferPosition()).toEqual([5, 5]) + }) + + describe('.unfoldAll()', () => { + it('unfolds every folded line', () => { + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(1) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + editor.unfoldAll() + expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + }) + }) + + describe('.foldAll()', () => { + it('folds every foldable line', () => { + editor.foldAll() + + 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]) + expect([fold3.start.row, fold3.end.row]).toEqual([4, 7]) + }) + }) + + describe('.foldBufferRow(bufferRow)', () => { + describe('when bufferRow can be folded', () => { + it('creates a fold based on the syntactic region starting at the given row', () => { + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 9]) + }) + }) + + describe("when bufferRow can't be folded", () => { + it('searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)', () => { + editor.foldBufferRow(8) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 9]) + }) + }) + + describe('when the bufferRow is already folded', () => { + it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => { + editor.foldBufferRow(2) + expect(editor.isFoldedAtBufferRow(0)).toBe(false) + expect(editor.isFoldedAtBufferRow(1)).toBe(true) + + editor.foldBufferRow(1) + expect(editor.isFoldedAtBufferRow(0)).toBe(true) + }) + }) + + describe('when the bufferRow is in a multi-line comment', () => { + it('searches upward and downward for surrounding comment lines and folds them as a single fold', () => { + editor.buffer.insert([1, 0], ' //this is a comment\n // and\n //more docs\n\n//second comment') + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 3]) + }) + }) + + describe('when the bufferRow is a single-line comment', () => { + it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => { + editor.buffer.insert([1, 0], ' //this is a single line comment\n') + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([0, 13]) + }) + }) + }) + + describe('.foldAllAtIndentLevel(indentLevel)', () => { + it('folds blocks of text at the given indentation level', () => { + editor.foldAllAtIndentLevel(0) + expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`) + expect(editor.getLastScreenRow()).toBe(0) + + 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) + + 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) + }) + }) + }) + + describe('folding with comments', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.unfoldAll()', () => { + it('unfolds every folded line', () => { + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(5) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + editor.unfoldAll() + expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + }) + }) + + describe('.foldAll()', () => { + it('folds every foldable line', () => { + editor.foldAll() + + const folds = editor.unfoldAll() + expect(folds.length).toBe(8) + expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) + expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4]) + expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27]) + expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8]) + expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16]) + expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20]) + expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22]) + expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25]) + }) + }) + + describe('.foldAllAtIndentLevel()', () => { + it('folds every foldable range at a given indentLevel', () => { + editor.foldAllAtIndentLevel(2) + + const folds = editor.unfoldAll() + expect(folds.length).toBe(5) + expect([folds[0].start.row, folds[0].end.row]).toEqual([6, 8]) + expect([folds[1].start.row, folds[1].end.row]).toEqual([11, 16]) + expect([folds[2].start.row, folds[2].end.row]).toEqual([17, 20]) + expect([folds[3].start.row, folds[3].end.row]).toEqual([21, 22]) + expect([folds[4].start.row, folds[4].end.row]).toEqual([24, 25]) + }) + + it('does not fold anything but the indentLevel', () => { + editor.foldAllAtIndentLevel(0) + + const folds = editor.unfoldAll() + expect(folds.length).toBe(1) + expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) + }) + }) + + describe('.isFoldableAtBufferRow(bufferRow)', () => { + it('returns true if the line starts a multi-line comment', () => { + expect(editor.isFoldableAtBufferRow(1)).toBe(true) + expect(editor.isFoldableAtBufferRow(6)).toBe(true) + expect(editor.isFoldableAtBufferRow(8)).toBe(false) + expect(editor.isFoldableAtBufferRow(11)).toBe(true) + expect(editor.isFoldableAtBufferRow(15)).toBe(false) + expect(editor.isFoldableAtBufferRow(17)).toBe(true) + expect(editor.isFoldableAtBufferRow(21)).toBe(true) + expect(editor.isFoldableAtBufferRow(24)).toBe(true) + expect(editor.isFoldableAtBufferRow(28)).toBe(false) + }) + + it('returns true for lines that end with a comment and are followed by an indented line', () => { + expect(editor.isFoldableAtBufferRow(5)).toBe(true) + }) + + it("does not return true for a line in the middle of a comment that's followed by an indented line", () => { + expect(editor.isFoldableAtBufferRow(7)).toBe(false) + editor.buffer.insert([8, 0], ' ') + expect(editor.isFoldableAtBufferRow(7)).toBe(false) + }) + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: true}) + await atom.packages.activatePackage('language-source') + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('suggestedIndentForBufferRow', () => { + it('does not return negative values (regression)', () => { + editor.setText('.test {\npadding: 0;\n}') + expect(editor.suggestedIndentForBufferRow(2)).toBe(0) + }) + }) + }) +}) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 3fd40cdad..82764c438 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3343,9 +3343,9 @@ describe('TextEditorComponent', () => { await component.getNextUpdatePromise() expect(editor.isFoldedAtScreenRow(5)).toBe(true) - target = element.querySelectorAll('.line-number')[6].querySelector('.icon-right') - component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 5)}) - expect(editor.isFoldedAtScreenRow(5)).toBe(false) + target = element.querySelectorAll('.line-number')[4].querySelector('.icon-right') + component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 4)}) + expect(editor.isFoldedAtScreenRow(4)).toBe(false) }) it('autoscrolls when dragging near the top or bottom of the gutter', async () => { diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index a8e01f798..134a1a0b1 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -1,8 +1,9 @@ const NullGrammar = require('../src/null-grammar') const TokenizedBuffer = require('../src/tokenized-buffer') const TextBuffer = require('text-buffer') -const {Point} = TextBuffer +const {Point, Range} = TextBuffer const _ = require('underscore-plus') +const dedent = require('dedent') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('TokenizedBuffer', () => { @@ -12,7 +13,6 @@ describe('TokenizedBuffer', () => { // enable async tokenization TokenizedBuffer.prototype.chunkSize = 5 jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') - await atom.packages.activatePackage('language-javascript') }) @@ -528,78 +528,6 @@ describe('TokenizedBuffer', () => { }) }) // } - describe('.isFoldableAtRow(row)', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') - buffer.insert([0, 0], '// multi-line\n// comment\n// block\n') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - }) - - it('includes the first line of multi-line comments', () => { - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent - expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false) - - buffer.insert([0, Infinity], '\n') - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false) - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) - }) // because of indent - - it('includes non-comment lines that precede an increase in indentation', () => { - buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable - - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([7, 0], ' ') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([7, 0], ' \n x\n') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([9, 0], ' ') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - }) - }) - describe('.tokenizedLineForRow(row)', () => { it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { buffer = atom.project.bufferForPathSync('sample.js') @@ -750,4 +678,239 @@ describe('TokenizedBuffer', () => { }) }) }) + + describe('.isFoldableAtRow(row)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') + buffer.insert([0, 0], '// multi-line\n// comment\n// block\n') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + it('includes the first line of multi-line comments', () => { + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent + expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false) + + buffer.insert([0, Infinity], '\n') + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + }) // because of indent + + it('includes non-comment lines that precede an increase in indentation', () => { + buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable + + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' \n x\n') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([9, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + }) + }) + + describe('.getFoldableRangesAtIndentLevel', () => { + it('returns the ranges that can be folded at the given indent level', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2))).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) {⋯ + } + `) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2))).toBe(dedent ` + if (a) { + b(); + if (c) {⋯ + } + h() + } + i() + if (j) { + k() + } + `) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2))).toBe(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) {⋯ + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + }) + }) + + describe('.getFoldableRanges', () => { + it('returns the ranges that can be folded', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(tokenizedBuffer.getFoldableRanges(2).map(r => r.toString())).toEqual([ + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2), + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2), + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2), + ].sort((a, b) => (a.start.row - b.start.row) || (a.end.row - b.end.row)).map(r => r.toString())) + }) + }) + + describe('.getFoldableRangeContainingPoint', () => { + it('returns the range for the smallest fold that contains the given range', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 5), 2)).toBeNull() + + let range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 10), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) { + k() + } + `) + + range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) { + k() + } + `) + + range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, 20), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) { + b(); + if (c) {⋯ + } + h() + } + i() + if (j) { + k() + } + `) + }) + }) + + function simulateFold (ranges) { + buffer.transact(() => { + for (const range of ranges.reverse()) { + buffer.setTextInRange(range, '⋯') + } + }) + let text = buffer.getText() + buffer.undo() + return text + } }) diff --git a/src/language-mode.coffee b/src/language-mode.coffee index 1839f1c59..953d328b2 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -1,9 +1,11 @@ -{Range} = require 'text-buffer' +{Range, Point} = require 'text-buffer' _ = require 'underscore-plus' {OnigRegExp} = require 'oniguruma' ScopeDescriptor = require './scope-descriptor' NullGrammar = require './null-grammar' +NON_WHITESPACE_REGEX = /\S/ + module.exports = class LanguageMode # Sets up a `LanguageMode` for the given {TextEditor}. @@ -90,148 +92,28 @@ class LanguageMode buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) return - # Folds all the foldable lines in the buffer. - foldAll: -> - @unfoldAll() - foldedRowRanges = {} - for currentRow in [0..@buffer.getLastRow()] by 1 - rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? - continue if foldedRowRanges[rowRange] - - @editor.foldBufferRowRange(startRow, endRow) - foldedRowRanges[rowRange] = true - return - - # Unfolds all the foldable lines in the buffer. - unfoldAll: -> - @editor.displayLayer.destroyAllFolds() - - # Fold all comment and code blocks at a given indentLevel - # - # indentLevel - A {Number} indicating indentLevel; 0 based. - foldAllAtIndentLevel: (indentLevel) -> - @unfoldAll() - foldedRowRanges = {} - for currentRow in [0..@buffer.getLastRow()] by 1 - rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? - continue if foldedRowRanges[rowRange] - - # assumption: startRow will always be the min indent level for the entire range - if @editor.indentationForBufferRow(startRow) is indentLevel - @editor.foldBufferRowRange(startRow, endRow) - foldedRowRanges[rowRange] = true - return - - # Given a buffer row, creates a fold at it. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns the new {Fold}. - foldBufferRow: (bufferRow) -> - for currentRow in [bufferRow..0] by -1 - [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? and startRow <= bufferRow <= endRow - unless @editor.isFoldedAtBufferRow(startRow) - return @editor.foldBufferRowRange(startRow, endRow) - - # Find the row range for a fold at a given bufferRow. Will handle comments - # and code. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns an {Array} of the [startRow, endRow]. Returns null if no range. - rowRangeForFoldAtBufferRow: (bufferRow) -> - rowRange = @rowRangeForCommentAtBufferRow(bufferRow) - rowRange ?= @rowRangeForCodeFoldAtBufferRow(bufferRow) - rowRange - - rowRangeForCommentAtBufferRow: (bufferRow) -> - return unless @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() - - startRow = bufferRow - endRow = bufferRow - - if bufferRow > 0 - for currentRow in [bufferRow-1..0] by -1 - break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment() - startRow = currentRow - - if bufferRow < @buffer.getLastRow() - for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1 - break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment() - endRow = currentRow - - return [startRow, endRow] if startRow isnt endRow - - rowRangeForCodeFoldAtBufferRow: (bufferRow) -> - return null unless @isFoldableAtBufferRow(bufferRow) - - startIndentLevel = @editor.indentationForBufferRow(bufferRow) - scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) - for row in [(bufferRow + 1)..@editor.getLastBufferRow()] by 1 - continue if @editor.isBufferRowBlank(row) - indentation = @editor.indentationForBufferRow(row) - if indentation <= startIndentLevel - includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row)) - foldEndRow = row if includeRowInFold - break - - foldEndRow = row - - [bufferRow, foldEndRow] - - isFoldableAtBufferRow: (bufferRow) -> - @editor.tokenizedBuffer.isFoldableAtRow(bufferRow) - - # Returns a {Boolean} indicating whether the line at the given buffer - # row is a comment. - isLineCommentedAtBufferRow: (bufferRow) -> - return false unless 0 <= bufferRow <= @editor.getLastBufferRow() - @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() ? false - # Find a row range for a 'paragraph' around specified bufferRow. A paragraph # is a block of text bounded by and empty line or a block of text that is not # the same type (comments next to source code). rowRangeForParagraphAtBufferRow: (bufferRow) -> - scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) - commentStrings = @editor.getCommentStrings(scope) - commentStartRegex = null - if commentStrings?.commentStartString? and not commentStrings.commentEndString? - commentStartRegexString = _.escapeRegExp(commentStrings.commentStartString).replace(/(\s+)$/, '(?:$1)?') - commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") + return unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(bufferRow)) - filterCommentStart = (line) -> - if commentStartRegex? - matches = commentStartRegex.searchSync(line) - line = line.substring(matches[0].end) if matches?.length - line - - return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(bufferRow))) - - if @isLineCommentedAtBufferRow(bufferRow) - isOriginalRowComment = true - range = @rowRangeForCommentAtBufferRow(bufferRow) - [firstRow, lastRow] = range or [bufferRow, bufferRow] - else - isOriginalRowComment = false - [firstRow, lastRow] = [0, @editor.getLastBufferRow()-1] + isCommented = @editor.tokenizedBuffer.isRowCommented(bufferRow) startRow = bufferRow - while startRow > firstRow - break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment - break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1))) + while startRow > 0 + break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(startRow - 1)) + break if @editor.tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented startRow-- endRow = bufferRow - lastRow = @editor.getLastBufferRow() - while endRow < lastRow - break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment - break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1))) + rowCount = @editor.getLineCount() + while endRow < rowCount + break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(endRow + 1)) + break if @editor.tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented endRow++ - new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length]) + new Range(new Point(startRow, 0), new Point(endRow, @editor.buffer.lineLengthForRow(endRow))) # Given a buffer row, this returns a suggested indentation level. # @@ -345,6 +227,3 @@ class LanguageMode decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) -> @cacheRegex(@editor.getDecreaseNextIndentPattern(scopeDescriptor)) - - foldEndRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getFoldEndPattern(scopeDescriptor)) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a84f6f631..117589750 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3311,13 +3311,14 @@ class TextEditor extends Model # indentation level up to the nearest following row with a lower indentation # level. foldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @foldBufferRow(bufferRow) + {row} = @getCursorBufferPosition() + range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) + @displayLayer.foldBufferRange(range) # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @unfoldBufferRow(bufferRow) + position = @getCursorBufferPosition() + @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Essential: Fold the given row in buffer coordinates based on its indentation # level. @@ -3327,13 +3328,26 @@ class TextEditor extends Model # # * `bufferRow` A {Number}. foldBufferRow: (bufferRow) -> - @languageMode.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) -> - @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))) + position = Point(bufferRow, Infinity) + @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Extended: For each selection, fold the rows it intersects. foldSelectedLines: -> @@ -3342,18 +3356,25 @@ class TextEditor extends Model # Extended: Fold all foldable lines. foldAll: -> - @languageMode.foldAll() + @displayLayer.destroyAllFolds() + for range in @tokenizedBuffer.getFoldableRanges(@getTabLength()) + @displayLayer.foldBufferRange(range) + return # Extended: Unfold all existing folds. unfoldAll: -> - @languageMode.unfoldAll() + result = @displayLayer.destroyAllFolds() @scrollToCursorPosition() + result # Extended: Fold all foldable lines at the given indent level. # # * `level` A {Number}. foldAllAtIndentLevel: (level) -> - @languageMode.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. # @@ -3547,6 +3568,7 @@ class TextEditor extends Model # 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. diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index fbb9de77f..fd5691740 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -6,8 +6,11 @@ const TokenIterator = require('./token-iterator') const ScopeDescriptor = require('./scope-descriptor') const TokenizedBufferIterator = require('./tokenized-buffer-iterator') const NullGrammar = require('./null-grammar') +const {OnigRegExp} = require('oniguruma') const {toFirstMateScopeId} = require('./first-mate-helpers') +const NON_WHITESPACE_REGEX = /\S/ + let nextId = 0 const prefixedScopes = new Map() @@ -26,6 +29,7 @@ class TokenizedBuffer { this.emitter = new Emitter() this.disposables = new CompositeDisposable() this.tokenIterator = new TokenIterator(this) + this.regexesByPattern = {} this.alive = true this.id = params.id != null ? params.id : nextId++ @@ -265,33 +269,7 @@ class TokenizedBuffer { } isFoldableAtRow (row) { - return this.isFoldableCodeAtRow(row) || this.isFoldableCommentAtRow(row) - } - - // Returns a {Boolean} indicating whether the given buffer row starts - // a a foldable row range due to the code's indentation patterns. - isFoldableCodeAtRow (row) { - if (row >= 0 && row <= this.buffer.getLastRow()) { - const nextRow = this.buffer.nextNonBlankRow(row) - const tokenizedLine = this.tokenizedLines[row] - if (this.buffer.isRowBlank(row) || (tokenizedLine && tokenizedLine.isComment()) || nextRow == null) { - return false - } else { - return this.indentLevelForRow(nextRow) > this.indentLevelForRow(row) - } - } else { - return false - } - } - - isFoldableCommentAtRow (row) { - const previousRow = row - 1 - const nextRow = row + 1 - return ( - (!this.tokenizedLines[previousRow] || !this.tokenizedLines[previousRow].isComment()) && - (this.tokenizedLines[row] && this.tokenizedLines[row].isComment()) && - (this.tokenizedLines[nextRow] && this.tokenizedLines[nextRow].isComment()) - ) + return this.endRowForFoldAtRow(row, 1) != null } buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { @@ -554,6 +532,116 @@ class TokenizedBuffer { return new Range(new Point(position.row, startColumn), new Point(position.row, endColumn)) } + isRowCommented (row) { + return this.tokenizedLines[row] && this.tokenizedLines[row].isComment() + } + + getFoldableRangeContainingPoint (point, tabLength) { + if (point.column >= this.buffer.lineLengthForRow(point.row)) { + const endRow = this.endRowForFoldAtRow(point.row, tabLength) + if (endRow != null) { + return Range(Point(point.row, Infinity), Point(endRow, Infinity)) + } + } + + for (let row = point.row - 1; row >= 0; row--) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null && endRow > point.row) { + return Range(Point(row, Infinity), Point(endRow, Infinity)) + } + } + return null + } + + getFoldableRangesAtIndentLevel (indentLevel, tabLength) { + const result = [] + let row = 0 + const lineCount = this.buffer.getLineCount() + while (row < lineCount) { + if (this.indentLevelForLine(this.buffer.lineForRow(row), tabLength) === indentLevel) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null) { + result.push(Range(Point(row, Infinity), Point(endRow, Infinity))) + row = endRow + 1 + continue + } + } + row++ + } + return result + } + + getFoldableRanges (tabLength) { + const result = [] + let row = 0 + const lineCount = this.buffer.getLineCount() + while (row < lineCount) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null) { + result.push(Range(Point(row, Infinity), Point(endRow, Infinity))) + } + row++ + } + return result + } + + endRowForFoldAtRow (row, tabLength) { + if (this.isRowCommented(row)) { + return this.endRowForCommentFoldAtRow(row) + } else { + return this.endRowForCodeFoldAtRow(row, tabLength) + } + } + + endRowForCommentFoldAtRow (row) { + if (this.isRowCommented(row - 1)) return + + let endRow + for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) { + if (!this.isRowCommented(nextRow)) break + endRow = nextRow + } + + return endRow + } + + endRowForCodeFoldAtRow (row, tabLength) { + let foldEndRow + const line = this.buffer.lineForRow(row) + if (!NON_WHITESPACE_REGEX.test(line)) return + const startIndentLevel = this.indentLevelForLine(line, tabLength) + const scopeDescriptor = this.scopeDescriptorForPosition([row, 0]) + const foldEndRegex = this.foldEndRegexForScopeDescriptor(scopeDescriptor) + for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) { + const line = this.buffer.lineForRow(nextRow) + if (!NON_WHITESPACE_REGEX.test(line)) continue + const indentation = this.indentLevelForLine(line, tabLength) + if (indentation < startIndentLevel) { + break + } else if (indentation === startIndentLevel) { + if (foldEndRegex && foldEndRegex.searchSync(line)) foldEndRow = nextRow + break + } + foldEndRow = nextRow + } + return foldEndRow + } + + foldEndRegexForScopeDescriptor (scopes) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getFoldEndPattern(scopes)) + } + } + + regexForPattern (pattern) { + if (pattern) { + if (!this.regexesByPattern[pattern]) { + this.regexesByPattern[pattern] = new OnigRegExp(pattern) + } + return this.regexesByPattern[pattern] + } + } + // Gets the row number of the last line. // // Returns a {Number}. From aed7c1c060ed9585047a596831daa7250e4fa6df Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 22 Sep 2017 21:36:41 +0200 Subject: [PATCH 227/448] :arrow_up: language-mustache@0.14.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ffc655280..0e17b69cb 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "language-json": "0.19.1", "language-less": "0.33.0", "language-make": "0.22.3", - "language-mustache": "0.14.2", + "language-mustache": "0.14.3", "language-objective-c": "0.15.1", "language-perl": "0.37.0", "language-php": "0.42.0", From 0884546d3cee83202a8600f919fc3da5e766d7e5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 14:33:02 -0700 Subject: [PATCH 228/448] Move everything but auto-indent out of LanguageMode --- spec/language-mode-spec.js | 436 +++++++++++++--------------------- spec/text-editor-spec.coffee | 52 ++++ spec/tokenized-buffer-spec.js | 24 ++ src/cursor.coffee | 2 +- src/language-mode.coffee | 106 --------- src/text-editor.coffee | 92 ++++++- 6 files changed, 333 insertions(+), 379 deletions(-) diff --git a/spec/language-mode-spec.js b/spec/language-mode-spec.js index 34f341bfc..cbb9377cb 100644 --- a/spec/language-mode-spec.js +++ b/spec/language-mode-spec.js @@ -9,70 +9,13 @@ describe('LanguageMode', () => { editor.destroy() }) - describe('javascript', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) - await atom.packages.activatePackage('language-javascript') - }) - - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('.toggleLineCommentsForBufferRows(start, end)', () => { - 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('.suggestedIndentForBufferRow', () => { + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') }) - }) - describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { - it('returns the start/end rows of the foldable region starting at the given row', () => { - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]]) - }) - }) - - describe('.suggestedIndentForBufferRow', () => { it('bases indentation off of the previous non-blank line', () => { expect(editor.suggestedIndentForBufferRow(0)).toBe(0) expect(editor.suggestedIndentForBufferRow(1)).toBe(1) @@ -95,120 +38,53 @@ describe('LanguageMode', () => { }) }) - describe('rowRangeForParagraphAtBufferRow', () => { - describe('with code and comments', () => { - beforeEach(() => - editor.setText(dedent ` - var quicksort = function () { - /* Single line comment block */ - var sort = function(items) {}; + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: true}) + await atom.packages.activatePackage('language-source') + await atom.packages.activatePackage('language-css') + }) - /* - 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; - } - - }; - `) - ) - - it('will limit paragraph range to comments', () => { - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(0)).toEqual([[0, 0], [0, 29]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(1)).toEqual([[1, 0], [1, 33]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(2)).toEqual([[2, 0], [2, 32]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(3)).toBeFalsy() - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(4)).toEqual([[4, 0], [7, 4]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(5)).toEqual([[4, 0], [7, 4]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(6)).toEqual([[4, 0], [7, 4]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(7)).toEqual([[4, 0], [7, 4]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(8)).toEqual([[8, 0], [8, 32]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(9)).toBeFalsy() - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(10)).toEqual([[10, 0], [13, 10]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(11)).toEqual([[10, 0], [13, 10]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(12)).toEqual([[10, 0], [13, 10]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(14)).toEqual([[14, 0], [14, 32]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(15)).toEqual([[15, 0], [15, 26]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(18)).toEqual([[17, 0], [19, 3]]) - }) + it('does not return negative values (regression)', () => { + editor.setText('.test {\npadding: 0;\n}') + expect(editor.suggestedIndentForBufferRow(2)).toBe(0) }) }) }) - describe('coffeescript', () => { - beforeEach(async () => { - editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) - await atom.packages.activatePackage('language-coffee-script') - }) - - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('.toggleLineCommentsForBufferRows(start, end)', () => { - 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 = []') + describe('.toggleLineCommentsForBufferRows', () => { + describe('xml', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.xml', {autoIndent: false}) + editor.setText('') + await atom.packages.activatePackage('language-xml') }) - it('comments/uncomments lines when empty line', () => { - 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(' # ') + 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('fold suggestion', () => { - describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { - it('returns the start/end rows of the foldable region starting at the given row', () => { - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]]) - }) + describe('less', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.less', {autoIndent: false}) + await atom.packages.activatePackage('language-less') + await atom.packages.activatePackage('language-css') + }) + + 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 () => { - editor = await atom.workspace.open('css.css', {autoIndent: false}) - await atom.packages.activatePackage('language-css') - }) + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: false}) + await atom.packages.activatePackage('language-css') + }) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('.toggleLineCommentsForBufferRows(start, end)', () => { it('comments/uncomments lines in the given range', () => { editor.toggleLineCommentsForBufferRows(0, 1) expect(editor.lineTextForBufferRow(0)).toBe('/*body {') @@ -247,67 +123,107 @@ describe('LanguageMode', () => { expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') }) }) - }) - describe('less', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.less', {autoIndent: false}) - await atom.packages.activatePackage('language-less') - await atom.packages.activatePackage('language-css') - }) + describe('coffeescript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) + await atom.packages.activatePackage('language-coffee-script') + }) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) + 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 = []') - describe('when commenting lines', () => { - it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`', () => { - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + 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('xml', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.xml', {autoIndent: false}) - editor.setText('') - await atom.packages.activatePackage('language-xml') - }) + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) + 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(' // }') - describe('when uncommenting lines', () => { - it('removes the leading whitespace from the comment end pattern match', () => { + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') + console.log(JSON.stringify(editor.lineTextForBufferRow(5))); + return + 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('test') + 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 () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) await atom.packages.activatePackage('language-javascript') }) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - it('maintains cursor buffer position when a folding/unfolding', () => { + it('maintains cursor buffer position when a folding/unfolding', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) editor.setCursorBufferPosition([5, 5]) editor.foldAll() expect(editor.getCursorBufferPosition()).toEqual([5, 5]) }) describe('.unfoldAll()', () => { - it('unfolds every folded line', () => { + it('unfolds every folded line', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + const initialScreenLineCount = editor.getScreenLineCount() editor.foldBufferRow(0) editor.foldBufferRow(1) @@ -315,20 +231,52 @@ describe('LanguageMode', () => { editor.unfoldAll() expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) }) + + it('unfolds every folded line with comments', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(5) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + editor.unfoldAll() + expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + }) }) describe('.foldAll()', () => { - it('folds every foldable line', () => { - editor.foldAll() + it('folds every foldable line', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + editor.foldAll() 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]) expect([fold3.start.row, fold3.end.row]).toEqual([4, 7]) }) + + it('works with multi-line comments', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + + editor.foldAll() + const folds = editor.unfoldAll() + expect(folds.length).toBe(8) + expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) + expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4]) + expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27]) + expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8]) + expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16]) + expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20]) + expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22]) + expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25]) + }) }) describe('.foldBufferRow(bufferRow)', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js') + }) + describe('when bufferRow can be folded', () => { it('creates a fold based on the syntactic region starting at the given row', () => { editor.foldBufferRow(1) @@ -376,7 +324,9 @@ describe('LanguageMode', () => { }) describe('.foldAllAtIndentLevel(indentLevel)', () => { - it('folds blocks of text at the given indentation level', () => { + it('folds blocks of text at the given indentation level', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + editor.foldAllAtIndentLevel(0) expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`) expect(editor.getLastScreenRow()).toBe(0) @@ -392,52 +342,11 @@ describe('LanguageMode', () => { expect(editor.lineTextForScreenRow(2)).toBe(' if (items.length <= 1) return items;') expect(editor.getLastScreenRow()).toBe(9) }) - }) - }) - describe('folding with comments', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) - await atom.packages.activatePackage('language-javascript') - }) + it('folds every foldable range at a given indentLevel', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('.unfoldAll()', () => { - it('unfolds every folded line', () => { - const initialScreenLineCount = editor.getScreenLineCount() - editor.foldBufferRow(0) - editor.foldBufferRow(5) - expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) - editor.unfoldAll() - expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) - }) - }) - - describe('.foldAll()', () => { - it('folds every foldable line', () => { - editor.foldAll() - - const folds = editor.unfoldAll() - expect(folds.length).toBe(8) - expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) - expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4]) - expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27]) - expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8]) - expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16]) - expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20]) - expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22]) - expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25]) - }) - }) - - describe('.foldAllAtIndentLevel()', () => { - it('folds every foldable range at a given indentLevel', () => { editor.foldAllAtIndentLevel(2) - const folds = editor.unfoldAll() expect(folds.length).toBe(5) expect([folds[0].start.row, folds[0].end.row]).toEqual([6, 8]) @@ -447,9 +356,10 @@ describe('LanguageMode', () => { expect([folds[4].start.row, folds[4].end.row]).toEqual([24, 25]) }) - it('does not fold anything but the indentLevel', () => { - editor.foldAllAtIndentLevel(0) + it('does not fold anything but the indentLevel', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + editor.foldAllAtIndentLevel(0) const folds = editor.unfoldAll() expect(folds.length).toBe(1) expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) @@ -457,7 +367,9 @@ describe('LanguageMode', () => { }) describe('.isFoldableAtBufferRow(bufferRow)', () => { - it('returns true if the line starts a multi-line comment', () => { + it('returns true if the line starts a multi-line comment', async () => { + editor = await atom.workspace.open('sample-with-comments.js') + expect(editor.isFoldableAtBufferRow(1)).toBe(true) expect(editor.isFoldableAtBufferRow(6)).toBe(true) expect(editor.isFoldableAtBufferRow(8)).toBe(false) @@ -469,35 +381,19 @@ describe('LanguageMode', () => { expect(editor.isFoldableAtBufferRow(28)).toBe(false) }) - it('returns true for lines that end with a comment and are followed by an indented line', () => { + it('returns true for lines that end with a comment and are followed by an indented line', async () => { + editor = await atom.workspace.open('sample-with-comments.js') + expect(editor.isFoldableAtBufferRow(5)).toBe(true) }) - it("does not return true for a line in the middle of a comment that's followed by an indented line", () => { + it("does not return true for a line in the middle of a comment that's followed by an indented line", async () => { + editor = await atom.workspace.open('sample-with-comments.js') + expect(editor.isFoldableAtBufferRow(7)).toBe(false) editor.buffer.insert([8, 0], ' ') expect(editor.isFoldableAtBufferRow(7)).toBe(false) }) }) }) - - describe('css', () => { - beforeEach(async () => { - editor = await atom.workspace.open('css.css', {autoIndent: true}) - await atom.packages.activatePackage('language-source') - await atom.packages.activatePackage('language-css') - }) - - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('suggestedIndentForBufferRow', () => { - it('does not return negative values (regression)', () => { - editor.setText('.test {\npadding: 0;\n}') - expect(editor.suggestedIndentForBufferRow(2)).toBe(0) - }) - }) - }) }) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index cb70d030c..efe3bf048 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1168,6 +1168,58 @@ describe "TextEditor", -> 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]) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 134a1a0b1..55db55fe9 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -901,6 +901,30 @@ describe('TokenizedBuffer', () => { } `) }) + + it('works for coffee-script', async () => { + const editor = await atom.workspace.open('coffee.coffee') + await atom.packages.activatePackage('language-coffee-script') + buffer = editor.buffer + tokenizedBuffer = editor.tokenizedBuffer + + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]]) + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]]) + }) + + it('works for javascript', async () => { + const editor = await atom.workspace.open('sample.js') + await atom.packages.activatePackage('language-javascript') + buffer = editor.buffer + tokenizedBuffer = editor.tokenizedBuffer + + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]]) + }) }) function simulateFold (ranges) { diff --git a/src/cursor.coffee b/src/cursor.coffee index 6273b0276..2acbfecf4 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -543,7 +543,7 @@ class Cursor extends Model # # Returns a {Range}. getCurrentParagraphBufferRange: -> - @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) + @editor.rowRangeForParagraphAtBufferRow(@getBufferRow()) # Public: Returns the characters preceding the cursor in the current word. getCurrentWordPrefix: -> diff --git a/src/language-mode.coffee b/src/language-mode.coffee index 953d328b2..6d306a38a 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -4,8 +4,6 @@ _ = require 'underscore-plus' ScopeDescriptor = require './scope-descriptor' NullGrammar = require './null-grammar' -NON_WHITESPACE_REGEX = /\S/ - module.exports = class LanguageMode # Sets up a `LanguageMode` for the given {TextEditor}. @@ -15,106 +13,6 @@ class LanguageMode {@buffer} = @editor @regexesByPattern = {} - destroy: -> - - toggleLineCommentForBufferRow: (row) -> - @toggleLineCommentsForBufferRows(row, row) - - # Wraps the lines between two rows in comments. - # - # If the language doesn't have comment, nothing happens. - # - # startRow - The row {Number} to start at - # endRow - The row {Number} to end at - toggleLineCommentsForBufferRows: (start, end) -> - scope = @editor.scopeDescriptorForBufferPosition([start, 0]) - commentStrings = @editor.getCommentStrings(scope) - return unless commentStrings?.commentStartString - {commentStartString, commentEndString} = commentStrings - - buffer = @editor.buffer - commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') - commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") - - if commentEndString - shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start)) - if shouldUncomment - commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') - commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$") - startMatch = commentStartRegex.searchSync(buffer.lineForRow(start)) - endMatch = commentEndRegex.searchSync(buffer.lineForRow(end)) - if startMatch and endMatch - buffer.transact -> - columnStart = startMatch[1].length - columnEnd = columnStart + startMatch[2].length - buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "") - - endLength = buffer.lineLengthForRow(end) - endMatch[2].length - endColumn = endLength - endMatch[1].length - buffer.setTextInRange([[end, endColumn], [end, endLength]], "") - else - buffer.transact -> - indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0 - buffer.insert([start, indentLength], commentStartString) - buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString) - else - allBlank = true - allBlankOrCommented = true - - for row in [start..end] by 1 - line = buffer.lineForRow(row) - blank = line?.match(/^\s*$/) - - allBlank = false unless blank - allBlankOrCommented = false unless blank or commentStartRegex.testSync(line) - - shouldUncomment = allBlankOrCommented and not allBlank - - if shouldUncomment - for row in [start..end] by 1 - if match = commentStartRegex.searchSync(buffer.lineForRow(row)) - columnStart = match[1].length - columnEnd = columnStart + match[2].length - buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "") - else - if start is end - indent = @editor.indentationForBufferRow(start) - else - indent = @minIndentLevelForRowRange(start, end) - indentString = @editor.buildIndentString(indent) - tabLength = @editor.getTabLength() - indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}") - for row in [start..end] by 1 - line = buffer.lineForRow(row) - if indentLength = line.match(indentRegex)?[0].length - buffer.insert([row, indentLength], commentStartString) - else - buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) - return - - # Find a row range for a 'paragraph' around specified bufferRow. A paragraph - # is a block of text bounded by and empty line or a block of text that is not - # the same type (comments next to source code). - rowRangeForParagraphAtBufferRow: (bufferRow) -> - return unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(bufferRow)) - - isCommented = @editor.tokenizedBuffer.isRowCommented(bufferRow) - - startRow = bufferRow - while startRow > 0 - break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(startRow - 1)) - break if @editor.tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented - startRow-- - - endRow = bufferRow - rowCount = @editor.getLineCount() - while endRow < rowCount - break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(endRow + 1)) - break if @editor.tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented - endRow++ - - new Range(new Point(startRow, 0), new Point(endRow, @editor.buffer.lineLengthForRow(endRow))) - # Given a buffer row, this returns a suggested indentation level. # # The indentation level provided is based on the current {LanguageMode}. @@ -166,10 +64,6 @@ class LanguageMode # endRow - The row {Number} to end at # # Returns a {Number} of the indent level of the block of lines. - minIndentLevelForRowRange: (startRow, endRow) -> - indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] by 1 when not @editor.isBufferRowBlank(row)) - indents = [0] unless indents.length - Math.min(indents...) # Indents all the rows between two buffer row numbers. # diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 117589750..d75276f06 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3,6 +3,7 @@ path = require 'path' fs = require 'fs-plus' Grim = require 'grim' {CompositeDisposable, Disposable, Emitter} = require 'event-kit' +{OnigRegExp} = require 'oniguruma' {Point, Range} = TextBuffer = require 'text-buffer' LanguageMode = require './language-mode' DecorationManager = require './decoration-manager' @@ -16,6 +17,7 @@ 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 @@ -482,7 +484,6 @@ class TextEditor extends Model @tokenizedBuffer.destroy() selection.destroy() for selection in @selections.slice() @buffer.release() - @languageMode.destroy() @gutterContainer.destroy() @emitter.emit 'did-destroy' @emitter.clear() @@ -3882,4 +3883,91 @@ class TextEditor extends Model toggleLineCommentForBufferRow: (row) -> @languageMode.toggleLineCommentsForBufferRow(row) - toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end) + toggleLineCommentsForBufferRows: (start, end) -> + scope = @scopeDescriptorForBufferPosition([start, 0]) + commentStrings = @getCommentStrings(scope) + return unless commentStrings?.commentStartString + {commentStartString, commentEndString} = commentStrings + + buffer = @buffer + commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') + commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") + + if commentEndString + shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start)) + if shouldUncomment + commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') + commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$") + startMatch = commentStartRegex.searchSync(buffer.lineForRow(start)) + endMatch = commentEndRegex.searchSync(buffer.lineForRow(end)) + if startMatch and endMatch + buffer.transact -> + columnStart = startMatch[1].length + columnEnd = columnStart + startMatch[2].length + buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "") + + endLength = buffer.lineLengthForRow(end) - endMatch[2].length + endColumn = endLength - endMatch[1].length + buffer.setTextInRange([[end, endColumn], [end, endLength]], "") + else + buffer.transact -> + indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0 + buffer.insert([start, indentLength], commentStartString) + buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString) + else + allBlank = true + allBlankOrCommented = true + + for row in [start..end] by 1 + line = buffer.lineForRow(row) + blank = line?.match(/^\s*$/) + + allBlank = false unless blank + allBlankOrCommented = false unless blank or commentStartRegex.testSync(line) + + shouldUncomment = allBlankOrCommented and not allBlank + + if shouldUncomment + for row in [start..end] by 1 + if match = commentStartRegex.searchSync(buffer.lineForRow(row)) + columnStart = match[1].length + columnEnd = columnStart + match[2].length + buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "") + else + indents = [] + for row in [start..end] by 1 + unless @isBufferRowBlank(row) + indents.push(@indentationForBufferRow(start)) + indents.push(0) if indents.length is 0 + indent = Math.min(indents...) + + indentString = @buildIndentString(indent) + tabLength = @getTabLength() + indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}") + for row in [start..end] by 1 + line = buffer.lineForRow(row) + if indentLength = line.match(indentRegex)?[0].length + buffer.insert([row, indentLength], commentStartString) + else + buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) + return + + 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))) From 8be9375508cfdd7614d6b575894ebee350b941b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 14:38:54 -0700 Subject: [PATCH 229/448] Remove unnecessary TokenizedBuffer methods --- src/tokenized-buffer.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index fd5691740..f7e96e88c 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -178,7 +178,7 @@ class TokenizedBuffer { while (this.firstInvalidRow() != null && rowsRemaining > 0) { var endRow, filledRegion const startRow = this.invalidRows.shift() - const lastRow = this.getLastRow() + const lastRow = this.buffer.getLastRow() if (startRow > lastRow) continue let row = startRow @@ -398,7 +398,7 @@ class TokenizedBuffer { if (line === '') { let nextRow = bufferRow + 1 - const lineCount = this.getLineCount() + const lineCount = this.buffer.getLineCount() while (nextRow < lineCount) { const nextLine = this.buffer.lineForRow(nextRow) if (nextLine !== '') { @@ -642,17 +642,6 @@ class TokenizedBuffer { } } - // Gets the row number of the last line. - // - // Returns a {Number}. - getLastRow () { - return this.buffer.getLastRow() - } - - getLineCount () { - return this.buffer.getLineCount() - } - logLines (start = 0, end = this.buffer.getLastRow()) { for (let row = start; row <= end; row++) { const line = this.tokenizedLines[row].text From f762bf9548895aec4155188bdb7f3f042a9ae618 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 22 Sep 2017 23:39:04 +0200 Subject: [PATCH 230/448] :arrow_up: first-mate@7.0.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e17b69cb..3ecdd6fd9 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.7", + "first-mate": "7.0.8", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From 62e94f7b96fdc225f2ae7ddd9472f4c857efb504 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 14:40:34 -0700 Subject: [PATCH 231/448] Rename language-mode-spec.js to text-editor-spec.js This gets the ball rolling toward converting the text editor specs to JS --- spec/{language-mode-spec.js => text-editor-spec.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spec/{language-mode-spec.js => text-editor-spec.js} (99%) diff --git a/spec/language-mode-spec.js b/spec/text-editor-spec.js similarity index 99% rename from spec/language-mode-spec.js rename to spec/text-editor-spec.js index cbb9377cb..e72417aca 100644 --- a/spec/language-mode-spec.js +++ b/spec/text-editor-spec.js @@ -2,7 +2,7 @@ const dedent = require('dedent') const {Point, Range} = require('text-buffer') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') -describe('LanguageMode', () => { +describe('TextEditor', () => { let editor afterEach(() => { From 67ec6fb4cf6466825a068ae1b907b4a1efa695bc Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 00:06:17 +0200 Subject: [PATCH 232/448] Revert ":arrow_up: first-mate@7.0.8" This reverts commit f762bf9548895aec4155188bdb7f3f042a9ae618. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ecdd6fd9..0e17b69cb 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.8", + "first-mate": "7.0.7", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From 274a699272fa1e894655d71e1eae88bb4e909c62 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 16:20:10 -0700 Subject: [PATCH 233/448] Remove unused method TokenizedBuffer.indentLevelForRow --- spec/tokenized-buffer-spec.js | 85 ----------------------------------- src/tokenized-buffer.js | 32 ------------- 2 files changed, 117 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 55db55fe9..c0bd29b50 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -443,91 +443,6 @@ describe('TokenizedBuffer', () => { }) }) - describe('.indentLevelForRow(row)', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - }) - - describe('when the line is non-empty', () => { - it('has an indent level based on the leading whitespace on the line', () => { - expect(tokenizedBuffer.indentLevelForRow(0)).toBe(0) - expect(tokenizedBuffer.indentLevelForRow(1)).toBe(1) - expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2) - buffer.insert([2, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2.5) - }) - }) - - describe('when the line is empty', () => { - it('assumes the indentation level of the first non-empty line below or above if one exists', () => { - buffer.insert([12, 0], ' ') - buffer.insert([12, Infinity], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(14)).toBe(2) - - buffer.insert([1, Infinity], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(3)).toBe(2) - - buffer.setText('\n\n\n') - expect(tokenizedBuffer.indentLevelForRow(1)).toBe(0) - }) - }) - - describe('when the changed lines are surrounded by whitespace-only lines', () => { - it('updates the indentLevel of empty lines that precede the change', () => { - expect(tokenizedBuffer.indentLevelForRow(12)).toBe(0) - - buffer.insert([12, 0], '\n') - buffer.insert([13, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(12)).toBe(1) - }) - - it('updates empty line indent guides when the empty line is the last line', () => { - buffer.insert([12, 2], '\n') - - // The newline and the tab need to be in two different operations to surface the bug - buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe(1) - - buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) - expect(tokenizedBuffer.tokenizedLines[14]).not.toBeDefined() - }) - - it('updates the indentLevel of empty lines surrounding a change that inserts lines', () => { - buffer.insert([7, 0], '\n\n') - buffer.insert([5, 0], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe(3) - expect(tokenizedBuffer.indentLevelForRow(6)).toBe(3) - expect(tokenizedBuffer.indentLevelForRow(9)).toBe(3) - expect(tokenizedBuffer.indentLevelForRow(10)).toBe(3) - expect(tokenizedBuffer.indentLevelForRow(11)).toBe(2) - - buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe(4) - expect(tokenizedBuffer.indentLevelForRow(6)).toBe(4) - expect(tokenizedBuffer.indentLevelForRow(11)).toBe(4) - expect(tokenizedBuffer.indentLevelForRow(12)).toBe(4) - expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) - }) - - it('updates the indentLevel of empty lines surrounding a change that removes lines', () => { - buffer.insert([7, 0], '\n\n') - buffer.insert([5, 0], '\n\n') - buffer.setTextInRange([[7, 0], [8, 65]], ' ok') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(6)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(7)).toBe(2) // new text - expect(tokenizedBuffer.indentLevelForRow(8)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(9)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(10)).toBe(2) - }) - }) - }) // } - describe('.tokenizedLineForRow(row)', () => { it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { buffer = atom.project.bufferForPathSync('sample.js') diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index f7e96e88c..f51baa950 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -392,38 +392,6 @@ class TokenizedBuffer { return scopes } - indentLevelForRow (bufferRow) { - const line = this.buffer.lineForRow(bufferRow) - let indentLevel = 0 - - if (line === '') { - let nextRow = bufferRow + 1 - const lineCount = this.buffer.getLineCount() - while (nextRow < lineCount) { - const nextLine = this.buffer.lineForRow(nextRow) - if (nextLine !== '') { - indentLevel = Math.ceil(this.indentLevelForLine(nextLine)) - break - } - nextRow++ - } - - let previousRow = bufferRow - 1 - while (previousRow >= 0) { - const previousLine = this.buffer.lineForRow(previousRow) - if (previousLine !== '') { - indentLevel = Math.max(Math.ceil(this.indentLevelForLine(previousLine)), indentLevel) - break - } - previousRow-- - } - - return indentLevel - } else { - return this.indentLevelForLine(line) - } - } - indentLevelForLine (line, tabLength = this.tabLength) { let indentLength = 0 for (let i = 0, {length} = line; i < length; i++) { From e14aa842ff60f64b270c8a7fcece4be7d3c7867f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 16:21:06 -0700 Subject: [PATCH 234/448] Move auto-indent code to TokenizedBuffer, :fire: LanguageMode --- src/language-mode.coffee | 123 --------------------------------------- src/selection.coffee | 2 +- src/text-editor.coffee | 46 ++++++++------- src/tokenized-buffer.js | 117 +++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 146 deletions(-) delete mode 100644 src/language-mode.coffee diff --git a/src/language-mode.coffee b/src/language-mode.coffee deleted file mode 100644 index 6d306a38a..000000000 --- a/src/language-mode.coffee +++ /dev/null @@ -1,123 +0,0 @@ -{Range, Point} = require 'text-buffer' -_ = require 'underscore-plus' -{OnigRegExp} = require 'oniguruma' -ScopeDescriptor = require './scope-descriptor' -NullGrammar = require './null-grammar' - -module.exports = -class LanguageMode - # Sets up a `LanguageMode` for the given {TextEditor}. - # - # editor - The {TextEditor} to associate with - constructor: (@editor) -> - {@buffer} = @editor - @regexesByPattern = {} - - # Given a buffer row, this returns a suggested indentation level. - # - # The indentation level provided is based on the current {LanguageMode}. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns a {Number}. - suggestedIndentForBufferRow: (bufferRow, options) -> - line = @buffer.lineForRow(bufferRow) - tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow) - @suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) - - suggestedIndentForLineAtBufferRow: (bufferRow, line, options) -> - tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line) - @suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) - - suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) -> - iterator = tokenizedLine.getTokenIterator() - iterator.next() - scopeDescriptor = new ScopeDescriptor(scopes: iterator.getScopes()) - - increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor) - decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor) - decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) - - if options?.skipBlankLines ? true - precedingRow = @buffer.previousNonBlankRow(bufferRow) - return 0 unless precedingRow? - else - precedingRow = bufferRow - 1 - return 0 if precedingRow < 0 - - desiredIndentLevel = @editor.indentationForBufferRow(precedingRow) - return desiredIndentLevel unless increaseIndentRegex - - unless @editor.isBufferRowCommented(precedingRow) - precedingLine = @buffer.lineForRow(precedingRow) - desiredIndentLevel += 1 if increaseIndentRegex?.testSync(precedingLine) - desiredIndentLevel -= 1 if decreaseNextIndentRegex?.testSync(precedingLine) - - unless @buffer.isRowBlank(precedingRow) - desiredIndentLevel -= 1 if decreaseIndentRegex?.testSync(line) - - Math.max(desiredIndentLevel, 0) - - # Calculate a minimum indent level for a range of lines excluding empty lines. - # - # startRow - The row {Number} to start at - # endRow - The row {Number} to end at - # - # Returns a {Number} of the indent level of the block of lines. - - # 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) -> - @autoIndentBufferRow(row) for row in [startRow..endRow] by 1 - return - - # Given a buffer row, this indents it. - # - # bufferRow - The row {Number}. - # options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. - autoIndentBufferRow: (bufferRow, options) -> - indentLevel = @suggestedIndentForBufferRow(bufferRow, options) - @editor.setIndentationForBufferRow(bufferRow, indentLevel, options) - - # Given a buffer row, this decreases the indentation. - # - # bufferRow - The row {Number} - autoDecreaseIndentForBufferRow: (bufferRow) -> - scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) - return unless decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor) - - line = @buffer.lineForRow(bufferRow) - return unless decreaseIndentRegex.testSync(line) - - currentIndentLevel = @editor.indentationForBufferRow(bufferRow) - return if currentIndentLevel is 0 - - precedingRow = @buffer.previousNonBlankRow(bufferRow) - return unless precedingRow? - - precedingLine = @buffer.lineForRow(precedingRow) - desiredIndentLevel = @editor.indentationForBufferRow(precedingRow) - - if increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor) - desiredIndentLevel -= 1 unless increaseIndentRegex.testSync(precedingLine) - - if decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) - desiredIndentLevel -= 1 if decreaseNextIndentRegex.testSync(precedingLine) - - if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel - @editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel) - - cacheRegex: (pattern) -> - if pattern - @regexesByPattern[pattern] ?= new OnigRegExp(pattern) - - increaseIndentRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getIncreaseIndentPattern(scopeDescriptor)) - - decreaseIndentRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getDecreaseIndentPattern(scopeDescriptor)) - - decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getDecreaseNextIndentPattern(scopeDescriptor)) diff --git a/src/selection.coffee b/src/selection.coffee index e361d0b5c..4d3fe8882 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -381,7 +381,7 @@ class Selection extends Model if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0 autoIndentFirstLine = true firstLine = precedingText + firstInsertedLine - desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) + desiredIndentLevel = @editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) indentAdjustment = desiredIndentLevel - @editor.indentLevelForLine(firstLine) @adjustIndent(remainingLines, indentAdjustment) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index d75276f06..8cbcc94f3 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -5,7 +5,6 @@ Grim = require 'grim' {CompositeDisposable, Disposable, Emitter} = require 'event-kit' {OnigRegExp} = require 'oniguruma' {Point, Range} = TextBuffer = require 'text-buffer' -LanguageMode = require './language-mode' DecorationManager = require './decoration-manager' TokenizedBuffer = require './tokenized-buffer' Cursor = require './cursor' @@ -80,7 +79,6 @@ class TextEditor extends Model serializationVersion: 1 buffer: null - languageMode: null cursors: null showCursorOnSelection: null selections: null @@ -245,8 +243,6 @@ class TextEditor extends Model initialColumn = Math.max(parseInt(initialColumn) or 0, 0) @addCursorAtBufferPosition([initialLine, initialColumn]) - @languageMode = new LanguageMode(this) - @gutterContainer = new GutterContainer(this) @lineNumberGutter = @gutterContainer.addGutter name: 'line-number' @@ -3085,7 +3081,8 @@ class TextEditor extends Model else endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length newIndentString = @buildIndentString(newLevel) - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + if newIndentString.length isnt endColumn + @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) # Extended: Indent rows intersecting selections by one level. indentSelectedRows: -> @@ -3626,18 +3623,6 @@ class TextEditor extends Model getCommentStrings: (scopes) -> @scopedSettingsDelegate?.getCommentStrings?(scopes) - getIncreaseIndentPattern: (scopes) -> - @scopedSettingsDelegate?.getIncreaseIndentPattern?(scopes) - - getDecreaseIndentPattern: (scopes) -> - @scopedSettingsDelegate?.getDecreaseIndentPattern?(scopes) - - getDecreaseNextIndentPattern: (scopes) -> - @scopedSettingsDelegate?.getDecreaseNextIndentPattern?(scopes) - - getFoldEndPattern: (scopes) -> - @scopedSettingsDelegate?.getFoldEndPattern?(scopes) - ### Section: Event Handlers ### @@ -3873,15 +3858,32 @@ class TextEditor extends Model Section: Language Mode Delegated Methods ### - suggestedIndentForBufferRow: (bufferRow, options) -> @languageMode.suggestedIndentForBufferRow(bufferRow, options) + suggestedIndentForBufferRow: (bufferRow, options) -> @tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options) - autoIndentBufferRow: (bufferRow, options) -> @languageMode.autoIndentBufferRow(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) - autoIndentBufferRows: (startRow, endRow) -> @languageMode.autoIndentBufferRows(startRow, endRow) + # 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) -> @languageMode.autoDecreaseIndentForBufferRow(bufferRow) + autoDecreaseIndentForBufferRow: (bufferRow) -> + indentLevel = @tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) + @setIndentationForBufferRow(bufferRow, indentLevel) - toggleLineCommentForBufferRow: (row) -> @languageMode.toggleLineCommentsForBufferRow(row) + toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) toggleLineCommentsForBufferRows: (start, end) -> scope = @scopeDescriptorForBufferPosition([start, 0]) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index f51baa950..4cb0e7b4e 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -57,6 +57,105 @@ class TokenizedBuffer { return !this.alive } + /* + Section - auto-indent + */ + + // Get the suggested indentation level for an existing line in the buffer. + // + // * bufferRow - A {Number} indicating the buffer row + // + // Returns a {Number}. + suggestedIndentForBufferRow (bufferRow, options) { + const line = this.buffer.lineForRow(bufferRow) + const tokenizedLine = this.tokenizedLineForRow(bufferRow) + return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) + } + + // Get the suggested indentation level for a given line of text, if it were inserted at the given + // row in the buffer. + // + // * bufferRow - A {Number} indicating the buffer row + // + // Returns a {Number}. + suggestedIndentForLineAtBufferRow (bufferRow, line, options) { + const tokenizedLine = this.buildTokenizedLineForRowWithText(bufferRow, line) + return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) + } + + // Get the suggested indentation level for a line in the buffer on which the user is currently + // typing. This may return a different result from {::suggestedIndentForBufferRow} in order + // to avoid unexpected changes in indentation. + // + // * bufferRow - The row {Number} + // + // Returns a {Number}. + suggestedIndentForEditedBufferRow (bufferRow) { + const line = this.buffer.lineForRow(bufferRow) + const currentIndentLevel = this.indentLevelForLine(line) + if (currentIndentLevel === 0) return currentIndentLevel + + const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0]) + const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) + if (!decreaseIndentRegex) return currentIndentLevel + + if (!decreaseIndentRegex.testSync(line)) return currentIndentLevel + + const precedingRow = this.buffer.previousNonBlankRow(bufferRow) + if (precedingRow == null) return currentIndentLevel + + const precedingLine = this.buffer.lineForRow(precedingRow) + let desiredIndentLevel = this.indentLevelForLine(precedingLine) + + const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor) + if (increaseIndentRegex) { + if (!increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1 + } + + const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) + if (decreaseNextIndentRegex) { + if (decreaseNextIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1 + } + + if (desiredIndentLevel < 0) return 0 + if (desiredIndentLevel > currentIndentLevel) return currentIndentLevel + return desiredIndentLevel + } + + _suggestedIndentForTokenizedLineAtBufferRow (bufferRow, line, tokenizedLine, options) { + const iterator = tokenizedLine.getTokenIterator() + iterator.next() + const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()}) + + const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor) + const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) + const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) + + let precedingRow + if (!options || options.skipBlankLines !== false) { + precedingRow = this.buffer.previousNonBlankRow(bufferRow) + if (precedingRow == null) return 0 + } else { + precedingRow = bufferRow - 1 + if (precedingRow < 0) return 0 + } + + const precedingLine = this.buffer.lineForRow(precedingRow) + let desiredIndentLevel = this.indentLevelForLine(precedingLine) + if (!increaseIndentRegex) return desiredIndentLevel + + if (!this.isRowCommented(precedingRow)) { + if (increaseIndentRegex && increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel += 1 + if (decreaseNextIndentRegex && decreaseNextIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1 + } + + if (!this.buffer.isRowBlank(precedingRow)) { + if (decreaseIndentRegex && decreaseIndentRegex.testSync(line)) desiredIndentLevel -= 1 + } + + return Math.max(desiredIndentLevel, 0) + } + buildIterator () { return new TokenizedBufferIterator(this) } @@ -595,6 +694,24 @@ class TokenizedBuffer { return foldEndRow } + increaseIndentRegexForScopeDescriptor (scopeDescriptor) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getIncreaseIndentPattern(scopeDescriptor)) + } + } + + decreaseIndentRegexForScopeDescriptor (scopeDescriptor) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseIndentPattern(scopeDescriptor)) + } + } + + decreaseNextIndentRegexForScopeDescriptor (scopeDescriptor) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseNextIndentPattern(scopeDescriptor)) + } + } + foldEndRegexForScopeDescriptor (scopes) { if (this.scopedSettingsDelegate) { return this.regexForPattern(this.scopedSettingsDelegate.getFoldEndPattern(scopes)) From 9abcad11e49d550dbfb4af9a5d04cd2c7ba7728a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 21:06:15 -0700 Subject: [PATCH 235/448] Add shim for TextEditor.languageMode, will deprecate later --- src/text-editor.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 8cbcc94f3..fe9d03a72 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -122,6 +122,8 @@ class TextEditor extends Model this ) + Object.defineProperty(@prototype, 'languageMode', get: -> @tokenizedBuffer) + @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? From a73de8c0b5dbdf28c8869587d64c7da60893c331 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 23 Sep 2017 00:22:11 -0700 Subject: [PATCH 236/448] Avoid spurious updates in autoDecreaseIndentForBufferRow --- src/text-editor.coffee | 5 ++--- src/tokenized-buffer.js | 13 +++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index fe9d03a72..2aad26f45 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3083,8 +3083,7 @@ class TextEditor extends Model else endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length newIndentString = @buildIndentString(newLevel) - if newIndentString.length isnt endColumn - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) # Extended: Indent rows intersecting selections by one level. indentSelectedRows: -> @@ -3883,7 +3882,7 @@ class TextEditor extends Model autoDecreaseIndentForBufferRow: (bufferRow) -> indentLevel = @tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) - @setIndentationForBufferRow(bufferRow, indentLevel) + @setIndentationForBufferRow(bufferRow, indentLevel) if indentLevel? toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 4cb0e7b4e..b0ee635da 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -85,7 +85,8 @@ class TokenizedBuffer { // Get the suggested indentation level for a line in the buffer on which the user is currently // typing. This may return a different result from {::suggestedIndentForBufferRow} in order - // to avoid unexpected changes in indentation. + // to avoid unexpected changes in indentation. It may also return undefined if no change should + // be made. // // * bufferRow - The row {Number} // @@ -93,16 +94,16 @@ class TokenizedBuffer { suggestedIndentForEditedBufferRow (bufferRow) { const line = this.buffer.lineForRow(bufferRow) const currentIndentLevel = this.indentLevelForLine(line) - if (currentIndentLevel === 0) return currentIndentLevel + if (currentIndentLevel === 0) return const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0]) const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) - if (!decreaseIndentRegex) return currentIndentLevel + if (!decreaseIndentRegex) return - if (!decreaseIndentRegex.testSync(line)) return currentIndentLevel + if (!decreaseIndentRegex.testSync(line)) return const precedingRow = this.buffer.previousNonBlankRow(bufferRow) - if (precedingRow == null) return currentIndentLevel + if (precedingRow == null) return const precedingLine = this.buffer.lineForRow(precedingRow) let desiredIndentLevel = this.indentLevelForLine(precedingLine) @@ -118,7 +119,7 @@ class TokenizedBuffer { } if (desiredIndentLevel < 0) return 0 - if (desiredIndentLevel > currentIndentLevel) return currentIndentLevel + if (desiredIndentLevel >= currentIndentLevel) return return desiredIndentLevel } From 22c573b16749ea2d20a85d89a295857ab05c2de1 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 23:21:09 +0200 Subject: [PATCH 237/448] :arrow_up: first-mate@7.0.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e17b69cb..3ecdd6fd9 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.7", + "first-mate": "7.0.8", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From da6866ba7fcf6130899edb5002bab9a53626b24f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 23:21:28 +0200 Subject: [PATCH 238/448] :arrow_up: language-html@0.48.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ecdd6fd9..cc8058287 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.1", "language-git": "0.19.1", "language-go": "0.44.2", - "language-html": "0.48.0", + "language-html": "0.48.1", "language-hyperlink": "0.16.2", "language-java": "0.27.4", "language-javascript": "0.127.5", From f58066148dcc89089b65aa17d60e3ffe185cebc7 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 23:21:41 +0200 Subject: [PATCH 239/448] :arrow_up: language-csharp@0.14.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc8058287..be3e9cd20 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "language-c": "0.58.1", "language-clojure": "0.22.4", "language-coffee-script": "0.49.1", - "language-csharp": "0.14.2", + "language-csharp": "0.14.3", "language-css": "0.42.6", "language-gfm": "0.90.1", "language-git": "0.19.1", From 22757d799f5a34b3ce0e132c451688ad59e322ef Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 23:21:56 +0200 Subject: [PATCH 240/448] :arrow_up: language-php@0.42.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be3e9cd20..575877dcb 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.0", + "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", "language-ruby": "0.71.3", From 6df3c27da06506dd969ed480c8e7ef15174fb4b5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 23 Sep 2017 15:56:55 -0700 Subject: [PATCH 241/448] Fix unfoldBufferRow --- src/text-editor.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 2aad26f45..5ded33bb1 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3316,7 +3316,8 @@ class TextEditor extends Model # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> - position = @getCursorBufferPosition() + {row} = @getCursorBufferPosition() + position = Point(row, Infinity) @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Essential: Fold the given row in buffer coordinates based on its indentation From 6e482412bdef9bed69844e390a01a70fe3b50a0a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 23 Sep 2017 16:48:01 -0700 Subject: [PATCH 242/448] :arrow_up: tabs, image-view, archive-view --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0e17b69cb..2b01789c4 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.3", + "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", @@ -113,7 +113,7 @@ "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.6", - "image-view": "0.62.3", + "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", @@ -129,7 +129,7 @@ "status-bar": "1.8.13", "styleguide": "0.49.7", "symbols-view": "0.118.0", - "tabs": "0.107.3", + "tabs": "0.107.4", "timecop": "0.36.0", "tree-view": "0.218.0", "update-package-dependencies": "0.12.0", From 7cd6e266b2ff1f3c67e0019aec613c22bc17986d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 24 Sep 2017 09:16:30 -0700 Subject: [PATCH 243/448] Add back some default properties of TokenizedBuffer --- src/tokenized-buffer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index b0ee635da..546678f57 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -32,6 +32,7 @@ class TokenizedBuffer { this.regexesByPattern = {} this.alive = true + this.visible = false this.id = params.id != null ? params.id : nextId++ this.buffer = params.buffer this.tabLength = params.tabLength @@ -736,6 +737,8 @@ class TokenizedBuffer { } } +module.exports.prototype.chunkSize = 50 + function selectorMatchesAnyScope (selector, scopes) { const targetClasses = selector.replace(/^\./, '').split('.') return scopes.some((scope) => { From cd1a265dd3e141e1e5eb54e0c5c5fe19c954ad78 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 24 Sep 2017 09:54:06 -0700 Subject: [PATCH 244/448] Move .suggestedIndentForBufferRow tests to tokenized-buffer-spec --- spec/text-editor-spec.js | 44 ---------------------------------- spec/tokenized-buffer-spec.js | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index e72417aca..6cbb926de 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -1,4 +1,3 @@ -const dedent = require('dedent') const {Point, Range} = require('text-buffer') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') @@ -9,49 +8,6 @@ describe('TextEditor', () => { editor.destroy() }) - describe('.suggestedIndentForBufferRow', () => { - describe('javascript', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) - await atom.packages.activatePackage('language-javascript') - }) - - it('bases indentation off of the previous non-blank line', () => { - expect(editor.suggestedIndentForBufferRow(0)).toBe(0) - expect(editor.suggestedIndentForBufferRow(1)).toBe(1) - expect(editor.suggestedIndentForBufferRow(2)).toBe(2) - expect(editor.suggestedIndentForBufferRow(5)).toBe(3) - expect(editor.suggestedIndentForBufferRow(7)).toBe(2) - expect(editor.suggestedIndentForBufferRow(9)).toBe(1) - expect(editor.suggestedIndentForBufferRow(11)).toBe(1) - }) - - it('does not take invisibles into account', () => { - editor.update({showInvisibles: true}) - expect(editor.suggestedIndentForBufferRow(0)).toBe(0) - expect(editor.suggestedIndentForBufferRow(1)).toBe(1) - expect(editor.suggestedIndentForBufferRow(2)).toBe(2) - expect(editor.suggestedIndentForBufferRow(5)).toBe(3) - expect(editor.suggestedIndentForBufferRow(7)).toBe(2) - expect(editor.suggestedIndentForBufferRow(9)).toBe(1) - expect(editor.suggestedIndentForBufferRow(11)).toBe(1) - }) - }) - - describe('css', () => { - beforeEach(async () => { - editor = await atom.workspace.open('css.css', {autoIndent: true}) - await atom.packages.activatePackage('language-source') - await atom.packages.activatePackage('language-css') - }) - - it('does not return negative values (regression)', () => { - editor.setText('.test {\npadding: 0;\n}') - expect(editor.suggestedIndentForBufferRow(2)).toBe(0) - }) - }) - }) - describe('.toggleLineCommentsForBufferRows', () => { describe('xml', () => { beforeEach(async () => { diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index c0bd29b50..f2e435538 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -594,6 +594,51 @@ describe('TokenizedBuffer', () => { }) }) + describe('.suggestedIndentForBufferRow', () => { + let editor + + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + it('bases indentation off of the previous non-blank line', () => { + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + + it('does not take invisibles into account', () => { + editor.update({showInvisibles: true}) + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: true}) + await atom.packages.activatePackage('language-source') + await atom.packages.activatePackage('language-css') + }) + + it('does not return negative values (regression)', () => { + editor.setText('.test {\npadding: 0;\n}') + expect(editor.suggestedIndentForBufferRow(2)).toBe(0) + }) + }) + }) + describe('.isFoldableAtRow(row)', () => { beforeEach(() => { buffer = atom.project.bufferForPathSync('sample.js') From 090b753d844c4e54a16578ad9be46c063ac86332 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 24 Sep 2017 10:34:34 -0700 Subject: [PATCH 245/448] Move toggleLineCommentsForBufferRows to TokenizedBuffer --- spec/text-editor-spec.js | 156 --------------------------------- spec/tokenized-buffer-spec.js | 158 ++++++++++++++++++++++++++++++++++ src/text-editor.coffee | 70 +-------------- src/tokenized-buffer.js | 94 ++++++++++++++++++++ 4 files changed, 253 insertions(+), 225 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 6cbb926de..82ad3bc90 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -8,162 +8,6 @@ describe('TextEditor', () => { editor.destroy() }) - describe('.toggleLineCommentsForBufferRows', () => { - describe('xml', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.xml', {autoIndent: false}) - editor.setText('') - await atom.packages.activatePackage('language-xml') - }) - - 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 () => { - editor = await atom.workspace.open('sample.less', {autoIndent: false}) - await atom.packages.activatePackage('language-less') - await atom.packages.activatePackage('language-css') - }) - - 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 () => { - editor = await atom.workspace.open('css.css', {autoIndent: false}) - await atom.packages.activatePackage('language-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 () => { - editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) - await atom.packages.activatePackage('language-coffee-script') - }) - - 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 () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) - await atom.packages.activatePackage('language-javascript') - }) - - 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();') - console.log(JSON.stringify(editor.lineTextForBufferRow(5))); - return - 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 f2e435538..ccd605800 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -639,6 +639,164 @@ describe('TokenizedBuffer', () => { }) }) + describe('.toggleLineCommentsForBufferRows', () => { + let editor + + describe('xml', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.xml', {autoIndent: false}) + editor.setText('') + await atom.packages.activatePackage('language-xml') + }) + + 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 () => { + editor = await atom.workspace.open('sample.less', {autoIndent: false}) + await atom.packages.activatePackage('language-less') + await atom.packages.activatePackage('language-css') + }) + + 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 () => { + editor = await atom.workspace.open('css.css', {autoIndent: false}) + await atom.packages.activatePackage('language-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 () => { + editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) + await atom.packages.activatePackage('language-coffee-script') + }) + + 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 () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + 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();') + console.log(JSON.stringify(editor.lineTextForBufferRow(5))); + return + 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('.isFoldableAtRow(row)', () => { beforeEach(() => { buffer = atom.project.bufferForPathSync('sample.js') diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 5ded33bb1..d85e36535 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3,7 +3,6 @@ path = require 'path' fs = require 'fs-plus' Grim = require 'grim' {CompositeDisposable, Disposable, Emitter} = require 'event-kit' -{OnigRegExp} = require 'oniguruma' {Point, Range} = TextBuffer = require 'text-buffer' DecorationManager = require './decoration-manager' TokenizedBuffer = require './tokenized-buffer' @@ -3887,74 +3886,7 @@ class TextEditor extends Model toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) - toggleLineCommentsForBufferRows: (start, end) -> - scope = @scopeDescriptorForBufferPosition([start, 0]) - commentStrings = @getCommentStrings(scope) - return unless commentStrings?.commentStartString - {commentStartString, commentEndString} = commentStrings - - buffer = @buffer - commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') - commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") - - if commentEndString - shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start)) - if shouldUncomment - commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') - commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$") - startMatch = commentStartRegex.searchSync(buffer.lineForRow(start)) - endMatch = commentEndRegex.searchSync(buffer.lineForRow(end)) - if startMatch and endMatch - buffer.transact -> - columnStart = startMatch[1].length - columnEnd = columnStart + startMatch[2].length - buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "") - - endLength = buffer.lineLengthForRow(end) - endMatch[2].length - endColumn = endLength - endMatch[1].length - buffer.setTextInRange([[end, endColumn], [end, endLength]], "") - else - buffer.transact -> - indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0 - buffer.insert([start, indentLength], commentStartString) - buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString) - else - allBlank = true - allBlankOrCommented = true - - for row in [start..end] by 1 - line = buffer.lineForRow(row) - blank = line?.match(/^\s*$/) - - allBlank = false unless blank - allBlankOrCommented = false unless blank or commentStartRegex.testSync(line) - - shouldUncomment = allBlankOrCommented and not allBlank - - if shouldUncomment - for row in [start..end] by 1 - if match = commentStartRegex.searchSync(buffer.lineForRow(row)) - columnStart = match[1].length - columnEnd = columnStart + match[2].length - buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "") - else - indents = [] - for row in [start..end] by 1 - unless @isBufferRowBlank(row) - indents.push(@indentationForBufferRow(start)) - indents.push(0) if indents.length is 0 - indent = Math.min(indents...) - - indentString = @buildIndentString(indent) - tabLength = @getTabLength() - indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}") - for row in [start..end] by 1 - line = buffer.lineForRow(row) - if indentLength = line.match(indentRegex)?[0].length - buffer.insert([row, indentLength], commentStartString) - else - buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) - return + 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 546678f57..bdfdc254b 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -158,6 +158,94 @@ class TokenizedBuffer { return Math.max(desiredIndentLevel, 0) } + /* + Section - Comments + */ + + toggleLineCommentsForBufferRows (start, end) { + const scope = this.scopeDescriptorForPosition([start, 0]) + const commentStrings = this.commentStringsForScopeDescriptor(scope) + if (!commentStrings) return + const {commentStartString, commentEndString} = commentStrings + if (!commentStartString) return + + const commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') + const commentStartRegex = new OnigRegExp(`^(\\s*)(${commentStartRegexString})`) + + if (commentEndString) { + const shouldUncomment = commentStartRegex.testSync(this.buffer.lineForRow(start)) + if (shouldUncomment) { + const commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') + const commentEndRegex = new OnigRegExp(`(${commentEndRegexString})(\\s*)$`) + const startMatch = commentStartRegex.searchSync(this.buffer.lineForRow(start)) + const endMatch = commentEndRegex.searchSync(this.buffer.lineForRow(end)) + if (startMatch && endMatch) { + this.buffer.transact(() => { + const columnStart = startMatch[1].length + const columnEnd = columnStart + startMatch[2].length + this.buffer.setTextInRange([[start, columnStart], [start, columnEnd]], '') + + const endLength = this.buffer.lineLengthForRow(end) - endMatch[2].length + const endColumn = endLength - endMatch[1].length + return this.buffer.setTextInRange([[end, endColumn], [end, endLength]], '') + }) + } + } else { + this.buffer.transact(() => { + const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length + this.buffer.insert([start, indentLength], commentStartString) + this.buffer.insert([end, this.buffer.lineLengthForRow(end)], commentEndString) + }) + } + } else { + let allBlank = true + let allBlankOrCommented = true + + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + const blank = line.match(/^\s*$/) + if (!blank) allBlank = false + if (!blank && !commentStartRegex.testSync(line)) allBlankOrCommented = false + } + + const shouldUncomment = allBlankOrCommented && !allBlank + + if (shouldUncomment) { + for (let row = start; row <= end; row++) { + const match = commentStartRegex.searchSync(this.buffer.lineForRow(row)) + if (match) { + const columnStart = match[1].length + const columnEnd = columnStart + match[2].length + this.buffer.setTextInRange([[row, columnStart], [row, columnEnd]], '') + } + } + } else { + const indents = [] + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + if (NON_WHITESPACE_REGEX.test(line)) { + indents.push(this.indentLevelForLine(line)) + } + } + if (indents.length === 0) indents.push(0) + const indent = Math.min(...indents) + + const tabLength = this.getTabLength() + const indentString = ' '.repeat(tabLength * indent) + const indentRegex = new RegExp(`(\t|[ ]{${tabLength}}){${Math.floor(indent)}}`) + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + const indentMatch = line.match(indentRegex) + if (indentMatch) { + this.buffer.insert([row, indentMatch[0].length], commentStartString) + } else { + this.buffer.insert([row, 0], indentString + commentStartString) + } + } + } + } + } + buildIterator () { return new TokenizedBufferIterator(this) } @@ -720,6 +808,12 @@ class TokenizedBuffer { } } + commentStringsForScopeDescriptor (scopes) { + if (this.scopedSettingsDelegate) { + return this.scopedSettingsDelegate.getCommentStrings(scopes) + } + } + regexForPattern (pattern) { if (pattern) { if (!this.regexesByPattern[pattern]) { From 88a32589abdbe89b2c269fc6a0fab108ed33aa4a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 25 Sep 2017 09:56:40 -0400 Subject: [PATCH 246/448] Restore a missing "typeof" --- src/package.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.coffee b/src/package.coffee index fdd89bc74..e0db21ccc 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -517,7 +517,7 @@ class Package console.error "Error deactivating package '#{@name}'", e.stack # We support then-able async promises as well as sync ones from deactivate - if deactivationResult?.then is 'function' + if typeof deactivationResult?.then is 'function' deactivationResult.then => @afterDeactivation() else @afterDeactivation() From 345e236d86bbf3ce4e4447b4d3ca2c835b2ae7c0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Sep 2017 09:52:15 -0700 Subject: [PATCH 247/448] Fix toggleLineCommentsForBufferRows --- spec/tokenized-buffer-spec.js | 202 +++++++++++++++++++--------------- src/text-editor-registry.js | 2 + src/tokenized-buffer.js | 41 +++++-- 3 files changed, 147 insertions(+), 98 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index ccd605800..b2324d392 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -5,6 +5,7 @@ const {Point, Range} = TextBuffer const _ = require('underscore-plus') const dedent = require('dedent') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') +const {ScopedSettingsDelegate} = require('../src/text-editor-registry') describe('TokenizedBuffer', () => { let tokenizedBuffer, buffer @@ -16,7 +17,10 @@ describe('TokenizedBuffer', () => { await atom.packages.activatePackage('language-javascript') }) - afterEach(() => tokenizedBuffer && tokenizedBuffer.destroy()) + afterEach(() => { + buffer && buffer.destroy() + tokenizedBuffer && tokenizedBuffer.destroy() + }) function startTokenizing (tokenizedBuffer) { tokenizedBuffer.setVisible(true) @@ -640,159 +644,181 @@ describe('TokenizedBuffer', () => { }) describe('.toggleLineCommentsForBufferRows', () => { - let editor - describe('xml', () => { beforeEach(async () => { - editor = await atom.workspace.open('sample.xml', {autoIndent: false}) - editor.setText('') 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', () => { - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('test') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('test') }) }) describe('less', () => { beforeEach(async () => { - editor = await atom.workspace.open('sample.less', {autoIndent: false}) 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', () => { - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('// @color: #4D926F;') }) }) describe('css', () => { beforeEach(async () => { - editor = await atom.workspace.open('css.css', {autoIndent: false}) 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', () => { - 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;') + 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;') - 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;') + 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;') - 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;') + 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', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe(' 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', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe('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', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') + buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') + tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) + expect(buffer.lineForRow(2)).toBe(' width: 110%; ') }) }) describe('coffeescript', () => { beforeEach(async () => { - editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) 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', () => { - editor.toggleLineCommentsForBufferRows(4, 6) - expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + tokenizedBuffer.toggleLineCommentsForBufferRows(4, 6) + expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()') + expect(buffer.lineForRow(5)).toBe(' # left = []') + expect(buffer.lineForRow(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 = []') + 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', () => { - 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(' # ') + 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(' # ') - 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(' # ') + 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 () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) 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', () => { - 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(' // }') + 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(' // }') - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') - expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') - console.log(JSON.stringify(editor.lineTextForBufferRow(5))); - return - expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(editor.lineTextForBufferRow(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(' // }') - editor.setText('\tvar i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') + buffer.setText('\tvar i;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('\t// var i;') - editor.setText('var i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// var i;') + buffer.setText('var i;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('// var i;') - editor.setText(' var i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') + buffer.setText(' var i;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe(' // var i;') - editor.setText(' ') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe(' // ') + buffer.setText(' ') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(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') + 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') - editor.setText(' \n // var i;') - editor.toggleLineCommentsForBufferRows(0, 1) - expect(editor.lineTextForBufferRow(0)).toBe(' ') - expect(editor.lineTextForBufferRow(1)).toBe(' var i;') + buffer.setText(' \n // var i;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) + expect(buffer.lineForRow(0)).toBe(' ') + expect(buffer.lineForRow(1)).toBe(' var i;') }) }) }) diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 35be27fd1..2cbf3093c 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -429,3 +429,5 @@ class ScopedSettingsDelegate { } } } + +TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index bdfdc254b..1d52411ae 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -38,6 +38,7 @@ class TokenizedBuffer { this.tabLength = params.tabLength this.largeFileMode = params.largeFileMode this.assert = params.assert + this.scopedSettingsDelegate = params.scopedSettingsDelegate this.setGrammar(params.grammar || NullGrammar) this.disposables.add(this.buffer.registerTextDecorationLayer(this)) @@ -220,26 +221,28 @@ class TokenizedBuffer { } } } else { - const indents = [] + let minIndentLevel = null for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) if (NON_WHITESPACE_REGEX.test(line)) { - indents.push(this.indentLevelForLine(line)) + const indentLevel = this.indentLevelForLine(line) + if (minIndentLevel == null || indentLevel < minIndentLevel) minIndentLevel = indentLevel } } - if (indents.length === 0) indents.push(0) - const indent = Math.min(...indents) + if (minIndentLevel == null) minIndentLevel = 0 const tabLength = this.getTabLength() - const indentString = ' '.repeat(tabLength * indent) - const indentRegex = new RegExp(`(\t|[ ]{${tabLength}}){${Math.floor(indent)}}`) + const indentString = ' '.repeat(tabLength * minIndentLevel) for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) - const indentMatch = line.match(indentRegex) - if (indentMatch) { - this.buffer.insert([row, indentMatch[0].length], commentStartString) + if (NON_WHITESPACE_REGEX.test(line)) { + const indentColumn = this.columnForIndentLevel(line, minIndentLevel) + this.buffer.insert(Point(row, indentColumn), commentStartString) } else { - this.buffer.insert([row, 0], indentString + commentStartString) + this.buffer.setTextInRange( + new Range(new Point(row, 0), new Point(row, Infinity)), + indentString + commentStartString + ) } } } @@ -581,6 +584,24 @@ 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++) { From 610fe4eb9f4bf5afa0f6f36977afb6ad780a1c20 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 25 Sep 2017 13:50:34 -0400 Subject: [PATCH 248/448] :arrow_up: autosave --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b01789c4..e17ba0c4f 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "autocomplete-plus": "2.35.10", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", - "autosave": "0.24.4", + "autosave": "0.24.5", "background-tips": "0.27.1", "bookmarks": "0.44.4", "bracket-matcher": "0.88.0", From c363c950f0882475b3e4fa3421baa6759a218c6d Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 25 Sep 2017 12:28:53 -0700 Subject: [PATCH 249/448] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e17ba0c4f..b0c5d1577 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.35.10", + "autocomplete-plus": "2.35.11", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.5", From 1880e1401d7745a2a644c4f7e72cbc625d06023d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 25 Sep 2017 16:16:34 -0400 Subject: [PATCH 250/448] :arrow_up: apm --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index e8d4321b1..5391c9972 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.7" + "atom-package-manager": "1.18.8" } } From f690b0d8c45c99a91c774dbf87ba3d0cbed53db1 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 25 Sep 2017 23:11:35 +0200 Subject: [PATCH 251/448] :arrow_up: first-mate@7.0.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 575877dcb..5b9d8e89b 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.8", + "first-mate": "7.0.9", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From dae51719f1e73b7eb420899a03d7f9ef3d36ee71 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Mon, 25 Sep 2017 14:18:51 -0700 Subject: [PATCH 252/448] 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 83c90e341a3f1fdf91737cca915e475a1591b028 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Sep 2017 17:29:09 -0700 Subject: [PATCH 253/448] Convert GitRepository to JS --- src/git-repository.coffee | 496 -------------------------------- src/git-repository.js | 591 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 591 insertions(+), 496 deletions(-) delete mode 100644 src/git-repository.coffee create mode 100644 src/git-repository.js diff --git a/src/git-repository.coffee b/src/git-repository.coffee deleted file mode 100644 index c7105baef..000000000 --- a/src/git-repository.coffee +++ /dev/null @@ -1,496 +0,0 @@ -{join} = require 'path' - -_ = require 'underscore-plus' -{Emitter, Disposable, CompositeDisposable} = require 'event-kit' -fs = require 'fs-plus' -path = require 'path' -GitUtils = require 'git-utils' - -Task = require './task' - -# Extended: Represents the underlying git operations performed by Atom. -# -# This class shouldn't be instantiated directly but instead by accessing the -# `atom.project` global and calling `getRepositories()`. Note that this will -# only be available when the project is backed by a Git repository. -# -# This class handles submodules automatically by taking a `path` argument to many -# of the methods. This `path` argument will determine which underlying -# repository is used. -# -# For a repository with submodules this would have the following outcome: -# -# ```coffee -# repo = atom.project.getRepositories()[0] -# repo.getShortHead() # 'master' -# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234' -# ``` -# -# ## Examples -# -# ### Logging the URL of the origin remote -# -# ```coffee -# git = atom.project.getRepositories()[0] -# console.log git.getOriginURL() -# ``` -# -# ### Requiring in packages -# -# ```coffee -# {GitRepository} = require 'atom' -# ``` -module.exports = -class GitRepository - @exists: (path) -> - if git = @open(path) - git.destroy() - true - else - false - - ### - Section: Construction and Destruction - ### - - # Public: Creates a new GitRepository instance. - # - # * `path` The {String} path to the Git repository to open. - # * `options` An optional {Object} with the following keys: - # * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and - # statuses when the window is focused. - # - # Returns a {GitRepository} instance or `null` if the repository could not be opened. - @open: (path, options) -> - return null unless path - try - new GitRepository(path, options) - catch - null - - constructor: (path, options={}) -> - @emitter = new Emitter - @subscriptions = new CompositeDisposable - - @repo = GitUtils.open(path) - unless @repo? - throw new Error("No Git repository found searching path: #{path}") - - @statuses = {} - @upstream = {ahead: 0, behind: 0} - for submodulePath, submoduleRepo of @repo.submodules - submoduleRepo.upstream = {ahead: 0, behind: 0} - - {@project, @config, refreshOnWindowFocus} = options - - refreshOnWindowFocus ?= true - if refreshOnWindowFocus - onWindowFocus = => - @refreshIndex() - @refreshStatus() - - window.addEventListener 'focus', onWindowFocus - @subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus) - - if @project? - @project.getBuffers().forEach (buffer) => @subscribeToBuffer(buffer) - @subscriptions.add @project.onDidAddBuffer (buffer) => @subscribeToBuffer(buffer) - - # Public: Destroy this {GitRepository} object. - # - # This destroys any tasks and subscriptions and releases the underlying - # libgit2 repository handle. This method is idempotent. - destroy: -> - if @emitter? - @emitter.emit 'did-destroy' - @emitter.dispose() - @emitter = null - - if @statusTask? - @statusTask.terminate() - @statusTask = null - - if @repo? - @repo.release() - @repo = null - - if @subscriptions? - @subscriptions.dispose() - @subscriptions = null - - # Public: Returns a {Boolean} indicating if this repository has been destroyed. - isDestroyed: -> - not @repo? - - # Public: Invoke the given callback when this GitRepository's destroy() method - # is invoked. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when a specific file's status has - # changed. When a file is updated, reloaded, etc, and the status changes, this - # will be fired. - # - # * `callback` {Function} - # * `event` {Object} - # * `path` {String} the old parameters the decoration used to have - # * `pathStatus` {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeStatus: (callback) -> - @emitter.on 'did-change-status', callback - - # Public: Invoke the given callback when a multiple files' statuses have - # changed. For example, on window focus, the status of all the paths in the - # repo is checked. If any of them have changed, this will be fired. Call - # {::getPathStatus(path)} to get the status for your path of choice. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeStatuses: (callback) -> - @emitter.on 'did-change-statuses', callback - - ### - Section: Repository Details - ### - - # Public: A {String} indicating the type of version control system used by - # this repository. - # - # Returns `"git"`. - getType: -> 'git' - - # Public: Returns the {String} path of the repository. - getPath: -> - @path ?= fs.absolute(@getRepo().getPath()) - - # Public: Returns the {String} working directory path of the repository. - getWorkingDirectory: -> @getRepo().getWorkingDirectory() - - # Public: Returns true if at the root, false if in a subfolder of the - # repository. - isProjectAtRoot: -> - @projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is '' - - # Public: Makes a path relative to the repository's working directory. - relativize: (path) -> @getRepo().relativize(path) - - # Public: Returns true if the given branch exists. - hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")? - - # Public: Retrieves a shortened version of the HEAD reference value. - # - # This removes the leading segments of `refs/heads`, `refs/tags`, or - # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 - # characters. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository contains submodules. - # - # Returns a {String}. - getShortHead: (path) -> @getRepo(path).getShortHead() - - # Public: Is the given path a submodule in the repository? - # - # * `path` The {String} path to check. - # - # Returns a {Boolean}. - isSubmodule: (path) -> - return false unless path - - repo = @getRepo(path) - if repo.isSubmodule(repo.relativize(path)) - true - else - # Check if the path is a working directory in a repo that isn't the root. - repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir' - - # Public: Returns the number of commits behind the current branch is from the - # its upstream remote branch. - # - # * `reference` The {String} branch reference name. - # * `path` The {String} path in the repository to get this information for, - # only needed if the repository contains submodules. - getAheadBehindCount: (reference, path) -> - @getRepo(path).getAheadBehindCount(reference) - - # Public: Get the cached ahead/behind commit counts for the current branch's - # upstream branch. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - # - # Returns an {Object} with the following keys: - # * `ahead` The {Number} of commits ahead. - # * `behind` The {Number} of commits behind. - getCachedUpstreamAheadBehindCount: (path) -> - @getRepo(path).upstream ? @upstream - - # Public: Returns the git configuration value specified by the key. - # - # * `key` The {String} key for the configuration to lookup. - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key) - - # Public: Returns the origin url of the repository. - # - # * `path` (optional) {String} path in the repository to get this information - # for, only needed if the repository has submodules. - getOriginURL: (path) -> @getConfigValue('remote.origin.url', path) - - # Public: Returns the upstream branch for the current HEAD, or null if there - # is no upstream branch for the current HEAD. - # - # * `path` An optional {String} path in the repo to get this information for, - # only needed if the repository contains submodules. - # - # Returns a {String} branch name such as `refs/remotes/origin/master`. - getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch() - - # Public: Gets all the local and remote references. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - # - # Returns an {Object} with the following keys: - # * `heads` An {Array} of head reference names. - # * `remotes` An {Array} of remote reference names. - # * `tags` An {Array} of tag reference names. - getReferences: (path) -> @getRepo(path).getReferences() - - # Public: Returns the current {String} SHA for the given reference. - # - # * `reference` The {String} reference to get the target of. - # * `path` An optional {String} path in the repo to get the reference target - # for. Only needed if the repository contains submodules. - getReferenceTarget: (reference, path) -> - @getRepo(path).getReferenceTarget(reference) - - ### - Section: Reading Status - ### - - # Public: Returns true if the given path is modified. - # - # * `path` The {String} path to check. - # - # Returns a {Boolean} that's true if the `path` is modified. - isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) - - # Public: Returns true if the given path is new. - # - # * `path` The {String} path to check. - # - # Returns a {Boolean} that's true if the `path` is new. - isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) - - # Public: Is the given path ignored? - # - # * `path` The {String} path to check. - # - # Returns a {Boolean} that's true if the `path` is ignored. - isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) - - # Public: Get the status of a directory in the repository's working directory. - # - # * `path` The {String} path to check. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - getDirectoryStatus: (directoryPath) -> - directoryPath = "#{@relativize(directoryPath)}/" - directoryStatus = 0 - for statusPath, status of @statuses - directoryStatus |= status if statusPath.indexOf(directoryPath) is 0 - directoryStatus - - # Public: Get the status of a single path in the repository. - # - # * `path` A {String} repository-relative path. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - getPathStatus: (path) -> - repo = @getRepo(path) - relativePath = @relativize(path) - currentPathStatus = @statuses[relativePath] ? 0 - pathStatus = repo.getStatus(repo.relativize(path)) ? 0 - pathStatus = 0 if repo.isStatusIgnored(pathStatus) - if pathStatus > 0 - @statuses[relativePath] = pathStatus - else - delete @statuses[relativePath] - if currentPathStatus isnt pathStatus - @emitter.emit 'did-change-status', {path, pathStatus} - - pathStatus - - # Public: Get the cached status for the given path. - # - # * `path` A {String} path in the repository, relative or absolute. - # - # Returns a status {Number} or null if the path is not in the cache. - getCachedPathStatus: (path) -> - @statuses[@relativize(path)] - - # Public: Returns true if the given status indicates modification. - # - # * `status` A {Number} representing the status. - # - # Returns a {Boolean} that's true if the `status` indicates modification. - isStatusModified: (status) -> @getRepo().isStatusModified(status) - - # Public: Returns true if the given status indicates a new path. - # - # * `status` A {Number} representing the status. - # - # Returns a {Boolean} that's true if the `status` indicates a new path. - isStatusNew: (status) -> @getRepo().isStatusNew(status) - - ### - Section: Retrieving Diffs - ### - - # Public: Retrieves the number of lines added and removed to a path. - # - # This compares the working directory contents of the path to the `HEAD` - # version. - # - # * `path` The {String} path to check. - # - # Returns an {Object} with the following keys: - # * `added` The {Number} of added lines. - # * `deleted` The {Number} of deleted lines. - getDiffStats: (path) -> - repo = @getRepo(path) - repo.getDiffStats(repo.relativize(path)) - - # Public: Retrieves the line diffs comparing the `HEAD` version of the given - # path and the given text. - # - # * `path` The {String} path relative to the repository. - # * `text` The {String} to compare against the `HEAD` contents - # - # Returns an {Array} of hunk {Object}s with the following keys: - # * `oldStart` The line {Number} of the old hunk. - # * `newStart` The line {Number} of the new hunk. - # * `oldLines` The {Number} of lines in the old hunk. - # * `newLines` The {Number} of lines in the new hunk - getLineDiffs: (path, text) -> - # Ignore eol of line differences on windows so that files checked in as - # LF don't report every line modified when the text contains CRLF endings. - options = ignoreEolWhitespace: process.platform is 'win32' - repo = @getRepo(path) - repo.getLineDiffs(repo.relativize(path), text, options) - - ### - Section: Checking Out - ### - - # Public: Restore the contents of a path in the working directory and index - # to the version at `HEAD`. - # - # This is essentially the same as running: - # - # ```sh - # git reset HEAD -- - # git checkout HEAD -- - # ``` - # - # * `path` The {String} path to checkout. - # - # Returns a {Boolean} that's true if the method was successful. - checkoutHead: (path) -> - repo = @getRepo(path) - headCheckedOut = repo.checkoutHead(repo.relativize(path)) - @getPathStatus(path) if headCheckedOut - headCheckedOut - - # Public: Checks out a branch in your repository. - # - # * `reference` The {String} reference to checkout. - # * `create` A {Boolean} value which, if true creates the new reference if - # it doesn't exist. - # - # Returns a Boolean that's true if the method was successful. - checkoutReference: (reference, create) -> - @getRepo().checkoutReference(reference, create) - - ### - Section: Private - ### - - # Subscribes to buffer events. - subscribeToBuffer: (buffer) -> - getBufferPathStatus = => - if bufferPath = buffer.getPath() - @getPathStatus(bufferPath) - - getBufferPathStatus() - bufferSubscriptions = new CompositeDisposable - bufferSubscriptions.add buffer.onDidSave(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidReload(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidChangePath(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidDestroy => - bufferSubscriptions.dispose() - @subscriptions.remove(bufferSubscriptions) - @subscriptions.add(bufferSubscriptions) - return - - # Subscribes to editor view event. - checkoutHeadForEditor: (editor) -> - buffer = editor.getBuffer() - if filePath = buffer.getPath() - @checkoutHead(filePath) - buffer.reload() - - # Returns the corresponding {Repository} - getRepo: (path) -> - if @repo? - @repo.submoduleForPath(path) ? @repo - else - throw new Error("Repository has been destroyed") - - # Reread the index to update any values that have changed since the - # last time the index was read. - refreshIndex: -> @getRepo().refreshIndex() - - # Refreshes the current git status in an outside process and asynchronously - # updates the relevant properties. - refreshStatus: -> - @handlerPath ?= require.resolve('./repository-status-handler') - - relativeProjectPaths = @project?.getPaths() - .map (projectPath) => @relativize(projectPath) - .filter (projectPath) -> projectPath.length > 0 and not path.isAbsolute(projectPath) - - @statusTask?.terminate() - new Promise (resolve) => - @statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) => - statusesUnchanged = _.isEqual(statuses, @statuses) and - _.isEqual(upstream, @upstream) and - _.isEqual(branch, @branch) and - _.isEqual(submodules, @submodules) - - @statuses = statuses - @upstream = upstream - @branch = branch - @submodules = submodules - - for submodulePath, submoduleRepo of @getRepo().submodules - submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} - - unless statusesUnchanged - @emitter.emit 'did-change-statuses' - resolve() diff --git a/src/git-repository.js b/src/git-repository.js new file mode 100644 index 000000000..503e5fa0d --- /dev/null +++ b/src/git-repository.js @@ -0,0 +1,591 @@ +/* + * 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 _ = require('underscore-plus') +const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const fs = require('fs-plus') +const path = require('path') +const GitUtils = require('git-utils') +const Task = require('./task') + +// Extended: Represents the underlying git operations performed by Atom. +// +// This class shouldn't be instantiated directly but instead by accessing the +// `atom.project` global and calling `getRepositories()`. Note that this will +// only be available when the project is backed by a Git repository. +// +// This class handles submodules automatically by taking a `path` argument to many +// of the methods. This `path` argument will determine which underlying +// repository is used. +// +// For a repository with submodules this would have the following outcome: +// +// ```coffee +// repo = atom.project.getRepositories()[0] +// repo.getShortHead() # 'master' +// repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234' +// ``` +// +// ## Examples +// +// ### Logging the URL of the origin remote +// +// ```coffee +// git = atom.project.getRepositories()[0] +// console.log git.getOriginURL() +// ``` +// +// ### Requiring in packages +// +// ```coffee +// {GitRepository} = require 'atom' +// ``` +module.exports = +class GitRepository { + static exists (path) { + const git = this.open(path) + if (git) { + git.destroy() + return true + } else { + return false + } + } + + /* + Section: Construction and Destruction + */ + + // Public: Creates a new GitRepository instance. + // + // * `path` The {String} path to the Git repository to open. + // * `options` An optional {Object} with the following keys: + // * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and + // statuses when the window is focused. + // + // Returns a {GitRepository} instance or `null` if the repository could not be opened. + static open (path, options) { + if (!path) { return null } + try { + return new GitRepository(path, options) + } catch (error) { + return null + } + } + + constructor (path, options = {}) { + this.emitter = new Emitter() + this.subscriptions = new CompositeDisposable() + this.repo = GitUtils.open(path) + if (this.repo == null) { + throw new Error(`No Git repository found searching path: ${path}`) + } + + this.statuses = {} + this.upstream = {ahead: 0, behind: 0} + for (let submodulePath in this.repo.submodules) { + const submoduleRepo = this.repo.submodules[submodulePath] + submoduleRepo.upstream = {ahead: 0, behind: 0} + } + + this.project = options.project + this.config = options.config + + if (options.refreshOnWindowFocus || options.refreshOnWindowFocus == null) { + const onWindowFocus = () => { + this.refreshIndex() + return this.refreshStatus() + } + + window.addEventListener('focus', onWindowFocus) + this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) + } + + if (this.project != null) { + this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) + this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) + } + } + + // Public: Destroy this {GitRepository} object. + // + // This destroys any tasks and subscriptions and releases the underlying + // libgit2 repository handle. This method is idempotent. + destroy () { + if (this.emitter != null) { + this.emitter.emit('did-destroy') + this.emitter.dispose() + this.emitter = null + } + + if (this.statusTask != null) { + this.statusTask.terminate() + this.statusTask = null + } + + if (this.repo != null) { + this.repo.release() + this.repo = null + } + + if (this.subscriptions != null) { + this.subscriptions.dispose() + this.subscriptions = null + } + } + + // Public: Returns a {Boolean} indicating if this repository has been destroyed. + isDestroyed () { + return this.repo == null + } + + // Public: Invoke the given callback when this GitRepository's destroy() method + // is invoked. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when a specific file's status has + // changed. When a file is updated, reloaded, etc, and the status changes, this + // will be fired. + // + // * `callback` {Function} + // * `event` {Object} + // * `path` {String} the old parameters the decoration used to have + // * `pathStatus` {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeStatus (callback) { + return this.emitter.on('did-change-status', callback) + } + + // Public: Invoke the given callback when a multiple files' statuses have + // changed. For example, on window focus, the status of all the paths in the + // repo is checked. If any of them have changed, this will be fired. Call + // {::getPathStatus(path)} to get the status for your path of choice. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeStatuses (callback) { + return this.emitter.on('did-change-statuses', callback) + } + + /* + Section: Repository Details + */ + + // Public: A {String} indicating the type of version control system used by + // this repository. + // + // Returns `"git"`. + getType () { return 'git' } + + // Public: Returns the {String} path of the repository. + getPath () { + if (this.path == null) { + this.path = fs.absolute(this.getRepo().getPath()) + } + return this.path + } + + // Public: Returns the {String} working directory path of the repository. + getWorkingDirectory () { + return this.getRepo().getWorkingDirectory() + } + + // Public: Returns true if at the root, false if in a subfolder of the + // repository. + isProjectAtRoot () { + if (this.projectAtRoot == null) { + this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) + } + return this.projectAtRoot + } + + // Public: Makes a path relative to the repository's working directory. + relativize (path) { + return this.getRepo().relativize(path) + } + + // Public: Returns true if the given branch exists. + hasBranch (branch) { + return this.getReferenceTarget(`refs/heads/${branch}`) != null + } + + // Public: Retrieves a shortened version of the HEAD reference value. + // + // This removes the leading segments of `refs/heads`, `refs/tags`, or + // `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 + // characters. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository contains submodules. + // + // Returns a {String}. + getShortHead (path) { + return this.getRepo(path).getShortHead() + } + + // Public: Is the given path a submodule in the repository? + // + // * `path` The {String} path to check. + // + // Returns a {Boolean}. + isSubmodule (path) { + if (!path) return false + + const repo = this.getRepo(path) + if (repo.isSubmodule(repo.relativize(path))) { + 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' + } + } + + // Public: Returns the number of commits behind the current branch is from the + // its upstream remote branch. + // + // * `reference` The {String} branch reference name. + // * `path` The {String} path in the repository to get this information for, + // only needed if the repository contains submodules. + getAheadBehindCount (reference, path) { + return this.getRepo(path).getAheadBehindCount(reference) + } + + // Public: Get the cached ahead/behind commit counts for the current branch's + // upstream branch. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + // + // Returns an {Object} with the following keys: + // * `ahead` The {Number} of commits ahead. + // * `behind` The {Number} of commits behind. + getCachedUpstreamAheadBehindCount (path) { + return this.getRepo(path).upstream || this.upstream + } + + // Public: Returns the git configuration value specified by the key. + // + // * `key` The {String} key for the configuration to lookup. + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + getConfigValue (key, path) { + return this.getRepo(path).getConfigValue(key) + } + + // Public: Returns the origin url of the repository. + // + // * `path` (optional) {String} path in the repository to get this information + // for, only needed if the repository has submodules. + getOriginURL (path) { + return this.getConfigValue('remote.origin.url', path) + } + + // Public: Returns the upstream branch for the current HEAD, or null if there + // is no upstream branch for the current HEAD. + // + // * `path` An optional {String} path in the repo to get this information for, + // only needed if the repository contains submodules. + // + // Returns a {String} branch name such as `refs/remotes/origin/master`. + getUpstreamBranch (path) { + return this.getRepo(path).getUpstreamBranch() + } + + // Public: Gets all the local and remote references. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + // + // Returns an {Object} with the following keys: + // * `heads` An {Array} of head reference names. + // * `remotes` An {Array} of remote reference names. + // * `tags` An {Array} of tag reference names. + getReferences (path) { + return this.getRepo(path).getReferences() + } + + // Public: Returns the current {String} SHA for the given reference. + // + // * `reference` The {String} reference to get the target of. + // * `path` An optional {String} path in the repo to get the reference target + // for. Only needed if the repository contains submodules. + getReferenceTarget (reference, path) { + return this.getRepo(path).getReferenceTarget(reference) + } + + /* + Section: Reading Status + */ + + // Public: Returns true if the given path is modified. + // + // * `path` The {String} path to check. + // + // Returns a {Boolean} that's true if the `path` is modified. + isPathModified (path) { + return this.isStatusModified(this.getPathStatus(path)) + } + + // Public: Returns true if the given path is new. + // + // * `path` The {String} path to check. + // + // Returns a {Boolean} that's true if the `path` is new. + isPathNew (path) { + return this.isStatusNew(this.getPathStatus(path)) + } + + // Public: Is the given path ignored? + // + // * `path` The {String} path to check. + // + // Returns a {Boolean} that's true if the `path` is ignored. + isPathIgnored (path) { + return this.getRepo().isIgnored(this.relativize(path)) + } + + // Public: Get the status of a directory in the repository's working directory. + // + // * `path` The {String} path to check. + // + // Returns a {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + getDirectoryStatus (directoryPath) { + directoryPath = `${this.relativize(directoryPath)}/` + let directoryStatus = 0 + for (let statusPath in this.statuses) { + const status = this.statuses[statusPath] + if (statusPath.startsWith(directoryPath)) directoryStatus |= status + } + return directoryStatus + } + + // Public: Get the status of a single path in the repository. + // + // * `path` A {String} repository-relative path. + // + // Returns a {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + getPathStatus (path) { + const repo = this.getRepo(path) + const relativePath = this.relativize(path) + const currentPathStatus = this.statuses[relativePath] || 0 + let pathStatus = repo.getStatus(repo.relativize(path)) || 0 + if (repo.isStatusIgnored(pathStatus)) pathStatus = 0 + if (pathStatus > 0) { + this.statuses[relativePath] = pathStatus + } else { + delete this.statuses[relativePath] + } + if (currentPathStatus !== pathStatus) { + this.emitter.emit('did-change-status', {path, pathStatus}) + } + + return pathStatus + } + + // Public: Get the cached status for the given path. + // + // * `path` A {String} path in the repository, relative or absolute. + // + // Returns a status {Number} or null if the path is not in the cache. + getCachedPathStatus (path) { + return this.statuses[this.relativize(path)] + } + + // Public: Returns true if the given status indicates modification. + // + // * `status` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `status` indicates modification. + isStatusModified (status) { return this.getRepo().isStatusModified(status) } + + // Public: Returns true if the given status indicates a new path. + // + // * `status` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `status` indicates a new path. + isStatusNew (status) { + return this.getRepo().isStatusNew(status) + } + + /* + Section: Retrieving Diffs + */ + + // Public: Retrieves the number of lines added and removed to a path. + // + // This compares the working directory contents of the path to the `HEAD` + // version. + // + // * `path` The {String} path to check. + // + // Returns an {Object} with the following keys: + // * `added` The {Number} of added lines. + // * `deleted` The {Number} of deleted lines. + getDiffStats (path) { + const repo = this.getRepo(path) + return repo.getDiffStats(repo.relativize(path)) + } + + // Public: Retrieves the line diffs comparing the `HEAD` version of the given + // path and the given text. + // + // * `path` The {String} path relative to the repository. + // * `text` The {String} to compare against the `HEAD` contents + // + // Returns an {Array} of hunk {Object}s with the following keys: + // * `oldStart` The line {Number} of the old hunk. + // * `newStart` The line {Number} of the new hunk. + // * `oldLines` The {Number} of lines in the old hunk. + // * `newLines` The {Number} of lines in the new hunk + getLineDiffs (path, text) { + // Ignore eol of line differences on windows so that files checked in as + // LF don't report every line modified when the text contains CRLF endings. + const options = {ignoreEolWhitespace: process.platform === 'win32'} + const repo = this.getRepo(path) + return repo.getLineDiffs(repo.relativize(path), text, options) + } + + /* + Section: Checking Out + */ + + // Public: Restore the contents of a path in the working directory and index + // to the version at `HEAD`. + // + // This is essentially the same as running: + // + // ```sh + // git reset HEAD -- + // git checkout HEAD -- + // ``` + // + // * `path` The {String} path to checkout. + // + // Returns a {Boolean} that's true if the method was successful. + checkoutHead (path) { + const repo = this.getRepo(path) + const headCheckedOut = repo.checkoutHead(repo.relativize(path)) + if (headCheckedOut) this.getPathStatus(path) + return headCheckedOut + } + + // Public: Checks out a branch in your repository. + // + // * `reference` The {String} reference to checkout. + // * `create` A {Boolean} value which, if true creates the new reference if + // it doesn't exist. + // + // Returns a Boolean that's true if the method was successful. + checkoutReference (reference, create) { + return this.getRepo().checkoutReference(reference, create) + } + + /* + Section: Private + */ + + // Subscribes to buffer events. + subscribeToBuffer (buffer) { + const getBufferPathStatus = () => { + const bufferPath = buffer.getPath() + if (bufferPath) this.getPathStatus(bufferPath) + } + + getBufferPathStatus() + const bufferSubscriptions = new CompositeDisposable() + bufferSubscriptions.add(buffer.onDidSave(getBufferPathStatus)) + bufferSubscriptions.add(buffer.onDidReload(getBufferPathStatus)) + bufferSubscriptions.add(buffer.onDidChangePath(getBufferPathStatus)) + bufferSubscriptions.add(buffer.onDidDestroy(() => { + bufferSubscriptions.dispose() + return this.subscriptions.remove(bufferSubscriptions) + })) + this.subscriptions.add(bufferSubscriptions) + } + + // Subscribes to editor view event. + checkoutHeadForEditor (editor) { + const buffer = editor.getBuffer() + const bufferPath = buffer.getPath() + if (bufferPath) { + this.checkoutHead(bufferPath) + return buffer.reload() + } + } + + // Returns the corresponding {Repository} + getRepo (path) { + if (this.repo) { + return this.repo.submoduleForPath(path) || this.repo + } else { + throw new Error('Repository has been destroyed') + } + } + + // Reread the index to update any values that have changed since the + // last time the index was read. + refreshIndex () { + return this.getRepo().refreshIndex() + } + + // Refreshes the current git status in an outside process and asynchronously + // updates the relevant properties. + refreshStatus () { + if (this.handlerPath == null) this.handlerPath = require.resolve('./repository-status-handler') + + const relativeProjectPaths = this.project && this.project.getPaths() + .map(projectPath => this.relativize(projectPath)) + .filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath)) + + if (this.statusTask) this.statusTask.terminate() + + return new Promise(resolve => { + this.statusTask = Task.once(this.handlerPath, this.getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) => { + const statusesUnchanged = + _.isEqual(statuses, this.statuses) && + _.isEqual(upstream, this.upstream) && + _.isEqual(branch, this.branch) && + _.isEqual(submodules, this.submodules) + + this.statuses = statuses + this.upstream = upstream + this.branch = branch + this.submodules = submodules + + const submodulesByPath = this.getRepo().submodules + for (let submodulePath in submodulesByPath) { + const submoduleRepo = submodulesByPath[submodulePath] + submoduleRepo.upstream = + (submodules[submodulePath] && submodules[submodulePath].upstream) || + {ahead: 0, behind: 0} + } + + if (!statusesUnchanged) { + this.emitter.emit('did-change-statuses') + } + + resolve() + }) + }) + } +} From 2b8f017bbdceca309abc75a6bee172043a8adaca Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Sep 2017 22:22:40 -0700 Subject: [PATCH 254/448] :arrow_up: git-utils (prerelease) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0c5d1577..5a3b4e117 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "fs-plus": "^3.0.1", "fstream": "0.1.24", "fuzzaldrin": "^2.1", - "git-utils": "5.0.0", + "git-utils": "5.0.1-0", "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", From 99f3ada86b6b37778dc5766ccbb094c40b93a4de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Sep 2017 22:23:07 -0700 Subject: [PATCH 255/448] Refresh git status in process using async APIs --- src/git-repository.js | 86 ++++++++++++++++------------ src/repository-status-handler.coffee | 36 ------------ 2 files changed, 48 insertions(+), 74 deletions(-) delete mode 100644 src/repository-status-handler.coffee diff --git a/src/git-repository.js b/src/git-repository.js index 503e5fa0d..6929cc1dd 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -11,7 +11,6 @@ const {Emitter, Disposable, CompositeDisposable} = require('event-kit') const fs = require('fs-plus') const path = require('path') const GitUtils = require('git-utils') -const Task = require('./task') // Extended: Represents the underlying git operations performed by Atom. // @@ -99,7 +98,7 @@ class GitRepository { if (options.refreshOnWindowFocus || options.refreshOnWindowFocus == null) { const onWindowFocus = () => { this.refreshIndex() - return this.refreshStatus() + this.refreshStatus() } window.addEventListener('focus', onWindowFocus) @@ -117,23 +116,17 @@ class GitRepository { // This destroys any tasks and subscriptions and releases the underlying // libgit2 repository handle. This method is idempotent. destroy () { - if (this.emitter != null) { + if (this.emitter) { this.emitter.emit('did-destroy') this.emitter.dispose() this.emitter = null } - if (this.statusTask != null) { - this.statusTask.terminate() - this.statusTask = null - } - - if (this.repo != null) { - this.repo.release() + if (this.repo) { this.repo = null } - if (this.subscriptions != null) { + if (this.subscriptions) { this.subscriptions.dispose() this.subscriptions = null } @@ -550,42 +543,59 @@ class GitRepository { // Refreshes the current git status in an outside process and asynchronously // updates the relevant properties. - refreshStatus () { - if (this.handlerPath == null) this.handlerPath = require.resolve('./repository-status-handler') + async refreshStatus () { + const repo = this.getRepo() const relativeProjectPaths = this.project && this.project.getPaths() .map(projectPath => this.relativize(projectPath)) .filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath)) - if (this.statusTask) this.statusTask.terminate() + const branch = await repo.getHeadAsync() + const upstream = await repo.getAheadBehindCountAsync() - return new Promise(resolve => { - this.statusTask = Task.once(this.handlerPath, this.getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) => { - const statusesUnchanged = - _.isEqual(statuses, this.statuses) && - _.isEqual(upstream, this.upstream) && - _.isEqual(branch, this.branch) && - _.isEqual(submodules, this.submodules) + const statuses = {} + const repoStatus = relativeProjectPaths.length > 0 + ? await repo.getStatusAsync(relativeProjectPaths) + : await repo.getStatusAsync() + for (let filePath in repoStatus) { + statuses[filePath] = repoStatus[filePath] + } - this.statuses = statuses - this.upstream = upstream - this.branch = branch - this.submodules = submodules + const submodules = {} + for (let submodulePath in repo.submodules) { + const submoduleRepo = repo.submodules[submodulePath] + submodules[submodulePath] = { + branch: await submoduleRepo.getHeadAsync(), + upstream: await submoduleRepo.getAheadBehindCountAsync() + } - const submodulesByPath = this.getRepo().submodules - for (let submodulePath in submodulesByPath) { - const submoduleRepo = submodulesByPath[submodulePath] - submoduleRepo.upstream = - (submodules[submodulePath] && submodules[submodulePath].upstream) || - {ahead: 0, behind: 0} - } + const workingDirectoryPath = submoduleRepo.getWorkingDirectory() + const submoduleStatus = await submoduleRepo.getStatusAsync() + for (let filePath in submoduleStatus) { + const absolutePath = path.join(workingDirectoryPath, filePath) + const relativizePath = repo.relativize(absolutePath) + statuses[relativizePath] = submoduleStatus[filePath] + } + } - if (!statusesUnchanged) { - this.emitter.emit('did-change-statuses') - } + const statusesUnchanged = + _.isEqual(branch, this.branch) && + _.isEqual(statuses, this.statuses) && + _.isEqual(upstream, this.upstream) && + _.isEqual(submodules, this.submodules) - resolve() - }) - }) + this.branch = branch + this.statuses = statuses + this.upstream = upstream + this.submodules = submodules + + for (let submodulePath in repo.submodules) { + const submoduleRepo = repo.submodules[submodulePath] + submoduleRepo.upstream = submodules[submodulePath].upstream + } + + if (!statusesUnchanged && !this.isDestroyed()) { + this.emitter.emit('did-change-statuses') + } } } diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee deleted file mode 100644 index 2fda9a335..000000000 --- a/src/repository-status-handler.coffee +++ /dev/null @@ -1,36 +0,0 @@ -Git = require 'git-utils' -path = require 'path' - -module.exports = (repoPath, paths = []) -> - repo = Git.open(repoPath) - - upstream = {} - statuses = {} - submodules = {} - branch = null - - if repo? - # Statuses in main repo - workingDirectoryPath = repo.getWorkingDirectory() - repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus()) - for filePath, status of repoStatus - statuses[filePath] = status - - # Statuses in submodules - for submodulePath, submoduleRepo of repo.submodules - submodules[submodulePath] = - branch: submoduleRepo.getHead() - upstream: submoduleRepo.getAheadBehindCount() - - workingDirectoryPath = submoduleRepo.getWorkingDirectory() - for filePath, status of submoduleRepo.getStatus() - absolutePath = path.join(workingDirectoryPath, filePath) - # Make path relative to parent repository - relativePath = repo.relativize(absolutePath) - statuses[relativePath] = status - - upstream = repo.getAheadBehindCount() - branch = repo.getHead() - repo.release() - - {statuses, upstream, branch, submodules} From c54e0782dae62a782d53175ed167d729892673c2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 10:18:12 -0700 Subject: [PATCH 256/448] Fix isProjectAtRoot --- src/git-repository.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository.js b/src/git-repository.js index 6929cc1dd..205572005 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -205,7 +205,7 @@ class GitRepository { // repository. isProjectAtRoot () { if (this.projectAtRoot == null) { - this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) + this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) === '' } return this.projectAtRoot } From c12a5b23b485ad52675a72769d71e338eb8bd748 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 10:53:31 -0700 Subject: [PATCH 257/448] Convert git-repository-provider-spec to JS --- spec/git-repository-provider-spec.coffee | 98 --------------------- spec/git-repository-provider-spec.js | 103 +++++++++++++++++++++++ 2 files changed, 103 insertions(+), 98 deletions(-) delete mode 100644 spec/git-repository-provider-spec.coffee create mode 100644 spec/git-repository-provider-spec.js diff --git a/spec/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee deleted file mode 100644 index 16ccf8938..000000000 --- a/spec/git-repository-provider-spec.coffee +++ /dev/null @@ -1,98 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -temp = require('temp').track() -{Directory} = require 'pathwatcher' -GitRepository = require '../src/git-repository' -GitRepositoryProvider = require '../src/git-repository-provider' - -describe "GitRepositoryProvider", -> - provider = null - - beforeEach -> - provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm) - - afterEach -> - try - temp.cleanupSync() - - describe ".repositoryForDirectory(directory)", -> - describe "when specified a Directory with a Git repository", -> - it "returns a Promise that resolves to a GitRepository", -> - waitsForPromise -> - directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git') - provider.repositoryForDirectory(directory).then (result) -> - expect(result).toBeInstanceOf GitRepository - expect(provider.pathToRepository[result.getPath()]).toBeTruthy() - expect(result.statusTask).toBeTruthy() - expect(result.getType()).toBe 'git' - - it "returns the same GitRepository for different Directory objects in the same repo", -> - firstRepo = null - secondRepo = null - - waitsForPromise -> - directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git') - provider.repositoryForDirectory(directory).then (result) -> firstRepo = result - - waitsForPromise -> - directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects') - provider.repositoryForDirectory(directory).then (result) -> secondRepo = result - - runs -> - expect(firstRepo).toBeInstanceOf GitRepository - expect(firstRepo).toBe secondRepo - - describe "when specified a Directory without a Git repository", -> - it "returns a Promise that resolves to null", -> - waitsForPromise -> - directory = new Directory temp.mkdirSync('dir') - provider.repositoryForDirectory(directory).then (result) -> - expect(result).toBe null - - describe "when specified a Directory with an invalid Git repository", -> - it "returns a Promise that resolves to null", -> - waitsForPromise -> - dirPath = temp.mkdirSync('dir') - fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '') - fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '') - fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '') - - directory = new Directory dirPath - provider.repositoryForDirectory(directory).then (result) -> - expect(result).toBe null - - describe "when specified a Directory with a valid gitfile-linked repository", -> - it "returns a Promise that resolves to a GitRepository", -> - waitsForPromise -> - gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git') - workDirPath = temp.mkdirSync('git-workdir') - fs.writeFileSync(path.join(workDirPath, '.git'), 'gitdir: ' + gitDirPath+'\n') - - directory = new Directory workDirPath - provider.repositoryForDirectory(directory).then (result) -> - expect(result).toBeInstanceOf GitRepository - expect(provider.pathToRepository[result.getPath()]).toBeTruthy() - expect(result.statusTask).toBeTruthy() - expect(result.getType()).toBe 'git' - - describe "when specified a Directory without existsSync()", -> - directory = null - provider = null - beforeEach -> - # An implementation of Directory that does not implement existsSync(). - subdirectory = {} - directory = - getSubdirectory: -> - isRoot: -> true - spyOn(directory, "getSubdirectory").andReturn(subdirectory) - - it "returns null", -> - repo = provider.repositoryForDirectorySync(directory) - expect(repo).toBe null - expect(directory.getSubdirectory).toHaveBeenCalledWith(".git") - - it "returns a Promise that resolves to null for the async implementation", -> - waitsForPromise -> - provider.repositoryForDirectory(directory).then (repo) -> - expect(repo).toBe null - expect(directory.getSubdirectory).toHaveBeenCalledWith(".git") diff --git a/spec/git-repository-provider-spec.js b/spec/git-repository-provider-spec.js new file mode 100644 index 000000000..e1d0168a9 --- /dev/null +++ b/spec/git-repository-provider-spec.js @@ -0,0 +1,103 @@ +const path = require('path') +const fs = require('fs-plus') +const temp = require('temp').track() +const {Directory} = require('pathwatcher') +const GitRepository = require('../src/git-repository') +const GitRepositoryProvider = require('../src/git-repository-provider') +const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers') + +describe('GitRepositoryProvider', () => { + let provider + + beforeEach(() => { + provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm) + }) + + describe('.repositoryForDirectory(directory)', () => { + describe('when specified a Directory with a Git repository', () => { + it('resolves with a GitRepository', async () => { + const directory = new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git')) + const result = await provider.repositoryForDirectory(directory) + expect(result).toBeInstanceOf(GitRepository) + expect(provider.pathToRepository[result.getPath()]).toBeTruthy() + expect(result.getType()).toBe('git') + + // Refresh should be started + await new Promise(resolve => result.onDidChangeStatuses(resolve)) + }) + + it('resolves with the same GitRepository for different Directory objects in the same repo', async () => { + const firstRepo = await provider.repositoryForDirectory( + new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git')) + ) + const secondRepo = await provider.repositoryForDirectory( + new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) + ) + + expect(firstRepo).toBeInstanceOf(GitRepository) + expect(firstRepo).toBe(secondRepo) + }) + }) + + describe('when specified a Directory without a Git repository', () => { + it('resolves with null', async () => { + const directory = new Directory(temp.mkdirSync('dir')) + const repo = await provider.repositoryForDirectory(directory) + expect(repo).toBe(null) + }) + }) + + describe('when specified a Directory with an invalid Git repository', () => { + it('resolves with null', async () => { + const dirPath = temp.mkdirSync('dir') + fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '') + fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '') + fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '') + + const directory = new Directory(dirPath) + const repo = await provider.repositoryForDirectory(directory) + expect(repo).toBe(null) + }) + }) + + describe('when specified a Directory with a valid gitfile-linked repository', () => { + it('returns a Promise that resolves to a GitRepository', async () => { + const gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git') + const workDirPath = temp.mkdirSync('git-workdir') + fs.writeFileSync(path.join(workDirPath, '.git'), `gitdir: ${gitDirPath}\n`) + + const directory = new Directory(workDirPath) + const result = await provider.repositoryForDirectory(directory) + expect(result).toBeInstanceOf(GitRepository) + expect(provider.pathToRepository[result.getPath()]).toBeTruthy() + expect(result.getType()).toBe('git') + }) + }) + + describe('when specified a Directory without existsSync()', () => { + let directory + + beforeEach(() => { + // An implementation of Directory that does not implement existsSync(). + const subdirectory = {} + directory = { + getSubdirectory () {}, + isRoot () { return true } + } + spyOn(directory, 'getSubdirectory').andReturn(subdirectory) + }) + + it('returns null', () => { + const repo = provider.repositoryForDirectorySync(directory) + expect(repo).toBe(null) + expect(directory.getSubdirectory).toHaveBeenCalledWith('.git') + }) + + it('returns a Promise that resolves to null for the async implementation', async () => { + const repo = await provider.repositoryForDirectory(directory) + expect(repo).toBe(null) + expect(directory.getSubdirectory).toHaveBeenCalledWith('.git') + }) + }) + }) +}) From e3abcebb76001f56fe1d4cf6ee62f586ff790916 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 11:28:32 -0700 Subject: [PATCH 258/448] Restore behavior where only one status refresh happens at a time --- src/git-repository.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/git-repository.js b/src/git-repository.js index 205572005..f831e1709 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -85,6 +85,7 @@ class GitRepository { throw new Error(`No Git repository found searching path: ${path}`) } + this.statusRefreshCount = 0 this.statuses = {} this.upstream = {ahead: 0, behind: 0} for (let submodulePath in this.repo.submodules) { @@ -544,6 +545,7 @@ class GitRepository { // Refreshes the current git status in an outside process and asynchronously // updates the relevant properties. async refreshStatus () { + const statusRefreshCount = ++this.statusRefreshCount const repo = this.getRepo() const relativeProjectPaths = this.project && this.project.getPaths() @@ -578,6 +580,8 @@ class GitRepository { } } + if (this.statusRefreshCount !== statusRefreshCount || this.isDestroyed()) return + const statusesUnchanged = _.isEqual(branch, this.branch) && _.isEqual(statuses, this.statuses) && @@ -590,12 +594,9 @@ class GitRepository { this.submodules = submodules for (let submodulePath in repo.submodules) { - const submoduleRepo = repo.submodules[submodulePath] - submoduleRepo.upstream = submodules[submodulePath].upstream + repo.submodules[submodulePath].upstream = submodules[submodulePath].upstream } - if (!statusesUnchanged && !this.isDestroyed()) { - this.emitter.emit('did-change-statuses') - } + if (!statusesUnchanged) this.emitter.emit('did-change-statuses') } } From 2dd73de418883085671b6d248845998611e72142 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 12:13:26 -0700 Subject: [PATCH 259/448] :arrow_up: fuzzy-finder for test fixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a3b4e117..0fb37392c 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.6", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.6.0", + "fuzzy-finder": "1.6.1", "github": "0.6.2", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 07614b3b38577e1c85fdf85f6b707a4ef8e12223 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 12:36:51 -0700 Subject: [PATCH 260/448] Update git-utils main path in startup snapshot blacklist --- script/lib/generate-startup-snapshot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 7701b6a34..2905bca1b 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -39,7 +39,7 @@ module.exports = function (packagedAppPath) { 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', 'lib', 'git.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') || From 5de1c9b9fefcb026407cc315ebd19808ed189b20 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 27 Sep 2017 12:37:12 -0700 Subject: [PATCH 261/448] 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 262/448] 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 263/448] 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 ce105ab9140302de496130e9079fb3b9e9fc91b5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 14:18:40 -0700 Subject: [PATCH 264/448] Give an id to each GitRepository --- src/git-repository.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/git-repository.js b/src/git-repository.js index f831e1709..057c5fcb7 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -12,6 +12,8 @@ const fs = require('fs-plus') const path = require('path') const GitUtils = require('git-utils') +let nextId = 0 + // Extended: Represents the underlying git operations performed by Atom. // // This class shouldn't be instantiated directly but instead by accessing the @@ -78,6 +80,7 @@ class GitRepository { } constructor (path, options = {}) { + this.id = nextId++ this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.repo = GitUtils.open(path) @@ -117,16 +120,14 @@ class GitRepository { // This destroys any tasks and subscriptions and releases the underlying // libgit2 repository handle. This method is idempotent. destroy () { + this.repo = null + if (this.emitter) { this.emitter.emit('did-destroy') this.emitter.dispose() this.emitter = null } - if (this.repo) { - this.repo = null - } - if (this.subscriptions) { this.subscriptions.dispose() this.subscriptions = null From 6a86a1c7bf61e5184282824e77a7416bb26612a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 14:19:05 -0700 Subject: [PATCH 265/448] Wait for repo to refresh in test --- spec/git-repository-spec.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/git-repository-spec.coffee b/spec/git-repository-spec.coffee index 47ca84580..e4d1e0c7f 100644 --- a/spec/git-repository-spec.coffee +++ b/spec/git-repository-spec.coffee @@ -283,11 +283,15 @@ describe "GitRepository", -> [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() From 05a81485ca85598e7e22d6902df86923f8c0d8f2 Mon Sep 17 00:00:00 2001 From: Thomas Johansen Date: Wed, 27 Sep 2017 23:51:08 +0200 Subject: [PATCH 266/448] :apple: Fix faded app icons on macOS 10.13 --- resources/app-icons/beta/atom.icns | Bin 1194491 -> 1193792 bytes resources/app-icons/dev/atom.icns | Bin 1145794 -> 1145071 bytes resources/app-icons/stable/atom.icns | Bin 1198254 -> 1197490 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/app-icons/beta/atom.icns b/resources/app-icons/beta/atom.icns index 69abe031ebca8ad211d139ac69bb4006e9d8a5fd..6737fa27f745ef073849ccd1b3abbceec8d2d8e2 100644 GIT binary patch delta 3420 zcmah}c{r5q+kVE38B6wUVu*|+uU*z8WiX6okStjT@fu?pOO(b|c9LOCvZbVrP_`1U zgjdMECE2ni+t`ZC_q@w-e81lx-|;=iah}(8-1l|g*LmLeeLVXNljcf}>8=nWtdr(WY#}&DcioumN0l+dK>lfdD&Q&Y&>srE9)Bs>z zjb?!TuXQCw?(3{t|JMvf9RQ#K24IsI0HIYjx3@P}rWFUUAWDD5Fp%cNVG^ZxjiQOFbL;xZ&kimouNZbM-noF5X$dX1p06{HgF(HenSOCKI zF`Ehb$QA<9?A?w4 zRUl_UJ+nwqnc^&{aaI9Ti);sVw<8n5yNule-E74o#6fGwoDI;{$ZL;L}O{<2czCGOaOWY+f2K1-A1_y~T@F`J%kS<{NUM1l`WEuln`#XUq_8dX zrcYg8c&F1W>vcB+W6^swx9*mG(`}_lr}~GHdXio{6sBmz^ZM(AvNuui9>*j*cRpot zN!7}wj_X@VF=);g>4D`-x4UsTf3mvQeRILfCOa-sk+9c4+Z=eOxdom!8qv9p`3zJ} zvEx_x>=hL}I7DS6QI*e><>Jy@JSh|Jq%= zPu7K*>E0p!{8%r|!0A7ohGtg}tBB)IZzS1QjYMJ*3njGv~N{(D3yKR}9tSG{jlHQ@r-4YCm%+ zk)a`H5;{9ldcWFeaNZH`)*arc8h!s%L1FZqe2p23<-+RY57Qa1(noAQMRsXxQ@gUi zKx-oawFTGK*50tec}2;AZOMCG93)MX@UziGCc$dSh5CqF?JGz9+XR7ydH>$rP$T1w z$UkNja@XhFLwcdvk*%#b8U^iqwGB}%B^dqbCBNecUpf<-&M9cQ8k@Bmo1nTW--M8@ zMLi66H3(f%3iK^*+2wg2!EZ6ZVfau$z3pYCp^BN5UTavCq142mH~BKA5|3}E2F=`C z|7=pone>3v64Fo{$`N$=rgZ!Rt?$+nu12IG;kO|kkv<43iCdNnFdU`Qli!qR-DTz3 z?SMIza&N3#j!-Z?AE5KRdp)}^PY9ymp!NGT+2`oF3S$ogcS3#b-Yw(us#BTzyd*^J$F;-`8{@idq-<@B~icJPg zt4*z1Saq)8+&z!%UKL&Gj$fy8?$=`*B(pI!yNva5c+r8zl2@GqbmCp8W}KUMZsz)J ztisJpb?oJrc-z=eI4eUfwfJ5tpU{`mbl@_pWxBs=7k)PakEB!xW0oRTJz9y6=QAWJ zAB0(cl%;+*KIgr#zjh^L)U0r&JEh3NZd)q}y1L|^;w^pnf#o^L$1`WY)%Jum5}C$o-my)W2Vv^_nNc0#AP zM0DQbOvX!NC`o|wp)F1>x6~!RD_Kdh;B{i>I%lrlp**}w_~u2|^Q3OO!Jb2jA%;>= z;dRhTS7XlgkNtdRUT-V)ag7g<)I~|d*Nwn+LNEF-_AW@clic? zDxLnQ|;(8H?yq3#Qmir7$0uar>8n^@wOk5 zH%7)pyA^i9h$S25^!6HLWCxjJ(6iWflKk|+*IaAL%9={4y{TcrpjqxDQy^F7L#8vc zE)=Kbmh>CJpJTk+!D6%stz2omAS&&@rfsKtOzXM{SM7!?t{$J<=Hn!D(k9`RMXh5x zQZ{X(Az@lX2Y=>;n9Hv0QY*njy8bAaCRw5j(cEQqqIR^pM8yl>_oY;Bg=Bq3}V z7rZHRdY9BAV&}LszpSgzjFC-3KT8R+p0JeCYUCL>Xi*ZsD){5NNGEfWtNUGtAM@n0 zbewQtr3&`Vy~zXP#yEyqRcZQREcIU4cP<_yg# zm$7pEAzKPJ?(C)A@eivYnpgcflCm}L01-*wMG4A=a`t1gnj8Ba(w^uaAHK8~?v8zT z6XR5#e~Nl?B5S+qla`CXwg>BW4;d@+o3Kmx!o#FDV}@8NTQx5`D=b z%F2Tf7ATArepJ*R9p)8G_6;D}qXRsC_acOl9nkK<1Rq~;*cKfU5XcWeZWTR5u<`K= zTtt&XiNpbR4$dRoyq7F&6#cv+$c|wAccmSgKy)XQi@Bm%ig}`;(J)(*yT6yMFCl=` zFDN7|;z;?Y;IC2pe^M{!p8D%A+EeU7B*&H5ni4K|LW-z zL)iug1cXFG*!V=H6;w6#E|^+7dwTo)P9%}9MkQtz6hC>>)cTik&@AMi2#Eh1!2tg| GZTlZBS|WA; delta 4128 zcma)82{@Ep8-5K9Bl|Lh(AcxoMApJgBWt!6qFuJEHL|3<(I6s3GswP$DN7PQTgcKz z_NGGiNoCDaV*VNRW%>O7|DEf4-g)ljoaa67b>>{p({a7U89mU^#uW>2sV!nvU7P@b zjtQU+)$E~#g`sR!S`Gk=Sxc0av6RI~gH68>?b$>nl2;*xk__znZT`hB?Yg^Grcmy7 zug75b%z9#GBko+UcSfwo5whzG$^tQK4KV-!tz0E4q0}8fWB%Sy3{-%&LjaJC=$x79 zjF6?}j#$M#005aF6d6LS=|BRz5d>raSOK6U$_T;)0i9RA6vN7wqWJ-U$vVG-ys$v- z$hTg@{?Y(I^ugTf3(P$b{hR;w3&YQ^sF8s%#@@b<^!=IFY z8-&Q4$rK7%XvM(Pvbjv5w5)6-Y!ey$&4p1{5S-n_@SBkE;0Cf6)>C;f7 z&17(uG>TizH_PH`H)B`1+IOq@X4$(9XK}>?u5oTWtZSr|bego0S&vE8>+wcZz2V2+dzYB@Hc+yAqZGQY)AuWr;M?21N#Ry z((V&#iNQb2x@3^HI&CPdHGqFSgYv&*>XUzy8jXKXr@0sdHtgeDz^~$~OW@ahsww>1 z$-g~N-MiJK8U7hk^<7=sJ@R`vI@9h8*m#MDDH9n6-K_!uCYPd53W~Eb*rN`i0D9|F zCn%EMMYjYsOrX0}0bo*@K(V6RAm5j_rkmWN2G9fN2&u-94QgZ-!Nm%-}}#3dBXlOl?%W*@qJ#p?FkoErJy#jSG`tcLXz zH(>G7gUZs4iM!s1-Rj4m$+Y|6%m{Nqrx{A8)#AwSlFlH~@83$Hr&b}~^^j$^L)o2}e{D^|&_);Bt_y2|pn@9cLiK(epLz1-|VzF9M* zvF$Y|0YU#5hy|3k7d2{*ikmJL+<@Z{d$t|~3iDwBHgyKUP8Z5g?{n|0d>e-y?pyM+ zK7J338Zf!zd7VxCN`SxE5{6NzTQ^2aS9{>uxkqpoGyUb^+}$-kF9RIV+}E?zurgU4 z4|A+fkxRCIFNJ6y(bNnrDSd36R?KNJ^J+dG4D3Ta7*`fVrU^|GmT~Tn8V^e6Ri4-Q z8C2dc0Y~d9HnD%{f7G6kUvNezH;#=j`14r7BP)qoVr7HE7OhtGu@To>vDYqSw^{hf zet0J*x$8@fUY|0Ly#k`OLfH5mk|6l(Ue^xNl=P7b*0RF7n_=kOYxKj~a66X*n;P=& zF%W+GXf@vdD{|u!ZmZNCVEZH};va2= zi(9UgW?9;jNA{mcwp?bIwtpg)ppH5%WFvq$o03MTKo>mjZzeGALvaO-ISyiETFZyt zOIl1fR>csykQg(6wV11ri-f^ei445+2x!RNu=s?&?C_+F;6=$F9}V_!iXAwsBdg5& zSOR`ICQUkdWMD!q%PY~u|G`lY2c5cO@5>PgP7BMVvrVvo6lAkb#$M9$?!=Na({GDB z{1S|17bPx-Lb^WO-(&Qk#llG7 zk&jo{Lj;9IWi$(wqDDM1^=HNJjY9)mF`1m|LMTgqjL(3QK_dv;i^aonJQwvergg8k zbvK-c2DwMv=gf;5OLbLM7#l_81XyxCf3o;-@A=f9(6*fZF59x88CS^g^TS~{weZ)$ zgfdtjA+^ZBEN+@1;JGG)WU%`{-v>p|8dlWoqT?Pm+9a@xCC&DcnW8zmV0OIIPvBGY zYmXr@t;Zs^J2iSPYNZPvlxuIE*0_-)rX^(wW9Q+UFB3+oJT)w-&Hs$4=r7XSn>Q5K z8c|E?L-6}HX5n`dGpsCI?c(ff3!a-!g|-TY2*bn)F6dr$60AOc&YOwXTs|Az!L%KY z#18m8+b`hml+7w49f3W4^VvZDj`&Zy%N+_vA|TzVudkcrzC+DlmZW%(dr6=04*Z-U z8hfHu$SB$d(et2=gVRkmGG6D+sZfb;z6-75iP1Ypq~lfX+9O50>5YAG=#K2~uN3v{ zy|isLZ9ngqHm-A9;ySTs&=4853>KCoJsl!dPQLG+N^l6o3uJNl2sb6!NpXp%8JlUM z7zXV*c%+W^^_?HTsv)^^j_+!-d2IH?6K$Wl9p_c)WDgK=;klE^-Ts!W6+iC6BY}uQ zCBxBE{Z`EC*V1`YCB3Kg`-b+jKqaoc1f&r74;`s4+IGk`qNnN9>y#JQ8^HIb0%|PL z&H>%AnPXV~#nI?kvJZ1+&64sSMXYdrA@^kb!~h8q#w9eLRW6*q1Yxf0@LxbUXo%nr zu%TE&_^H4$0pfg8cYzqQ$XW!ItkarFB07u(Lvj| zruit$_dR8V^5F`y4!ybHD8CV71N%=FNv$(+T&E4@-l))}m^5RzGs`p79LvT!ULAW= zB$qy;eUdNzi$9;S%@49$O7yIr#{<5-s8C&V@sEM^?n6xI+r;mNusMk2?J8VUw@AP? z%`?}?DR)5q8w(?9++QCiTxAfwp(KvxL^Wg|*2yHkLNjEXCf8YtfLSfoD6^iIkBGWF z44xm{lQDLe=$V)a{gci&C0kOxe;nQVp_wz=;bEE@Ug4UCO8vXu^Df3wHi$d!8N8yG z7)>91x@l80nacb+BhP)vCnof;Ufx3Lp+h-;!Ik-jI z+FD~4JUf06UM9SEi46i@j25*S^trZOGLq?<=a9kkm`P>IgiTOnURCJmdAdD%ilIN& z)CO-%>51vYu^K?QeHkoV9DPd`?26wBxpp2Lbspkc`r=}knJ_3kqJGM4@3{n3^OJ%f z?c;Y}JR?x)<+`re&YUq?%4z?vHq+pi(Z6FYwD zL8<443=7rJzA}9HW>$+>*VS@89{&B|({b1MNBR8xJjL2R*$wsf!d}-kIaO@Q^cTMO zx$FtfTdnf@tt-Bx2N?UJ%Si1)s3AZl zU+mo0zss}!QiiFZMGENI34i7Ue|tfiw-Gs0(%26V9x5%C;qiTvf5ha`<(iX*$0%ju zHWomW;Y6PE;Uis)`aagSZGP(fAqh#Zb{s1G>00_=|03uwHz47|7QVGaZL12Bz2?o1 zXD`yb{vzKO-~ess%d4V!8)P;57`34h`>NA!+1F|^7aw|dfij;Bav|J_`;N)KavUZ~ z^M#Z_Q^Hz}s|QPo<-&f8H!Cdio%pla<}?W|J5%M7^E7g8C>F#w+`*LD=5PzK{CDLF zKpf8@qvUN@?}#c#qy4;wbAwKYd|P5UO3Ga}{y4w?zEIJAn|Ddar!6H$c{MDOMVM0l z!n~En1&Xu}mn>k||vQRKAm6sISQrV_;x-LjbTpm%SqSZ>@H>l%_pQS04c8 zx!Eq~{w>v~)`+p^Vfcgpr$)o?2LOQ(GjLEG06$nZy|FP}b}#{e!b`Ri_|UBqcq{;c zzoGL5-@pN&D1|Rj1n6v~fQn9T8XuB74AL+cB<^1{2)+$6)A&%PCfFpfr3vJu@C9=K z0M<ZNVj(-QCM_WTc zKHULfzs~_5|`ln!WJ9@Gr#iQ$X2vo5sbF)pw2{O zVkSd)Tgh%Ro3vqy+hY&&;ufVJ2TNkhmjqH3#5c1lNDmHd){qW7PSmifxoYxc4ST!! zRn+)BFZsv6k^AcWo@^iI!Q+@Zj^3R6Dks0>*mj*wRdTVVF;-MN3nEYkPVSFRrWbwA z=pG(rm#R>r^xPNn3}ecu&gDI$_?KfAk|rfj)-k(aDM!%cD9eYEOVTrW37gku97uSn zjWOrV+ZW4D2SYVH_?ah6>X^=L_?b2Ir*8vO*J|5Z{jF+sT3ufY>%N32|6-kAf!fvi z=}=npp`m;g+M>eTs^t(*%4PRxK~7l)JORJ`tftPv7i+%g+^B~f5O5dwzJvEL_@g|x z!7jmqg}T^S7+nBSIr-yEx;5uV^4k!3I5X^$(`>Pz%jeGX!3O%x)y8(R>+lwYJ;#z> zx-TQB^ZanqOU7_=w?};dN{i!}v#-0<ne&V`?RDj^Lql^9(;kKi_K^sWAO zKO$~`_Cpp+Jj!rvGLbV|wu_0R2sINE9|m26^w+9pjMO&*Trc;p_p)I**O2+?B(9!d)1zgr;fJl zq#Z`vq#Z~oe)I~uIPD}>l`)8~RU1+5(6IX5oCUMVm#rJVAZ(R@`q4nOg+8~KY#f2L z=Fr7Wt!LLCd@%DB)B5wgoKToyzn)rNfM?YQOcaVF{-O1o%&e2>(8Ukbnv9!_&;l(= zpKj^Uf<*ZA!LmiFW7=v^Tsq>AbbE_^0ZsYHi_+XNjm=s|&a>~B3{t>rCsoeT=wy#B zj1F_(9vhC)v603I0mdK7%XQaLhso$K3xTh$s4~WzZPDcP%axMHaM9LOE;Rz~wrH#{ zaqYpGx8aI~qq*(VVLPkRWy}hubC{k1Xsf!q`y74auDudZ{D_gMg;i29aqB4+TC|2{ zS#pdR`}?q-fxdjvZ@qE;m+L3xzpu;+RtZEY8cKL)uiprQZ+FCGJpJAsAYA3`nx!xD7%kB5yt3LBO;FRog^Z1F0&rKnFvzD$aDyW89rNp8Qu8q42 zXOZMip6{0J=Bb;H*!pajI!^tn!|Ab1NHcT`2_a*?iXJv{h#M~s(%4HY=ZFp?BE7tv z(pcIG~HM1g|) zD-AUNOWY1Qw~RI!l$A{PmUQ>Et!YPT`}|3vwItH8Q|Wx(s!0MB8fxdAR|pX`Du0SJ-818ArvAL~NySU!Q*f;|Mp1^7M~o zxEH4MqL&R~UPi>YZ*EdgInVwpaV$h;FL5LL&qJ934&L)C@@val-xlbGAt$b*&-!BZ zrUu`_;Rn_YlpPv4Le;7|3C`u+b(`L-84CF(hTU!xqgT=BLkkU1RZ<%-EwtW`5*>bchAL+t zFZ#Vrq0`Uds9)dM*$aOfT$X1wHm=%l@7cKNS~sX=J*`?e-aMr^j+0lKon3u7q#Y^r zLZ2NysvzG$i*{=9@5rBS{ub!>Wv;AjSKd<@)@xH^Q9Y( zK=LM&?~27j?~2C?#0t6uc?bHr5DCFSeKI>`<<9)A|6DwcBT#?#`jI_PI9m`2;h+QW zu*iF|sM9B$9sV+MPD1>Iv#npGpRd_JetNO;E@8pJ;js{eBxbjko}tOH<4$LNFI>Dt f3L;alCTA7iebmHm`xzVz3;DYO3jfsq7S4YFxPckS delta 4124 zcma)82{@EnAAfDb*vih-H5ihuj3qPKGRR36-cAy06Tw~f3nLv(^tzh zS~_Jd1}SrEiMjQ-Z>`>!vKFT(tSzVjq^&lj0RW_Sg{XzlcK}KIdqZgu7-NS3ppeox zH`kYSZk3>g48oJ)GgzkC_*EpcAO{?q_KqUh5~ zy@eu)zxkiPP~ztmm63fWLRUG&d)BHn8c3t{i2rtIOsv-#cw*Nt!yn@t$u!#dA3qEn zog2$ETIY|A5H^s(UmStH!j^>%48N#_EdY@H-(?yt8-UFIFMSq*+eijiNL=PhzEPG* z-iTe{$O9|+M%lo+qxZuDu5zwFo2#TB>1_XcW-acoUyIkH`gMO6h7PVar5mkA|ICdb zGyf!4m>CE_TtWZLhyWluzm5i1;dxQ|A&6c@tV^RAr;NRM9s3(LGp-MMiQ(VOnq-)< zI%DiuZ2*6JhLwKG^rz?sHJbmP&Tz3uuiM9b^v~ieOAuFmsu|+y$-g`hqxAazInlZP z`g9}4wef5C9${P$(BTSqCXUxoh_VI%n0Z|2f=TnR`skVI0W40g&a`czN0p>yPj0thJb#lB2oQ)ok3K2N_&Or!L8Gw3fW*BgBNK36L zLVqP_Al3m70KzhiU^2Fn{um#`#X%1!JviyXMGtOz@X&*o9-HW~xgv;*Z{C9`lK$QT z87Bky_`v_aF1;4a*g((P9!jutIATo^``+Q=P14@=?xA>~Jua%W51D|$q;bNoUCWbE zf}XRT(6@x&;@!wbiDcH>L;3+W~wI$mM6^MHKOeEL@s)k9XJ*+s}?KL@*tzj2*rxg?_dZFacr8jS#5#QfG4LPHi zXFgWsY6(mDY%>~n;Z*Ek3h%Kl{FP3fYNQUzJpK4c@&ov?0xdW3BTDw=*1|D4&^-k@&3saJ3FmTX3@UWuVWVa+VQ>I zefm^<3i;s40SSAP5B?%2RHy$k{~8+VD_F9G=S)CCgJatS6WW>uZo|5S?NAYj_&HzW z2zKMVkD~-7)F)TI(>>8v>WX?w>ZlP1)#u_TQD|qwJtD5XQN9v$#qT{x+m5Tt;jFiJ z=eDDV&t=DSFG#gO#C0fQ%^>FFYrI?{eu7hkBYmp8I*&h_6KgQzTG_CVRn2J9H?k=) zIvQWvX%H>(Vt=ghMJH{yO?|v&w*QneFv@HhhqHKo#jNSWI&xowg(zE~8d9N`= z9#{*}QUzv*wvzp0c+(|td~>k$*4}$h6l|gwn8o3=c?*Vq#7^zkjjN<8+y5 zG``3^n1Pwc`mi+ls8f7qzJccMhNi5V&(($}76>Tgy368T3Eg-X{TTf=D2rlP&hlm7 zActx0@4?4~4fXeGHs*Zf_GVE*H7*WrEt9d;;-9*G>C(rAZvx|of-J7Omp|sbAKZ8b z^4e{$^CjMyC*>M56fnvc2G>8IepgD`Cu6tgjhE2Ss7}%aMR)x0Y~+$| zae*eexAEllJZrgj{*HXA*O{2pEKW`>q3p<%bmfVm*{96HP2eKVL%2=SH6on3ez#^+ zVm@1|e^N(GVnG#Y+}!qFv0z7FxHC7OVarw! zBV?Epa*rxDhY^%a5yVz)5tYh)wp4qe!e*vzPdN2s&|aLTcepU`fN>meX>MNm8Xukw)SbC&UlrBdiO8KojVX0)b2OxSeZd$`!OLY5-ukz7zQ z#pB&@ah;mvxM#ZIL)(#4M$Bi zgs%Ch_1udV+*vY+69cQWj9jbLySv=l)U9`D_kOJ`Cm#q&2pPF62oEozSQ(hy5H=TH zJmGpc%Gy|Oyz#Mb`?(qOqXGAQ4;tg5sxS|=bX!bb?Bnh05*#wlP}dts?v<;$a{8M! zTJ2$KWSbWW)M+c<)25cn8SB(e4dM}#N~Closov*3@s`parZRBCzuZH9XGpvk)95kR zO806BrB2KjWt3Ev|1v%=G|GE4CeoJFfarX6S++4~JKrdeQIu7j?%Td3FRAG4IgPHz zeqUR^2?rKWUP(je2$kT5m3pb(lW@W9=d&jGiSoFrEKb5H&z_4F;l}Kf`^0c(r0X)$duW0p`&%`H$}7TD|e)jvTQ4jcD2-H z8EN4WLti&7ouoC9zV?4E==H9fpVV||LdlAjgoD!FdDy+a#~tuh2OXuYGg^iCiJb{> zfr>5m+P4ELn_h;E__SM6@+h_@gBZL+%Q|IosrdN*_nn=!)QAGR zI+1|7Ld+cZLiy~ePyu%j1U@_q4mbKTOD3^6jSVg9$x;vRS$_8B?I%O>{D;DcE`gB- zNSWtNl|d^>h@1*W4`Kbryd*_)+N>~n`eloGZgmBXDI@-RnuqVCKBI3mHr!`$2jdp) z>xrZ{kFb75-<68s`xo6DNrwP~AXNcif{W2c{)-um-m-oif>fh7m*GE}%YQLxb;qeR F_#d^x<>>$b diff --git a/resources/app-icons/stable/atom.icns b/resources/app-icons/stable/atom.icns index 2f3246bb82b0a45775f6e424a529761a2ffa2004..73ef963309813e418e4d2ba7d1a687bbcac19195 100644 GIT binary patch delta 3398 zcmaKpc{o&U+{e$1F*6LBgl6m^lx>9KA(SnTC0lkLW6RbM#*$_1OM|hF?2JefDMDpS zBJ{|<6j`%WJhDw$<~`5yUf28Ad#>yI`Q6v=cg}r(zxS!l(0@wSM|-*u0su~(r=BkI z7XW~7H5Vnyfon1m<#@^m$6W#dlMfkaT>XuF6S%wr0PI@lA~nFdkp_pzZq}v=Gvxm^ zNH|3RVDh_Srgt?OKqyx&(dkQ7%CP_xQF#>0fF4yMq5ue@H<1DBMF2oaGJ~lEkUL5S zCvrJt1|(+!#Kbm;?SC)^uEr>229zQNzQlAS1!N~PVA%iwZ%t%CTH#Ru6rTOh#@X;# z03k&|j%{y`QKX`QrY23!6(|zSqzEuUm2!!SCvb@`6iDt&JIZdpu#nKi45pojW8L2vQYOym-|$fBmg2fLSaBg z1QP(rrL_zOWbI-c0O4QFVn9~;V*m(pFoyvdL`DNZN-hIP0k4B^HWF2NMMVG2lrt(cs1RfM@q0Ko^)r(9a?UbSAq9dR&wN-C}w`-#wTl@Mp#B zfN6GO1QB2~40Q{PxkbfL02Ev=r+sRBd#XK051t4#wO{1iVnMjb;{cWq!MQcpV7SDs zYcNpEpx~eoppc+gK(T^Cfnsagy2h^i0Wy`VjOD+xLRD`6SO%Kwz;*hQJ@L649g4{Y+(fa$=D~Ayo9T}ds~Ky zthsKG$hBr4p>SR{x3_p+HxpJ9j2Zlw+H#Y1LZik-Rl4}p7j+Kr*c8S}+h|eQoe}oq zw~F=Hz9ilRNSB!SZ6mEGKj%)hWs)xMN7s~%o*eXV*Ly8c7HOey9C#4SAj*7rObd$N zwLA6p5)yqzmQAF;H;?D4ZfC;7(xmfdYZ_R6(n@XN&8o}u>iaM4-#^tDm4;r{9>{^a zPc5(GTGSVYo_E{i(HnY$0{i@VomkeJ$X7S z(N9gJj6Q2zlg(j%{E#K|r#eJm+FG;H`4DQPfL1f&F<*6SBcGj&*hM~gz;1qPdH7r_ zo^*&;u|0fUz-rUj_Fc#ccY~L9rhsneUO6u+Xe5k5(r(s}k?NtWnC=gAP%HGcfx2iN zU^u%jzS^QK6vxFPy;8;zG$S3euKz#@ea@hVLq4QBuCy_C(b@6Gk>Pjo0Xg4cuW3gP ziLlwO-T8i)NC2VsuGkZ?HnyCM^RBJZq~o!vlvbbf>FtbX%b zO|>JUpKakwhtV4mc}1f?;!cDOc8F?G?s_;DDx{vmOYpN#l?6}XPh!Le@(WkKlekJ= z(3IX^?XS8W;qXMHzbtxhg6Kk6$Ta}4h!)K`|&cFpmOZ{)*w`NC>FzLdtEw2M# z#-|cX-f9vsN@s7R9Lv6nB(^u(DP$$;oX$@8Jwx)Y3&G|xkL5tu!kW?QyWK^(cP`Dp z1=<Y}V=u4QhhW3g<7zB2R+)4Jv%%ZHAIq^~bi1j7KGXr!e$ z)x&7dJ_2<;cRo-ND z?$q26tL)sp&%KyB)C{C=Ls+|2V+lNJZCO1<;V6y>F?kr1!S zHIsnE^^e6@&PBjY9i9!;wMR=_2h_L*@2 zY$lw6p6<{=c5;kvhWwBp|H{o7XO{3zD>n7lMPu`*hJvoWUrM8GOcHS4(p1?yUQ~Mz zaasA^9hKsYd^tmfP(HqDA)T++Sl@z6RoqJ}VEL-A)@a@z$x|_P zmZib{mtW~3+i+Z;0xrIe?r2vS(0Ot3*?{{5Zdk;o7$Q+F8r@Uu=IfM^L048y7kliq zK4E(tjfa!gQ6-aCv+{dQi};xb>o@MAClD`9?D*x7dQOgkg$9+$1=^;pv~(Vy76*RL zz2EPXN#eTSr3kKWA4mdIcw4=NH$CaOJ#}*Yqr$3uCCY{1J3DUKBmc8h^|3?f@=ey$srMbq`sW)+S>?>(Z?2x@74FyxtHlerbLB4P z7?8Txv8j_&&88^v*U6!=pR&r+X({lm3J@}E=F^|;w$#GPxTXy%W7@< zYrIIICIO3KGt=>wk$Tj1g6h*bKr{1N!*>XFH)+sCU;J!TtjG{D=+oEs9?vB7=gV*Q z_Fq{4lExtxE*h3kttu*gC;2(RN!T-bsaE{SAxSgO46*y}cIHs05Kxe3x8LE`=`VEHxv=Og=*7Iab}QynaN@UO%`DStSC*xt;)bU| zLZrf-tje-td|3fhR!@31_=5VR*>VNZ`ykllQT%z=zlu|5tipyX5f9+Qhr6#P*!W&H z@1&1sHSu*Hv}yc25?$Gq6i)zr^mUB2Yp*)RcoN)w0RVz>_Ve)!2*lw=Ay6h5GaSonEf(bF zAK>Xruom-m_HuIx46qTy`@48}f^7@2Kwm#j0FqEzfk2+*Jat2iK=kn;jj*ty*f`Ku z#^$o#ZlM9TVEyk?OEDK8d_X`Mdla<{9 delta 4156 zcmaJ@2{@GN`~EBqLnOl3$ylOe3}O%=+aM$kDP&7iNR|-ICvhlCF&N6els06ojxE$= zWDpWXF@#8sZK&qVZ?u`p|NmasJ>T=(_x-%fH}A~#R3@OyQqX)DPyZl*-@FXtsj3A4 zwiCD&F;xKo8eo1f2mrVR*1!T>$%a+Qi)Y(HR8au1&=hCsvWWo*0~~dhE}Sf^5Tq&q?M}qI9p_xH5RwA`AvqSb9sFZl(s51(pbT&^ zz|FvB26!0YWnc>fe0533x0-je^x~iE@T-D94!|@Cz~IovhW1X-B**Z8bVot@@Q3#Z z8MurXI*MI@)%bKpY=&L_5ssJ#-6+93C1X+&>B;v_5ZVoB=X(l;a&D0Aj|P|SjnOID zWR`8Bmd9wm%ZBv%b>D&Ys@y$A14H=$V5$U0Bq;0SNn>c=8the;Z`BXNeT5)6|uZD1@k`b*2hDH6M6of@jjm} z0V@Z3Kg=P0CxKDn`>p~&$)kuKMIhL7vaCBwV?a_b6wE96H=Rd+QL*)4-Tl)ux^oaJ zRTBXHv5l{+oImK)y38tF-+D&L2*M*AWASETrTc6VPQ6w$-`y(_gT`Z7#;{M~|Dwo9 zdk23$pBwwSY~)#)!iqX1zp+F=Q>;__@pzVUf=Yj?$B@Hr`~SkNI<6Mz?Q@2Tf6$Lk z*t@Tczz#Z!?|hgYy-ATvNY?)mr777o__6NNgVvBZnXR@&Rnq2hW8neE=QB!MT|+t~ ziC9hVte{@wt4B?zbFX@3Pv%A4r1bW{!X+XQ^)V-82P%TcxVHU6%v|N73L*%fh-OF` zrx;6G=c2fi%Uxc137Z7ocV4USPCl!Ad-2ZxDit6IqKXkhYCSd4zer!|&gw@;GI=E}(EG=54j2ePWWjX5lCi8=P2u zsj@8}t(a-q7R)v>UhVT%zjss+-eZ48mnZH~ahvfDGll!uR3{xt_?>OHt+t=ho2r8B zcU)4O2U%BNL^QIzr)a`l`7N4j<4ie6ZtY$i8N-@dPhK##owRl;iF}E9hc0To@h~rX z=t!ysu3N)l_P`aLf-b|IB5_?GgGffi%B3M4yKE`N<`K)!-9}^~8>K@(&gs+v2T;LEu`OqtgQ(NNMpo?KhWZUEP(gQT z?f6xbh5b2YZ&5e%$_bwXb`71Q;ayE$CaDM~-s_SdE%EEDEV_N_LP5u^2F!swL9aZm z#BhAJyoM8m^WIdCqyD=BY1(hJyH4@!w1Y{Dr5w}Ush0Y_!!-P~NXrPH;LI@0nh+o- zIvNgUKQ5Va$YB#@1&c#lWqdB)C0$kR6!{cC5e?621CNv7yIq$!cS|CyL-Ty@pN)6G z&Fuv}IK2lS9ZLuqLkKUU@71WSQqR%%?-rd(X6F5VqQuHR0!{TLF)V&Vmv zm0vpNJ#%ha{p0K8&>CNnE^b_I0;qSRthh+rW@!MgXGjx>!M1m&`E6J0dz6W2OC1|T zy#!;C>b8#eS%oGNKtKmk4(AYG1Opd5mpPxn@50uBH7}XC3S>q39 zAh=&=*9wvA4K=FkFUSx%In``zt=puZSyRl9qi*bH@i{i7VCL2~oqM#f2Ba8J4nO7% zE12G;u_a_IVZt)xtebjK@qKQLD88YPoJ6WBeX_&k2`=`aRhy4+WI`0z$2h9n=&-7J zYuPClnOZ$TG5lFlV75tWjJ!a+PW9cbx)%i4`F07E-U^EvsUyE=maA4X-eTiwj$xb3 z^g1I&wT)ZenPKhWJ-z#|$Q)?oRlH?bmMv_w#n5<+EzO%XP~q$|^pm;E4{z(`+h~!l zY0I{!6<5O3uk5y?HS$?h%~M8znJZAZq*iTHoI}Y1QvY(%f_!W4_yMtoS#p7f+`sIO z9t~k7GUh>Im2oWx=N#+yDL`i_yX}`&h(a1^@Ap1*XuTwBZUjwx9-*qK^~m|_qBv)l$*$1y_r!N z`7)rsd_vPDwX_i5evdr+>3#V{KcUMnZ}RF%8PtUnc9YH`6_JK1*dJbu6z zO?(9d!b^{pBJMhT0oiTh1H(xg;dyKVXtvaUulz$8N!|gU(SJ}|kspmvbnmn-7irpO zI%9WpIKU{O?0J8vckpY7fqVy*7Fp|el_T6D9mk_2(m=T<-;yo5H%G)0@z=t#@>Ty$ ziYFrdsfsn%8vF^L=d&?VEYjSuJbRR6aCEqUR;vtm=-zRx_JoUy? zv^5gda`o(sxwxG zuGIM);iN!0g4QzD4|S`Ktdp#uFUs!(0I+keZS~vWJXGE{wX!nRSAK|z{h>GUb_uLjPp zjb%E$>+3-hH;}>aTq0uzv84?R->D@r0LcBnWjZ|mPWfCZVjg^dhM`ureMvOzf5v;oOPHKj8p-$Rg@N;hN9{*28alt0OJ zh5`XH*0VpO3;=*F{zijL`67&Q38I;Z-@@p%TgE~7js2MtIM#FkM*hkC5|GzsUmGgS z2Jokc{MR>`@d?{t4Z@G|H5W(pZ|7Ky{wuuI{|78ze=w-h@s*YFQq(t<;~MSTW@*>f7UOvGbL;dc3?gqmrPK2O#CP-e62x!c zefq#R0sK)l@Z074{W31}|8RbL_Fwunzoh)@g33S|vmp%tAPwt8!`g`R(vszofi&;{O3Rb`}T# From c82b6dae0b8e05744f9155b3a3ca7b0a78ee0e32 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 14:53:11 -0700 Subject: [PATCH 267/448] :arrow_up: autosave, text-buffer --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b0c5d1577..e3bcd004c 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.3.4", + "text-buffer": "13.3.5-0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -97,7 +97,7 @@ "autocomplete-plus": "2.35.11", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", - "autosave": "0.24.5", + "autosave": "0.24.6", "background-tips": "0.27.1", "bookmarks": "0.44.4", "bracket-matcher": "0.88.0", From 91559e885778516815289cac2225489a36ce5ccb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 15:05:12 -0700 Subject: [PATCH 268/448] :arrow_up: git-utils --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0fb37392c..e294f081c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "fs-plus": "^3.0.1", "fstream": "0.1.24", "fuzzaldrin": "^2.1", - "git-utils": "5.0.1-0", + "git-utils": "5.1.0", "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", From c7258c7c616faa2d97fdc180bd69a1b5c860e56b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 15:37:12 -0700 Subject: [PATCH 269/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3bcd004c..d5afd5ac1 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.3.5-0", + "text-buffer": "13.3.5", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 5ae9f094011c02281c36218aaa246a3dfdce8c8f Mon Sep 17 00:00:00 2001 From: Thomas Johansen Date: Thu, 28 Sep 2017 13:39:22 +0200 Subject: [PATCH 270/448] :arrow_up: python@2.7.14 --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index ecc17b8e9..9bbf49249 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -2.7.13 +2.7.14 From 596fe5fae3adc369eaad145ad274371a3d04e087 Mon Sep 17 00:00:00 2001 From: Thomas Johansen Date: Thu, 28 Sep 2017 13:53:56 +0200 Subject: [PATCH 271/448] Revert ":arrow_up: python@2.7.14" This reverts commit 5ae9f094011c02281c36218aaa246a3dfdce8c8f. --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index 9bbf49249..ecc17b8e9 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -2.7.14 +2.7.13 From 84a10bb014bd6ee8a5d5904895dea22f55219682 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 11:56:00 -0700 Subject: [PATCH 272/448] :arrow_up: symbols-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc8791493..2df7ee676 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "spell-check": "0.72.2", "status-bar": "1.8.13", "styleguide": "0.49.7", - "symbols-view": "0.118.0", + "symbols-view": "0.118.1", "tabs": "0.107.4", "timecop": "0.36.0", "tree-view": "0.218.0", From 596b6ed9ff0dea43441609fbf5c1999c9c732100 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 12:01:30 -0700 Subject: [PATCH 273/448] :arrow_up: markdown-preview --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2df7ee676..0a115377a 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.13", + "markdown-preview": "0.159.14", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From 7124b4d949d86f82f83c1a752b00e4ef755d2606 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 12:28:42 -0700 Subject: [PATCH 274/448] Don't pass addCursor options through to markBufferPosition Fixes #15646 --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index d85e36535..c9813e445 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2208,7 +2208,7 @@ class TextEditor extends Model # # Returns a {Cursor}. addCursorAtBufferPosition: (bufferPosition, options) -> - @selectionsMarkerLayer.markBufferPosition(bufferPosition, Object.assign({invalidate: 'never'}, options)) + @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor From 1798abf4c61089dad75648f68ca33e3c35f2d7c4 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Thu, 28 Sep 2017 22:01:25 +0200 Subject: [PATCH 275/448] :arrow_up: atom-keymap@8.2.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a115377a..2324f476f 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.5", + "atom-keymap": "8.2.6", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", From f9110f7708d019c666a327e6d5885d09ff159385 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 13:55:57 -0700 Subject: [PATCH 276/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a115377a..303a134b5 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.3.5", + "text-buffer": "13.4.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 5eecd8d7483673da0d0464a2a869c0b13888983f Mon Sep 17 00:00:00 2001 From: Hubot Date: Thu, 28 Sep 2017 16:32:44 -0500 Subject: [PATCH 277/448] 1.23.0-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 303a134b5..3da649c93 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.22.0-dev", + "version": "1.23.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { From a7db6ce7b1cfcfdaff5418f6b15458f842d16376 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 16:18:56 -0700 Subject: [PATCH 278/448] Convert package-manager-spec to JS --- spec/package-manager-spec.coffee | 1410 --------------------------- spec/package-manager-spec.js | 1547 ++++++++++++++++++++++++++++++ 2 files changed, 1547 insertions(+), 1410 deletions(-) delete mode 100644 spec/package-manager-spec.coffee create mode 100644 spec/package-manager-spec.js diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee deleted file mode 100644 index 9b7d46340..000000000 --- a/spec/package-manager-spec.coffee +++ /dev/null @@ -1,1410 +0,0 @@ -path = require 'path' -Package = require '../src/package' -PackageManager = require '../src/package-manager' -temp = require('temp').track() -fs = require 'fs-plus' -{Disposable} = require 'atom' -{buildKeydownEvent} = require '../src/keymap-extensions' -{mockLocalStorage} = require './spec-helper' -ModuleCache = require '../src/module-cache' - -describe "PackageManager", -> - createTestElement = (className) -> - element = document.createElement('div') - element.className = className - element - - beforeEach -> - spyOn(ModuleCache, 'add') - - afterEach -> - try - temp.cleanupSync() - - describe "initialize", -> - it "adds regular package path", -> - packageManger = new PackageManager({}) - configDirPath = path.join('~', 'someConfig') - packageManger.initialize({configDirPath}) - expect(packageManger.packageDirPaths.length).toBe 1 - expect(packageManger.packageDirPaths[0]).toBe path.join(configDirPath, 'packages') - - it "adds regular package path and dev package path in dev mode", -> - packageManger = new PackageManager({}) - configDirPath = path.join('~', 'someConfig') - packageManger.initialize({configDirPath, devMode: true}) - expect(packageManger.packageDirPaths.length).toBe 2 - expect(packageManger.packageDirPaths).toContain path.join(configDirPath, 'packages') - expect(packageManger.packageDirPaths).toContain path.join(configDirPath, 'dev', 'packages') - - describe "::getApmPath()", -> - it "returns the path to the apm command", -> - apmPath = path.join(process.resourcesPath, "app", "apm", "bin", "apm") - if process.platform is 'win32' - apmPath += ".cmd" - expect(atom.packages.getApmPath()).toBe apmPath - - describe "when the core.apmPath setting is set", -> - beforeEach -> - atom.config.set("core.apmPath", "/path/to/apm") - - it "returns the value of the core.apmPath config setting", -> - expect(atom.packages.getApmPath()).toBe "/path/to/apm" - - describe "::loadPackages()", -> - beforeEach -> - spyOn(atom.packages, 'loadAvailablePackage') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - it "sets hasLoadedInitialPackages", -> - expect(atom.packages.hasLoadedInitialPackages()).toBe false - atom.packages.loadPackages() - expect(atom.packages.hasLoadedInitialPackages()).toBe true - - describe "::loadPackage(name)", -> - beforeEach -> - atom.config.set("core.disabledPackages", []) - - it "returns the package", -> - pack = atom.packages.loadPackage("package-with-index") - expect(pack instanceof Package).toBe true - expect(pack.metadata.name).toBe "package-with-index" - - it "returns the package if it has an invalid keymap", -> - spyOn(atom, 'inSpecMode').andReturn(false) - pack = atom.packages.loadPackage("package-with-broken-keymap") - expect(pack instanceof Package).toBe true - expect(pack.metadata.name).toBe "package-with-broken-keymap" - - it "returns the package if it has an invalid stylesheet", -> - spyOn(atom, 'inSpecMode').andReturn(false) - pack = atom.packages.loadPackage("package-with-invalid-styles") - expect(pack instanceof Package).toBe true - expect(pack.metadata.name).toBe "package-with-invalid-styles" - expect(pack.stylesheets.length).toBe 0 - - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> pack.reloadStylesheets()).not.toThrow() - expect(addErrorHandler.callCount).toBe 2 - expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to reload the package-with-invalid-styles package stylesheets") - expect(addErrorHandler.argsForCall[1][0].options.packageName).toEqual "package-with-invalid-styles" - - it "returns null if the package has an invalid package.json", -> - spyOn(atom, 'inSpecMode').andReturn(false) - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(atom.packages.loadPackage("package-with-broken-package-json")).toBeNull() - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-broken-package-json" - - it "returns null if the package name or path starts with a dot", -> - expect(atom.packages.loadPackage("/Users/user/.atom/packages/.git")).toBeNull() - - it "normalizes short repository urls in package.json", -> - {metadata} = atom.packages.loadPackage("package-with-short-url-package-json") - expect(metadata.repository.type).toBe "git" - expect(metadata.repository.url).toBe "https://github.com/example/repo" - - {metadata} = atom.packages.loadPackage("package-with-invalid-url-package-json") - expect(metadata.repository.type).toBe "git" - expect(metadata.repository.url).toBe "foo" - - it "trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ", -> - {metadata} = atom.packages.loadPackage("package-with-prefixed-and-suffixed-repo-url") - expect(metadata.repository.type).toBe "git" - expect(metadata.repository.url).toBe "https://github.com/example/repo" - - it "returns null if the package is not found in any package directory", -> - spyOn(console, 'warn') - expect(atom.packages.loadPackage("this-package-cannot-be-found")).toBeNull() - expect(console.warn.callCount).toBe(1) - expect(console.warn.argsForCall[0][0]).toContain("Could not resolve") - - describe "when the package is deprecated", -> - it "returns null", -> - spyOn(console, 'warn') - expect(atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'packages', 'wordcount'))).toBeNull() - expect(atom.packages.isDeprecatedPackage('wordcount', '2.1.9')).toBe true - expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.0')).toBe true - expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.1')).toBe false - expect(atom.packages.getDeprecatedPackageMetadata('wordcount').version).toBe '<=2.2.0' - - it "invokes ::onDidLoadPackage listeners with the loaded package", -> - loadedPackage = null - atom.packages.onDidLoadPackage (pack) -> loadedPackage = pack - - atom.packages.loadPackage("package-with-main") - - expect(loadedPackage.name).toBe "package-with-main" - - it "registers any deserializers specified in the package's package.json", -> - pack = atom.packages.loadPackage("package-with-deserializers") - - state1 = {deserializer: 'Deserializer1', a: 'b'} - expect(atom.deserializers.deserialize(state1)).toEqual { - wasDeserializedBy: 'deserializeMethod1' - state: state1 - } - - state2 = {deserializer: 'Deserializer2', c: 'd'} - expect(atom.deserializers.deserialize(state2)).toEqual { - wasDeserializedBy: 'deserializeMethod2' - state: state2 - } - - it "early-activates any atom.directory-provider or atom.repository-provider services that the package provide", -> - jasmine.useRealClock() - - providers = [] - atom.packages.serviceHub.consume 'atom.directory-provider', '^0.1.0', (provider) -> - providers.push(provider) - - atom.packages.loadPackage('package-with-directory-provider') - expect(providers.map((p) -> p.name)).toEqual(['directory provider from package-with-directory-provider']) - - describe "when there are view providers specified in the package's package.json", -> - model1 = {worksWithViewProvider1: true} - model2 = {worksWithViewProvider2: true} - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackage('package-with-view-providers') - runs -> - atom.packages.unloadPackage('package-with-view-providers') - - it "does not load the view providers immediately", -> - pack = atom.packages.loadPackage("package-with-view-providers") - expect(pack.mainModule).toBeNull() - - expect(-> atom.views.getView(model1)).toThrow() - expect(-> atom.views.getView(model2)).toThrow() - - it "registers the view providers when the package is activated", -> - pack = atom.packages.loadPackage("package-with-view-providers") - - waitsForPromise -> - atom.packages.activatePackage("package-with-view-providers").then -> - element1 = atom.views.getView(model1) - expect(element1 instanceof HTMLDivElement).toBe true - expect(element1.dataset.createdBy).toBe 'view-provider-1' - - element2 = atom.views.getView(model2) - expect(element2 instanceof HTMLDivElement).toBe true - expect(element2.dataset.createdBy).toBe 'view-provider-2' - - it "registers the view providers when any of the package's deserializers are used", -> - pack = atom.packages.loadPackage("package-with-view-providers") - - spyOn(atom.views, 'addViewProvider').andCallThrough() - atom.deserializers.deserialize({ - deserializer: 'DeserializerFromPackageWithViewProviders', - a: 'b' - }) - expect(atom.views.addViewProvider.callCount).toBe 2 - - atom.deserializers.deserialize({ - deserializer: 'DeserializerFromPackageWithViewProviders', - a: 'b' - }) - expect(atom.views.addViewProvider.callCount).toBe 2 - - element1 = atom.views.getView(model1) - expect(element1 instanceof HTMLDivElement).toBe true - expect(element1.dataset.createdBy).toBe 'view-provider-1' - - element2 = atom.views.getView(model2) - expect(element2 instanceof HTMLDivElement).toBe true - expect(element2.dataset.createdBy).toBe 'view-provider-2' - - it "registers the config schema in the package's metadata, if present", -> - pack = atom.packages.loadPackage("package-with-json-config-schema") - expect(atom.config.getSchema('package-with-json-config-schema')).toEqual { - type: 'object' - properties: { - a: {type: 'number', default: 5} - b: {type: 'string', default: 'five'} - } - } - - expect(pack.mainModule).toBeNull() - - atom.packages.unloadPackage('package-with-json-config-schema') - atom.config.clear() - - pack = atom.packages.loadPackage("package-with-json-config-schema") - expect(atom.config.getSchema('package-with-json-config-schema')).toEqual { - type: 'object' - properties: { - a: {type: 'number', default: 5} - b: {type: 'string', default: 'five'} - } - } - - describe "when a package does not have deserializers, view providers or a config schema in its package.json", -> - beforeEach -> - mockLocalStorage() - - it "defers loading the package's main module if the package previously used no Atom APIs when its main module was required", -> - pack1 = atom.packages.loadPackage('package-with-main') - expect(pack1.mainModule).toBeDefined() - - atom.packages.unloadPackage('package-with-main') - - pack2 = atom.packages.loadPackage('package-with-main') - expect(pack2.mainModule).toBeNull() - - it "does not defer loading the package's main module if the package previously used Atom APIs when its main module was required", -> - pack1 = atom.packages.loadPackage('package-with-eval-time-api-calls') - expect(pack1.mainModule).toBeDefined() - - atom.packages.unloadPackage('package-with-eval-time-api-calls') - - pack2 = atom.packages.loadPackage('package-with-eval-time-api-calls') - expect(pack2.mainModule).not.toBeNull() - - describe "::loadAvailablePackage(availablePackage)", -> - describe "if the package was preloaded", -> - it "adds the package path to the module cache", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - metadata = atom.packages.loadPackageMetadata(availablePackage) - atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - atom.packages.loadAvailablePackage(availablePackage) - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true) - expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata) - - it "deactivates it if it had been disabled", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - metadata = atom.packages.loadPackageMetadata(availablePackage) - preloadedPackage = atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - - atom.packages.loadAvailablePackage(availablePackage, new Set([availablePackage.name])) - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - expect(preloadedPackage.keymapActivated).toBe(false) - expect(preloadedPackage.settingsActivated).toBe(false) - expect(preloadedPackage.menusActivated).toBe(false) - - it "deactivates it and reloads the new one if trying to load the same package outside of the bundle", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - metadata = atom.packages.loadPackageMetadata(availablePackage) - preloadedPackage = atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - - availablePackage.isBundled = false - atom.packages.loadAvailablePackage(availablePackage) - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true) - expect(preloadedPackage.keymapActivated).toBe(false) - expect(preloadedPackage.settingsActivated).toBe(false) - expect(preloadedPackage.menusActivated).toBe(false) - - describe "if the package was not preloaded", -> - it "adds the package path to the module cache", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - metadata = atom.packages.loadPackageMetadata(availablePackage) - atom.packages.loadAvailablePackage(availablePackage) - expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata) - - describe "preloading", -> - it "requires the main module, loads the config schema and activates keymaps, menus and settings without reactivating them during package activation", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - metadata = atom.packages.loadPackageMetadata(availablePackage) - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - atom.packages.packagesCache = {} - atom.packages.packagesCache[availablePackage.name] = { - main: path.join(availablePackage.path, metadata.main), - grammarPaths: [] - } - preloadedPackage = atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - expect(preloadedPackage.mainModule).toBeTruthy() - expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy() - - spyOn(atom.keymaps, 'add') - spyOn(atom.menu, 'add') - spyOn(atom.contextMenu, 'add') - spyOn(atom.config, 'setSchema') - - atom.packages.loadAvailablePackage(availablePackage) - expect(preloadedPackage.getMainModulePath()).toBe(path.join(availablePackage.path, metadata.main)) - - atom.packages.activatePackage(availablePackage.name) - expect(atom.keymaps.add).not.toHaveBeenCalled() - expect(atom.menu.add).not.toHaveBeenCalled() - expect(atom.contextMenu.add).not.toHaveBeenCalled() - expect(atom.config.setSchema).not.toHaveBeenCalled() - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - expect(preloadedPackage.mainModule).toBeTruthy() - expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy() - - it "deactivates disabled keymaps during package activation", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - metadata = atom.packages.loadPackageMetadata(availablePackage) - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - atom.packages.packagesCache = {} - atom.packages.packagesCache[availablePackage.name] = { - main: path.join(availablePackage.path, metadata.main), - grammarPaths: [] - } - preloadedPackage = atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - - atom.packages.loadAvailablePackage(availablePackage) - atom.config.set("core.packagesWithKeymapsDisabled", [availablePackage.name]) - atom.packages.activatePackage(availablePackage.name) - - expect(preloadedPackage.keymapActivated).toBe(false) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - - describe "::unloadPackage(name)", -> - describe "when the package is active", -> - it "throws an error", -> - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-main').then (p) -> pack = p - - runs -> - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - expect( -> atom.packages.unloadPackage(pack.name)).toThrow() - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - - describe "when the package is not loaded", -> - it "throws an error", -> - expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() - expect( -> atom.packages.unloadPackage('unloaded')).toThrow() - expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() - - describe "when the package is loaded", -> - it "no longers reports it as being loaded", -> - pack = atom.packages.loadPackage('package-with-main') - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - atom.packages.unloadPackage(pack.name) - expect(atom.packages.isPackageLoaded(pack.name)).toBeFalsy() - - it "invokes ::onDidUnloadPackage listeners with the unloaded package", -> - atom.packages.loadPackage('package-with-main') - unloadedPackage = null - atom.packages.onDidUnloadPackage (pack) -> unloadedPackage = pack - atom.packages.unloadPackage('package-with-main') - expect(unloadedPackage.name).toBe 'package-with-main' - - describe "::activatePackage(id)", -> - describe "when called multiple times", -> - it "it only calls activate on the package once", -> - spyOn(Package.prototype, 'activateNow').andCallThrough() - waitsForPromise -> - atom.packages.activatePackage('package-with-index') - waitsForPromise -> - atom.packages.activatePackage('package-with-index') - waitsForPromise -> - atom.packages.activatePackage('package-with-index') - - runs -> - expect(Package.prototype.activateNow.callCount).toBe 1 - - describe "when the package has a main module", -> - describe "when the metadata specifies a main module path˜", -> - it "requires the module at the specified path", -> - mainModule = require('./fixtures/packages/package-with-main/main-module') - spyOn(mainModule, 'activate') - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-main').then (p) -> pack = p - - runs -> - expect(mainModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe mainModule - - describe "when the metadata does not specify a main module", -> - it "requires index.coffee", -> - indexModule = require('./fixtures/packages/package-with-index/index') - spyOn(indexModule, 'activate') - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-index').then (p) -> pack = p - - runs -> - expect(indexModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe indexModule - - it "assigns config schema, including defaults when package contains a schema", -> - expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined() - - waitsForPromise -> - atom.packages.activatePackage('package-with-config-schema') - - runs -> - expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 1 - expect(atom.config.get('package-with-config-schema.numbers.two')).toBe 2 - - expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe false - expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe true - expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 10 - - describe "when the package metadata includes `activationCommands`", -> - [mainModule, promise, workspaceCommandListener, registration] = [] - - beforeEach -> - jasmine.attachToDOM(atom.workspace.getElement()) - mainModule = require './fixtures/packages/package-with-activation-commands/index' - mainModule.activationCommandCallCount = 0 - spyOn(mainModule, 'activate').andCallThrough() - spyOn(Package.prototype, 'requireMainModule').andCallThrough() - - workspaceCommandListener = jasmine.createSpy('workspaceCommandListener') - registration = atom.commands.add '.workspace', 'activation-command', workspaceCommandListener - - promise = atom.packages.activatePackage('package-with-activation-commands') - - afterEach -> - registration?.dispose() - mainModule = null - - it "defers requiring/activating the main module until an activation event bubbles to the root view", -> - expect(Package.prototype.requireMainModule.callCount).toBe 0 - - atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', bubbles: true)) - - waitsForPromise -> - promise - - runs -> - expect(Package.prototype.requireMainModule.callCount).toBe 1 - - it "triggers the activation event on all handlers registered during activation", -> - waitsForPromise -> - atom.workspace.open() - - runs -> - editorElement = atom.workspace.getActiveTextEditor().getElement() - editorCommandListener = jasmine.createSpy("editorCommandListener") - atom.commands.add 'atom-text-editor', 'activation-command', editorCommandListener - atom.commands.dispatch(editorElement, 'activation-command') - expect(mainModule.activate.callCount).toBe 1 - expect(mainModule.activationCommandCallCount).toBe 1 - expect(editorCommandListener.callCount).toBe 1 - expect(workspaceCommandListener.callCount).toBe 1 - atom.commands.dispatch(editorElement, 'activation-command') - expect(mainModule.activationCommandCallCount).toBe 2 - expect(editorCommandListener.callCount).toBe 2 - expect(workspaceCommandListener.callCount).toBe 2 - expect(mainModule.activate.callCount).toBe 1 - - it "activates the package immediately when the events are empty", -> - mainModule = require './fixtures/packages/package-with-empty-activation-commands/index' - spyOn(mainModule, 'activate').andCallThrough() - - waitsForPromise -> - atom.packages.activatePackage('package-with-empty-activation-commands') - - runs -> - expect(mainModule.activate.callCount).toBe 1 - - it "adds a notification when the activation commands are invalid", -> - spyOn(atom, 'inSpecMode').andReturn(false) - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> atom.packages.activatePackage('package-with-invalid-activation-commands')).not.toThrow() - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to activate the package-with-invalid-activation-commands package") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-activation-commands" - - it "adds a notification when the context menu is invalid", -> - spyOn(atom, 'inSpecMode').andReturn(false) - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> atom.packages.activatePackage('package-with-invalid-context-menu')).not.toThrow() - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to activate the package-with-invalid-context-menu package") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-context-menu" - - it "adds a notification when the grammar is invalid", -> - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - - expect(-> atom.packages.activatePackage('package-with-invalid-grammar')).not.toThrow() - - waitsFor -> - addErrorHandler.callCount > 0 - - runs -> - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load a package-with-invalid-grammar package grammar") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-grammar" - - it "adds a notification when the settings are invalid", -> - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - - expect(-> atom.packages.activatePackage('package-with-invalid-settings')).not.toThrow() - - waitsFor -> - addErrorHandler.callCount > 0 - - runs -> - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-invalid-settings package settings") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-settings" - - describe "when the package metadata includes `activationHooks`", -> - [mainModule, promise] = [] - - beforeEach -> - mainModule = require './fixtures/packages/package-with-activation-hooks/index' - spyOn(mainModule, 'activate').andCallThrough() - spyOn(Package.prototype, 'requireMainModule').andCallThrough() - - it "defers requiring/activating the main module until an triggering of an activation hook occurs", -> - promise = atom.packages.activatePackage('package-with-activation-hooks') - expect(Package.prototype.requireMainModule.callCount).toBe 0 - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - - waitsForPromise -> - promise - - runs -> - expect(Package.prototype.requireMainModule.callCount).toBe 1 - - it "does not double register activation hooks when deactivating and reactivating", -> - promise = atom.packages.activatePackage('package-with-activation-hooks') - expect(mainModule.activate.callCount).toBe 0 - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - - waitsForPromise -> - promise - - runs -> - expect(mainModule.activate.callCount).toBe 1 - - waitsForPromise -> - atom.packages.deactivatePackage('package-with-activation-hooks') - - runs -> - promise = atom.packages.activatePackage('package-with-activation-hooks') - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - - waitsForPromise -> - promise - - runs -> - expect(mainModule.activate.callCount).toBe 2 - - it "activates the package immediately when activationHooks is empty", -> - mainModule = require './fixtures/packages/package-with-empty-activation-hooks/index' - spyOn(mainModule, 'activate').andCallThrough() - - runs -> - expect(Package.prototype.requireMainModule.callCount).toBe 0 - - waitsForPromise -> - atom.packages.activatePackage('package-with-empty-activation-hooks') - - runs -> - expect(mainModule.activate.callCount).toBe 1 - expect(Package.prototype.requireMainModule.callCount).toBe 1 - - it "activates the package immediately if the activation hook had already been triggered", -> - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - expect(Package.prototype.requireMainModule.callCount).toBe 0 - - waitsForPromise -> - atom.packages.activatePackage('package-with-activation-hooks') - - runs -> - expect(Package.prototype.requireMainModule.callCount).toBe 1 - - describe "when the package has no main module", -> - it "does not throw an exception", -> - spyOn(console, "error") - spyOn(console, "warn").andCallThrough() - expect(-> atom.packages.activatePackage('package-without-module')).not.toThrow() - expect(console.error).not.toHaveBeenCalled() - expect(console.warn).not.toHaveBeenCalled() - - describe "when the package does not export an activate function", -> - it "activates the package and does not throw an exception or log a warning", -> - spyOn(console, "warn") - expect(-> atom.packages.activatePackage('package-with-no-activate')).not.toThrow() - - waitsFor -> - atom.packages.isPackageActive('package-with-no-activate') - - runs -> - expect(console.warn).not.toHaveBeenCalled() - - it "passes the activate method the package's previously serialized state if it exists", -> - pack = null - waitsForPromise -> - atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p - runs -> - expect(pack.mainModule.someNumber).not.toBe 77 - pack.mainModule.someNumber = 77 - atom.packages.serializePackage("package-with-serialization") - waitsForPromise -> - atom.packages.deactivatePackage("package-with-serialization") - runs -> - spyOn(pack.mainModule, 'activate').andCallThrough() - waitsForPromise -> - atom.packages.activatePackage("package-with-serialization") - runs -> - expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) - - it "invokes ::onDidActivatePackage listeners with the activated package", -> - activatedPackage = null - atom.packages.onDidActivatePackage (pack) -> - activatedPackage = pack - - atom.packages.activatePackage('package-with-main') - - waitsFor -> activatedPackage? - runs -> expect(activatedPackage.name).toBe 'package-with-main' - - describe "when the package's main module throws an error on load", -> - it "adds a notification instead of throwing an exception", -> - spyOn(atom, 'inSpecMode').andReturn(false) - atom.config.set("core.disabledPackages", []) - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).not.toThrow() - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-that-throws-an-exception package") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-that-throws-an-exception" - - it "re-throws the exception in test mode", -> - atom.config.set("core.disabledPackages", []) - addErrorHandler = jasmine.createSpy() - expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).toThrow("This package throws an exception") - - describe "when the package is not found", -> - it "rejects the promise", -> - atom.config.set("core.disabledPackages", []) - - onSuccess = jasmine.createSpy('onSuccess') - onFailure = jasmine.createSpy('onFailure') - spyOn(console, 'warn') - - atom.packages.activatePackage("this-doesnt-exist").then(onSuccess, onFailure) - - waitsFor "promise to be rejected", -> - onFailure.callCount > 0 - - runs -> - expect(console.warn.callCount).toBe 1 - expect(onFailure.mostRecentCall.args[0] instanceof Error).toBe true - expect(onFailure.mostRecentCall.args[0].message).toContain "Failed to load package 'this-doesnt-exist'" - - describe "keymap loading", -> - describe "when the metadata does not contain a 'keymaps' manifest", -> - it "loads all the .cson/.json files in the keymaps directory", -> - element1 = createTestElement('test-1') - element2 = createTestElement('test-2') - element3 = createTestElement('test-3') - - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element2)).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element3)).toHaveLength 0 - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps") - - runs -> - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe "test-1" - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element2)[0].command).toBe "test-2" - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element3)).toHaveLength 0 - - describe "when the metadata contains a 'keymaps' manifest", -> - it "loads only the keymaps specified by the manifest, in the specified order", -> - element1 = createTestElement('test-1') - element3 = createTestElement('test-3') - - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps-manifest") - - runs -> - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe 'keymap-1' - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-n', target: element1)[0].command).toBe 'keymap-2' - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-y', target: element3)).toHaveLength 0 - - describe "when the keymap file is empty", -> - it "does not throw an error on activation", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-empty-keymap") - - runs -> - expect(atom.packages.isPackageActive("package-with-empty-keymap")).toBe true - - describe "when the package's keymaps have been disabled", -> - it "does not add the keymaps", -> - element1 = createTestElement('test-1') - - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - - atom.config.set("core.packagesWithKeymapsDisabled", ["package-with-keymaps-manifest"]) - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps-manifest") - - runs -> - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - - describe "when setting core.packagesWithKeymapsDisabled", -> - it "ignores package names in the array that aren't loaded", -> - atom.packages.observePackagesWithKeymapsDisabled() - - expect(-> atom.config.set("core.packagesWithKeymapsDisabled", ["package-does-not-exist"])).not.toThrow() - expect(-> atom.config.set("core.packagesWithKeymapsDisabled", [])).not.toThrow() - - describe "when the package's keymaps are disabled and re-enabled after it is activated", -> - it "removes and re-adds the keymaps", -> - element1 = createTestElement('test-1') - atom.packages.observePackagesWithKeymapsDisabled() - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps-manifest") - - runs -> - atom.config.set("core.packagesWithKeymapsDisabled", ['package-with-keymaps-manifest']) - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - - atom.config.set("core.packagesWithKeymapsDisabled", []) - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe 'keymap-1' - - describe "when the package is de-activated and re-activated", -> - [element, events, userKeymapPath] = [] - - beforeEach -> - userKeymapPath = path.join(temp.mkdirSync(), "user-keymaps.cson") - spyOn(atom.keymaps, "getUserKeymapPath").andReturn(userKeymapPath) - - element = createTestElement('test-1') - jasmine.attachToDOM(element) - - events = [] - element.addEventListener 'user-command', (e) -> events.push(e) - element.addEventListener 'test-1', (e) -> events.push(e) - - afterEach -> - element.remove() - - # Avoid leaking user keymap subscription - atom.keymaps.watchSubscriptions[userKeymapPath].dispose() - delete atom.keymaps.watchSubscriptions[userKeymapPath] - - temp.cleanupSync() - - it "doesn't override user-defined keymaps", -> - fs.writeFileSync userKeymapPath, """ - ".test-1": - "ctrl-z": "user-command" - """ - atom.keymaps.loadUserKeymap() - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps") - - runs -> - atom.keymaps.handleKeyboardEvent(buildKeydownEvent("z", ctrl: true, target: element)) - - expect(events.length).toBe(1) - expect(events[0].type).toBe("user-command") - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-keymaps") - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps") - - runs -> - atom.keymaps.handleKeyboardEvent(buildKeydownEvent("z", ctrl: true, target: element)) - - expect(events.length).toBe(2) - expect(events[1].type).toBe("user-command") - - describe "menu loading", -> - beforeEach -> - atom.contextMenu.definitions = [] - atom.menu.template = [] - - describe "when the metadata does not contain a 'menus' manifest", -> - it "loads all the .cson/.json files in the menus directory", -> - element = createTestElement('test-1') - - expect(atom.contextMenu.templateForElement(element)).toEqual [] - - waitsForPromise -> - atom.packages.activatePackage("package-with-menus") - - runs -> - expect(atom.menu.template.length).toBe 2 - expect(atom.menu.template[0].label).toBe "Second to Last" - expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1" - expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2" - expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 3" - - describe "when the metadata contains a 'menus' manifest", -> - it "loads only the menus specified by the manifest, in the specified order", -> - element = createTestElement('test-1') - - expect(atom.contextMenu.templateForElement(element)).toEqual [] - - waitsForPromise -> - atom.packages.activatePackage("package-with-menus-manifest") - - runs -> - expect(atom.menu.template[0].label).toBe "Second to Last" - expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 2" - expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 1" - expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() - - describe "when the menu file is empty", -> - it "does not throw an error on activation", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-empty-menu") - - runs -> - expect(atom.packages.isPackageActive("package-with-empty-menu")).toBe true - - describe "stylesheet loading", -> - describe "when the metadata contains a 'styleSheets' manifest", -> - it "loads style sheets from the styles directory as specified by the manifest", -> - one = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/1.css") - two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less") - three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css") - - expect(atom.themes.stylesheetElementForId(one)).toBeNull() - expect(atom.themes.stylesheetElementForId(two)).toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - - waitsForPromise -> - atom.packages.activatePackage("package-with-style-sheets-manifest") - - runs -> - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - - expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe '1px' - - describe "when the metadata does not contain a 'styleSheets' manifest", -> - it "loads all style sheets from the styles directory", -> - one = require.resolve("./fixtures/packages/package-with-styles/styles/1.css") - two = require.resolve("./fixtures/packages/package-with-styles/styles/2.less") - three = require.resolve("./fixtures/packages/package-with-styles/styles/3.test-context.css") - four = require.resolve("./fixtures/packages/package-with-styles/styles/4.css") - - expect(atom.themes.stylesheetElementForId(one)).toBeNull() - expect(atom.themes.stylesheetElementForId(two)).toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - expect(atom.themes.stylesheetElementForId(four)).toBeNull() - - waitsForPromise -> - atom.packages.activatePackage("package-with-styles") - - runs -> - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(four)).not.toBeNull() - expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe '3px' - - it "assigns the stylesheet's context based on the filename", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-styles") - - runs -> - count = 0 - - for styleElement in atom.styles.getStyleElements() - if styleElement.sourcePath.match /1.css/ - expect(styleElement.context).toBe undefined - count++ - - if styleElement.sourcePath.match /2.less/ - expect(styleElement.context).toBe undefined - count++ - - if styleElement.sourcePath.match /3.test-context.css/ - expect(styleElement.context).toBe 'test-context' - count++ - - if styleElement.sourcePath.match /4.css/ - expect(styleElement.context).toBe undefined - count++ - - expect(count).toBe 4 - - describe "grammar loading", -> - it "loads the package's grammars", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-grammars') - - runs -> - expect(atom.grammars.selectGrammar('a.alot').name).toBe 'Alot' - expect(atom.grammars.selectGrammar('a.alittle').name).toBe 'Alittle' - - describe "scoped-property loading", -> - it "loads the scoped properties", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-settings") - - runs -> - expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBe '^a' - - describe "service registration", -> - it "registers the package's provided and consumed services", -> - consumerModule = require "./fixtures/packages/package-with-consumed-services" - firstServiceV3Disposed = false - firstServiceV4Disposed = false - secondServiceDisposed = false - spyOn(consumerModule, 'consumeFirstServiceV3').andReturn(new Disposable -> firstServiceV3Disposed = true) - spyOn(consumerModule, 'consumeFirstServiceV4').andReturn(new Disposable -> firstServiceV4Disposed = true) - spyOn(consumerModule, 'consumeSecondService').andReturn(new Disposable -> secondServiceDisposed = true) - - waitsForPromise -> - atom.packages.activatePackage("package-with-consumed-services") - - waitsForPromise -> - atom.packages.activatePackage("package-with-provided-services") - - runs -> - expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) - expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') - expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') - expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') - - consumerModule.consumeFirstServiceV3.reset() - consumerModule.consumeFirstServiceV4.reset() - consumerModule.consumeSecondService.reset() - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-provided-services") - - runs -> - expect(firstServiceV3Disposed).toBe true - expect(firstServiceV4Disposed).toBe true - expect(secondServiceDisposed).toBe true - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-consumed-services") - - waitsForPromise -> - atom.packages.activatePackage("package-with-provided-services") - - runs -> - expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled() - expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() - expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() - - it "ignores provided and consumed services that do not exist", -> - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - - waitsForPromise -> - atom.packages.activatePackage("package-with-missing-consumed-services") - - waitsForPromise -> - atom.packages.activatePackage("package-with-missing-provided-services") - - runs -> - expect(atom.packages.isPackageActive("package-with-missing-consumed-services")).toBe true - expect(atom.packages.isPackageActive("package-with-missing-provided-services")).toBe true - expect(addErrorHandler.callCount).toBe 0 - - describe "::serialize", -> - it "does not serialize packages that threw an error during activation", -> - spyOn(atom, 'inSpecMode').andReturn(false) - spyOn(console, 'warn') - badPack = null - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p - - runs -> - spyOn(badPack.mainModule, 'serialize').andCallThrough() - - atom.packages.serialize() - expect(badPack.mainModule.serialize).not.toHaveBeenCalled() - - it "absorbs exceptions that are thrown by the package module's serialize method", -> - spyOn(console, 'error') - - waitsForPromise -> - atom.packages.activatePackage('package-with-serialize-error') - - waitsForPromise -> - atom.packages.activatePackage('package-with-serialization') - - runs -> - atom.packages.serialize() - expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() - expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1 - expect(console.error).toHaveBeenCalled() - - describe "::deactivatePackages()", -> - it "deactivates all packages but does not serialize them", -> - [pack1, pack2] = [] - - waitsForPromise -> - atom.packages.activatePackage("package-with-deactivate").then (p) -> pack1 = p - atom.packages.activatePackage("package-with-serialization").then (p) -> pack2 = p - - runs -> - spyOn(pack1.mainModule, 'deactivate') - spyOn(pack2.mainModule, 'serialize') - - waitsForPromise -> - atom.packages.deactivatePackages() - - runs -> - expect(pack1.mainModule.deactivate).toHaveBeenCalled() - expect(pack2.mainModule.serialize).not.toHaveBeenCalled() - - describe "::deactivatePackage(id)", -> - afterEach -> - atom.packages.unloadPackages() - - it "calls `deactivate` on the package's main module if activate was successful", -> - spyOn(atom, 'inSpecMode').andReturn(false) - pack = null - waitsForPromise -> - atom.packages.activatePackage("package-with-deactivate").then (p) -> pack = p - - runs -> - expect(atom.packages.isPackageActive("package-with-deactivate")).toBeTruthy() - spyOn(pack.mainModule, 'deactivate').andCallThrough() - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-deactivate") - - runs -> - expect(pack.mainModule.deactivate).toHaveBeenCalled() - expect(atom.packages.isPackageActive("package-with-module")).toBeFalsy() - - spyOn(console, 'warn') - - badPack = null - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p - - runs -> - expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeTruthy() - spyOn(badPack.mainModule, 'deactivate').andCallThrough() - - waitsForPromise -> - atom.packages.deactivatePackage("package-that-throws-on-activate") - - runs -> - expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() - expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy() - - it "absorbs exceptions that are thrown by the package module's deactivate method", -> - spyOn(console, 'error') - thrownError = null - - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-deactivate") - - waitsForPromise -> - try - atom.packages.deactivatePackage("package-that-throws-on-deactivate") - catch error - thrownError = error - - runs -> - expect(thrownError).toBeNull() - expect(console.error).toHaveBeenCalled() - - it "removes the package's grammars", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-grammars') - - waitsForPromise -> - atom.packages.deactivatePackage('package-with-grammars') - - runs -> - expect(atom.grammars.selectGrammar('a.alot').name).toBe 'Null Grammar' - expect(atom.grammars.selectGrammar('a.alittle').name).toBe 'Null Grammar' - - it "removes the package's keymaps", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-keymaps') - - waitsForPromise -> - atom.packages.deactivatePackage('package-with-keymaps') - - runs -> - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: createTestElement('test-1'))).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: createTestElement('test-2'))).toHaveLength 0 - - it "removes the package's stylesheets", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-styles') - - waitsForPromise -> - atom.packages.deactivatePackage('package-with-styles') - - runs -> - one = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/1.css") - two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less") - three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css") - expect(atom.themes.stylesheetElementForId(one)).not.toExist() - expect(atom.themes.stylesheetElementForId(two)).not.toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() - - it "removes the package's scoped-properties", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-settings") - - runs -> - expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBe '^a' - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-settings") - - runs -> - expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBeUndefined() - - it "invokes ::onDidDeactivatePackage listeners with the deactivated package", -> - deactivatedPackage = null - - waitsForPromise -> - atom.packages.activatePackage("package-with-main") - - runs -> - atom.packages.onDidDeactivatePackage (pack) -> deactivatedPackage = pack - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-main") - - runs -> - expect(deactivatedPackage.name).toBe "package-with-main" - - describe "::activate()", -> - beforeEach -> - spyOn(atom, 'inSpecMode').andReturn(false) - jasmine.snapshotDeprecations() - spyOn(console, 'warn') - atom.packages.loadPackages() - - loadedPackages = atom.packages.getLoadedPackages() - expect(loadedPackages.length).toBeGreaterThan 0 - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - jasmine.restoreDeprecationsSnapshot() - - it "sets hasActivatedInitialPackages", -> - spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) - spyOn(atom.packages, 'activatePackages') - expect(atom.packages.hasActivatedInitialPackages()).toBe false - waitsForPromise -> atom.packages.activate() - runs -> expect(atom.packages.hasActivatedInitialPackages()).toBe true - - it "activates all the packages, and none of the themes", -> - packageActivator = spyOn(atom.packages, 'activatePackages') - themeActivator = spyOn(atom.themes, 'activatePackages') - - atom.packages.activate() - - expect(packageActivator).toHaveBeenCalled() - expect(themeActivator).toHaveBeenCalled() - - packages = packageActivator.mostRecentCall.args[0] - expect(['atom', 'textmate']).toContain(pack.getType()) for pack in packages - - themes = themeActivator.mostRecentCall.args[0] - expect(['theme']).toContain(theme.getType()) for theme in themes - - it "calls callbacks registered with ::onDidActivateInitialPackages", -> - package1 = atom.packages.loadPackage('package-with-main') - package2 = atom.packages.loadPackage('package-with-index') - package3 = atom.packages.loadPackage('package-with-activation-commands') - spyOn(atom.packages, 'getLoadedPackages').andReturn([package1, package2, package3]) - spyOn(atom.themes, 'activatePackages') - activateSpy = jasmine.createSpy('activateSpy') - atom.packages.onDidActivateInitialPackages(activateSpy) - - atom.packages.activate() - waitsFor -> activateSpy.callCount > 0 - runs -> - jasmine.unspy(atom.packages, 'getLoadedPackages') - expect(package1 in atom.packages.getActivePackages()).toBe true - expect(package2 in atom.packages.getActivePackages()).toBe true - expect(package3 in atom.packages.getActivePackages()).toBe false - - describe "::enablePackage(id) and ::disablePackage(id)", -> - describe "with packages", -> - it "enables a disabled package", -> - packageName = 'package-with-main' - atom.config.pushAtKeyPath('core.disabledPackages', packageName) - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).toContain packageName - - pack = atom.packages.enablePackage(packageName) - loadedPackages = atom.packages.getLoadedPackages() - activatedPackages = null - waitsFor -> - activatedPackages = atom.packages.getActivePackages() - activatedPackages.length > 0 - - runs -> - expect(loadedPackages).toContain(pack) - expect(activatedPackages).toContain(pack) - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - it "disables an enabled package", -> - packageName = 'package-with-main' - pack = null - activatedPackages = null - - waitsForPromise -> - atom.packages.activatePackage(packageName) - - runs -> - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - pack = atom.packages.disablePackage(packageName) - - waitsFor -> - activatedPackages = atom.packages.getActivePackages() - activatedPackages.length is 0 - - runs -> - expect(activatedPackages).not.toContain(pack) - expect(atom.config.get('core.disabledPackages')).toContain packageName - - it "returns null if the package cannot be loaded", -> - spyOn(console, 'warn') - expect(atom.packages.enablePackage("this-doesnt-exist")).toBeNull() - expect(console.warn.callCount).toBe 1 - - it "does not disable an already disabled package", -> - packageName = 'package-with-main' - atom.config.pushAtKeyPath('core.disabledPackages', packageName) - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).toContain packageName - - atom.packages.disablePackage(packageName) - packagesDisabled = atom.config.get('core.disabledPackages').filter((pack) -> pack is packageName) - expect(packagesDisabled.length).toEqual 1 - - describe "with themes", -> - didChangeActiveThemesHandler = null - - beforeEach -> - waitsForPromise -> - atom.themes.activateThemes() - - afterEach -> - waitsForPromise -> - atom.themes.deactivateThemes() - - it "enables and disables a theme", -> - packageName = 'theme-with-package-file' - - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - # enabling of theme - pack = atom.packages.enablePackage(packageName) - - waitsFor 'theme to enable', 500, -> - pack in atom.packages.getActivePackages() - - runs -> - expect(atom.config.get('core.themes')).toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - didChangeActiveThemesHandler = jasmine.createSpy('didChangeActiveThemesHandler') - didChangeActiveThemesHandler.reset() - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler - - pack = atom.packages.disablePackage(packageName) - - waitsFor 'did-change-active-themes event to fire', 500, -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - expect(atom.packages.getActivePackages()).not.toContain pack - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js new file mode 100644 index 000000000..3cd25383f --- /dev/null +++ b/spec/package-manager-spec.js @@ -0,0 +1,1547 @@ +const path = require('path') +const Package = require('../src/package') +const PackageManager = require('../src/package-manager') +const temp = require('temp').track() +const fs = require('fs-plus') +const {Disposable} = require('atom') +const {buildKeydownEvent} = require('../src/keymap-extensions') +const {mockLocalStorage} = require('./spec-helper') +const ModuleCache = require('../src/module-cache') + +describe('PackageManager', () => { + function createTestElement (className) { + const element = document.createElement('div') + element.className = className + return element + } + + beforeEach(() => { + spyOn(ModuleCache, 'add') + }) + + describe('initialize', () => { + it('adds regular package path', () => { + const packageManger = new PackageManager({}) + const configDirPath = path.join('~', 'someConfig') + packageManger.initialize({configDirPath}) + expect(packageManger.packageDirPaths.length).toBe(1) + expect(packageManger.packageDirPaths[0]).toBe(path.join(configDirPath, 'packages')) + }) + + it('adds regular package path and dev package path in dev mode', () => { + const packageManger = new PackageManager({}) + const configDirPath = path.join('~', 'someConfig') + packageManger.initialize({configDirPath, devMode: true}) + expect(packageManger.packageDirPaths.length).toBe(2) + expect(packageManger.packageDirPaths).toContain(path.join(configDirPath, 'packages')) + expect(packageManger.packageDirPaths).toContain(path.join(configDirPath, 'dev', 'packages')) + }) + }) + + describe('::getApmPath()', () => { + it('returns the path to the apm command', () => { + let apmPath = path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm') + if (process.platform === 'win32') { + apmPath += '.cmd' + } + expect(atom.packages.getApmPath()).toBe(apmPath) + }) + + describe('when the core.apmPath setting is set', () => { + beforeEach(() => atom.config.set('core.apmPath', '/path/to/apm')) + + it('returns the value of the core.apmPath config setting', () => expect(atom.packages.getApmPath()).toBe('/path/to/apm')) + }) + }) + + describe('::loadPackages()', () => { + beforeEach(() => spyOn(atom.packages, 'loadAvailablePackage')) + + afterEach(() => { + waitsForPromise(() => atom.packages.deactivatePackages()) + runs(() => atom.packages.unloadPackages()) + }) + + it('sets hasLoadedInitialPackages', () => { + expect(atom.packages.hasLoadedInitialPackages()).toBe(false) + atom.packages.loadPackages() + expect(atom.packages.hasLoadedInitialPackages()).toBe(true) + }) + }) + + describe('::loadPackage(name)', () => { + beforeEach(() => atom.config.set('core.disabledPackages', [])) + + it('returns the package', () => { + const pack = atom.packages.loadPackage('package-with-index') + expect(pack instanceof Package).toBe(true) + expect(pack.metadata.name).toBe('package-with-index') + }) + + it('returns the package if it has an invalid keymap', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const pack = atom.packages.loadPackage('package-with-broken-keymap') + expect(pack instanceof Package).toBe(true) + expect(pack.metadata.name).toBe('package-with-broken-keymap') + }) + + it('returns the package if it has an invalid stylesheet', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const pack = atom.packages.loadPackage('package-with-invalid-styles') + expect(pack instanceof Package).toBe(true) + expect(pack.metadata.name).toBe('package-with-invalid-styles') + expect(pack.stylesheets.length).toBe(0) + + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => pack.reloadStylesheets()).not.toThrow() + expect(addErrorHandler.callCount).toBe(2) + expect(addErrorHandler.argsForCall[1][0].message).toContain('Failed to reload the package-with-invalid-styles package stylesheets') + expect(addErrorHandler.argsForCall[1][0].options.packageName).toEqual('package-with-invalid-styles') + }) + + it('returns null if the package has an invalid package.json', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(atom.packages.loadPackage('package-with-broken-package-json')).toBeNull() + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-with-broken-package-json package') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-broken-package-json') + }) + + it('returns null if the package name or path starts with a dot', () => expect(atom.packages.loadPackage('/Users/user/.atom/packages/.git')).toBeNull()) + + it('normalizes short repository urls in package.json', () => { + let {metadata} = atom.packages.loadPackage('package-with-short-url-package-json') + expect(metadata.repository.type).toBe('git') + expect(metadata.repository.url).toBe('https://github.com/example/repo'); + + ({metadata} = atom.packages.loadPackage('package-with-invalid-url-package-json')) + expect(metadata.repository.type).toBe('git') + expect(metadata.repository.url).toBe('foo') + }) + + it('trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ', () => { + const {metadata} = atom.packages.loadPackage('package-with-prefixed-and-suffixed-repo-url') + expect(metadata.repository.type).toBe('git') + expect(metadata.repository.url).toBe('https://github.com/example/repo') + }) + + it('returns null if the package is not found in any package directory', () => { + spyOn(console, 'warn') + expect(atom.packages.loadPackage('this-package-cannot-be-found')).toBeNull() + expect(console.warn.callCount).toBe(1) + expect(console.warn.argsForCall[0][0]).toContain('Could not resolve') + }) + + describe('when the package is deprecated', () => { + it('returns null', () => { + spyOn(console, 'warn') + expect(atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'packages', 'wordcount'))).toBeNull() + expect(atom.packages.isDeprecatedPackage('wordcount', '2.1.9')).toBe(true) + expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.0')).toBe(true) + expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.1')).toBe(false) + expect(atom.packages.getDeprecatedPackageMetadata('wordcount').version).toBe('<=2.2.0') + }) + }) + + it('invokes ::onDidLoadPackage listeners with the loaded package', () => { + let loadedPackage = null + + atom.packages.onDidLoadPackage(pack => { + loadedPackage = pack + }) + + atom.packages.loadPackage('package-with-main') + + expect(loadedPackage.name).toBe('package-with-main') + }) + + it("registers any deserializers specified in the package's package.json", () => { + atom.packages.loadPackage('package-with-deserializers') + + const state1 = {deserializer: 'Deserializer1', a: 'b'} + expect(atom.deserializers.deserialize(state1)).toEqual({ + wasDeserializedBy: 'deserializeMethod1', + state: state1 + }) + + const state2 = {deserializer: 'Deserializer2', c: 'd'} + expect(atom.deserializers.deserialize(state2)).toEqual({ + wasDeserializedBy: 'deserializeMethod2', + state: state2 + }) + }) + + it('early-activates any atom.directory-provider or atom.repository-provider services that the package provide', () => { + jasmine.useRealClock() + + const providers = [] + atom.packages.serviceHub.consume('atom.directory-provider', '^0.1.0', provider => providers.push(provider)) + + atom.packages.loadPackage('package-with-directory-provider') + expect(providers.map(p => p.name)).toEqual(['directory provider from package-with-directory-provider']) + }) + + describe("when there are view providers specified in the package's package.json", () => { + const model1 = {worksWithViewProvider1: true} + const model2 = {worksWithViewProvider2: true} + + afterEach(() => { + waitsForPromise(() => atom.packages.deactivatePackage('package-with-view-providers')) + runs(() => atom.packages.unloadPackage('package-with-view-providers')) + }) + + it('does not load the view providers immediately', () => { + const pack = atom.packages.loadPackage('package-with-view-providers') + expect(pack.mainModule).toBeNull() + + expect(() => atom.views.getView(model1)).toThrow() + expect(() => atom.views.getView(model2)).toThrow() + }) + + it('registers the view providers when the package is activated', () => { + atom.packages.loadPackage('package-with-view-providers') + + waitsForPromise(() => + atom.packages.activatePackage('package-with-view-providers').then(() => { + const element1 = atom.views.getView(model1) + expect(element1 instanceof HTMLDivElement).toBe(true) + expect(element1.dataset.createdBy).toBe('view-provider-1') + + const element2 = atom.views.getView(model2) + expect(element2 instanceof HTMLDivElement).toBe(true) + expect(element2.dataset.createdBy).toBe('view-provider-2') + }) + ) + }) + + it("registers the view providers when any of the package's deserializers are used", () => { + atom.packages.loadPackage('package-with-view-providers') + + spyOn(atom.views, 'addViewProvider').andCallThrough() + atom.deserializers.deserialize({ + deserializer: 'DeserializerFromPackageWithViewProviders', + a: 'b' + }) + expect(atom.views.addViewProvider.callCount).toBe(2) + + atom.deserializers.deserialize({ + deserializer: 'DeserializerFromPackageWithViewProviders', + a: 'b' + }) + expect(atom.views.addViewProvider.callCount).toBe(2) + + const element1 = atom.views.getView(model1) + expect(element1 instanceof HTMLDivElement).toBe(true) + expect(element1.dataset.createdBy).toBe('view-provider-1') + + const element2 = atom.views.getView(model2) + expect(element2 instanceof HTMLDivElement).toBe(true) + expect(element2.dataset.createdBy).toBe('view-provider-2') + }) + }) + + it("registers the config schema in the package's metadata, if present", () => { + let pack = atom.packages.loadPackage('package-with-json-config-schema') + expect(atom.config.getSchema('package-with-json-config-schema')).toEqual({ + type: 'object', + properties: { + a: {type: 'number', default: 5}, + b: {type: 'string', default: 'five'} + } + }) + + expect(pack.mainModule).toBeNull() + + atom.packages.unloadPackage('package-with-json-config-schema') + atom.config.clear() + + pack = atom.packages.loadPackage('package-with-json-config-schema') + expect(atom.config.getSchema('package-with-json-config-schema')).toEqual({ + type: 'object', + properties: { + a: {type: 'number', default: 5}, + b: {type: 'string', default: 'five'} + } + }) + }) + + describe('when a package does not have deserializers, view providers or a config schema in its package.json', () => { + beforeEach(() => mockLocalStorage()) + + it("defers loading the package's main module if the package previously used no Atom APIs when its main module was required", () => { + const pack1 = atom.packages.loadPackage('package-with-main') + expect(pack1.mainModule).toBeDefined() + + atom.packages.unloadPackage('package-with-main') + + const pack2 = atom.packages.loadPackage('package-with-main') + expect(pack2.mainModule).toBeNull() + }) + + it("does not defer loading the package's main module if the package previously used Atom APIs when its main module was required", () => { + const pack1 = atom.packages.loadPackage('package-with-eval-time-api-calls') + expect(pack1.mainModule).toBeDefined() + + atom.packages.unloadPackage('package-with-eval-time-api-calls') + + const pack2 = atom.packages.loadPackage('package-with-eval-time-api-calls') + expect(pack2.mainModule).not.toBeNull() + }) + }) + }) + + describe('::loadAvailablePackage(availablePackage)', () => { + describe('if the package was preloaded', () => { + it('adds the package path to the module cache', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + const metadata = atom.packages.loadPackageMetadata(availablePackage) + atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + atom.packages.loadAvailablePackage(availablePackage) + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true) + expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata) + }) + + it('deactivates it if it had been disabled', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + const metadata = atom.packages.loadPackageMetadata(availablePackage) + const preloadedPackage = atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + + atom.packages.loadAvailablePackage(availablePackage, new Set([availablePackage.name])) + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + expect(preloadedPackage.keymapActivated).toBe(false) + expect(preloadedPackage.settingsActivated).toBe(false) + expect(preloadedPackage.menusActivated).toBe(false) + }) + + it('deactivates it and reloads the new one if trying to load the same package outside of the bundle', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + const metadata = atom.packages.loadPackageMetadata(availablePackage) + const preloadedPackage = atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + + availablePackage.isBundled = false + atom.packages.loadAvailablePackage(availablePackage) + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true) + expect(preloadedPackage.keymapActivated).toBe(false) + expect(preloadedPackage.settingsActivated).toBe(false) + expect(preloadedPackage.menusActivated).toBe(false) + }) + }) + + describe('if the package was not preloaded', () => { + it('adds the package path to the module cache', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + const metadata = atom.packages.loadPackageMetadata(availablePackage) + atom.packages.loadAvailablePackage(availablePackage) + expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata) + }) + }) + }) + + describe('preloading', () => { + it('requires the main module, loads the config schema and activates keymaps, menus and settings without reactivating them during package activation', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + const metadata = atom.packages.loadPackageMetadata(availablePackage) + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + atom.packages.packagesCache = {} + atom.packages.packagesCache[availablePackage.name] = { + main: path.join(availablePackage.path, metadata.main), + grammarPaths: [] + } + const preloadedPackage = atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + expect(preloadedPackage.mainModule).toBeTruthy() + expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy() + + spyOn(atom.keymaps, 'add') + spyOn(atom.menu, 'add') + spyOn(atom.contextMenu, 'add') + spyOn(atom.config, 'setSchema') + + atom.packages.loadAvailablePackage(availablePackage) + expect(preloadedPackage.getMainModulePath()).toBe(path.join(availablePackage.path, metadata.main)) + + atom.packages.activatePackage(availablePackage.name) + expect(atom.keymaps.add).not.toHaveBeenCalled() + expect(atom.menu.add).not.toHaveBeenCalled() + expect(atom.contextMenu.add).not.toHaveBeenCalled() + expect(atom.config.setSchema).not.toHaveBeenCalled() + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + expect(preloadedPackage.mainModule).toBeTruthy() + expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy() + }) + + it('deactivates disabled keymaps during package activation', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + const metadata = atom.packages.loadPackageMetadata(availablePackage) + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + atom.packages.packagesCache = {} + atom.packages.packagesCache[availablePackage.name] = { + main: path.join(availablePackage.path, metadata.main), + grammarPaths: [] + } + const preloadedPackage = atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + + atom.packages.loadAvailablePackage(availablePackage) + atom.config.set('core.packagesWithKeymapsDisabled', [availablePackage.name]) + atom.packages.activatePackage(availablePackage.name) + + expect(preloadedPackage.keymapActivated).toBe(false) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + }) + }) + + describe('::unloadPackage(name)', () => { + describe('when the package is active', () => { + it('throws an error', () => { + let pack + + waitsForPromise(() => atom.packages.activatePackage('package-with-main').then(p => { + pack = p + })) + + runs(() => { + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() + expect(() => atom.packages.unloadPackage(pack.name)).toThrow() + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() + }) + }) + }) + + describe('when the package is not loaded', () => { + it('throws an error', () => { + expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() + expect(() => atom.packages.unloadPackage('unloaded')).toThrow() + expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() + }) + }) + + describe('when the package is loaded', () => { + it('no longers reports it as being loaded', () => { + const pack = atom.packages.loadPackage('package-with-main') + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + atom.packages.unloadPackage(pack.name) + expect(atom.packages.isPackageLoaded(pack.name)).toBeFalsy() + }) + }) + + it('invokes ::onDidUnloadPackage listeners with the unloaded package', () => { + atom.packages.loadPackage('package-with-main') + let unloadedPackage + atom.packages.onDidUnloadPackage(pack => { + unloadedPackage = pack + }) + atom.packages.unloadPackage('package-with-main') + expect(unloadedPackage.name).toBe('package-with-main') + }) + }) + + describe('::activatePackage(id)', () => { + describe('when called multiple times', () => { + it('it only calls activate on the package once', () => { + spyOn(Package.prototype, 'activateNow').andCallThrough() + waitsForPromise(() => atom.packages.activatePackage('package-with-index')) + waitsForPromise(() => atom.packages.activatePackage('package-with-index')) + waitsForPromise(() => atom.packages.activatePackage('package-with-index')) + + runs(() => expect(Package.prototype.activateNow.callCount).toBe(1)) + }) + }) + + describe('when the package has a main module', () => { + describe('when the metadata specifies a main module path˜', () => { + it('requires the module at the specified path', () => { + const mainModule = require('./fixtures/packages/package-with-main/main-module') + spyOn(mainModule, 'activate') + + let pack + waitsForPromise(() => atom.packages.activatePackage('package-with-main').then(p => { + pack = p + })) + + runs(() => { + expect(mainModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe(mainModule) + }) + }) + }) + + describe('when the metadata does not specify a main module', () => { + it('requires index.coffee', () => { + const indexModule = require('./fixtures/packages/package-with-index/index') + spyOn(indexModule, 'activate') + let pack + waitsForPromise(() => atom.packages.activatePackage('package-with-index').then(p => { + pack = p + })) + + runs(() => { + expect(indexModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe(indexModule) + }) + }) + }) + + it('assigns config schema, including defaults when package contains a schema', () => { + expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined() + + waitsForPromise(() => atom.packages.activatePackage('package-with-config-schema')) + + runs(() => { + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(1) + expect(atom.config.get('package-with-config-schema.numbers.two')).toBe(2) + + expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe(false) + expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe(true) + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(10) + }) + }) + + describe('when the package metadata includes `activationCommands`', () => { + let mainModule, promise, workspaceCommandListener, registration + + beforeEach(() => { + jasmine.attachToDOM(atom.workspace.getElement()) + mainModule = require('./fixtures/packages/package-with-activation-commands/index') + mainModule.activationCommandCallCount = 0 + spyOn(mainModule, 'activate').andCallThrough() + spyOn(Package.prototype, 'requireMainModule').andCallThrough() + + workspaceCommandListener = jasmine.createSpy('workspaceCommandListener') + registration = atom.commands.add('.workspace', 'activation-command', workspaceCommandListener) + + promise = atom.packages.activatePackage('package-with-activation-commands') + }) + + afterEach(() => { + if (registration) { + registration.dispose() + } + mainModule = null + }) + + it('defers requiring/activating the main module until an activation event bubbles to the root view', () => { + expect(Package.prototype.requireMainModule.callCount).toBe(0) + + atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', {bubbles: true})) + + waitsForPromise(() => promise) + + runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + }) + + it('triggers the activation event on all handlers registered during activation', () => { + waitsForPromise(() => atom.workspace.open()) + + runs(() => { + const editorElement = atom.workspace.getActiveTextEditor().getElement() + const editorCommandListener = jasmine.createSpy('editorCommandListener') + atom.commands.add('atom-text-editor', 'activation-command', editorCommandListener) + atom.commands.dispatch(editorElement, 'activation-command') + expect(mainModule.activate.callCount).toBe(1) + expect(mainModule.activationCommandCallCount).toBe(1) + expect(editorCommandListener.callCount).toBe(1) + expect(workspaceCommandListener.callCount).toBe(1) + atom.commands.dispatch(editorElement, 'activation-command') + expect(mainModule.activationCommandCallCount).toBe(2) + expect(editorCommandListener.callCount).toBe(2) + expect(workspaceCommandListener.callCount).toBe(2) + expect(mainModule.activate.callCount).toBe(1) + }) + }) + + it('activates the package immediately when the events are empty', () => { + mainModule = require('./fixtures/packages/package-with-empty-activation-commands/index') + spyOn(mainModule, 'activate').andCallThrough() + + waitsForPromise(() => atom.packages.activatePackage('package-with-empty-activation-commands')) + + runs(() => expect(mainModule.activate.callCount).toBe(1)) + }) + + it('adds a notification when the activation commands are invalid', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => atom.packages.activatePackage('package-with-invalid-activation-commands')).not.toThrow() + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to activate the package-with-invalid-activation-commands package') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-activation-commands') + }) + + it('adds a notification when the context menu is invalid', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => atom.packages.activatePackage('package-with-invalid-context-menu')).not.toThrow() + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to activate the package-with-invalid-context-menu package') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-context-menu') + }) + + it('adds a notification when the grammar is invalid', () => { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + + expect(() => atom.packages.activatePackage('package-with-invalid-grammar')).not.toThrow() + + waitsFor(() => addErrorHandler.callCount > 0) + + runs(() => { + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load a package-with-invalid-grammar package grammar') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-grammar') + }) + }) + + it('adds a notification when the settings are invalid', () => { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + + expect(() => atom.packages.activatePackage('package-with-invalid-settings')).not.toThrow() + + waitsFor(() => addErrorHandler.callCount > 0) + + runs(() => { + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-with-invalid-settings package settings') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-settings') + }) + }) + }) + }) + + describe('when the package metadata includes `activationHooks`', () => { + let mainModule, promise + + beforeEach(() => { + mainModule = require('./fixtures/packages/package-with-activation-hooks/index') + spyOn(mainModule, 'activate').andCallThrough() + spyOn(Package.prototype, 'requireMainModule').andCallThrough() + }) + + it('defers requiring/activating the main module until an triggering of an activation hook occurs', () => { + promise = atom.packages.activatePackage('package-with-activation-hooks') + expect(Package.prototype.requireMainModule.callCount).toBe(0) + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + + waitsForPromise(() => promise) + + runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + }) + + it('does not double register activation hooks when deactivating and reactivating', () => { + promise = atom.packages.activatePackage('package-with-activation-hooks') + expect(mainModule.activate.callCount).toBe(0) + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + + waitsForPromise(() => promise) + + runs(() => expect(mainModule.activate.callCount).toBe(1)) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-activation-hooks')) + + runs(() => { + promise = atom.packages.activatePackage('package-with-activation-hooks') + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + }) + + waitsForPromise(() => promise) + + runs(() => expect(mainModule.activate.callCount).toBe(2)) + }) + + it('activates the package immediately when activationHooks is empty', () => { + mainModule = require('./fixtures/packages/package-with-empty-activation-hooks/index') + spyOn(mainModule, 'activate').andCallThrough() + + runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(0)) + + waitsForPromise(() => atom.packages.activatePackage('package-with-empty-activation-hooks')) + + runs(() => { + expect(mainModule.activate.callCount).toBe(1) + expect(Package.prototype.requireMainModule.callCount).toBe(1) + }) + }) + + it('activates the package immediately if the activation hook had already been triggered', () => { + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + expect(Package.prototype.requireMainModule.callCount).toBe(0) + + waitsForPromise(() => atom.packages.activatePackage('package-with-activation-hooks')) + + runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + }) + }) + + describe('when the package has no main module', () => { + it('does not throw an exception', () => { + spyOn(console, 'error') + spyOn(console, 'warn').andCallThrough() + expect(() => atom.packages.activatePackage('package-without-module')).not.toThrow() + expect(console.error).not.toHaveBeenCalled() + expect(console.warn).not.toHaveBeenCalled() + }) + }) + + describe('when the package does not export an activate function', () => { + it('activates the package and does not throw an exception or log a warning', () => { + spyOn(console, 'warn') + expect(() => atom.packages.activatePackage('package-with-no-activate')).not.toThrow() + + waitsFor(() => atom.packages.isPackageActive('package-with-no-activate')) + + runs(() => expect(console.warn).not.toHaveBeenCalled()) + }) + }) + + it("passes the activate method the package's previously serialized state if it exists", () => { + let pack + waitsForPromise(() => atom.packages.activatePackage('package-with-serialization').then(p => { + pack = p + })) + runs(() => { + expect(pack.mainModule.someNumber).not.toBe(77) + pack.mainModule.someNumber = 77 + atom.packages.serializePackage('package-with-serialization') + }) + waitsForPromise(() => atom.packages.deactivatePackage('package-with-serialization')) + runs(() => spyOn(pack.mainModule, 'activate').andCallThrough()) + waitsForPromise(() => atom.packages.activatePackage('package-with-serialization')) + runs(() => expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})) + }) + + it('invokes ::onDidActivatePackage listeners with the activated package', () => { + let activatedPackage + atom.packages.onDidActivatePackage(pack => { + activatedPackage = pack + }) + + atom.packages.activatePackage('package-with-main') + + waitsFor(() => activatedPackage) + runs(() => expect(activatedPackage.name).toBe('package-with-main')) + }) + + describe("when the package's main module throws an error on load", () => { + it('adds a notification instead of throwing an exception', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + atom.config.set('core.disabledPackages', []) + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => atom.packages.activatePackage('package-that-throws-an-exception')).not.toThrow() + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-that-throws-an-exception package') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-that-throws-an-exception') + }) + + it('re-throws the exception in test mode', () => { + atom.config.set('core.disabledPackages', []) + expect(() => atom.packages.activatePackage('package-that-throws-an-exception')).toThrow('This package throws an exception') + }) + }) + + describe('when the package is not found', () => { + it('rejects the promise', () => { + atom.config.set('core.disabledPackages', []) + + const onSuccess = jasmine.createSpy('onSuccess') + const onFailure = jasmine.createSpy('onFailure') + spyOn(console, 'warn') + + atom.packages.activatePackage('this-doesnt-exist').then(onSuccess, onFailure) + + waitsFor('promise to be rejected', () => onFailure.callCount > 0) + + runs(() => { + expect(console.warn.callCount).toBe(1) + expect(onFailure.mostRecentCall.args[0] instanceof Error).toBe(true) + expect(onFailure.mostRecentCall.args[0].message).toContain("Failed to load package 'this-doesnt-exist'") + }) + }) + }) + + describe('keymap loading', () => { + describe("when the metadata does not contain a 'keymaps' manifest", () => { + it('loads all the .cson/.json files in the keymaps directory', () => { + const element1 = createTestElement('test-1') + const element2 = createTestElement('test-2') + const element3 = createTestElement('test-3') + + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})).toHaveLength(0) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + + runs(() => { + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('test-1') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})[0].command).toBe('test-2') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) + }) + }) + }) + + describe("when the metadata contains a 'keymaps' manifest", () => { + it('loads only the keymaps specified by the manifest, in the specified order', () => { + const element1 = createTestElement('test-1') + const element3 = createTestElement('test-3') + + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) + + runs(() => { + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-n', target: element1})[0].command).toBe('keymap-2') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-y', target: element3})).toHaveLength(0) + }) + }) + }) + + describe('when the keymap file is empty', () => { + it('does not throw an error on activation', () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-empty-keymap')) + + runs(() => expect(atom.packages.isPackageActive('package-with-empty-keymap')).toBe(true)) + }) + }) + + describe("when the package's keymaps have been disabled", () => { + it('does not add the keymaps', () => { + const element1 = createTestElement('test-1') + + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + + atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) + + runs(() => expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)) + }) + }) + + describe('when setting core.packagesWithKeymapsDisabled', () => { + it("ignores package names in the array that aren't loaded", () => { + atom.packages.observePackagesWithKeymapsDisabled() + + expect(() => atom.config.set('core.packagesWithKeymapsDisabled', ['package-does-not-exist'])).not.toThrow() + expect(() => atom.config.set('core.packagesWithKeymapsDisabled', [])).not.toThrow() + }) + }) + + describe("when the package's keymaps are disabled and re-enabled after it is activated", () => { + it('removes and re-adds the keymaps', () => { + const element1 = createTestElement('test-1') + atom.packages.observePackagesWithKeymapsDisabled() + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) + + runs(() => { + atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + + atom.config.set('core.packagesWithKeymapsDisabled', []) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') + }) + }) + }) + + describe('when the package is de-activated and re-activated', () => { + let element, events, userKeymapPath + + beforeEach(() => { + userKeymapPath = path.join(temp.mkdirSync(), 'user-keymaps.cson') + spyOn(atom.keymaps, 'getUserKeymapPath').andReturn(userKeymapPath) + + element = createTestElement('test-1') + jasmine.attachToDOM(element) + + events = [] + element.addEventListener('user-command', e => events.push(e)) + element.addEventListener('test-1', e => events.push(e)) + }) + + afterEach(() => { + element.remove() + + // Avoid leaking user keymap subscription + atom.keymaps.watchSubscriptions[userKeymapPath].dispose() + delete atom.keymaps.watchSubscriptions[userKeymapPath] + + temp.cleanupSync() + }) + + it("doesn't override user-defined keymaps", () => { + fs.writeFileSync(userKeymapPath, `".test-1": {"ctrl-z": "user-command"}`) + atom.keymaps.loadUserKeymap() + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + + runs(() => { + atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) + + expect(events.length).toBe(1) + expect(events[0].type).toBe('user-command') + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-keymaps')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + + runs(() => { + atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) + expect(events.length).toBe(2) + expect(events[1].type).toBe('user-command') + }) + }) + }) + }) + + describe('menu loading', () => { + beforeEach(() => { + atom.contextMenu.definitions = [] + atom.menu.template = [] + }) + + describe("when the metadata does not contain a 'menus' manifest", () => { + it('loads all the .cson/.json files in the menus directory', () => { + const element = createTestElement('test-1') + + expect(atom.contextMenu.templateForElement(element)).toEqual([]) + + waitsForPromise(() => atom.packages.activatePackage('package-with-menus')) + + runs(() => { + expect(atom.menu.template.length).toBe(2) + expect(atom.menu.template[0].label).toBe('Second to Last') + expect(atom.menu.template[1].label).toBe('Last') + expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 1') + expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 2') + expect(atom.contextMenu.templateForElement(element)[2].label).toBe('Menu item 3') + }) + }) + }) + + describe("when the metadata contains a 'menus' manifest", () => { + it('loads only the menus specified by the manifest, in the specified order', () => { + const element = createTestElement('test-1') + + expect(atom.contextMenu.templateForElement(element)).toEqual([]) + + waitsForPromise(() => atom.packages.activatePackage('package-with-menus-manifest')) + + runs(() => { + expect(atom.menu.template[0].label).toBe('Second to Last') + expect(atom.menu.template[1].label).toBe('Last') + expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 2') + expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 1') + expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() + }) + }) + }) + + describe('when the menu file is empty', () => { + it('does not throw an error on activation', () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-empty-menu')) + runs(() => expect(atom.packages.isPackageActive('package-with-empty-menu')).toBe(true)) + }) + }) + }) + + describe('stylesheet loading', () => { + describe("when the metadata contains a 'styleSheets' manifest", () => { + it('loads style sheets from the styles directory as specified by the manifest', () => { + const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') + const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') + const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') + + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + + waitsForPromise(() => atom.packages.activatePackage('package-with-style-sheets-manifest')) + + runs(() => { + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + + expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('1px') + }) + }) + }) + + describe("when the metadata does not contain a 'styleSheets' manifest", () => { + it('loads all style sheets from the styles directory', () => { + const one = require.resolve('./fixtures/packages/package-with-styles/styles/1.css') + const two = require.resolve('./fixtures/packages/package-with-styles/styles/2.less') + const three = require.resolve('./fixtures/packages/package-with-styles/styles/3.test-context.css') + const four = require.resolve('./fixtures/packages/package-with-styles/styles/4.css') + + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + expect(atom.themes.stylesheetElementForId(four)).toBeNull() + + waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + + runs(() => { + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(four)).not.toBeNull() + expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('3px') + }) + }) + }) + + it("assigns the stylesheet's context based on the filename", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + + runs(() => { + let count = 0 + + for (let styleElement of atom.styles.getStyleElements()) { + if (styleElement.sourcePath.match(/1.css/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + + if (styleElement.sourcePath.match(/2.less/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + + if (styleElement.sourcePath.match(/3.test-context.css/)) { + expect(styleElement.context).toBe('test-context') + count++ + } + + if (styleElement.sourcePath.match(/4.css/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + } + + expect(count).toBe(4) + }) + }) + }) + + describe('grammar loading', () => { + it("loads the package's grammars", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-grammars')) + + runs(() => { + expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot') + expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle') + }) + }) + }) + + describe('scoped-property loading', () => { + it('loads the scoped properties', () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-settings')) + + runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')) + }) + }) + + describe('service registration', () => { + it("registers the package's provided and consumed services", () => { + const consumerModule = require('./fixtures/packages/package-with-consumed-services') + let firstServiceV3Disposed = false + let firstServiceV4Disposed = false + let secondServiceDisposed = false + spyOn(consumerModule, 'consumeFirstServiceV3').andReturn(new Disposable(() => { firstServiceV3Disposed = true })) + spyOn(consumerModule, 'consumeFirstServiceV4').andReturn(new Disposable(() => { firstServiceV4Disposed = true })) + spyOn(consumerModule, 'consumeSecondService').andReturn(new Disposable(() => { secondServiceDisposed = true })) + + waitsForPromise(() => atom.packages.activatePackage('package-with-consumed-services')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-provided-services')) + + runs(() => { + expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) + expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') + expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') + expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') + + consumerModule.consumeFirstServiceV3.reset() + consumerModule.consumeFirstServiceV4.reset() + consumerModule.consumeSecondService.reset() + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-provided-services')) + + runs(() => { + expect(firstServiceV3Disposed).toBe(true) + expect(firstServiceV4Disposed).toBe(true) + expect(secondServiceDisposed).toBe(true) + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-consumed-services')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-provided-services')) + + runs(() => { + expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled() + expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() + expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() + }) + }) + + it('ignores provided and consumed services that do not exist', () => { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + + waitsForPromise(() => atom.packages.activatePackage('package-with-missing-consumed-services')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-missing-provided-services')) + + runs(() => { + expect(atom.packages.isPackageActive('package-with-missing-consumed-services')).toBe(true) + expect(atom.packages.isPackageActive('package-with-missing-provided-services')).toBe(true) + expect(addErrorHandler.callCount).toBe(0) + }) + }) + }) + }) + + describe('::serialize', () => { + it('does not serialize packages that threw an error during activation', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + spyOn(console, 'warn') + + let badPack + waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-activate').then(p => { + badPack = p + })) + + runs(() => { + spyOn(badPack.mainModule, 'serialize').andCallThrough() + + atom.packages.serialize() + expect(badPack.mainModule.serialize).not.toHaveBeenCalled() + }) + }) + + it("absorbs exceptions that are thrown by the package module's serialize method", () => { + spyOn(console, 'error') + + waitsForPromise(() => atom.packages.activatePackage('package-with-serialize-error')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-serialization')) + + runs(() => { + atom.packages.serialize() + expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() + expect(atom.packages.packageStates['package-with-serialization']).toEqual({someNumber: 1}) + expect(console.error).toHaveBeenCalled() + }) + }) + }) + + describe('::deactivatePackages()', () => { + it('deactivates all packages but does not serialize them', () => { + let pack1, pack2 + + waitsForPromise(() => { + atom.packages.activatePackage('package-with-deactivate').then(p => { + pack1 = p + }) + return atom.packages.activatePackage('package-with-serialization').then(p => { + pack2 = p + }) + }) + + runs(() => { + spyOn(pack1.mainModule, 'deactivate') + spyOn(pack2.mainModule, 'serialize') + }) + + waitsForPromise(() => atom.packages.deactivatePackages()) + + runs(() => { + expect(pack1.mainModule.deactivate).toHaveBeenCalled() + expect(pack2.mainModule.serialize).not.toHaveBeenCalled() + }) + }) + }) + + describe('::deactivatePackage(id)', () => { + afterEach(() => atom.packages.unloadPackages()) + + it("calls `deactivate` on the package's main module if activate was successful", () => { + spyOn(atom, 'inSpecMode').andReturn(false) + + let pack + waitsForPromise(() => atom.packages.activatePackage('package-with-deactivate').then(p => { + pack = p + })) + + runs(() => { + expect(atom.packages.isPackageActive('package-with-deactivate')).toBeTruthy() + spyOn(pack.mainModule, 'deactivate').andCallThrough() + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-deactivate')) + + runs(() => { + expect(pack.mainModule.deactivate).toHaveBeenCalled() + expect(atom.packages.isPackageActive('package-with-module')).toBeFalsy() + + spyOn(console, 'warn') + }) + + let badPack = null + waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-activate').then(p => { + badPack = p + })) + + runs(() => { + expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeTruthy() + spyOn(badPack.mainModule, 'deactivate').andCallThrough() + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-that-throws-on-activate')) + + runs(() => { + expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() + expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeFalsy() + }) + }) + + it("absorbs exceptions that are thrown by the package module's deactivate method", () => { + spyOn(console, 'error') + let thrownError = null + + waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-deactivate')) + + waitsForPromise(() => { + try { + return atom.packages.deactivatePackage('package-that-throws-on-deactivate') + } catch (error) { + thrownError = error + } + }) + + runs(() => { + expect(thrownError).toBeNull() + expect(console.error).toHaveBeenCalled() + }) + }) + + it("removes the package's grammars", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-grammars')) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-grammars')) + + runs(() => { + expect(atom.grammars.selectGrammar('a.alot').name).toBe('Null Grammar') + expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Null Grammar') + }) + }) + + it("removes the package's keymaps", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-keymaps')) + + runs(() => { + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-1')})).toHaveLength(0) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-2')})).toHaveLength(0) + }) + }) + + it("removes the package's stylesheets", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-styles')) + + runs(() => { + const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') + const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') + const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') + expect(atom.themes.stylesheetElementForId(one)).not.toExist() + expect(atom.themes.stylesheetElementForId(two)).not.toExist() + expect(atom.themes.stylesheetElementForId(three)).not.toExist() + }) + }) + + it("removes the package's scoped-properties", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-settings')) + + runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-settings')) + + runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBeUndefined()) + }) + + it('invokes ::onDidDeactivatePackage listeners with the deactivated package', () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-main')) + + let deactivatedPackage + runs(() => { + atom.packages.onDidDeactivatePackage(pack => { + deactivatedPackage = pack + }) + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-main')) + + runs(() => expect(deactivatedPackage.name).toBe('package-with-main')) + }) + }) + + describe('::activate()', () => { + beforeEach(() => { + spyOn(atom, 'inSpecMode').andReturn(false) + jasmine.snapshotDeprecations() + spyOn(console, 'warn') + atom.packages.loadPackages() + + const loadedPackages = atom.packages.getLoadedPackages() + expect(loadedPackages.length).toBeGreaterThan(0) + }) + + afterEach(() => { + waitsForPromise(() => atom.packages.deactivatePackages()) + runs(() => { + atom.packages.unloadPackages() + jasmine.restoreDeprecationsSnapshot() + }) + }) + + it('sets hasActivatedInitialPackages', () => { + spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) + spyOn(atom.packages, 'activatePackages') + expect(atom.packages.hasActivatedInitialPackages()).toBe(false) + waitsForPromise(() => atom.packages.activate()) + runs(() => expect(atom.packages.hasActivatedInitialPackages()).toBe(true)) + }) + + it('activates all the packages, and none of the themes', () => { + const packageActivator = spyOn(atom.packages, 'activatePackages') + const themeActivator = spyOn(atom.themes, 'activatePackages') + + atom.packages.activate() + + expect(packageActivator).toHaveBeenCalled() + expect(themeActivator).toHaveBeenCalled() + + const packages = packageActivator.mostRecentCall.args[0] + for (let pack of packages) { expect(['atom', 'textmate']).toContain(pack.getType()) } + + const themes = themeActivator.mostRecentCall.args[0] + themes.map((theme) => expect(['theme']).toContain(theme.getType())) + }) + + it('calls callbacks registered with ::onDidActivateInitialPackages', () => { + const package1 = atom.packages.loadPackage('package-with-main') + const package2 = atom.packages.loadPackage('package-with-index') + const package3 = atom.packages.loadPackage('package-with-activation-commands') + spyOn(atom.packages, 'getLoadedPackages').andReturn([package1, package2, package3]) + spyOn(atom.themes, 'activatePackages') + const activateSpy = jasmine.createSpy('activateSpy') + atom.packages.onDidActivateInitialPackages(activateSpy) + + atom.packages.activate() + waitsFor(() => activateSpy.callCount > 0) + runs(() => { + let needle, needle1, needle2 + jasmine.unspy(atom.packages, 'getLoadedPackages') + expect(atom.packages.getActivePackages().includes(package1)).toBe(true) + expect(atom.packages.getActivePackages().includes(package2)).toBe(true) + expect(atom.packages.getActivePackages().includes(package3)).toBe(false) + }) + }) + }) + + describe('::enablePackage(id) and ::disablePackage(id)', () => { + describe('with packages', () => { + it('enables a disabled package', () => { + const packageName = 'package-with-main' + atom.config.pushAtKeyPath('core.disabledPackages', packageName) + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).toContain(packageName) + + const pack = atom.packages.enablePackage(packageName) + const loadedPackages = atom.packages.getLoadedPackages() + let activatedPackages = null + waitsFor(() => { + activatedPackages = atom.packages.getActivePackages() + return activatedPackages.length > 0 + }) + + runs(() => { + expect(loadedPackages).toContain(pack) + expect(activatedPackages).toContain(pack) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + }) + }) + + it('disables an enabled package', () => { + const packageName = 'package-with-main' + let pack = null + let activatedPackages = null + + waitsForPromise(() => atom.packages.activatePackage(packageName)) + + runs(() => { + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + + pack = atom.packages.disablePackage(packageName) + }) + + waitsFor(() => { + activatedPackages = atom.packages.getActivePackages() + return activatedPackages.length === 0 + }) + + runs(() => { + expect(activatedPackages).not.toContain(pack) + expect(atom.config.get('core.disabledPackages')).toContain(packageName) + }) + }) + + it('returns null if the package cannot be loaded', () => { + spyOn(console, 'warn') + expect(atom.packages.enablePackage('this-doesnt-exist')).toBeNull() + expect(console.warn.callCount).toBe(1) + }) + + it('does not disable an already disabled package', () => { + const packageName = 'package-with-main' + atom.config.pushAtKeyPath('core.disabledPackages', packageName) + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).toContain(packageName) + + atom.packages.disablePackage(packageName) + const packagesDisabled = atom.config.get('core.disabledPackages').filter(pack => pack === packageName) + expect(packagesDisabled.length).toEqual(1) + }) + }) + + describe('with themes', () => { + let didChangeActiveThemesHandler = null + + beforeEach(() => { + waitsForPromise(() => atom.themes.activateThemes()) + }) + + afterEach(() => { + waitsForPromise(() => atom.themes.deactivateThemes()) + }) + + it('enables and disables a theme', () => { + const packageName = 'theme-with-package-file' + + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + + // enabling of theme + let pack = atom.packages.enablePackage(packageName) + + waitsFor('theme to enable', 500, () => { + return atom.packages.getActivePackages().includes(pack) + }) + + runs(() => { + expect(atom.config.get('core.themes')).toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + + didChangeActiveThemesHandler = jasmine.createSpy('didChangeActiveThemesHandler') + didChangeActiveThemesHandler.reset() + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler) + + pack = atom.packages.disablePackage(packageName) + }) + + waitsFor('did-change-active-themes event to fire', 500, () => didChangeActiveThemesHandler.callCount === 1) + + runs(() => { + expect(atom.packages.getActivePackages()).not.toContain(pack) + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + }) + }) + }) + }) +}) From 1d0dfe2213185891795f3f1a7db76f009153292c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 17:04:46 -0700 Subject: [PATCH 279/448] Use async/await in package-manager-spec --- spec/package-manager-spec.js | 944 ++++++++++++++--------------------- 1 file changed, 366 insertions(+), 578 deletions(-) diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 3cd25383f..9b28ab437 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -7,6 +7,7 @@ const {Disposable} = require('atom') const {buildKeydownEvent} = require('../src/keymap-extensions') const {mockLocalStorage} = require('./spec-helper') const ModuleCache = require('../src/module-cache') +const {it, fit, ffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('PackageManager', () => { function createTestElement (className) { @@ -57,9 +58,9 @@ describe('PackageManager', () => { describe('::loadPackages()', () => { beforeEach(() => spyOn(atom.packages, 'loadAvailablePackage')) - afterEach(() => { - waitsForPromise(() => atom.packages.deactivatePackages()) - runs(() => atom.packages.unloadPackages()) + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() }) it('sets hasLoadedInitialPackages', () => { @@ -188,9 +189,9 @@ describe('PackageManager', () => { const model1 = {worksWithViewProvider1: true} const model2 = {worksWithViewProvider2: true} - afterEach(() => { - waitsForPromise(() => atom.packages.deactivatePackage('package-with-view-providers')) - runs(() => atom.packages.unloadPackage('package-with-view-providers')) + afterEach(async () => { + await atom.packages.deactivatePackage('package-with-view-providers') + atom.packages.unloadPackage('package-with-view-providers') }) it('does not load the view providers immediately', () => { @@ -201,20 +202,18 @@ describe('PackageManager', () => { expect(() => atom.views.getView(model2)).toThrow() }) - it('registers the view providers when the package is activated', () => { + it('registers the view providers when the package is activated', async () => { atom.packages.loadPackage('package-with-view-providers') - waitsForPromise(() => - atom.packages.activatePackage('package-with-view-providers').then(() => { - const element1 = atom.views.getView(model1) - expect(element1 instanceof HTMLDivElement).toBe(true) - expect(element1.dataset.createdBy).toBe('view-provider-1') + await atom.packages.activatePackage('package-with-view-providers') - const element2 = atom.views.getView(model2) - expect(element2 instanceof HTMLDivElement).toBe(true) - expect(element2.dataset.createdBy).toBe('view-provider-2') - }) - ) + const element1 = atom.views.getView(model1) + expect(element1 instanceof HTMLDivElement).toBe(true) + expect(element1.dataset.createdBy).toBe('view-provider-1') + + const element2 = atom.views.getView(model2) + expect(element2 instanceof HTMLDivElement).toBe(true) + expect(element2.dataset.createdBy).toBe('view-provider-2') }) it("registers the view providers when any of the package's deserializers are used", () => { @@ -458,20 +457,14 @@ describe('PackageManager', () => { describe('::unloadPackage(name)', () => { describe('when the package is active', () => { - it('throws an error', () => { - let pack + it('throws an error', async () => { + const pack = await atom.packages.activatePackage('package-with-main') + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - waitsForPromise(() => atom.packages.activatePackage('package-with-main').then(p => { - pack = p - })) - - runs(() => { - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - expect(() => atom.packages.unloadPackage(pack.name)).toThrow() - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - }) + expect(() => atom.packages.unloadPackage(pack.name)).toThrow() + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() }) }) @@ -505,63 +498,48 @@ describe('PackageManager', () => { describe('::activatePackage(id)', () => { describe('when called multiple times', () => { - it('it only calls activate on the package once', () => { + it('it only calls activate on the package once', async () => { spyOn(Package.prototype, 'activateNow').andCallThrough() - waitsForPromise(() => atom.packages.activatePackage('package-with-index')) - waitsForPromise(() => atom.packages.activatePackage('package-with-index')) - waitsForPromise(() => atom.packages.activatePackage('package-with-index')) + await atom.packages.activatePackage('package-with-index') + await atom.packages.activatePackage('package-with-index') + await atom.packages.activatePackage('package-with-index') - runs(() => expect(Package.prototype.activateNow.callCount).toBe(1)) + expect(Package.prototype.activateNow.callCount).toBe(1) }) }) describe('when the package has a main module', () => { describe('when the metadata specifies a main module path˜', () => { - it('requires the module at the specified path', () => { + it('requires the module at the specified path', async () => { const mainModule = require('./fixtures/packages/package-with-main/main-module') spyOn(mainModule, 'activate') - let pack - waitsForPromise(() => atom.packages.activatePackage('package-with-main').then(p => { - pack = p - })) - - runs(() => { - expect(mainModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe(mainModule) - }) + const pack = await atom.packages.activatePackage('package-with-main') + expect(mainModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe(mainModule) }) }) describe('when the metadata does not specify a main module', () => { - it('requires index.coffee', () => { + it('requires index.coffee', async () => { const indexModule = require('./fixtures/packages/package-with-index/index') spyOn(indexModule, 'activate') - let pack - waitsForPromise(() => atom.packages.activatePackage('package-with-index').then(p => { - pack = p - })) - runs(() => { - expect(indexModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe(indexModule) - }) + const pack = await atom.packages.activatePackage('package-with-index') + expect(indexModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe(indexModule) }) }) - it('assigns config schema, including defaults when package contains a schema', () => { + it('assigns config schema, including defaults when package contains a schema', async () => { expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined() - waitsForPromise(() => atom.packages.activatePackage('package-with-config-schema')) - - runs(() => { - expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(1) - expect(atom.config.get('package-with-config-schema.numbers.two')).toBe(2) - - expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe(false) - expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe(true) - expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(10) - }) + await atom.packages.activatePackage('package-with-config-schema') + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(1) + expect(atom.config.get('package-with-config-schema.numbers.two')).toBe(2) + expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe(false) + expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe(true) + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(10) }) describe('when the package metadata includes `activationCommands`', () => { @@ -587,43 +565,42 @@ describe('PackageManager', () => { mainModule = null }) - it('defers requiring/activating the main module until an activation event bubbles to the root view', () => { + it('defers requiring/activating the main module until an activation event bubbles to the root view', async () => { expect(Package.prototype.requireMainModule.callCount).toBe(0) atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', {bubbles: true})) - waitsForPromise(() => promise) - - runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + await promise + expect(Package.prototype.requireMainModule.callCount).toBe(1) }) - it('triggers the activation event on all handlers registered during activation', () => { - waitsForPromise(() => atom.workspace.open()) + it('triggers the activation event on all handlers registered during activation', async () => { + await atom.workspace.open() - runs(() => { - const editorElement = atom.workspace.getActiveTextEditor().getElement() - const editorCommandListener = jasmine.createSpy('editorCommandListener') - atom.commands.add('atom-text-editor', 'activation-command', editorCommandListener) - atom.commands.dispatch(editorElement, 'activation-command') - expect(mainModule.activate.callCount).toBe(1) - expect(mainModule.activationCommandCallCount).toBe(1) - expect(editorCommandListener.callCount).toBe(1) - expect(workspaceCommandListener.callCount).toBe(1) - atom.commands.dispatch(editorElement, 'activation-command') - expect(mainModule.activationCommandCallCount).toBe(2) - expect(editorCommandListener.callCount).toBe(2) - expect(workspaceCommandListener.callCount).toBe(2) - expect(mainModule.activate.callCount).toBe(1) - }) + const editorElement = atom.workspace.getActiveTextEditor().getElement() + const editorCommandListener = jasmine.createSpy('editorCommandListener') + atom.commands.add('atom-text-editor', 'activation-command', editorCommandListener) + + atom.commands.dispatch(editorElement, 'activation-command') + expect(mainModule.activate.callCount).toBe(1) + expect(mainModule.activationCommandCallCount).toBe(1) + expect(editorCommandListener.callCount).toBe(1) + expect(workspaceCommandListener.callCount).toBe(1) + + atom.commands.dispatch(editorElement, 'activation-command') + expect(mainModule.activationCommandCallCount).toBe(2) + expect(editorCommandListener.callCount).toBe(2) + expect(workspaceCommandListener.callCount).toBe(2) + expect(mainModule.activate.callCount).toBe(1) }) - it('activates the package immediately when the events are empty', () => { + it('activates the package immediately when the events are empty', async () => { mainModule = require('./fixtures/packages/package-with-empty-activation-commands/index') spyOn(mainModule, 'activate').andCallThrough() - waitsForPromise(() => atom.packages.activatePackage('package-with-empty-activation-commands')) + atom.packages.activatePackage('package-with-empty-activation-commands') - runs(() => expect(mainModule.activate.callCount).toBe(1)) + expect(mainModule.activate.callCount).toBe(1) }) it('adds a notification when the activation commands are invalid', () => { @@ -646,34 +623,38 @@ describe('PackageManager', () => { expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-context-menu') }) - it('adds a notification when the grammar is invalid', () => { - const addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) + it('adds a notification when the grammar is invalid', async () => { + let notificationEvent - expect(() => atom.packages.activatePackage('package-with-invalid-grammar')).not.toThrow() + await new Promise(resolve => { + const subscription = atom.notifications.onDidAddNotification(event => { + notificationEvent = event + subscription.dispose() + resolve() + }) - waitsFor(() => addErrorHandler.callCount > 0) - - runs(() => { - expect(addErrorHandler.callCount).toBe(1) - expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load a package-with-invalid-grammar package grammar') - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-grammar') + atom.packages.activatePackage('package-with-invalid-grammar') }) + + expect(notificationEvent.message).toContain('Failed to load a package-with-invalid-grammar package grammar') + expect(notificationEvent.options.packageName).toEqual('package-with-invalid-grammar') }) - it('adds a notification when the settings are invalid', () => { - const addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) + it('adds a notification when the settings are invalid', async () => { + let notificationEvent - expect(() => atom.packages.activatePackage('package-with-invalid-settings')).not.toThrow() + await new Promise(resolve => { + const subscription = atom.notifications.onDidAddNotification(event => { + notificationEvent = event + subscription.dispose() + resolve() + }) - waitsFor(() => addErrorHandler.callCount > 0) - - runs(() => { - expect(addErrorHandler.callCount).toBe(1) - expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-with-invalid-settings package settings') - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-settings') + atom.packages.activatePackage('package-with-invalid-settings') }) + + expect(notificationEvent.message).toContain('Failed to load the package-with-invalid-settings package settings') + expect(notificationEvent.options.packageName).toEqual('package-with-invalid-settings') }) }) }) @@ -687,62 +668,53 @@ describe('PackageManager', () => { spyOn(Package.prototype, 'requireMainModule').andCallThrough() }) - it('defers requiring/activating the main module until an triggering of an activation hook occurs', () => { + it('defers requiring/activating the main module until an triggering of an activation hook occurs', async () => { promise = atom.packages.activatePackage('package-with-activation-hooks') expect(Package.prototype.requireMainModule.callCount).toBe(0) atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() - waitsForPromise(() => promise) - - runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + await promise + expect(Package.prototype.requireMainModule.callCount).toBe(1) }) - it('does not double register activation hooks when deactivating and reactivating', () => { + it('does not double register activation hooks when deactivating and reactivating', async () => { promise = atom.packages.activatePackage('package-with-activation-hooks') expect(mainModule.activate.callCount).toBe(0) atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() - waitsForPromise(() => promise) + await promise + expect(mainModule.activate.callCount).toBe(1) - runs(() => expect(mainModule.activate.callCount).toBe(1)) + await atom.packages.deactivatePackage('package-with-activation-hooks') - waitsForPromise(() => atom.packages.deactivatePackage('package-with-activation-hooks')) + promise = atom.packages.activatePackage('package-with-activation-hooks') + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() - runs(() => { - promise = atom.packages.activatePackage('package-with-activation-hooks') - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - }) - - waitsForPromise(() => promise) - - runs(() => expect(mainModule.activate.callCount).toBe(2)) + await promise + expect(mainModule.activate.callCount).toBe(2) }) - it('activates the package immediately when activationHooks is empty', () => { + it('activates the package immediately when activationHooks is empty', async () => { mainModule = require('./fixtures/packages/package-with-empty-activation-hooks/index') spyOn(mainModule, 'activate').andCallThrough() - runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(0)) + expect(Package.prototype.requireMainModule.callCount).toBe(0) - waitsForPromise(() => atom.packages.activatePackage('package-with-empty-activation-hooks')) - - runs(() => { - expect(mainModule.activate.callCount).toBe(1) - expect(Package.prototype.requireMainModule.callCount).toBe(1) - }) + await atom.packages.activatePackage('package-with-empty-activation-hooks') + expect(mainModule.activate.callCount).toBe(1) + expect(Package.prototype.requireMainModule.callCount).toBe(1) }) - it('activates the package immediately if the activation hook had already been triggered', () => { + it('activates the package immediately if the activation hook had already been triggered', async () => { atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() expect(Package.prototype.requireMainModule.callCount).toBe(0) - waitsForPromise(() => atom.packages.activatePackage('package-with-activation-hooks')) - - runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + await atom.packages.activatePackage('package-with-activation-hooks') + expect(Package.prototype.requireMainModule.callCount).toBe(1) }) }) @@ -757,42 +729,33 @@ describe('PackageManager', () => { }) describe('when the package does not export an activate function', () => { - it('activates the package and does not throw an exception or log a warning', () => { + it('activates the package and does not throw an exception or log a warning', async () => { spyOn(console, 'warn') - expect(() => atom.packages.activatePackage('package-with-no-activate')).not.toThrow() - - waitsFor(() => atom.packages.isPackageActive('package-with-no-activate')) - - runs(() => expect(console.warn).not.toHaveBeenCalled()) + await atom.packages.activatePackage('package-with-no-activate') + expect(console.warn).not.toHaveBeenCalled() }) }) - it("passes the activate method the package's previously serialized state if it exists", () => { - let pack - waitsForPromise(() => atom.packages.activatePackage('package-with-serialization').then(p => { - pack = p - })) - runs(() => { - expect(pack.mainModule.someNumber).not.toBe(77) - pack.mainModule.someNumber = 77 - atom.packages.serializePackage('package-with-serialization') - }) - waitsForPromise(() => atom.packages.deactivatePackage('package-with-serialization')) - runs(() => spyOn(pack.mainModule, 'activate').andCallThrough()) - waitsForPromise(() => atom.packages.activatePackage('package-with-serialization')) - runs(() => expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})) + it("passes the activate method the package's previously serialized state if it exists", async () => { + const pack = await atom.packages.activatePackage('package-with-serialization') + expect(pack.mainModule.someNumber).not.toBe(77) + pack.mainModule.someNumber = 77 + atom.packages.serializePackage('package-with-serialization') + await atom.packages.deactivatePackage('package-with-serialization') + + spyOn(pack.mainModule, 'activate').andCallThrough() + await atom.packages.activatePackage('package-with-serialization') + expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) }) - it('invokes ::onDidActivatePackage listeners with the activated package', () => { + it('invokes ::onDidActivatePackage listeners with the activated package', async () => { let activatedPackage atom.packages.onDidActivatePackage(pack => { activatedPackage = pack }) - atom.packages.activatePackage('package-with-main') - - waitsFor(() => activatedPackage) - runs(() => expect(activatedPackage.name).toBe('package-with-main')) + await atom.packages.activatePackage('package-with-main') + expect(activatedPackage.name).toBe('package-with-main') }) describe("when the package's main module throws an error on load", () => { @@ -814,82 +777,65 @@ describe('PackageManager', () => { }) describe('when the package is not found', () => { - it('rejects the promise', () => { + it('rejects the promise', async () => { + spyOn(console, 'warn') atom.config.set('core.disabledPackages', []) - const onSuccess = jasmine.createSpy('onSuccess') - const onFailure = jasmine.createSpy('onFailure') - spyOn(console, 'warn') - - atom.packages.activatePackage('this-doesnt-exist').then(onSuccess, onFailure) - - waitsFor('promise to be rejected', () => onFailure.callCount > 0) - - runs(() => { + try { + await atom.packages.activatePackage('this-doesnt-exist') + expect('Error to be thrown').toBe('') + } catch (error) { expect(console.warn.callCount).toBe(1) - expect(onFailure.mostRecentCall.args[0] instanceof Error).toBe(true) - expect(onFailure.mostRecentCall.args[0].message).toContain("Failed to load package 'this-doesnt-exist'") - }) + expect(error.message).toContain("Failed to load package 'this-doesnt-exist'") + } }) }) describe('keymap loading', () => { describe("when the metadata does not contain a 'keymaps' manifest", () => { - it('loads all the .cson/.json files in the keymaps directory', () => { + it('loads all the .cson/.json files in the keymaps directory', async () => { const element1 = createTestElement('test-1') const element2 = createTestElement('test-2') const element3 = createTestElement('test-3') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})).toHaveLength(0) expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) - - runs(() => { - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('test-1') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})[0].command).toBe('test-2') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) - }) + await atom.packages.activatePackage('package-with-keymaps') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('test-1') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})[0].command).toBe('test-2') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) }) }) describe("when the metadata contains a 'keymaps' manifest", () => { - it('loads only the keymaps specified by the manifest, in the specified order', () => { + it('loads only the keymaps specified by the manifest, in the specified order', async () => { const element1 = createTestElement('test-1') const element3 = createTestElement('test-3') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) - - runs(() => { - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-n', target: element1})[0].command).toBe('keymap-2') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-y', target: element3})).toHaveLength(0) - }) + await atom.packages.activatePackage('package-with-keymaps-manifest') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-n', target: element1})[0].command).toBe('keymap-2') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-y', target: element3})).toHaveLength(0) }) }) describe('when the keymap file is empty', () => { - it('does not throw an error on activation', () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-empty-keymap')) - - runs(() => expect(atom.packages.isPackageActive('package-with-empty-keymap')).toBe(true)) + it('does not throw an error on activation', async () => { + await atom.packages.activatePackage('package-with-empty-keymap') + expect(atom.packages.isPackageActive('package-with-empty-keymap')).toBe(true) }) }) describe("when the package's keymaps have been disabled", () => { - it('does not add the keymaps', () => { + it('does not add the keymaps', async () => { const element1 = createTestElement('test-1') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) - - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) - - runs(() => expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)) + await atom.packages.activatePackage('package-with-keymaps-manifest') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) }) }) @@ -903,19 +849,17 @@ describe('PackageManager', () => { }) describe("when the package's keymaps are disabled and re-enabled after it is activated", () => { - it('removes and re-adds the keymaps', () => { + it('removes and re-adds the keymaps', async () => { const element1 = createTestElement('test-1') atom.packages.observePackagesWithKeymapsDisabled() - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) + await atom.packages.activatePackage('package-with-keymaps-manifest') - runs(() => { - atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) - atom.config.set('core.packagesWithKeymapsDisabled', []) - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') - }) + atom.config.set('core.packagesWithKeymapsDisabled', []) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') }) }) @@ -944,28 +888,20 @@ describe('PackageManager', () => { temp.cleanupSync() }) - it("doesn't override user-defined keymaps", () => { + it("doesn't override user-defined keymaps", async () => { fs.writeFileSync(userKeymapPath, `".test-1": {"ctrl-z": "user-command"}`) atom.keymaps.loadUserKeymap() - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + await atom.packages.activatePackage('package-with-keymaps') + atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) + expect(events.length).toBe(1) + expect(events[0].type).toBe('user-command') - runs(() => { - atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) - - expect(events.length).toBe(1) - expect(events[0].type).toBe('user-command') - }) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-keymaps')) - - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) - - runs(() => { - atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) - expect(events.length).toBe(2) - expect(events[1].type).toBe('user-command') - }) + await atom.packages.deactivatePackage('package-with-keymaps') + await atom.packages.activatePackage('package-with-keymaps') + atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) + expect(events.length).toBe(2) + expect(events[1].type).toBe('user-command') }) }) }) @@ -977,53 +913,45 @@ describe('PackageManager', () => { }) describe("when the metadata does not contain a 'menus' manifest", () => { - it('loads all the .cson/.json files in the menus directory', () => { + it('loads all the .cson/.json files in the menus directory', async () => { const element = createTestElement('test-1') - expect(atom.contextMenu.templateForElement(element)).toEqual([]) - waitsForPromise(() => atom.packages.activatePackage('package-with-menus')) - - runs(() => { - expect(atom.menu.template.length).toBe(2) - expect(atom.menu.template[0].label).toBe('Second to Last') - expect(atom.menu.template[1].label).toBe('Last') - expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 1') - expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 2') - expect(atom.contextMenu.templateForElement(element)[2].label).toBe('Menu item 3') - }) + await atom.packages.activatePackage('package-with-menus') + expect(atom.menu.template.length).toBe(2) + expect(atom.menu.template[0].label).toBe('Second to Last') + expect(atom.menu.template[1].label).toBe('Last') + expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 1') + expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 2') + expect(atom.contextMenu.templateForElement(element)[2].label).toBe('Menu item 3') }) }) describe("when the metadata contains a 'menus' manifest", () => { - it('loads only the menus specified by the manifest, in the specified order', () => { + it('loads only the menus specified by the manifest, in the specified order', async () => { const element = createTestElement('test-1') - expect(atom.contextMenu.templateForElement(element)).toEqual([]) - waitsForPromise(() => atom.packages.activatePackage('package-with-menus-manifest')) - - runs(() => { - expect(atom.menu.template[0].label).toBe('Second to Last') - expect(atom.menu.template[1].label).toBe('Last') - expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 2') - expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 1') - expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() - }) + await atom.packages.activatePackage('package-with-menus-manifest') + expect(atom.menu.template[0].label).toBe('Second to Last') + expect(atom.menu.template[1].label).toBe('Last') + expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 2') + expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 1') + expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() }) }) describe('when the menu file is empty', () => { - it('does not throw an error on activation', () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-empty-menu')) - runs(() => expect(atom.packages.isPackageActive('package-with-empty-menu')).toBe(true)) + it('does not throw an error on activation', async () => { + await atom.packages.activatePackage('package-with-empty-menu') + expect(atom.packages.isPackageActive('package-with-empty-menu')).toBe(true) }) }) }) describe('stylesheet loading', () => { describe("when the metadata contains a 'styleSheets' manifest", () => { - it('loads style sheets from the styles directory as specified by the manifest', () => { + it('loads style sheets from the styles directory as specified by the manifest', async () => { const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') @@ -1032,20 +960,16 @@ describe('PackageManager', () => { expect(atom.themes.stylesheetElementForId(two)).toBeNull() expect(atom.themes.stylesheetElementForId(three)).toBeNull() - waitsForPromise(() => atom.packages.activatePackage('package-with-style-sheets-manifest')) - - runs(() => { - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - - expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('1px') - }) + await atom.packages.activatePackage('package-with-style-sheets-manifest') + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('1px') }) }) describe("when the metadata does not contain a 'styleSheets' manifest", () => { - it('loads all style sheets from the styles directory', () => { + it('loads all style sheets from the styles directory', async () => { const one = require.resolve('./fixtures/packages/package-with-styles/styles/1.css') const two = require.resolve('./fixtures/packages/package-with-styles/styles/2.less') const three = require.resolve('./fixtures/packages/package-with-styles/styles/3.test-context.css') @@ -1056,73 +980,64 @@ describe('PackageManager', () => { expect(atom.themes.stylesheetElementForId(three)).toBeNull() expect(atom.themes.stylesheetElementForId(four)).toBeNull() - waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) - - runs(() => { - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(four)).not.toBeNull() - expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('3px') - }) + await atom.packages.activatePackage('package-with-styles') + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(four)).not.toBeNull() + expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('3px') }) }) - it("assigns the stylesheet's context based on the filename", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + it("assigns the stylesheet's context based on the filename", async () => { + await atom.packages.activatePackage('package-with-styles') - runs(() => { - let count = 0 - - for (let styleElement of atom.styles.getStyleElements()) { - if (styleElement.sourcePath.match(/1.css/)) { - expect(styleElement.context).toBe(undefined) - count++ - } - - if (styleElement.sourcePath.match(/2.less/)) { - expect(styleElement.context).toBe(undefined) - count++ - } - - if (styleElement.sourcePath.match(/3.test-context.css/)) { - expect(styleElement.context).toBe('test-context') - count++ - } - - if (styleElement.sourcePath.match(/4.css/)) { - expect(styleElement.context).toBe(undefined) - count++ - } + let count = 0 + for (let styleElement of atom.styles.getStyleElements()) { + if (styleElement.sourcePath.match(/1.css/)) { + expect(styleElement.context).toBe(undefined) + count++ } - expect(count).toBe(4) - }) + if (styleElement.sourcePath.match(/2.less/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + + if (styleElement.sourcePath.match(/3.test-context.css/)) { + expect(styleElement.context).toBe('test-context') + count++ + } + + if (styleElement.sourcePath.match(/4.css/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + } + + expect(count).toBe(4) }) }) describe('grammar loading', () => { - it("loads the package's grammars", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-grammars')) - - runs(() => { - expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot') - expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle') - }) + it("loads the package's grammars", async () => { + await atom.packages.activatePackage('package-with-grammars') + expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot') + expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle') }) }) describe('scoped-property loading', () => { - it('loads the scoped properties', () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-settings')) - - runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')) + it('loads the scoped properties', async () => { + await atom.packages.activatePackage('package-with-settings') + expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a') }) }) describe('service registration', () => { - it("registers the package's provided and consumed services", () => { + it("registers the package's provided and consumed services", async () => { const consumerModule = require('./fixtures/packages/package-with-consumed-services') + let firstServiceV3Disposed = false let firstServiceV4Disposed = false let secondServiceDisposed = false @@ -1130,241 +1045,154 @@ describe('PackageManager', () => { spyOn(consumerModule, 'consumeFirstServiceV4').andReturn(new Disposable(() => { firstServiceV4Disposed = true })) spyOn(consumerModule, 'consumeSecondService').andReturn(new Disposable(() => { secondServiceDisposed = true })) - waitsForPromise(() => atom.packages.activatePackage('package-with-consumed-services')) + await atom.packages.activatePackage('package-with-consumed-services') + await atom.packages.activatePackage('package-with-provided-services') + expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) + expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') + expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') + expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') - waitsForPromise(() => atom.packages.activatePackage('package-with-provided-services')) + consumerModule.consumeFirstServiceV3.reset() + consumerModule.consumeFirstServiceV4.reset() + consumerModule.consumeSecondService.reset() - runs(() => { - expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) - expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') - expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') - expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') + await atom.packages.deactivatePackage('package-with-provided-services') + expect(firstServiceV3Disposed).toBe(true) + expect(firstServiceV4Disposed).toBe(true) + expect(secondServiceDisposed).toBe(true) - consumerModule.consumeFirstServiceV3.reset() - consumerModule.consumeFirstServiceV4.reset() - consumerModule.consumeSecondService.reset() - }) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-provided-services')) - - runs(() => { - expect(firstServiceV3Disposed).toBe(true) - expect(firstServiceV4Disposed).toBe(true) - expect(secondServiceDisposed).toBe(true) - }) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-consumed-services')) - - waitsForPromise(() => atom.packages.activatePackage('package-with-provided-services')) - - runs(() => { - expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled() - expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() - expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() - }) + await atom.packages.deactivatePackage('package-with-consumed-services') + await atom.packages.activatePackage('package-with-provided-services') + expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled() + expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() + expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() }) - it('ignores provided and consumed services that do not exist', () => { + it('ignores provided and consumed services that do not exist', async () => { const addErrorHandler = jasmine.createSpy() atom.notifications.onDidAddNotification(addErrorHandler) - waitsForPromise(() => atom.packages.activatePackage('package-with-missing-consumed-services')) - - waitsForPromise(() => atom.packages.activatePackage('package-with-missing-provided-services')) - - runs(() => { - expect(atom.packages.isPackageActive('package-with-missing-consumed-services')).toBe(true) - expect(atom.packages.isPackageActive('package-with-missing-provided-services')).toBe(true) - expect(addErrorHandler.callCount).toBe(0) - }) + await atom.packages.activatePackage('package-with-missing-consumed-services') + await atom.packages.activatePackage('package-with-missing-provided-services') + expect(atom.packages.isPackageActive('package-with-missing-consumed-services')).toBe(true) + expect(atom.packages.isPackageActive('package-with-missing-provided-services')).toBe(true) + expect(addErrorHandler.callCount).toBe(0) }) }) }) describe('::serialize', () => { - it('does not serialize packages that threw an error during activation', () => { + it('does not serialize packages that threw an error during activation', async () => { spyOn(atom, 'inSpecMode').andReturn(false) spyOn(console, 'warn') - let badPack - waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-activate').then(p => { - badPack = p - })) + const badPack = await atom.packages.activatePackage('package-that-throws-on-activate') + spyOn(badPack.mainModule, 'serialize').andCallThrough() - runs(() => { - spyOn(badPack.mainModule, 'serialize').andCallThrough() - - atom.packages.serialize() - expect(badPack.mainModule.serialize).not.toHaveBeenCalled() - }) + atom.packages.serialize() + expect(badPack.mainModule.serialize).not.toHaveBeenCalled() }) - it("absorbs exceptions that are thrown by the package module's serialize method", () => { + it("absorbs exceptions that are thrown by the package module's serialize method", async () => { spyOn(console, 'error') - waitsForPromise(() => atom.packages.activatePackage('package-with-serialize-error')) - - waitsForPromise(() => atom.packages.activatePackage('package-with-serialization')) - - runs(() => { - atom.packages.serialize() - expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() - expect(atom.packages.packageStates['package-with-serialization']).toEqual({someNumber: 1}) - expect(console.error).toHaveBeenCalled() - }) + await atom.packages.activatePackage('package-with-serialize-error') + await atom.packages.activatePackage('package-with-serialization') + atom.packages.serialize() + expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() + expect(atom.packages.packageStates['package-with-serialization']).toEqual({someNumber: 1}) + expect(console.error).toHaveBeenCalled() }) }) describe('::deactivatePackages()', () => { - it('deactivates all packages but does not serialize them', () => { - let pack1, pack2 + it('deactivates all packages but does not serialize them', async () => { + const pack1 = await atom.packages.activatePackage('package-with-deactivate') + const pack2 = await atom.packages.activatePackage('package-with-serialization') - waitsForPromise(() => { - atom.packages.activatePackage('package-with-deactivate').then(p => { - pack1 = p - }) - return atom.packages.activatePackage('package-with-serialization').then(p => { - pack2 = p - }) - }) - - runs(() => { - spyOn(pack1.mainModule, 'deactivate') - spyOn(pack2.mainModule, 'serialize') - }) - - waitsForPromise(() => atom.packages.deactivatePackages()) - - runs(() => { - expect(pack1.mainModule.deactivate).toHaveBeenCalled() - expect(pack2.mainModule.serialize).not.toHaveBeenCalled() - }) + spyOn(pack1.mainModule, 'deactivate') + spyOn(pack2.mainModule, 'serialize') + await atom.packages.deactivatePackages() + expect(pack1.mainModule.deactivate).toHaveBeenCalled() + expect(pack2.mainModule.serialize).not.toHaveBeenCalled() }) }) describe('::deactivatePackage(id)', () => { afterEach(() => atom.packages.unloadPackages()) - it("calls `deactivate` on the package's main module if activate was successful", () => { + it("calls `deactivate` on the package's main module if activate was successful", async () => { spyOn(atom, 'inSpecMode').andReturn(false) - let pack - waitsForPromise(() => atom.packages.activatePackage('package-with-deactivate').then(p => { - pack = p - })) + const pack = await atom.packages.activatePackage('package-with-deactivate') + expect(atom.packages.isPackageActive('package-with-deactivate')).toBeTruthy() + spyOn(pack.mainModule, 'deactivate').andCallThrough() - runs(() => { - expect(atom.packages.isPackageActive('package-with-deactivate')).toBeTruthy() - spyOn(pack.mainModule, 'deactivate').andCallThrough() - }) + await atom.packages.deactivatePackage('package-with-deactivate') + expect(pack.mainModule.deactivate).toHaveBeenCalled() + expect(atom.packages.isPackageActive('package-with-module')).toBeFalsy() - waitsForPromise(() => atom.packages.deactivatePackage('package-with-deactivate')) + spyOn(console, 'warn') + const badPack = await atom.packages.activatePackage('package-that-throws-on-activate') + expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeTruthy() + spyOn(badPack.mainModule, 'deactivate').andCallThrough() - runs(() => { - expect(pack.mainModule.deactivate).toHaveBeenCalled() - expect(atom.packages.isPackageActive('package-with-module')).toBeFalsy() - - spyOn(console, 'warn') - }) - - let badPack = null - waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-activate').then(p => { - badPack = p - })) - - runs(() => { - expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeTruthy() - spyOn(badPack.mainModule, 'deactivate').andCallThrough() - }) - - waitsForPromise(() => atom.packages.deactivatePackage('package-that-throws-on-activate')) - - runs(() => { - expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() - expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeFalsy() - }) + await atom.packages.deactivatePackage('package-that-throws-on-activate') + expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() + expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeFalsy() }) - it("absorbs exceptions that are thrown by the package module's deactivate method", () => { + it("absorbs exceptions that are thrown by the package module's deactivate method", async () => { spyOn(console, 'error') - let thrownError = null - - waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-deactivate')) - - waitsForPromise(() => { - try { - return atom.packages.deactivatePackage('package-that-throws-on-deactivate') - } catch (error) { - thrownError = error - } - }) - - runs(() => { - expect(thrownError).toBeNull() - expect(console.error).toHaveBeenCalled() - }) + await atom.packages.activatePackage('package-that-throws-on-deactivate') + await atom.packages.deactivatePackage('package-that-throws-on-deactivate') + expect(console.error).toHaveBeenCalled() }) - it("removes the package's grammars", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-grammars')) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-grammars')) - - runs(() => { - expect(atom.grammars.selectGrammar('a.alot').name).toBe('Null Grammar') - expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Null Grammar') - }) + it("removes the package's grammars", async () => { + await atom.packages.activatePackage('package-with-grammars') + await atom.packages.deactivatePackage('package-with-grammars') + expect(atom.grammars.selectGrammar('a.alot').name).toBe('Null Grammar') + expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Null Grammar') }) - it("removes the package's keymaps", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-keymaps')) - - runs(() => { - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-1')})).toHaveLength(0) - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-2')})).toHaveLength(0) - }) + it("removes the package's keymaps", async () => { + await atom.packages.activatePackage('package-with-keymaps') + await atom.packages.deactivatePackage('package-with-keymaps') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-1')})).toHaveLength(0) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-2')})).toHaveLength(0) }) - it("removes the package's stylesheets", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + it("removes the package's stylesheets", async () => { + await atom.packages.activatePackage('package-with-styles') + await atom.packages.deactivatePackage('package-with-styles') - waitsForPromise(() => atom.packages.deactivatePackage('package-with-styles')) - - runs(() => { - const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') - const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') - const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') - expect(atom.themes.stylesheetElementForId(one)).not.toExist() - expect(atom.themes.stylesheetElementForId(two)).not.toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() - }) + const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') + const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') + const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') + expect(atom.themes.stylesheetElementForId(one)).not.toExist() + expect(atom.themes.stylesheetElementForId(two)).not.toExist() + expect(atom.themes.stylesheetElementForId(three)).not.toExist() }) - it("removes the package's scoped-properties", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-settings')) + it("removes the package's scoped-properties", async () => { + await atom.packages.activatePackage('package-with-settings') + expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a') - runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-settings')) - - runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBeUndefined()) + await atom.packages.deactivatePackage('package-with-settings') + expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBeUndefined() }) - it('invokes ::onDidDeactivatePackage listeners with the deactivated package', () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-main')) + it('invokes ::onDidDeactivatePackage listeners with the deactivated package', async () => { + await atom.packages.activatePackage('package-with-main') let deactivatedPackage - runs(() => { - atom.packages.onDidDeactivatePackage(pack => { - deactivatedPackage = pack - }) + atom.packages.onDidDeactivatePackage(pack => { + deactivatedPackage = pack }) - waitsForPromise(() => atom.packages.deactivatePackage('package-with-main')) - - runs(() => expect(deactivatedPackage.name).toBe('package-with-main')) + await atom.packages.deactivatePackage('package-with-main') + expect(deactivatedPackage.name).toBe('package-with-main') }) }) @@ -1379,20 +1207,19 @@ describe('PackageManager', () => { expect(loadedPackages.length).toBeGreaterThan(0) }) - afterEach(() => { - waitsForPromise(() => atom.packages.deactivatePackages()) - runs(() => { - atom.packages.unloadPackages() - jasmine.restoreDeprecationsSnapshot() - }) + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + jasmine.restoreDeprecationsSnapshot() }) - it('sets hasActivatedInitialPackages', () => { + it('sets hasActivatedInitialPackages', async () => { spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) spyOn(atom.packages, 'activatePackages') expect(atom.packages.hasActivatedInitialPackages()).toBe(false) - waitsForPromise(() => atom.packages.activate()) - runs(() => expect(atom.packages.hasActivatedInitialPackages()).toBe(true)) + + await atom.packages.activate() + expect(atom.packages.hasActivatedInitialPackages()).toBe(true) }) it('activates all the packages, and none of the themes', () => { @@ -1411,73 +1238,52 @@ describe('PackageManager', () => { themes.map((theme) => expect(['theme']).toContain(theme.getType())) }) - it('calls callbacks registered with ::onDidActivateInitialPackages', () => { + it('calls callbacks registered with ::onDidActivateInitialPackages', async () => { const package1 = atom.packages.loadPackage('package-with-main') const package2 = atom.packages.loadPackage('package-with-index') const package3 = atom.packages.loadPackage('package-with-activation-commands') spyOn(atom.packages, 'getLoadedPackages').andReturn([package1, package2, package3]) spyOn(atom.themes, 'activatePackages') - const activateSpy = jasmine.createSpy('activateSpy') - atom.packages.onDidActivateInitialPackages(activateSpy) atom.packages.activate() - waitsFor(() => activateSpy.callCount > 0) - runs(() => { - let needle, needle1, needle2 - jasmine.unspy(atom.packages, 'getLoadedPackages') - expect(atom.packages.getActivePackages().includes(package1)).toBe(true) - expect(atom.packages.getActivePackages().includes(package2)).toBe(true) - expect(atom.packages.getActivePackages().includes(package3)).toBe(false) - }) + await new Promise(resolve => atom.packages.onDidActivateInitialPackages(resolve)) + + jasmine.unspy(atom.packages, 'getLoadedPackages') + expect(atom.packages.getActivePackages().includes(package1)).toBe(true) + expect(atom.packages.getActivePackages().includes(package2)).toBe(true) + expect(atom.packages.getActivePackages().includes(package3)).toBe(false) }) }) describe('::enablePackage(id) and ::disablePackage(id)', () => { describe('with packages', () => { - it('enables a disabled package', () => { + it('enables a disabled package', async () => { const packageName = 'package-with-main' atom.config.pushAtKeyPath('core.disabledPackages', packageName) atom.packages.observeDisabledPackages() expect(atom.config.get('core.disabledPackages')).toContain(packageName) const pack = atom.packages.enablePackage(packageName) - const loadedPackages = atom.packages.getLoadedPackages() - let activatedPackages = null - waitsFor(() => { - activatedPackages = atom.packages.getActivePackages() - return activatedPackages.length > 0 - }) + await new Promise(resolve => atom.packages.onDidActivatePackage(resolve)) - runs(() => { - expect(loadedPackages).toContain(pack) - expect(activatedPackages).toContain(pack) - expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - }) + expect(atom.packages.getLoadedPackages()).toContain(pack) + expect(atom.packages.getActivePackages()).toContain(pack) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) }) - it('disables an enabled package', () => { + it('disables an enabled package', async () => { const packageName = 'package-with-main' - let pack = null - let activatedPackages = null + const pack = await atom.packages.activatePackage(packageName) - waitsForPromise(() => atom.packages.activatePackage(packageName)) - - runs(() => { - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - - pack = atom.packages.disablePackage(packageName) + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + await new Promise(resolve => { + atom.packages.onDidDeactivatePackage(resolve) + atom.packages.disablePackage(packageName) }) - waitsFor(() => { - activatedPackages = atom.packages.getActivePackages() - return activatedPackages.length === 0 - }) - - runs(() => { - expect(activatedPackages).not.toContain(pack) - expect(atom.config.get('core.disabledPackages')).toContain(packageName) - }) + expect(atom.packages.getActivePackages()).not.toContain(pack) + expect(atom.config.get('core.disabledPackages')).toContain(packageName) }) it('returns null if the package cannot be loaded', () => { @@ -1499,48 +1305,30 @@ describe('PackageManager', () => { }) describe('with themes', () => { - let didChangeActiveThemesHandler = null + beforeEach(() => atom.themes.activateThemes()) + afterEach(() => atom.themes.deactivateThemes()) - beforeEach(() => { - waitsForPromise(() => atom.themes.activateThemes()) - }) - - afterEach(() => { - waitsForPromise(() => atom.themes.deactivateThemes()) - }) - - it('enables and disables a theme', () => { + it('enables and disables a theme', async () => { const packageName = 'theme-with-package-file' - expect(atom.config.get('core.themes')).not.toContain(packageName) expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) // enabling of theme - let pack = atom.packages.enablePackage(packageName) + const pack = atom.packages.enablePackage(packageName) + await new Promise(resolve => atom.packages.onDidActivatePackage(resolve)) + expect(atom.packages.isPackageActive(packageName)).toBe(true) + expect(atom.config.get('core.themes')).toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - waitsFor('theme to enable', 500, () => { - return atom.packages.getActivePackages().includes(pack) + await new Promise(resolve => { + atom.themes.onDidChangeActiveThemes(resolve) + atom.packages.disablePackage(packageName) }) - runs(() => { - expect(atom.config.get('core.themes')).toContain(packageName) - expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - - didChangeActiveThemesHandler = jasmine.createSpy('didChangeActiveThemesHandler') - didChangeActiveThemesHandler.reset() - atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler) - - pack = atom.packages.disablePackage(packageName) - }) - - waitsFor('did-change-active-themes event to fire', 500, () => didChangeActiveThemesHandler.callCount === 1) - - runs(() => { - expect(atom.packages.getActivePackages()).not.toContain(pack) - expect(atom.config.get('core.themes')).not.toContain(packageName) - expect(atom.config.get('core.themes')).not.toContain(packageName) - expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - }) + expect(atom.packages.getActivePackages()).not.toContain(pack) + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) }) }) }) From a778d5e09c7a5a4e92aa32ef9af5ae6955be1a62 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 17:09:55 -0700 Subject: [PATCH 280/448] :art: --- spec/package-manager-spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 9b28ab437..1d949859d 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -51,7 +51,9 @@ describe('PackageManager', () => { describe('when the core.apmPath setting is set', () => { beforeEach(() => atom.config.set('core.apmPath', '/path/to/apm')) - it('returns the value of the core.apmPath config setting', () => expect(atom.packages.getApmPath()).toBe('/path/to/apm')) + it('returns the value of the core.apmPath config setting', () => { + expect(atom.packages.getApmPath()).toBe('/path/to/apm') + }) }) }) @@ -111,7 +113,9 @@ describe('PackageManager', () => { expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-broken-package-json') }) - it('returns null if the package name or path starts with a dot', () => expect(atom.packages.loadPackage('/Users/user/.atom/packages/.git')).toBeNull()) + it('returns null if the package name or path starts with a dot', () => { + expect(atom.packages.loadPackage('/Users/user/.atom/packages/.git')).toBeNull() + }) it('normalizes short repository urls in package.json', () => { let {metadata} = atom.packages.loadPackage('package-with-short-url-package-json') From be2aa7b6f57286508d457ac26bb9fcb20c37cf99 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Fri, 29 Sep 2017 09:37:36 -0700 Subject: [PATCH 281/448] :arrow_up: github@0.6.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73a317d5a..9ed2dc6a0 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.2", + "github": "0.6.3", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.6", From 179225cb3f726321e7ed850e4b00e9862914b239 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Fri, 29 Sep 2017 18:41:15 +0200 Subject: [PATCH 282/448] Update the stale markComment message --- .github/stale.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 415a830c2..2378bc1dc 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -13,9 +13,14 @@ exemptLabels: 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. It will be closed if no further activity occurs. Thank you - for your contributions. + 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 then + comment in the issue and verify that you can still reproduce in the latest version. + 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 # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable From e537a2429acecbc1a9dcde12737dec1993dc7110 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 29 Sep 2017 13:29:06 -0600 Subject: [PATCH 283/448] :arrow_up: autocomplete-plus@2.36.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ed2dc6a0..1fd8ca343 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.35.11", + "autocomplete-plus": "2.36.0", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From eb46d0a5c6d24d970db8f41b479d0ff654993be1 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Fri, 29 Sep 2017 14:34:43 -0700 Subject: [PATCH 284/448] Fix mouseup listener cleanup when dragging in text editor --- src/text-editor-component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 093f2590e..5c3b6d1bc 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1883,7 +1883,7 @@ class TextEditorComponent { function didMouseUp () { window.removeEventListener('mousemove', didMouseMove) - window.removeEventListener('mouseup', didMouseUp) + window.removeEventListener('mouseup', didMouseUp, {capture: true}) bufferWillChangeDisposable.dispose() if (dragging) { dragging = false From 72d89625e8242bf6886b229b5f9221e3a8d7515b Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Thu, 28 Sep 2017 13:45:07 -0700 Subject: [PATCH 285/448] :arrow_up: electron@1.6.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fd8ca343..98efa107a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.6.9", + "electronVersion": "1.6.14", "dependencies": { "@atom/source-map-support": "^0.3.4", "async": "0.2.6", From 9f12b4f5692d58e86f9132be848ca2532e988662 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2017 14:38:49 -0700 Subject: [PATCH 286/448] Convert Cursor to JS --- src/cursor.coffee | 659 ---------------------------------------- src/cursor.js | 754 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 754 insertions(+), 659 deletions(-) delete mode 100644 src/cursor.coffee create mode 100644 src/cursor.js diff --git a/src/cursor.coffee b/src/cursor.coffee deleted file mode 100644 index 2acbfecf4..000000000 --- a/src/cursor.coffee +++ /dev/null @@ -1,659 +0,0 @@ -{Point, Range} = require 'text-buffer' -{Emitter} = require 'event-kit' -_ = require 'underscore-plus' -Model = require './model' - -EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g - -# Extended: The `Cursor` class represents the little blinking line identifying -# where text can be inserted. -# -# Cursors belong to {TextEditor}s and have some metadata attached in the form -# of a {DisplayMarker}. -module.exports = -class Cursor extends Model - screenPosition: null - bufferPosition: null - goalColumn: null - - # Instantiated by a {TextEditor} - constructor: ({@editor, @marker, id}) -> - @emitter = new Emitter - @assignId(id) - - destroy: -> - @marker.destroy() - - ### - Section: Event Subscription - ### - - # Public: Calls your `callback` when the cursor has been moved. - # - # * `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. - onDidChangePosition: (callback) -> - @emitter.on 'did-change-position', callback - - # Public: Calls your `callback` when the cursor is destroyed - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - ### - Section: Managing Cursor Position - ### - - # Public: Moves a cursor to a given screen position. - # - # * `screenPosition` {Array} of two numbers: the screen row, and the screen column. - # * `options` (optional) {Object} with the following keys: - # * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever - # the cursor moves to. - setScreenPosition: (screenPosition, options={}) -> - @changePosition options, => - @marker.setHeadScreenPosition(screenPosition, options) - - # Public: Returns the screen position of the cursor as a {Point}. - getScreenPosition: -> - @marker.getHeadScreenPosition() - - # Public: Moves a cursor to a given buffer position. - # - # * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. - # * `options` (optional) {Object} with the following keys: - # * `autoscroll` {Boolean} indicating whether to autoscroll to the new - # position. Defaults to `true` if this is the most recently added cursor, - # `false` otherwise. - setBufferPosition: (bufferPosition, options={}) -> - @changePosition options, => - @marker.setHeadBufferPosition(bufferPosition, options) - - # Public: Returns the current buffer position as an Array. - getBufferPosition: -> - @marker.getHeadBufferPosition() - - # Public: Returns the cursor's current screen row. - getScreenRow: -> - @getScreenPosition().row - - # Public: Returns the cursor's current screen column. - getScreenColumn: -> - @getScreenPosition().column - - # Public: Retrieves the cursor's current buffer row. - getBufferRow: -> - @getBufferPosition().row - - # Public: Returns the cursor's current buffer column. - getBufferColumn: -> - @getBufferPosition().column - - # Public: Returns the cursor's current buffer row of text excluding its line - # ending. - getCurrentBufferLine: -> - @editor.lineTextForBufferRow(@getBufferRow()) - - # Public: Returns whether the cursor is at the start of a line. - isAtBeginningOfLine: -> - @getBufferPosition().column is 0 - - # Public: Returns whether the cursor is on the line return character. - isAtEndOfLine: -> - @getBufferPosition().isEqual(@getCurrentLineBufferRange().end) - - ### - Section: Cursor Position Details - ### - - # Public: Returns the underlying {DisplayMarker} for the cursor. - # Useful with overlay {Decoration}s. - getMarker: -> @marker - - # Public: Identifies if the cursor is surrounded by whitespace. - # - # "Surrounded" here means that the character directly before and after the - # cursor are both whitespace. - # - # Returns a {Boolean}. - isSurroundedByWhitespace: -> - {row, column} = @getBufferPosition() - range = [[row, column - 1], [row, column + 1]] - /^\s+$/.test @editor.getTextInBufferRange(range) - - # Public: Returns whether the cursor is currently between a word and non-word - # character. The non-word characters are defined by the - # `editor.nonWordCharacters` config value. - # - # This method returns false if the character before or after the cursor is - # whitespace. - # - # Returns a Boolean. - isBetweenWordAndNonWord: -> - return false if @isAtBeginningOfLine() or @isAtEndOfLine() - - {row, column} = @getBufferPosition() - range = [[row, column - 1], [row, column + 1]] - [before, after] = @editor.getTextInBufferRange(range) - return false if /\s/.test(before) or /\s/.test(after) - - nonWordCharacters = @getNonWordCharacters() - nonWordCharacters.includes(before) isnt nonWordCharacters.includes(after) - - # Public: Returns whether this cursor is between a word's start and end. - # - # * `options` (optional) {Object} - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - # - # Returns a {Boolean} - isInsideWord: (options) -> - {row, column} = @getBufferPosition() - range = [[row, column], [row, Infinity]] - @editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) is 0 - - # Public: Returns the indentation level of the current line. - getIndentLevel: -> - if @editor.getSoftTabs() - @getBufferColumn() / @editor.getTabLength() - else - @getBufferColumn() - - # Public: Retrieves the scope descriptor for the cursor's current position. - # - # Returns a {ScopeDescriptor} - getScopeDescriptor: -> - @editor.scopeDescriptorForBufferPosition(@getBufferPosition()) - - # Public: Returns true if this cursor has no non-whitespace characters before - # its current position. - hasPrecedingCharactersOnLine: -> - bufferPosition = @getBufferPosition() - line = @editor.lineTextForBufferRow(bufferPosition.row) - firstCharacterColumn = line.search(/\S/) - - if firstCharacterColumn is -1 - false - else - bufferPosition.column > firstCharacterColumn - - # Public: Identifies if this cursor is the last in the {TextEditor}. - # - # "Last" is defined as the most recently added cursor. - # - # Returns a {Boolean}. - isLastCursor: -> - this is @editor.getLastCursor() - - ### - Section: Moving the Cursor - ### - - # Public: Moves the cursor up one screen row. - # - # * `rowCount` (optional) {Number} number of rows to move (default: 1) - # * `options` (optional) {Object} with the following keys: - # * `moveToEndOfSelection` if true, move to the left of the selection if a - # selection exists. - moveUp: (rowCount=1, {moveToEndOfSelection}={}) -> - range = @marker.getScreenRange() - if moveToEndOfSelection and not range.isEmpty() - {row, column} = range.start - else - {row, column} = @getScreenPosition() - - column = @goalColumn if @goalColumn? - @setScreenPosition({row: row - rowCount, column: column}, skipSoftWrapIndentation: true) - @goalColumn = column - - # Public: Moves the cursor down one screen row. - # - # * `rowCount` (optional) {Number} number of rows to move (default: 1) - # * `options` (optional) {Object} with the following keys: - # * `moveToEndOfSelection` if true, move to the left of the selection if a - # selection exists. - moveDown: (rowCount=1, {moveToEndOfSelection}={}) -> - range = @marker.getScreenRange() - if moveToEndOfSelection and not range.isEmpty() - {row, column} = range.end - else - {row, column} = @getScreenPosition() - - column = @goalColumn if @goalColumn? - @setScreenPosition({row: row + rowCount, column: column}, skipSoftWrapIndentation: true) - @goalColumn = column - - # Public: Moves the cursor left one screen column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - # * `options` (optional) {Object} with the following keys: - # * `moveToEndOfSelection` if true, move to the left of the selection if a - # selection exists. - moveLeft: (columnCount=1, {moveToEndOfSelection}={}) -> - range = @marker.getScreenRange() - if moveToEndOfSelection and not range.isEmpty() - @setScreenPosition(range.start) - else - {row, column} = @getScreenPosition() - - while columnCount > column and row > 0 - columnCount -= column - column = @editor.lineLengthForScreenRow(--row) - columnCount-- # subtract 1 for the row move - - column = column - columnCount - @setScreenPosition({row, column}, clipDirection: 'backward') - - # Public: Moves the cursor right one screen column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - # * `options` (optional) {Object} with the following keys: - # * `moveToEndOfSelection` if true, move to the right of the selection if a - # selection exists. - moveRight: (columnCount=1, {moveToEndOfSelection}={}) -> - range = @marker.getScreenRange() - if moveToEndOfSelection and not range.isEmpty() - @setScreenPosition(range.end) - else - {row, column} = @getScreenPosition() - maxLines = @editor.getScreenLineCount() - rowLength = @editor.lineLengthForScreenRow(row) - columnsRemainingInLine = rowLength - column - - while columnCount > columnsRemainingInLine and row < maxLines - 1 - columnCount -= columnsRemainingInLine - columnCount-- # subtract 1 for the row move - - column = 0 - rowLength = @editor.lineLengthForScreenRow(++row) - columnsRemainingInLine = rowLength - - column = column + columnCount - @setScreenPosition({row, column}, clipDirection: 'forward') - - # Public: Moves the cursor to the top of the buffer. - moveToTop: -> - @setBufferPosition([0, 0]) - - # Public: Moves the cursor to the bottom of the buffer. - moveToBottom: -> - @setBufferPosition(@editor.getEofBufferPosition()) - - # Public: Moves the cursor to the beginning of the line. - moveToBeginningOfScreenLine: -> - @setScreenPosition([@getScreenRow(), 0]) - - # Public: Moves the cursor to the beginning of the buffer line. - moveToBeginningOfLine: -> - @setBufferPosition([@getBufferRow(), 0]) - - # Public: Moves the cursor to the beginning of the first character in the - # line. - moveToFirstCharacterOfLine: -> - screenRow = @getScreenRow() - screenLineStart = @editor.clipScreenPosition([screenRow, 0], skipSoftWrapIndentation: true) - screenLineEnd = [screenRow, Infinity] - screenLineBufferRange = @editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd]) - - firstCharacterColumn = null - @editor.scanInBufferRange /\S/, screenLineBufferRange, ({range, stop}) -> - firstCharacterColumn = range.start.column - stop() - - if firstCharacterColumn? and firstCharacterColumn isnt @getBufferColumn() - targetBufferColumn = firstCharacterColumn - else - targetBufferColumn = screenLineBufferRange.start.column - - @setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn]) - - # Public: Moves the cursor to the end of the line. - moveToEndOfScreenLine: -> - @setScreenPosition([@getScreenRow(), Infinity]) - - # Public: Moves the cursor to the end of the buffer line. - moveToEndOfLine: -> - @setBufferPosition([@getBufferRow(), Infinity]) - - # Public: Moves the cursor to the beginning of the word. - moveToBeginningOfWord: -> - @setBufferPosition(@getBeginningOfCurrentWordBufferPosition()) - - # Public: Moves the cursor to the end of the word. - moveToEndOfWord: -> - if position = @getEndOfCurrentWordBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the beginning of the next word. - moveToBeginningOfNextWord: -> - if position = @getBeginningOfNextWordBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the previous word boundary. - moveToPreviousWordBoundary: -> - if position = @getPreviousWordBoundaryBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the next word boundary. - moveToNextWordBoundary: -> - if position = @getNextWordBoundaryBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the previous subword boundary. - moveToPreviousSubwordBoundary: -> - options = {wordRegex: @subwordRegExp(backwards: true)} - if position = @getPreviousWordBoundaryBufferPosition(options) - @setBufferPosition(position) - - # Public: Moves the cursor to the next subword boundary. - moveToNextSubwordBoundary: -> - options = {wordRegex: @subwordRegExp()} - if position = @getNextWordBoundaryBufferPosition(options) - @setBufferPosition(position) - - # Public: Moves the cursor to the beginning of the buffer line, skipping all - # whitespace. - skipLeadingWhitespace: -> - position = @getBufferPosition() - scanRange = @getCurrentLineBufferRange() - endOfLeadingWhitespace = null - @editor.scanInBufferRange /^[ \t]*/, scanRange, ({range}) -> - endOfLeadingWhitespace = range.end - - @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position) - - # Public: Moves the cursor to the beginning of the next paragraph - moveToBeginningOfNextParagraph: -> - if position = @getBeginningOfNextParagraphBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the beginning of the previous paragraph - moveToBeginningOfPreviousParagraph: -> - if position = @getBeginningOfPreviousParagraphBufferPosition() - @setBufferPosition(position) - - ### - Section: Local Positions and Ranges - ### - - # Public: Returns buffer position of previous word boundary. It might be on - # the current word, or the previous word. - # - # * `options` (optional) {Object} with the following keys: - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}) - getPreviousWordBoundaryBufferPosition: (options = {}) -> - currentBufferPosition = @getBufferPosition() - previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) - scanRange = [[previousNonBlankRow ? 0, 0], currentBufferPosition] - - beginningOfWordPosition = null - @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> - if range.start.row < currentBufferPosition.row and 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 - else - beginningOfWordPosition = range.start - - if not beginningOfWordPosition?.isEqual(currentBufferPosition) - stop() - - beginningOfWordPosition or currentBufferPosition - - # Public: Returns buffer position of the next word boundary. It might be on - # the current word, or the previous word. - # - # * `options` (optional) {Object} with the following keys: - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}) - getNextWordBoundaryBufferPosition: (options = {}) -> - currentBufferPosition = @getBufferPosition() - scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] - - endOfWordPosition = null - @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> - 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 - else - endOfWordPosition = range.end - - if not endOfWordPosition?.isEqual(currentBufferPosition) - stop() - - endOfWordPosition or currentBufferPosition - - # Public: Retrieves the buffer position of where the current word starts. - # - # * `options` (optional) An {Object} with the following keys: - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - # * `includeNonWordCharacters` A {Boolean} indicating whether to include - # non-word characters in the default word regex. - # Has no effect if wordRegex is set. - # * `allowPrevious` A {Boolean} indicating whether the beginning of the - # previous word can be returned. - # - # Returns a {Range}. - getBeginningOfCurrentWordBufferPosition: (options = {}) -> - allowPrevious = options.allowPrevious ? true - currentBufferPosition = @getBufferPosition() - previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) ? 0 - scanRange = [[previousNonBlankRow, 0], currentBufferPosition] - - beginningOfWordPosition = null - @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) -> - # Ignore 'empty line' matches between '\r' and '\n' - return if matchText is '' and range.start.column isnt 0 - - if range.start.isLessThan(currentBufferPosition) - if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious - beginningOfWordPosition = range.start - stop() - - if beginningOfWordPosition? - beginningOfWordPosition - else if allowPrevious - new Point(0, 0) - else - currentBufferPosition - - # Public: Retrieves the buffer position of where the current word ends. - # - # * `options` (optional) {Object} with the following keys: - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}) - # * `includeNonWordCharacters` A Boolean indicating whether to include - # non-word characters in the default word regex. Has no effect if - # wordRegex is set. - # - # Returns a {Range}. - getEndOfCurrentWordBufferPosition: (options = {}) -> - allowNext = options.allowNext ? true - currentBufferPosition = @getBufferPosition() - scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] - - endOfWordPosition = null - @editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) -> - # Ignore 'empty line' matches between '\r' and '\n' - return if matchText is '' and range.start.column isnt 0 - - if range.end.isGreaterThan(currentBufferPosition) - if allowNext or range.start.isLessThanOrEqual(currentBufferPosition) - endOfWordPosition = range.end - stop() - - endOfWordPosition ? currentBufferPosition - - # Public: Retrieves the buffer position of where the next word starts. - # - # * `options` (optional) {Object} - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - # - # Returns a {Range} - getBeginningOfNextWordBufferPosition: (options = {}) -> - currentBufferPosition = @getBufferPosition() - start = if @isInsideWord(options) then @getEndOfCurrentWordBufferPosition(options) else currentBufferPosition - scanRange = [start, @editor.getEofBufferPosition()] - - beginningOfNextWordPosition = null - @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> - beginningOfNextWordPosition = range.start - stop() - - beginningOfNextWordPosition or currentBufferPosition - - # Public: Returns the buffer Range occupied by the word located under the cursor. - # - # * `options` (optional) {Object} - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - getCurrentWordBufferRange: (options={}) -> - startOptions = Object.assign(_.clone(options), allowPrevious: false) - endOptions = Object.assign(_.clone(options), allowNext: false) - new Range(@getBeginningOfCurrentWordBufferPosition(startOptions), @getEndOfCurrentWordBufferPosition(endOptions)) - - # Public: Returns the buffer Range for the current line. - # - # * `options` (optional) {Object} - # * `includeNewline` A {Boolean} which controls whether the Range should - # include the newline. - getCurrentLineBufferRange: (options) -> - @editor.bufferRangeForBufferRow(@getBufferRow(), options) - - # Public: Retrieves the range for the current paragraph. - # - # A paragraph is defined as a block of text surrounded by empty lines or comments. - # - # Returns a {Range}. - getCurrentParagraphBufferRange: -> - @editor.rowRangeForParagraphAtBufferRow(@getBufferRow()) - - # Public: Returns the characters preceding the cursor in the current word. - getCurrentWordPrefix: -> - @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) - - ### - Section: Visibility - ### - - ### - Section: Comparing to another cursor - ### - - # Public: Compare this cursor's buffer position to another cursor's buffer position. - # - # See {Point::compare} for more details. - # - # * `otherCursor`{Cursor} to compare against - compare: (otherCursor) -> - @getBufferPosition().compare(otherCursor.getBufferPosition()) - - ### - Section: Utilities - ### - - # Public: Deselects the current selection. - clearSelection: (options) -> - @selection?.clear(options) - - # Public: Get the RegExp used by the cursor to determine what a "word" is. - # - # * `options` (optional) {Object} with the following keys: - # * `includeNonWordCharacters` A {Boolean} indicating whether to include - # non-word characters in the regex. (default: true) - # - # Returns a {RegExp}. - wordRegExp: (options) -> - nonWordCharacters = _.escapeRegExp(@getNonWordCharacters()) - source = "^[\t ]*$|[^\\s#{nonWordCharacters}]+" - if options?.includeNonWordCharacters ? true - source += "|" + "[#{nonWordCharacters}]+" - new RegExp(source, "g") - - # Public: Get the RegExp used by the cursor to determine what a "subword" is. - # - # * `options` (optional) {Object} with the following keys: - # * `backwards` A {Boolean} indicating whether to look forwards or backwards - # for the next subword. (default: false) - # - # Returns a {RegExp}. - subwordRegExp: (options={}) -> - nonWordCharacters = @getNonWordCharacters() - lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF' - uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE' - snakeCamelSegment = "[#{uppercaseLetters}]?[#{lowercaseLetters}]+" - segments = [ - "^[\t ]+", - "[\t ]+$", - "[#{uppercaseLetters}]+(?![#{lowercaseLetters}])", - "\\d+" - ] - if options.backwards - segments.push("#{snakeCamelSegment}_*") - segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+\\s*") - else - segments.push("_*#{snakeCamelSegment}") - segments.push("\\s*[#{_.escapeRegExp(nonWordCharacters)}]+") - segments.push("_+") - new RegExp(segments.join("|"), "g") - - ### - Section: Private - ### - - getNonWordCharacters: -> - @editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray()) - - changePosition: (options, fn) -> - @clearSelection(autoscroll: false) - fn() - @autoscroll() if options.autoscroll ? @isLastCursor() - - getScreenRange: -> - {row, column} = @getScreenPosition() - new Range(new Point(row, column), new Point(row, column + 1)) - - autoscroll: (options = {}) -> - options.clip = false - @editor.scrollToScreenRange(@getScreenRange(), options) - - getBeginningOfNextParagraphBufferPosition: -> - start = @getBufferPosition() - eof = @editor.getEofBufferPosition() - scanRange = [start, eof] - - {row, column} = eof - position = new Point(row, column - 1) - - @editor.scanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) -> - position = range.start.traverse(Point(1, 0)) - stop() unless position.isEqual(start) - position - - getBeginningOfPreviousParagraphBufferPosition: -> - start = @getBufferPosition() - - {row, column} = start - scanRange = [[row-1, column], [0, 0]] - position = new Point(0, 0) - @editor.backwardsScanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) -> - position = range.start.traverse(Point(1, 0)) - stop() unless position.isEqual(start) - position diff --git a/src/cursor.js b/src/cursor.js new file mode 100644 index 000000000..712847bc7 --- /dev/null +++ b/src/cursor.js @@ -0,0 +1,754 @@ +const {Point, Range} = require('text-buffer') +const {Emitter} = require('event-kit') +const _ = require('underscore-plus') +const Model = require('./model') + +const EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g + +// Extended: The `Cursor` class represents the little blinking line identifying +// where text can be inserted. +// +// Cursors belong to {TextEditor}s and have some metadata attached in the form +// of a {DisplayMarker}. +module.exports = +class Cursor extends Model { + // Instantiated by a {TextEditor} + constructor (params) { + super(params) + this.editor = params.editor + this.marker = params.marker + this.emitter = new Emitter() + } + + destroy () { + this.marker.destroy() + } + + /* + Section: Event Subscription + */ + + // Public: Calls your `callback` when the cursor has been moved. + // + // * `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. + onDidChangePosition (callback) { + return this.emitter.on('did-change-position', callback) + } + + // Public: Calls your `callback` when the cursor 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: Managing Cursor Position + */ + + // Public: Moves a cursor to a given screen position. + // + // * `screenPosition` {Array} of two numbers: the screen row, and the screen column. + // * `options` (optional) {Object} with the following keys: + // * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever + // the cursor moves to. + setScreenPosition (screenPosition, options = {}) { + this.changePosition(options, () => { + this.marker.setHeadScreenPosition(screenPosition, options) + }) + } + + // Public: Returns the screen position of the cursor as a {Point}. + getScreenPosition () { + return this.marker.getHeadScreenPosition() + } + + // Public: Moves a cursor to a given buffer position. + // + // * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. + // * `options` (optional) {Object} with the following keys: + // * `autoscroll` {Boolean} indicating whether to autoscroll to the new + // position. Defaults to `true` if this is the most recently added cursor, + // `false` otherwise. + setBufferPosition (bufferPosition, options = {}) { + this.changePosition(options, () => { + this.marker.setHeadBufferPosition(bufferPosition, options) + }) + } + + // Public: Returns the current buffer position as an Array. + getBufferPosition () { + return this.marker.getHeadBufferPosition() + } + + // Public: Returns the cursor's current screen row. + getScreenRow () { + return this.getScreenPosition().row + } + + // Public: Returns the cursor's current screen column. + getScreenColumn () { + return this.getScreenPosition().column + } + + // Public: Retrieves the cursor's current buffer row. + getBufferRow () { + return this.getBufferPosition().row + } + + // Public: Returns the cursor's current buffer column. + getBufferColumn () { + return this.getBufferPosition().column + } + + // Public: Returns the cursor's current buffer row of text excluding its line + // ending. + getCurrentBufferLine () { + return this.editor.lineTextForBufferRow(this.getBufferRow()) + } + + // Public: Returns whether the cursor is at the start of a line. + isAtBeginningOfLine () { + return this.getBufferPosition().column === 0 + } + + // Public: Returns whether the cursor is on the line return character. + isAtEndOfLine () { + return this.getBufferPosition().isEqual(this.getCurrentLineBufferRange().end) + } + + /* + Section: Cursor Position Details + */ + + // Public: Returns the underlying {DisplayMarker} for the cursor. + // Useful with overlay {Decoration}s. + getMarker () { return this.marker } + + // Public: Identifies if the cursor is surrounded by whitespace. + // + // "Surrounded" here means that the character directly before and after the + // cursor are both whitespace. + // + // Returns a {Boolean}. + isSurroundedByWhitespace () { + const {row, column} = this.getBufferPosition() + const range = [[row, column - 1], [row, column + 1]] + return /^\s+$/.test(this.editor.getTextInBufferRange(range)) + } + + // Public: Returns whether the cursor is currently between a word and non-word + // character. The non-word characters are defined by the + // `editor.nonWordCharacters` config value. + // + // This method returns false if the character before or after the cursor is + // whitespace. + // + // Returns a Boolean. + isBetweenWordAndNonWord () { + if (this.isAtBeginningOfLine() || this.isAtEndOfLine()) return false + + const {row, column} = this.getBufferPosition() + const range = [[row, column - 1], [row, column + 1]] + const text = this.editor.getTextInBufferRange(range) + if (/\s/.test(text[0]) || /\s/.test(text[1])) return false + + const nonWordCharacters = this.getNonWordCharacters() + return nonWordCharacters.includes(text[0]) !== nonWordCharacters.includes(text[1]) + } + + // Public: Returns whether this cursor is between a word's start and end. + // + // * `options` (optional) {Object} + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}). + // + // Returns a {Boolean} + isInsideWord (options) { + const {row, column} = this.getBufferPosition() + const range = [[row, column], [row, Infinity]] + const text = this.editor.getTextInBufferRange(range) + return text.search((options && options.wordRegex) || this.wordRegExp()) === 0 + } + + // Public: Returns the indentation level of the current line. + getIndentLevel () { + if (this.editor.getSoftTabs()) { + return this.getBufferColumn() / this.editor.getTabLength() + } else { + return this.getBufferColumn() + } + } + + // Public: Retrieves the scope descriptor for the cursor's current position. + // + // Returns a {ScopeDescriptor} + getScopeDescriptor () { + return this.editor.scopeDescriptorForBufferPosition(this.getBufferPosition()) + } + + // Public: Returns true if this cursor has no non-whitespace characters before + // its current position. + hasPrecedingCharactersOnLine () { + const bufferPosition = this.getBufferPosition() + const line = this.editor.lineTextForBufferRow(bufferPosition.row) + const firstCharacterColumn = line.search(/\S/) + + if (firstCharacterColumn === -1) { + return false + } else { + return bufferPosition.column > firstCharacterColumn + } + } + + // Public: Identifies if this cursor is the last in the {TextEditor}. + // + // "Last" is defined as the most recently added cursor. + // + // Returns a {Boolean}. + isLastCursor () { + return this === this.editor.getLastCursor() + } + + /* + Section: Moving the Cursor + */ + + // Public: Moves the cursor up one screen row. + // + // * `rowCount` (optional) {Number} number of rows to move (default: 1) + // * `options` (optional) {Object} with the following keys: + // * `moveToEndOfSelection` if true, move to the left of the selection if a + // selection exists. + moveUp (rowCount = 1, {moveToEndOfSelection} = {}) { + let row, column + const range = this.marker.getScreenRange() + if (moveToEndOfSelection && !range.isEmpty()) { + ({row, column} = range.start) + } else { + ({row, column} = this.getScreenPosition()) + } + + if (this.goalColumn != null) column = this.goalColumn + this.setScreenPosition({row: row - rowCount, column}, {skipSoftWrapIndentation: true}) + this.goalColumn = column + } + + // Public: Moves the cursor down one screen row. + // + // * `rowCount` (optional) {Number} number of rows to move (default: 1) + // * `options` (optional) {Object} with the following keys: + // * `moveToEndOfSelection` if true, move to the left of the selection if a + // selection exists. + moveDown (rowCount = 1, {moveToEndOfSelection} = {}) { + let row, column + const range = this.marker.getScreenRange() + if (moveToEndOfSelection && !range.isEmpty()) { + ({row, column} = range.end) + } else { + ({row, column} = this.getScreenPosition()) + } + + if (this.goalColumn != null) column = this.goalColumn + this.setScreenPosition({row: row + rowCount, column}, {skipSoftWrapIndentation: true}) + this.goalColumn = column + } + + // Public: Moves the cursor left one screen column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + // * `options` (optional) {Object} with the following keys: + // * `moveToEndOfSelection` if true, move to the left of the selection if a + // selection exists. + moveLeft (columnCount = 1, {moveToEndOfSelection} = {}) { + const range = this.marker.getScreenRange() + if (moveToEndOfSelection && !range.isEmpty()) { + this.setScreenPosition(range.start) + } else { + let {row, column} = this.getScreenPosition() + + while (columnCount > column && row > 0) { + columnCount -= column + column = this.editor.lineLengthForScreenRow(--row) + columnCount-- + } // subtract 1 for the row move + + column = column - columnCount + this.setScreenPosition({row, column}, {clipDirection: 'backward'}) + } + } + + // Public: Moves the cursor right one screen column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + // * `options` (optional) {Object} with the following keys: + // * `moveToEndOfSelection` if true, move to the right of the selection if a + // selection exists. + moveRight (columnCount = 1, {moveToEndOfSelection} = {}) { + const range = this.marker.getScreenRange() + if (moveToEndOfSelection && !range.isEmpty()) { + this.setScreenPosition(range.end) + } else { + let {row, column} = this.getScreenPosition() + const maxLines = this.editor.getScreenLineCount() + let rowLength = this.editor.lineLengthForScreenRow(row) + let columnsRemainingInLine = rowLength - column + + while (columnCount > columnsRemainingInLine && row < maxLines - 1) { + columnCount -= columnsRemainingInLine + columnCount-- // subtract 1 for the row move + + column = 0 + rowLength = this.editor.lineLengthForScreenRow(++row) + columnsRemainingInLine = rowLength + } + + column = column + columnCount + this.setScreenPosition({row, column}, {clipDirection: 'forward'}) + } + } + + // Public: Moves the cursor to the top of the buffer. + moveToTop () { + this.setBufferPosition([0, 0]) + } + + // Public: Moves the cursor to the bottom of the buffer. + moveToBottom () { + this.setBufferPosition(this.editor.getEofBufferPosition()) + } + + // Public: Moves the cursor to the beginning of the line. + moveToBeginningOfScreenLine () { + this.setScreenPosition([this.getScreenRow(), 0]) + } + + // Public: Moves the cursor to the beginning of the buffer line. + moveToBeginningOfLine () { + this.setBufferPosition([this.getBufferRow(), 0]) + } + + // Public: Moves the cursor to the beginning of the first character in the + // line. + moveToFirstCharacterOfLine () { + let targetBufferColumn + const screenRow = this.getScreenRow() + const screenLineStart = this.editor.clipScreenPosition([screenRow, 0], {skipSoftWrapIndentation: true}) + const screenLineEnd = [screenRow, Infinity] + const screenLineBufferRange = this.editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd]) + + let firstCharacterColumn = null + this.editor.scanInBufferRange(/\S/, screenLineBufferRange, ({range, stop}) => { + firstCharacterColumn = range.start.column + stop() + }) + + if (firstCharacterColumn != null && firstCharacterColumn !== this.getBufferColumn()) { + targetBufferColumn = firstCharacterColumn + } else { + targetBufferColumn = screenLineBufferRange.start.column + } + + this.setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn]) + } + + // Public: Moves the cursor to the end of the line. + moveToEndOfScreenLine () { + this.setScreenPosition([this.getScreenRow(), Infinity]) + } + + // Public: Moves the cursor to the end of the buffer line. + moveToEndOfLine () { + this.setBufferPosition([this.getBufferRow(), Infinity]) + } + + // Public: Moves the cursor to the beginning of the word. + moveToBeginningOfWord () { + this.setBufferPosition(this.getBeginningOfCurrentWordBufferPosition()) + } + + // Public: Moves the cursor to the end of the word. + moveToEndOfWord () { + const position = this.getEndOfCurrentWordBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the beginning of the next word. + moveToBeginningOfNextWord () { + const position = this.getBeginningOfNextWordBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the previous word boundary. + moveToPreviousWordBoundary () { + const position = this.getPreviousWordBoundaryBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the next word boundary. + moveToNextWordBoundary () { + const position = this.getNextWordBoundaryBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the previous subword boundary. + moveToPreviousSubwordBoundary () { + const options = {wordRegex: this.subwordRegExp({backwards: true})} + const position = this.getPreviousWordBoundaryBufferPosition(options) + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the next subword boundary. + moveToNextSubwordBoundary () { + const options = {wordRegex: this.subwordRegExp()} + const position = this.getNextWordBoundaryBufferPosition(options) + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the beginning of the buffer line, skipping all + // whitespace. + skipLeadingWhitespace () { + const position = this.getBufferPosition() + const scanRange = this.getCurrentLineBufferRange() + let endOfLeadingWhitespace = null + this.editor.scanInBufferRange(/^[ \t]*/, scanRange, ({range}) => { + endOfLeadingWhitespace = range.end + }) + + if (endOfLeadingWhitespace.isGreaterThan(position)) this.setBufferPosition(endOfLeadingWhitespace) + } + + // Public: Moves the cursor to the beginning of the next paragraph + moveToBeginningOfNextParagraph () { + const position = this.getBeginningOfNextParagraphBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the beginning of the previous paragraph + moveToBeginningOfPreviousParagraph () { + const position = this.getBeginningOfPreviousParagraphBufferPosition() + if (position) this.setBufferPosition(position) + } + + /* + Section: Local Positions and Ranges + */ + + // Public: Returns buffer position of previous word boundary. It might be on + // the current word, or the previous word. + // + // * `options` (optional) {Object} with the following keys: + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}) + getPreviousWordBoundaryBufferPosition (options = {}) { + const currentBufferPosition = this.getBufferPosition() + const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row) + const scanRange = [[previousNonBlankRow || 0, 0], currentBufferPosition] + + let beginningOfWordPosition + this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => { + 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 + } else { + beginningOfWordPosition = range.start + } + + if (!beginningOfWordPosition.isEqual(currentBufferPosition)) stop() + }) + + return beginningOfWordPosition || currentBufferPosition + } + + // Public: Returns buffer position of the next word boundary. It might be on + // the current word, or the previous word. + // + // * `options` (optional) {Object} with the following keys: + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}) + getNextWordBoundaryBufferPosition (options = {}) { + const currentBufferPosition = this.getBufferPosition() + const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()] + + let endOfWordPosition + this.editor.scanInBufferRange((options.wordRegex != null ? options.wordRegex : this.wordRegExp()), scanRange, function ({range, stop}) { + 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 + } else { + endOfWordPosition = range.end + } + + if (!endOfWordPosition.isEqual(currentBufferPosition)) stop() + }) + + return endOfWordPosition || currentBufferPosition + } + + // Public: Retrieves the buffer position of where the current word starts. + // + // * `options` (optional) An {Object} with the following keys: + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}). + // * `includeNonWordCharacters` A {Boolean} indicating whether to include + // non-word characters in the default word regex. + // Has no effect if wordRegex is set. + // * `allowPrevious` A {Boolean} indicating whether the beginning of the + // previous word can be returned. + // + // Returns a {Range}. + getBeginningOfCurrentWordBufferPosition (options = {}) { + const allowPrevious = options.allowPrevious !== false + const currentBufferPosition = this.getBufferPosition() + const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row) || 0 + const scanRange = [[previousNonBlankRow, 0], currentBufferPosition] + + let beginningOfWordPosition + this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(options), scanRange, ({range, matchText, stop}) => { + // Ignore 'empty line' matches between '\r' and '\n' + if ((matchText === '') && range.start.column !== 0) return + + if (range.start.isLessThan(currentBufferPosition)) { + if (range.end.isGreaterThanOrEqual(currentBufferPosition) || allowPrevious) { + beginningOfWordPosition = range.start + } + stop() + } + }) + + if (beginningOfWordPosition) { + return beginningOfWordPosition + } else if (allowPrevious) { + return new Point(0, 0) + } else { + return currentBufferPosition + } + } + + // Public: Retrieves the buffer position of where the current word ends. + // + // * `options` (optional) {Object} with the following keys: + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}) + // * `includeNonWordCharacters` A Boolean indicating whether to include + // non-word characters in the default word regex. Has no effect if + // wordRegex is set. + // + // Returns a {Range}. + getEndOfCurrentWordBufferPosition (options = {}) { + const allowNext = options.allowNext !== false + const currentBufferPosition = this.getBufferPosition() + const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()] + + let endOfWordPosition + this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(options), scanRange, ({range, matchText, stop}) => { + // Ignore 'empty line' matches between '\r' and '\n' + if (matchText === '' && range.start.column !== 0) return + + if (range.end.isGreaterThan(currentBufferPosition)) { + if (allowNext || range.start.isLessThanOrEqual(currentBufferPosition)) { + endOfWordPosition = range.end + } + stop() + } + }) + + return endOfWordPosition || currentBufferPosition + } + + // Public: Retrieves the buffer position of where the next word starts. + // + // * `options` (optional) {Object} + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}). + // + // Returns a {Range} + getBeginningOfNextWordBufferPosition (options = {}) { + const currentBufferPosition = this.getBufferPosition() + const start = this.isInsideWord(options) ? this.getEndOfCurrentWordBufferPosition(options) : currentBufferPosition + const scanRange = [start, this.editor.getEofBufferPosition()] + + let beginningOfNextWordPosition + this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => { + beginningOfNextWordPosition = range.start + stop() + }) + + return beginningOfNextWordPosition || currentBufferPosition + } + + // Public: Returns the buffer Range occupied by the word located under the cursor. + // + // * `options` (optional) {Object} + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}). + getCurrentWordBufferRange (options = {}) { + const startOptions = Object.assign(_.clone(options), {allowPrevious: false}) + const endOptions = Object.assign(_.clone(options), {allowNext: false}) + return new Range(this.getBeginningOfCurrentWordBufferPosition(startOptions), this.getEndOfCurrentWordBufferPosition(endOptions)) + } + + // Public: Returns the buffer Range for the current line. + // + // * `options` (optional) {Object} + // * `includeNewline` A {Boolean} which controls whether the Range should + // include the newline. + getCurrentLineBufferRange (options) { + return this.editor.bufferRangeForBufferRow(this.getBufferRow(), options) + } + + // Public: Retrieves the range for the current paragraph. + // + // A paragraph is defined as a block of text surrounded by empty lines or comments. + // + // Returns a {Range}. + getCurrentParagraphBufferRange () { + return this.editor.rowRangeForParagraphAtBufferRow(this.getBufferRow()) + } + + // Public: Returns the characters preceding the cursor in the current word. + getCurrentWordPrefix () { + return this.editor.getTextInBufferRange([this.getBeginningOfCurrentWordBufferPosition(), this.getBufferPosition()]) + } + + /* + Section: Visibility + */ + + /* + Section: Comparing to another cursor + */ + + // Public: Compare this cursor's buffer position to another cursor's buffer position. + // + // See {Point::compare} for more details. + // + // * `otherCursor`{Cursor} to compare against + compare (otherCursor) { + return this.getBufferPosition().compare(otherCursor.getBufferPosition()) + } + + /* + Section: Utilities + */ + + // Public: Deselects the current selection. + clearSelection (options) { + if (this.selection) this.selection.clear(options) + } + + // Public: Get the RegExp used by the cursor to determine what a "word" is. + // + // * `options` (optional) {Object} with the following keys: + // * `includeNonWordCharacters` A {Boolean} indicating whether to include + // non-word characters in the regex. (default: true) + // + // Returns a {RegExp}. + wordRegExp (options) { + const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters()) + let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+` + if (!options || options.includeNonWordCharacters !== false) { + source += `|${`[${nonWordCharacters}]+`}` + } + return new RegExp(source, 'g') + } + + // Public: Get the RegExp used by the cursor to determine what a "subword" is. + // + // * `options` (optional) {Object} with the following keys: + // * `backwards` A {Boolean} indicating whether to look forwards or backwards + // for the next subword. (default: false) + // + // Returns a {RegExp}. + subwordRegExp (options = {}) { + const nonWordCharacters = this.getNonWordCharacters() + const lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF' + const uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE' + const snakeCamelSegment = `[${uppercaseLetters}]?[${lowercaseLetters}]+` + const segments = [ + '^[\t ]+', + '[\t ]+$', + `[${uppercaseLetters}]+(?![${lowercaseLetters}])`, + '\\d+' + ] + if (options.backwards) { + segments.push(`${snakeCamelSegment}_*`) + segments.push(`[${_.escapeRegExp(nonWordCharacters)}]+\\s*`) + } else { + segments.push(`_*${snakeCamelSegment}`) + segments.push(`\\s*[${_.escapeRegExp(nonWordCharacters)}]+`) + } + segments.push('_+') + return new RegExp(segments.join('|'), 'g') + } + + /* + Section: Private + */ + + getNonWordCharacters () { + return this.editor.getNonWordCharacters(this.getScopeDescriptor().getScopesArray()) + } + + changePosition (options, fn) { + this.clearSelection({autoscroll: false}) + fn() + const autoscroll = (options && options.autoscroll != null) + ? options.autoscroll + : this.isLastCursor() + if (autoscroll) this.autoscroll() + } + + getScreenRange () { + const {row, column} = this.getScreenPosition() + return new Range(new Point(row, column), new Point(row, column + 1)) + } + + autoscroll (options = {}) { + options.clip = false + this.editor.scrollToScreenRange(this.getScreenRange(), options) + } + + getBeginningOfNextParagraphBufferPosition () { + const start = this.getBufferPosition() + const eof = this.editor.getEofBufferPosition() + const scanRange = [start, eof] + + const {row, column} = eof + let position = new Point(row, column - 1) + + this.editor.scanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => { + position = range.start.traverse(Point(1, 0)) + if (!position.isEqual(start)) stop() + }) + return position + } + + getBeginningOfPreviousParagraphBufferPosition () { + const start = this.getBufferPosition() + + const {row, column} = start + const scanRange = [[row - 1, column], [0, 0]] + let position = new Point(0, 0) + this.editor.backwardsScanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => { + position = range.start.traverse(Point(1, 0)) + if (!position.isEqual(start)) stop() + }) + return position + } +} From 6c4a9c1987708fa795967c5507d2500c309a8b4c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2017 15:04:21 -0700 Subject: [PATCH 287/448] Optimize getCurrentWordBufferRange --- src/cursor.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cursor.js b/src/cursor.js index 712847bc7..37fbcb78b 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -598,9 +598,14 @@ class Cursor extends Model { // * `wordRegex` A {RegExp} indicating what constitutes a "word" // (default: {::wordRegExp}). getCurrentWordBufferRange (options = {}) { - const startOptions = Object.assign(_.clone(options), {allowPrevious: false}) - const endOptions = Object.assign(_.clone(options), {allowNext: false}) - return new Range(this.getBeginningOfCurrentWordBufferPosition(startOptions), this.getEndOfCurrentWordBufferPosition(endOptions)) + const position = this.getBufferPosition() + const ranges = this.editor.buffer.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + new Range(new Point(position.row, 0), new Point(position.row, Infinity)) + ) + return ranges.find(range => + range.end.column >= position.column && range.start.column <= position.column + ) || new Range(position, position) } // Public: Returns the buffer Range for the current line. From 43aa3c788fd10fe9879f1cf27d37d94df855d276 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2017 16:50:59 -0700 Subject: [PATCH 288/448] Optimize cursor methods that find the current word --- package.json | 2 +- src/cursor.js | 64 +++++++++++++++++++++++---------------------------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 1fd8ca343..9726214e9 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.4.0", + "text-buffer": "13.4.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/src/cursor.js b/src/cursor.js index 37fbcb78b..2e300ca9d 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -514,30 +514,24 @@ class Cursor extends Model { // Returns a {Range}. getBeginningOfCurrentWordBufferPosition (options = {}) { const allowPrevious = options.allowPrevious !== false - const currentBufferPosition = this.getBufferPosition() - const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row) || 0 - const scanRange = [[previousNonBlankRow, 0], currentBufferPosition] + const position = this.getBufferPosition() - let beginningOfWordPosition - this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(options), scanRange, ({range, matchText, stop}) => { - // Ignore 'empty line' matches between '\r' and '\n' - if ((matchText === '') && range.start.column !== 0) return + const scanRange = allowPrevious + ? new Range(new Point(position.row - 1, 0), position) + : new Range(new Point(position.row, 0), position) - if (range.start.isLessThan(currentBufferPosition)) { - if (range.end.isGreaterThanOrEqual(currentBufferPosition) || allowPrevious) { - beginningOfWordPosition = range.start - } - stop() - } - }) + const ranges = this.editor.buffer.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + scanRange + ) - if (beginningOfWordPosition) { - return beginningOfWordPosition - } else if (allowPrevious) { - return new Point(0, 0) - } else { - return currentBufferPosition + let result + for (let range of ranges) { + if (position.isLessThanOrEqual(range.start)) break + if (allowPrevious || position.isLessThanOrEqual(range.end)) result = range.start } + + return result || (allowPrevious ? new Point(0, 0) : position) } // Public: Retrieves the buffer position of where the current word ends. @@ -552,23 +546,23 @@ class Cursor extends Model { // Returns a {Range}. getEndOfCurrentWordBufferPosition (options = {}) { const allowNext = options.allowNext !== false - const currentBufferPosition = this.getBufferPosition() - const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()] + const position = this.getBufferPosition() - let endOfWordPosition - this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(options), scanRange, ({range, matchText, stop}) => { - // Ignore 'empty line' matches between '\r' and '\n' - if (matchText === '' && range.start.column !== 0) return + const scanRange = allowNext + ? new Range(position, new Point(position.row + 2, 0)) + : new Range(position, new Point(position.row, Infinity)) - if (range.end.isGreaterThan(currentBufferPosition)) { - if (allowNext || range.start.isLessThanOrEqual(currentBufferPosition)) { - endOfWordPosition = range.end - } - stop() - } - }) + const ranges = this.editor.buffer.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + scanRange + ) - return endOfWordPosition || currentBufferPosition + for (let range of ranges) { + if (position.isLessThan(range.start) && !allowNext) break + if (position.isLessThan(range.end)) return range.end + } + + return allowNext ? this.editor.getEofBufferPosition() : position } // Public: Retrieves the buffer position of where the next word starts. @@ -666,7 +660,7 @@ class Cursor extends Model { // Returns a {RegExp}. wordRegExp (options) { const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters()) - let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+` + let source = `^[\t\r ]*$|[^\\s${nonWordCharacters}]+` if (!options || options.includeNonWordCharacters !== false) { source += `|${`[${nonWordCharacters}]+`}` } From 887ebd913b6e8afd05bb6dbe1ba78e2cf72372ae Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 30 Sep 2017 23:38:16 -0700 Subject: [PATCH 289/448] :arrow_up: text-buffer --- package.json | 2 +- src/cursor.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9726214e9..b08f4c6bd 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.4.2", + "text-buffer": "13.5.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/src/cursor.js b/src/cursor.js index 2e300ca9d..004921b94 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -520,7 +520,7 @@ class Cursor extends Model { ? new Range(new Point(position.row - 1, 0), position) : new Range(new Point(position.row, 0), position) - const ranges = this.editor.buffer.buffer.findAllInRangeSync( + const ranges = this.editor.buffer.findAllInRangeSync( options.wordRegex || this.wordRegExp(), scanRange ) @@ -552,7 +552,7 @@ class Cursor extends Model { ? new Range(position, new Point(position.row + 2, 0)) : new Range(position, new Point(position.row, Infinity)) - const ranges = this.editor.buffer.buffer.findAllInRangeSync( + const ranges = this.editor.buffer.findAllInRangeSync( options.wordRegex || this.wordRegExp(), scanRange ) @@ -593,7 +593,7 @@ class Cursor extends Model { // (default: {::wordRegExp}). getCurrentWordBufferRange (options = {}) { const position = this.getBufferPosition() - const ranges = this.editor.buffer.buffer.findAllInRangeSync( + const ranges = this.editor.buffer.findAllInRangeSync( options.wordRegex || this.wordRegExp(), new Range(new Point(position.row, 0), new Point(position.row, Infinity)) ) From 97e07fc59cc2ebf6c17a6a6ebf94984111a38129 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 1 Oct 2017 09:38:38 -0700 Subject: [PATCH 290/448] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b08f4c6bd..d2fa2961c 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.1", + "text-buffer": "13.5.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 8a6ef7061126ed5d51949f882363642a23ac1902 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 1 Oct 2017 09:39:51 -0700 Subject: [PATCH 291/448] Fix comment misplaced by decaffeinate --- src/cursor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cursor.js b/src/cursor.js index 004921b94..1425f5b49 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -281,8 +281,8 @@ class Cursor extends Model { while (columnCount > column && row > 0) { columnCount -= column column = this.editor.lineLengthForScreenRow(--row) - columnCount-- - } // subtract 1 for the row move + columnCount-- // subtract 1 for the row move + } column = column - columnCount this.setScreenPosition({row, column}, {clipDirection: 'backward'}) From c019eb2cf9a1f149df36996f1fd16482f68d6a75 Mon Sep 17 00:00:00 2001 From: Arnav Borborah Date: Sun, 1 Oct 2017 12:50:13 -0400 Subject: [PATCH 292/448] Shortened last three methods in color class Used the ternary operator to shorten the last three methods in color.js --- src/color.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/color.js b/src/color.js index 6208d6837..2f2947e16 100644 --- a/src/color.js +++ b/src/color.js @@ -112,27 +112,15 @@ export default class Color { function parseColor (colorString) { const color = parseInt(colorString, 10) - if (isNaN(color)) { - return 0 - } else { - return Math.min(Math.max(color, 0), 255) - } + return isNaN(color) ? 0 : Math.min(Math.max(color, 0), 255) } function parseAlpha (alphaString) { const alpha = parseFloat(alphaString) - if (isNaN(alpha)) { - return 1 - } else { - return Math.min(Math.max(alpha, 0), 1) - } + return isNaN(alpha) ? 1 : Math.min(Math.max(alpha, 0), 1) } function numberToHexString (number) { const hex = number.toString(16) - if (number < 16) { - return `0${hex}` - } else { - return hex - } + return number < 16 ? `0${hex}` : hex } From 9444ce6ac507ff1f299cffb68979225f8d6d22a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Oct 2017 13:15:06 -0700 Subject: [PATCH 293/448] :arrow_up: encoding-selector --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c908cf70..12d9e5d5b 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "dalek": "0.2.1", "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", - "encoding-selector": "0.23.6", + "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", "fuzzy-finder": "1.6.1", From 3a3c58e04e37f3fe653d742134a0067bc8adb8dc Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Mon, 2 Oct 2017 15:00:01 -0600 Subject: [PATCH 294/448] :arrow_up: autocomplete-plus@2.36.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12d9e5d5b..d3612e972 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.0", + "autocomplete-plus": "2.36.1", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From 88118e508a12f7918104260ae1ff8f5a12dc9161 Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Tue, 3 Oct 2017 09:36:38 -0700 Subject: [PATCH 295/448] Make the comment instructions easier to follow --- .github/stale.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 2378bc1dc..1f0f5820b 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -18,8 +18,14 @@ markComment: > 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 then - comment in the issue and verify that you can still reproduce in the latest version. + 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 + 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 1075b39d99ab864ea297960b23abf9015588e3cc Mon Sep 17 00:00:00 2001 From: Lee Dohm <1038121+lee-dohm@users.noreply.github.com> Date: Tue, 3 Oct 2017 09:50:56 -0700 Subject: [PATCH 296/448] Grammarize --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index 1f0f5820b..2adc475b5 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -17,7 +17,7 @@ markComment: > 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 + [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 From 293b52d797208568f527c6ee97ddd3a2f4a20e12 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 3 Oct 2017 10:32:29 -0600 Subject: [PATCH 297/448] Fix rendering bug when folds hide the vertical scrollbar w/ soft wrap on --- spec/text-editor-component-spec.js | 25 +++++++++++++++++++++++++ src/text-editor-component.js | 26 ++++++++++++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 82764c438..fa72e42ef 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -286,6 +286,31 @@ describe('TextEditorComponent', () => { expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeNull() }) + it('gracefully handles folds that change the soft-wrap boundary by causing the vertical scrollbar to disappear (regression)', async () => { + const text = ('x'.repeat(100) + '\n') + 'y\n'.repeat(28) + ' z\n'.repeat(50) + const {component, element, editor} = buildComponent({text, height: 1000, width: 500}) + + element.addEventListener('scroll', (event) => { + event.stopPropagation() + }, true) + + editor.setSoftWrapped(true) + jasmine.attachToDOM(element) + await component.getNextUpdatePromise() + + const firstScreenLineLengthWithVerticalScrollbar = element.querySelector('.line').textContent.length + + setScrollTop(component, 620) + await component.getNextUpdatePromise() + + editor.foldBufferRow(28) + await component.getNextUpdatePromise() + + const firstLineElement = element.querySelector('.line') + expect(firstLineElement.dataset.screenRow).toBe('0') + expect(firstLineElement.textContent.length).toBeGreaterThan(firstScreenLineLengthWithVerticalScrollbar) + }) + it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => { const {component, element, editor} = buildComponent({text: 'abc\n de\nfghijklm\n no', softWrapped: true}) await setEditorWidthInCharacters(component, 5) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5c3b6d1bc..3060b6857 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -362,7 +362,7 @@ class TextEditorComponent { this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column) this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column) } - this.populateVisibleRowRange() + this.populateVisibleRowRange(this.getRenderedStartRow()) this.populateVisibleTiles() this.queryScreenLinesToRender() this.queryLongestLine() @@ -2096,14 +2096,29 @@ class TextEditorComponent { return marginInBaseCharacters * this.getBaseCharacterWidth() } + // This method is called at the beginning of a frame render to relay any + // potential changes in the editor's width into the model before proceeding. updateModelSoftWrapColumn () { const {model} = this.props const newEditorWidthInChars = this.getScrollContainerClientWidthInBaseCharacters() if (newEditorWidthInChars !== model.getEditorWidthInChars()) { this.suppressUpdates = true + + const renderedStartRow = this.getRenderedStartRow() this.props.model.setEditorWidthInChars(newEditorWidthInChars) - // Wrapping may cause a vertical scrollbar to appear, which will change the width again. + + // Relaying a change in to the editor's client width may cause the + // vertical scrollbar to appear or disappear, which causes the editor's + // client width to change *again*. Make sure the display layer is fully + // populated for the visible area before recalculating the editor's + // width in characters. Then update the display layer *again* just in + // case a change in scrollbar visibility causes lines to wrap + // differently. We capture the renderedStartRow before resetting the + // display layer because once it has been reset, we can't compute the + // rendered start row accurately. 😥 + this.populateVisibleRowRange(renderedStartRow) this.props.model.setEditorWidthInChars(this.getScrollContainerClientWidthInBaseCharacters()) + this.suppressUpdates = false } } @@ -2867,12 +2882,11 @@ class TextEditorComponent { } } - // Ensure the spatial index is populated with rows that are currently - // visible so we *at least* get the longest row in the visible range. - populateVisibleRowRange () { + // Ensure the spatial index is populated with rows that are currently visible + populateVisibleRowRange (renderedStartRow) { const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() const visibleTileCount = Math.ceil(editorHeightInTiles) + 1 - const lastRenderedRow = this.getRenderedStartRow() + (visibleTileCount * this.getRowsPerTile()) + const lastRenderedRow = renderedStartRow + (visibleTileCount * this.getRowsPerTile()) this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, lastRenderedRow) } From 44d6868855c16f8d608ea321c907347c3d9f1936 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2017 10:14:45 -0700 Subject: [PATCH 298/448] Preserve indentation when toggling comments on whitespace-only lines --- spec/tokenized-buffer-spec.js | 2 +- src/tokenized-buffer.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index b2324d392..ba43f9ff3 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -807,7 +807,7 @@ describe('TokenizedBuffer', () => { buffer.setText(' ') tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe('// ') + expect(buffer.lineForRow(0)).toBe(' // ') buffer.setText(' a\n \n b') tokenizedBuffer.toggleLineCommentsForBufferRows(0, 2) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 1d52411ae..8b7569cca 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -222,13 +222,18 @@ class TokenizedBuffer { } } else { let minIndentLevel = null + let minBlankIndentLevel for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) if (NON_WHITESPACE_REGEX.test(line)) { const indentLevel = this.indentLevelForLine(line) if (minIndentLevel == null || indentLevel < minIndentLevel) minIndentLevel = indentLevel + } else if (minIndentLevel == null) { + const indentLevel = this.indentLevelForLine(line) + if (minBlankIndentLevel == null || indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel } } + if (minIndentLevel == null) minIndentLevel = minBlankIndentLevel if (minIndentLevel == null) minIndentLevel = 0 const tabLength = this.getTabLength() From 96d8b3db55ba7ffc1ef502f67ed4e34a2377782a Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Tue, 3 Oct 2017 11:39:34 -0600 Subject: [PATCH 299/448] :arrow_up: autocomplete-plus@2.36.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3612e972..70c3f2f34 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.1", + "autocomplete-plus": "2.36.2", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From 4d057a16d6d28cd17fccbf42ff0c5293e0e4e4a1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2017 10:01:53 -0700 Subject: [PATCH 300/448] Prompt to save when unloading if editor is in conflict --- spec/text-editor-spec.coffee | 31 ---------------------- spec/text-editor-spec.js | 50 ++++++++++++++++++++++++++++++++++++ src/text-editor.coffee | 2 +- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index efe3bf048..53011fdcc 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5324,37 +5324,6 @@ describe "TextEditor", -> [[6, 3], [6, 4]], ]) - describe ".shouldPromptToSave()", -> - it "returns true when buffer changed", -> - jasmine.unspy(editor, 'shouldPromptToSave') - expect(editor.shouldPromptToSave()).toBeFalsy() - buffer.setText('changed') - expect(editor.shouldPromptToSave()).toBeTruthy() - - it "returns false when an edit session's buffer is in use by more than one session", -> - jasmine.unspy(editor, 'shouldPromptToSave') - buffer.setText('changed') - - editor2 = null - waitsForPromise -> - atom.workspace.getActivePane().splitRight() - atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor2 = o - - runs -> - expect(editor.shouldPromptToSave()).toBeFalsy() - editor2.destroy() - expect(editor.shouldPromptToSave()).toBeTruthy() - - it "returns false when close of a window requested and edit session opened inside project", -> - jasmine.unspy(editor, 'shouldPromptToSave') - buffer.setText('changed') - expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: true)).toBeFalsy() - - it "returns true when close of a window requested and edit session opened without project", -> - jasmine.unspy(editor, 'shouldPromptToSave') - buffer.setText('changed') - expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: false)).toBeTruthy() - describe "when the editor contains surrogate pair characters", -> it "correctly backspaces over them", -> editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 82ad3bc90..c81df8089 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -1,3 +1,5 @@ +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') @@ -8,6 +10,54 @@ describe('TextEditor', () => { editor.destroy() }) + describe('.shouldPromptToSave()', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js') + jasmine.unspy(editor, 'shouldPromptToSave') + }) + + it('returns true when buffer has unsaved changes', () => { + expect(editor.shouldPromptToSave()).toBeFalsy() + editor.setText('changed') + expect(editor.shouldPromptToSave()).toBeTruthy() + }) + + it("returns false when an editor's buffer is in use by more than one buffer", async () => { + editor.setText('changed') + + atom.workspace.getActivePane().splitRight() + const editor2 = await atom.workspace.open('sample.js', {autoIndent: false}) + expect(editor.shouldPromptToSave()).toBeFalsy() + + editor2.destroy() + expect(editor.shouldPromptToSave()).toBeTruthy() + }) + + it('returns true when the window is closing if the file has changed on disk', async () => { + jasmine.useRealClock() + + editor.setText('initial stuff') + await editor.saveAs(temp.openSync('test-file').path) + + editor.setText('other stuff') + fs.writeFileSync(editor.getPath(), 'new stuff') + expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeFalsy() + + await new Promise(resolve => editor.onDidConflict(resolve)) + expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeTruthy() + }) + + it('returns false when the window is closing and the project has one or more directory paths', () => { + editor.setText('changed') + expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeFalsy() + }) + + it('returns false when the window is closing and the project has no directory paths', () => { + editor.setText('changed') + expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: false})).toBeTruthy() + }) + }) + describe('folding', () => { beforeEach(async () => { await atom.packages.activatePackage('language-javascript') diff --git a/src/text-editor.coffee b/src/text-editor.coffee index c9813e445..c00508f09 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -961,7 +961,7 @@ class TextEditor extends Model # this editor. shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) -> if windowCloseRequested and projectHasPaths and atom.stateStore.isConnected() - false + @buffer.isInConflict() else @isModified() and not @buffer.hasMultipleEditors() From 4975f659c0932a96f931acf063655f1bdb2861ee Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2017 11:03:37 -0700 Subject: [PATCH 301/448] :art: toggleLineCommentsForBufferRows --- src/tokenized-buffer.js | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 8b7569cca..4eed4110a 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -199,17 +199,20 @@ class TokenizedBuffer { }) } } else { - let allBlank = true - let allBlankOrCommented = true - + let hasCommentedLines = false + let hasUncommentedLines = false for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) - const blank = line.match(/^\s*$/) - if (!blank) allBlank = false - if (!blank && !commentStartRegex.testSync(line)) allBlankOrCommented = false + if (NON_WHITESPACE_REGEX.test(line)) { + if (commentStartRegex.testSync(line)) { + hasCommentedLines = true + } else { + hasUncommentedLines = true + } + } } - const shouldUncomment = allBlankOrCommented && !allBlank + const shouldUncomment = hasCommentedLines && !hasUncommentedLines if (shouldUncomment) { for (let row = start; row <= end; row++) { @@ -221,20 +224,22 @@ class TokenizedBuffer { } } } else { - let minIndentLevel = null - let minBlankIndentLevel + 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)) { - const indentLevel = this.indentLevelForLine(line) - if (minIndentLevel == null || indentLevel < minIndentLevel) minIndentLevel = indentLevel - } else if (minIndentLevel == null) { - const indentLevel = this.indentLevelForLine(line) - if (minBlankIndentLevel == null || indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel + if (indentLevel < minIndentLevel) minIndentLevel = indentLevel + } else { + if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel } } - if (minIndentLevel == null) minIndentLevel = minBlankIndentLevel - if (minIndentLevel == null) minIndentLevel = 0 + minIndentLevel = Number.isFinite(minIndentLevel) + ? minIndentLevel + : Number.isFinite(minBlankIndentLevel) + ? minBlankIndentLevel + : 0 const tabLength = this.getTabLength() const indentString = ' '.repeat(tabLength * minIndentLevel) From c9c495792188693ca234e4ac08efcb83b0036cf7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2017 12:31:33 -0700 Subject: [PATCH 302/448] Avoid unnecessary work in TokenizedBuffer.isFoldableAtRow --- src/tokenized-buffer.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 4eed4110a..b4bc0d41c 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -471,7 +471,7 @@ class TokenizedBuffer { } isFoldableAtRow (row) { - return this.endRowForFoldAtRow(row, 1) != null + return this.endRowForFoldAtRow(row, 1, true) != null } buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { @@ -773,27 +773,28 @@ class TokenizedBuffer { return result } - endRowForFoldAtRow (row, tabLength) { + endRowForFoldAtRow (row, tabLength, existenceOnly = false) { if (this.isRowCommented(row)) { - return this.endRowForCommentFoldAtRow(row) + return this.endRowForCommentFoldAtRow(row, existenceOnly) } else { - return this.endRowForCodeFoldAtRow(row, tabLength) + return this.endRowForCodeFoldAtRow(row, tabLength, existenceOnly) } } - endRowForCommentFoldAtRow (row) { + endRowForCommentFoldAtRow (row, existenceOnly) { if (this.isRowCommented(row - 1)) return let endRow for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) { if (!this.isRowCommented(nextRow)) break endRow = nextRow + if (existenceOnly) break } return endRow } - endRowForCodeFoldAtRow (row, tabLength) { + endRowForCodeFoldAtRow (row, tabLength, existenceOnly) { let foldEndRow const line = this.buffer.lineForRow(row) if (!NON_WHITESPACE_REGEX.test(line)) return @@ -811,6 +812,7 @@ class TokenizedBuffer { break } foldEndRow = nextRow + if (existenceOnly) break } return foldEndRow } From 0b62381d51d05c8f7017af4e358173b7db994b2a Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 3 Oct 2017 12:47:01 -0700 Subject: [PATCH 303/448] :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 304/448] 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 6e3b8cb9f8420593f7e8128497122a1401544a1c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 4 Oct 2017 20:10:47 +0200 Subject: [PATCH 305/448] Introduce randomized test for TextEditorComponent --- package.json | 3 + spec/helpers/random.js | 42 + spec/helpers/words.js | 46891 +++++++++++++++++++++++++++ spec/text-editor-component-spec.js | 79 +- 4 files changed, 47014 insertions(+), 1 deletion(-) create mode 100644 spec/helpers/random.js create mode 100644 spec/helpers/words.js diff --git a/package.json b/package.json index 418fa0b75..8ddd8cfab 100644 --- a/package.json +++ b/package.json @@ -186,5 +186,8 @@ "atom", "snapshotResult" ] + }, + "devDependencies": { + "random-seed": "^0.3.0" } } diff --git a/spec/helpers/random.js b/spec/helpers/random.js new file mode 100644 index 000000000..62f0e1920 --- /dev/null +++ b/spec/helpers/random.js @@ -0,0 +1,42 @@ +const WORDS = require('./words') +const {Point, Range} = require('text-buffer') + +exports.getRandomBufferRange = function getRandomBufferRange (random, buffer) { + const endRow = random(buffer.getLineCount()) + const startRow = random.intBetween(0, endRow) + const startColumn = random(buffer.lineForRow(startRow).length + 1) + const endColumn = random(buffer.lineForRow(endRow).length + 1) + return Range(Point(startRow, startColumn), Point(endRow, endColumn)) +} + +exports.buildRandomLines = function buildRandomLines (random, maxLines) { + const lines = [] + + for (let i = 0; i < random(maxLines); i++) { + lines.push(buildRandomLine(random)) + } + + return lines.join('\n') +} + +function buildRandomLine (random) { + const line = [] + + for (let i = 0; i < random(5); i++) { + const n = random(10) + + if (n < 2) { + line.push('\t') + } else if (n < 4) { + line.push(' ') + } else { + if (line.length > 0 && !/\s/.test(line[line.length - 1])) { + line.push(' ') + } + + line.push(WORDS[random(WORDS.length)]) + } + } + + return line.join('') +} diff --git a/spec/helpers/words.js b/spec/helpers/words.js new file mode 100644 index 000000000..f5ae07149 --- /dev/null +++ b/spec/helpers/words.js @@ -0,0 +1,46891 @@ +/** + * List of single words from COMMON.txt + * http://www.gutenberg.org/ebooks/3201 + * Public Domain + */ + +module.exports = [ + 'a', + 'aa', + 'aalii', + 'aardvark', + 'aardwolf', + 'aba', + 'abaca', + 'abacist', + 'aback', + 'abacus', + 'abaft', + 'abalone', + 'abamp', + 'abampere', + 'abandon', + 'abandoned', + 'abase', + 'abash', + 'abate', + 'abatement', + 'abatis', + 'abattoir', + 'abaxial', + 'abb', + 'abba', + 'abbacy', + 'abbatial', + 'abbess', + 'abbey', + 'abbot', + 'abbreviate', + 'abbreviated', + 'abbreviation', + 'abcoulomb', + 'abdicate', + 'abdication', + 'abdomen', + 'abdominal', + 'abdominous', + 'abduce', + 'abducent', + 'abduct', + 'abduction', + 'abductor', + 'abeam', + 'abecedarian', + 'abecedarium', + 'abecedary', + 'abed', + 'abele', + 'abelmosk', + 'aberrant', + 'aberration', + 'abessive', + 'abet', + 'abettor', + 'abeyance', + 'abeyant', + 'abfarad', + 'abhenry', + 'abhor', + 'abhorrence', + 'abhorrent', + 'abide', + 'abiding', + 'abigail', + 'ability', + 'abiogenesis', + 'abiogenetic', + 'abiosis', + 'abiotic', + 'abirritant', + 'abirritate', + 'abject', + 'abjuration', + 'abjure', + 'ablate', + 'ablation', + 'ablative', + 'ablaut', + 'ablaze', + 'able', + 'ablepsia', + 'abloom', + 'ablution', + 'ably', + 'abmho', + 'abnegate', + 'abnormal', + 'abnormality', + 'abnormity', + 'aboard', + 'abode', + 'abohm', + 'abolish', + 'abolition', + 'abomasum', + 'abominable', + 'abominate', + 'abomination', + 'aboral', + 'aboriginal', + 'aborigine', + 'aborning', + 'abort', + 'aborticide', + 'abortifacient', + 'abortion', + 'abortionist', + 'abortive', + 'aboulia', + 'abound', + 'about', + 'above', + 'aboveboard', + 'aboveground', + 'abracadabra', + 'abradant', + 'abrade', + 'abranchiate', + 'abrasion', + 'abrasive', + 'abraxas', + 'abreact', + 'abreaction', + 'abreast', + 'abri', + 'abridge', + 'abridgment', + 'abroach', + 'abroad', + 'abrogate', + 'abrupt', + 'abruption', + 'abscess', + 'abscind', + 'abscise', + 'abscissa', + 'abscission', + 'abscond', + 'abseil', + 'absence', + 'absent', + 'absentee', + 'absenteeism', + 'absently', + 'absentminded', + 'absinthe', + 'absinthism', + 'absolute', + 'absolutely', + 'absolution', + 'absolutism', + 'absolve', + 'absonant', + 'absorb', + 'absorbance', + 'absorbed', + 'absorbefacient', + 'absorbent', + 'absorber', + 'absorbing', + 'absorptance', + 'absorption', + 'absorptivity', + 'absquatulate', + 'abstain', + 'abstemious', + 'abstention', + 'abstergent', + 'abstinence', + 'abstract', + 'abstracted', + 'abstraction', + 'abstractionism', + 'abstractionist', + 'abstriction', + 'abstruse', + 'absurd', + 'absurdity', + 'abulia', + 'abundance', + 'abundant', + 'abuse', + 'abusive', + 'abut', + 'abutilon', + 'abutment', + 'abuttal', + 'abuttals', + 'abutter', + 'abutting', + 'abuzz', + 'abvolt', + 'abwatt', + 'aby', + 'abysm', + 'abysmal', + 'abyss', + 'abyssal', + 'acacia', + 'academe', + 'academia', + 'academic', + 'academician', + 'academicism', + 'academy', + 'acaleph', + 'acanthaceous', + 'acanthocephalan', + 'acanthoid', + 'acanthopterygian', + 'acanthous', + 'acanthus', + 'acariasis', + 'acaricide', + 'acarid', + 'acaroid', + 'acarology', + 'acarpous', + 'acarus', + 'acatalectic', + 'acaudal', + 'acaulescent', + 'accede', + 'accelerando', + 'accelerant', + 'accelerate', + 'acceleration', + 'accelerator', + 'accelerometer', + 'accent', + 'accentor', + 'accentual', + 'accentuate', + 'accentuation', + 'accept', + 'acceptable', + 'acceptance', + 'acceptant', + 'acceptation', + 'accepted', + 'accepter', + 'acceptor', + 'access', + 'accessary', + 'accessible', + 'accession', + 'accessory', + 'acciaccatura', + 'accidence', + 'accident', + 'accidental', + 'accidie', + 'accipiter', + 'accipitrine', + 'acclaim', + 'acclamation', + 'acclimate', + 'acclimatize', + 'acclivity', + 'accolade', + 'accommodate', + 'accommodating', + 'accommodation', + 'accommodative', + 'accompaniment', + 'accompanist', + 'accompany', + 'accompanyist', + 'accomplice', + 'accomplish', + 'accomplished', + 'accomplishment', + 'accord', + 'accordance', + 'accordant', + 'according', + 'accordingly', + 'accordion', + 'accost', + 'accouchement', + 'accoucheur', + 'account', + 'accountable', + 'accountancy', + 'accountant', + 'accounting', + 'accouplement', + 'accouter', + 'accouterment', + 'accoutre', + 'accredit', + 'accrescent', + 'accrete', + 'accretion', + 'accroach', + 'accrual', + 'accrue', + 'acculturate', + 'acculturation', + 'acculturize', + 'accumbent', + 'accumulate', + 'accumulation', + 'accumulative', + 'accumulator', + 'accuracy', + 'accurate', + 'accursed', + 'accusal', + 'accusation', + 'accusative', + 'accusatorial', + 'accusatory', + 'accuse', + 'accused', + 'accustom', + 'accustomed', + 'ace', + 'acedia', + 'acentric', + 'acephalous', + 'acerate', + 'acerb', + 'acerbate', + 'acerbic', + 'acerbity', + 'acerose', + 'acervate', + 'acescent', + 'acetabulum', + 'acetal', + 'acetaldehyde', + 'acetamide', + 'acetanilide', + 'acetate', + 'acetic', + 'acetify', + 'acetometer', + 'acetone', + 'acetophenetidin', + 'acetous', + 'acetum', + 'acetyl', + 'acetylate', + 'acetylcholine', + 'acetylene', + 'acetylide', + 'ache', + 'achene', + 'achieve', + 'achievement', + 'achlamydeous', + 'achlorhydria', + 'achondrite', + 'achondroplasia', + 'achromat', + 'achromatic', + 'achromaticity', + 'achromatin', + 'achromatism', + 'achromatize', + 'achromatous', + 'achromic', + 'acicula', + 'acicular', + 'aciculate', + 'aciculum', + 'acid', + 'acidic', + 'acidify', + 'acidimeter', + 'acidimetry', + 'acidity', + 'acidophil', + 'acidosis', + 'acidulant', + 'acidulate', + 'acidulent', + 'acidulous', + 'acierate', + 'acinaciform', + 'aciniform', + 'acinus', + 'acknowledge', + 'acknowledgment', + 'acme', + 'acne', + 'acnode', + 'acolyte', + 'aconite', + 'acorn', + 'acosmism', + 'acotyledon', + 'acoustic', + 'acoustician', + 'acoustics', + 'acquaint', + 'acquaintance', + 'acquainted', + 'acquiesce', + 'acquiescence', + 'acquiescent', + 'acquire', + 'acquirement', + 'acquisition', + 'acquisitive', + 'acquit', + 'acquittal', + 'acquittance', + 'acre', + 'acreage', + 'acred', + 'acrid', + 'acridine', + 'acriflavine', + 'acrimonious', + 'acrimony', + 'acrobat', + 'acrobatic', + 'acrobatics', + 'acrocarpous', + 'acrodont', + 'acrodrome', + 'acrogen', + 'acrolein', + 'acrolith', + 'acromegaly', + 'acromion', + 'acronym', + 'acropetal', + 'acrophobia', + 'acropolis', + 'acrospire', + 'across', + 'acrostic', + 'acroter', + 'acroterion', + 'acrylic', + 'acrylonitrile', + 'acrylyl', + 'act', + 'actable', + 'actin', + 'actinal', + 'acting', + 'actinia', + 'actinic', + 'actiniform', + 'actinism', + 'actinium', + 'actinochemistry', + 'actinoid', + 'actinolite', + 'actinology', + 'actinometer', + 'actinomorphic', + 'actinomycete', + 'actinomycin', + 'actinomycosis', + 'actinon', + 'actinopod', + 'actinotherapy', + 'actinouranium', + 'actinozoan', + 'action', + 'actionable', + 'activate', + 'activator', + 'active', + 'activism', + 'activist', + 'activity', + 'actomyosin', + 'actor', + 'actress', + 'actual', + 'actuality', + 'actualize', + 'actually', + 'actuary', + 'actuate', + 'acuate', + 'acuity', + 'aculeate', + 'aculeus', + 'acumen', + 'acuminate', + 'acupuncture', + 'acutance', + 'acute', + 'acyclic', + 'acyl', + 'ad', + 'adactylous', + 'adage', + 'adagietto', + 'adagio', + 'adamant', + 'adamantine', + 'adamsite', + 'adapt', + 'adaptable', + 'adaptation', + 'adapter', + 'adaptive', + 'adaxial', + 'add', + 'addax', + 'addend', + 'addendum', + 'adder', + 'addict', + 'addicted', + 'addiction', + 'addictive', + 'additament', + 'addition', + 'additional', + 'additive', + 'additory', + 'addle', + 'addlebrained', + 'addlepated', + 'address', + 'addressee', + 'adduce', + 'adduct', + 'adduction', + 'adductor', + 'ademption', + 'adenectomy', + 'adenine', + 'adenitis', + 'adenocarcinoma', + 'adenoid', + 'adenoidal', + 'adenoidectomy', + 'adenoma', + 'adenosine', + 'adenovirus', + 'adept', + 'adequacy', + 'adequate', + 'adermin', + 'adessive', + 'adhere', + 'adherence', + 'adherent', + 'adhesion', + 'adhesive', + 'adhibit', + 'adiabatic', + 'adiaphorism', + 'adiaphorous', + 'adiathermancy', + 'adieu', + 'adios', + 'adipocere', + 'adipose', + 'adit', + 'adjacency', + 'adjacent', + 'adjectival', + 'adjective', + 'adjoin', + 'adjoining', + 'adjoint', + 'adjourn', + 'adjournment', + 'adjudge', + 'adjudicate', + 'adjudication', + 'adjunct', + 'adjunction', + 'adjure', + 'adjust', + 'adjustment', + 'adjutant', + 'adjuvant', + 'adman', + 'admass', + 'admeasure', + 'admeasurement', + 'adminicle', + 'administer', + 'administrate', + 'administration', + 'administrative', + 'administrator', + 'admirable', + 'admiral', + 'admiralty', + 'admiration', + 'admire', + 'admissible', + 'admission', + 'admissive', + 'admit', + 'admittance', + 'admittedly', + 'admix', + 'admixture', + 'admonish', + 'admonition', + 'admonitory', + 'adnate', + 'ado', + 'adobe', + 'adolescence', + 'adolescent', + 'adopt', + 'adopted', + 'adoptive', + 'adorable', + 'adoration', + 'adore', + 'adorn', + 'adornment', + 'adown', + 'adrenal', + 'adrenaline', + 'adrenocorticotropic', + 'adrift', + 'adroit', + 'adscititious', + 'adscription', + 'adsorb', + 'adsorbate', + 'adsorbent', + 'adularia', + 'adulate', + 'adulation', + 'adult', + 'adulterant', + 'adulterate', + 'adulteration', + 'adulterer', + 'adulteress', + 'adulterine', + 'adulterous', + 'adultery', + 'adulthood', + 'adumbral', + 'adumbrate', + 'adust', + 'advance', + 'advanced', + 'advancement', + 'advantage', + 'advantageous', + 'advection', + 'advent', + 'adventitia', + 'adventitious', + 'adventure', + 'adventurer', + 'adventuresome', + 'adventuress', + 'adventurism', + 'adventurous', + 'adverb', + 'adverbial', + 'adversaria', + 'adversary', + 'adversative', + 'adverse', + 'adversity', + 'advert', + 'advertence', + 'advertent', + 'advertise', + 'advertisement', + 'advertising', + 'advice', + 'advisable', + 'advise', + 'advised', + 'advisedly', + 'advisee', + 'advisement', + 'adviser', + 'advisory', + 'advocaat', + 'advocacy', + 'advocate', + 'advocation', + 'advowson', + 'adynamia', + 'adytum', + 'adz', + 'adze', + 'aeciospore', + 'aecium', + 'aedes', + 'aedile', + 'aegis', + 'aegrotat', + 'aeneous', + 'aeolipile', + 'aeolotropic', + 'aeon', + 'aeonian', + 'aerate', + 'aerator', + 'aerial', + 'aerialist', + 'aerie', + 'aerification', + 'aeriform', + 'aerify', + 'aero', + 'aeroballistics', + 'aerobatics', + 'aerobe', + 'aerobic', + 'aerobiology', + 'aerobiosis', + 'aerodonetics', + 'aerodontia', + 'aerodrome', + 'aerodynamics', + 'aerodyne', + 'aeroembolism', + 'aerogram', + 'aerograph', + 'aerography', + 'aerolite', + 'aerology', + 'aeromancy', + 'aeromarine', + 'aeromechanic', + 'aeromechanics', + 'aeromedical', + 'aerometeorograph', + 'aerometer', + 'aerometry', + 'aeronaut', + 'aeronautics', + 'aeroneurosis', + 'aeropause', + 'aerophagia', + 'aerophobia', + 'aerophone', + 'aerophyte', + 'aeroplane', + 'aeroscope', + 'aerosol', + 'aerospace', + 'aerosphere', + 'aerostat', + 'aerostatic', + 'aerostatics', + 'aerostation', + 'aerotherapeutics', + 'aerothermodynamics', + 'aerugo', + 'aery', + 'aesthesia', + 'aesthete', + 'aesthetic', + 'aesthetically', + 'aestheticism', + 'aesthetics', + 'aestival', + 'aestivate', + 'aestivation', + 'aether', + 'aetiology', + 'afar', + 'afeard', + 'afebrile', + 'affable', + 'affair', + 'affaire', + 'affairs', + 'affect', + 'affectation', + 'affected', + 'affecting', + 'affection', + 'affectional', + 'affectionate', + 'affective', + 'affenpinscher', + 'afferent', + 'affettuoso', + 'affiance', + 'affianced', + 'affiant', + 'affiche', + 'affidavit', + 'affiliate', + 'affiliation', + 'affinal', + 'affine', + 'affined', + 'affinitive', + 'affinity', + 'affirm', + 'affirmation', + 'affirmative', + 'affirmatory', + 'affix', + 'affixation', + 'afflatus', + 'afflict', + 'affliction', + 'afflictive', + 'affluence', + 'affluent', + 'afflux', + 'afford', + 'afforest', + 'affranchise', + 'affray', + 'affricate', + 'affricative', + 'affright', + 'affront', + 'affusion', + 'afghan', + 'afghani', + 'aficionado', + 'afield', + 'afire', + 'aflame', + 'afloat', + 'aflutter', + 'afoot', + 'afore', + 'aforementioned', + 'aforesaid', + 'aforethought', + 'aforetime', + 'afoul', + 'afraid', + 'afreet', + 'afresh', + 'afrit', + 'aft', + 'after', + 'afterbirth', + 'afterbody', + 'afterbrain', + 'afterburner', + 'afterburning', + 'aftercare', + 'afterclap', + 'afterdamp', + 'afterdeck', + 'aftereffect', + 'afterglow', + 'aftergrowth', + 'afterguard', + 'afterheat', + 'afterimage', + 'afterlife', + 'aftermath', + 'aftermost', + 'afternoon', + 'afternoons', + 'afterpiece', + 'aftersensation', + 'aftershaft', + 'aftershock', + 'aftertaste', + 'afterthought', + 'aftertime', + 'afterward', + 'afterwards', + 'afterword', + 'afterworld', + 'afteryears', + 'aftmost', + 'aga', + 'again', + 'against', + 'agalloch', + 'agama', + 'agamete', + 'agamic', + 'agamogenesis', + 'agapanthus', + 'agape', + 'agar', + 'agaric', + 'agate', + 'agateware', + 'agave', + 'age', + 'aged', + 'agee', + 'ageless', + 'agency', + 'agenda', + 'agenesis', + 'agent', + 'agential', + 'agentival', + 'agentive', + 'ageratum', + 'agger', + 'aggiornamento', + 'agglomerate', + 'agglomeration', + 'agglutinate', + 'agglutination', + 'agglutinative', + 'agglutinin', + 'agglutinogen', + 'aggrade', + 'aggrandize', + 'aggravate', + 'aggravation', + 'aggregate', + 'aggregation', + 'aggress', + 'aggression', + 'aggressive', + 'aggressor', + 'aggrieve', + 'aggrieved', + 'agha', + 'aghast', + 'agile', + 'agility', + 'agio', + 'agiotage', + 'agist', + 'agitate', + 'agitation', + 'agitato', + 'agitator', + 'agitprop', + 'agleam', + 'aglet', + 'agley', + 'aglimmer', + 'aglitter', + 'aglow', + 'agma', + 'agminate', + 'agnail', + 'agnate', + 'agnomen', + 'agnosia', + 'agnostic', + 'agnosticism', + 'ago', + 'agog', + 'agon', + 'agone', + 'agonic', + 'agonist', + 'agonistic', + 'agonize', + 'agonized', + 'agonizing', + 'agony', + 'agora', + 'agoraphobia', + 'agouti', + 'agraffe', + 'agranulocytosis', + 'agrapha', + 'agraphia', + 'agrarian', + 'agree', + 'agreeable', + 'agreed', + 'agreement', + 'agrestic', + 'agribusiness', + 'agriculture', + 'agriculturist', + 'agrimony', + 'agrobiology', + 'agrology', + 'agronomics', + 'agronomy', + 'agrostology', + 'aground', + 'ague', + 'agueweed', + 'aguish', + 'ah', + 'aha', + 'ahead', + 'ahem', + 'ahimsa', + 'ahoy', + 'ai', + 'aid', + 'aide', + 'aiglet', + 'aigrette', + 'aiguille', + 'aiguillette', + 'aikido', + 'ail', + 'ailanthus', + 'aileron', + 'ailing', + 'ailment', + 'ailurophile', + 'ailurophobe', + 'aim', + 'aimless', + 'ain', + 'air', + 'airboat', + 'airborne', + 'airbrush', + 'airburst', + 'aircraft', + 'aircraftman', + 'aircrew', + 'aircrewman', + 'airdrome', + 'airdrop', + 'airfield', + 'airflow', + 'airfoil', + 'airframe', + 'airglow', + 'airhead', + 'airily', + 'airiness', + 'airing', + 'airless', + 'airlift', + 'airlike', + 'airline', + 'airliner', + 'airmail', + 'airman', + 'airplane', + 'airport', + 'airs', + 'airscrew', + 'airship', + 'airsick', + 'airsickness', + 'airspace', + 'airspeed', + 'airstrip', + 'airt', + 'airtight', + 'airwaves', + 'airway', + 'airwoman', + 'airworthy', + 'airy', + 'aisle', + 'ait', + 'aitch', + 'aitchbone', + 'ajar', + 'akee', + 'akene', + 'akimbo', + 'akin', + 'akvavit', + 'ala', + 'alabaster', + 'alack', + 'alacrity', + 'alameda', + 'alamode', + 'alanine', + 'alar', + 'alarm', + 'alarmist', + 'alarum', + 'alary', + 'alas', + 'alate', + 'alb', + 'alba', + 'albacore', + 'albata', + 'albatross', + 'albedo', + 'albeit', + 'albertite', + 'albertype', + 'albescent', + 'albinism', + 'albino', + 'albite', + 'album', + 'albumen', + 'albumenize', + 'albumin', + 'albuminate', + 'albuminoid', + 'albuminous', + 'albuminuria', + 'albumose', + 'alburnum', + 'alcahest', + 'alcaide', + 'alcalde', + 'alcazar', + 'alchemist', + 'alchemize', + 'alchemy', + 'alcheringa', + 'alcohol', + 'alcoholic', + 'alcoholicity', + 'alcoholism', + 'alcoholize', + 'alcoholometer', + 'alcove', + 'aldehyde', + 'alder', + 'alderman', + 'aldol', + 'aldose', + 'aldosterone', + 'aldrin', + 'ale', + 'aleatory', + 'alectryomancy', + 'alee', + 'alegar', + 'alehouse', + 'alembic', + 'aleph', + 'alerion', + 'alert', + 'aleuromancy', + 'aleurone', + 'alevin', + 'alewife', + 'alexandrite', + 'alexia', + 'alexin', + 'alexipharmic', + 'alfalfa', + 'alfilaria', + 'alforja', + 'alfresco', + 'alga', + 'algae', + 'algarroba', + 'algebra', + 'algebraic', + 'algebraist', + 'algesia', + 'algetic', + 'algicide', + 'algid', + 'algin', + 'alginate', + 'algoid', + 'algolagnia', + 'algology', + 'algometer', + 'algophobia', + 'algor', + 'algorism', + 'algorithm', + 'alias', + 'alibi', + 'alible', + 'alicyclic', + 'alidade', + 'alien', + 'alienable', + 'alienage', + 'alienate', + 'alienation', + 'alienee', + 'alienism', + 'alienist', + 'alienor', + 'aliform', + 'alight', + 'align', + 'alignment', + 'alike', + 'aliment', + 'alimentary', + 'alimentation', + 'alimony', + 'aline', + 'aliped', + 'aliphatic', + 'aliquant', + 'aliquot', + 'alit', + 'aliunde', + 'alive', + 'alizarin', + 'alkahest', + 'alkali', + 'alkalify', + 'alkalimeter', + 'alkaline', + 'alkalinity', + 'alkalinize', + 'alkalize', + 'alkaloid', + 'alkalosis', + 'alkane', + 'alkanet', + 'alkene', + 'alkyd', + 'alkyl', + 'alkylation', + 'alkyne', + 'all', + 'allanite', + 'allantoid', + 'allantois', + 'allargando', + 'allative', + 'allay', + 'allegation', + 'allege', + 'alleged', + 'allegedly', + 'allegiance', + 'allegorical', + 'allegorist', + 'allegorize', + 'allegory', + 'allegretto', + 'allegro', + 'allele', + 'allelomorph', + 'alleluia', + 'allemande', + 'allergen', + 'allergic', + 'allergist', + 'allergy', + 'allethrin', + 'alleviate', + 'alleviation', + 'alleviative', + 'alleviator', + 'alley', + 'alleyway', + 'allheal', + 'alliaceous', + 'alliance', + 'allied', + 'allies', + 'alligator', + 'alliterate', + 'alliteration', + 'alliterative', + 'allium', + 'allness', + 'allocate', + 'allocation', + 'allochthonous', + 'allocution', + 'allodial', + 'allodium', + 'allogamy', + 'allograph', + 'allomerism', + 'allometry', + 'allomorph', + 'allomorphism', + 'allonge', + 'allonym', + 'allopath', + 'allopathy', + 'allopatric', + 'allophane', + 'allophone', + 'alloplasm', + 'allot', + 'allotment', + 'allotrope', + 'allotropy', + 'allottee', + 'allover', + 'allow', + 'allowable', + 'allowance', + 'allowed', + 'allowedly', + 'alloy', + 'allseed', + 'allspice', + 'allude', + 'allure', + 'allurement', + 'alluring', + 'allusion', + 'allusive', + 'alluvial', + 'alluvion', + 'alluvium', + 'ally', + 'allyl', + 'almanac', + 'almandine', + 'almemar', + 'almighty', + 'almond', + 'almoner', + 'almonry', + 'almost', + 'alms', + 'almsgiver', + 'almshouse', + 'almsman', + 'almswoman', + 'almucantar', + 'almuce', + 'alodium', + 'aloe', + 'aloes', + 'aloeswood', + 'aloft', + 'aloha', + 'aloin', + 'alone', + 'along', + 'alongshore', + 'alongside', + 'aloof', + 'alopecia', + 'aloud', + 'alow', + 'alp', + 'alpaca', + 'alpenglow', + 'alpenhorn', + 'alpenstock', + 'alpestrine', + 'alpha', + 'alphabet', + 'alphabetic', + 'alphabetical', + 'alphabetize', + 'alphanumeric', + 'alphitomancy', + 'alphorn', + 'alphosis', + 'alpine', + 'alpinist', + 'already', + 'alright', + 'also', + 'alt', + 'altar', + 'altarpiece', + 'altazimuth', + 'alter', + 'alterable', + 'alterant', + 'alteration', + 'alterative', + 'altercate', + 'altercation', + 'alternant', + 'alternate', + 'alternately', + 'alternation', + 'alternative', + 'alternator', + 'althorn', + 'although', + 'altigraph', + 'altimeter', + 'altimetry', + 'altissimo', + 'altitude', + 'alto', + 'altocumulus', + 'altogether', + 'altostratus', + 'altricial', + 'altruism', + 'altruist', + 'altruistic', + 'aludel', + 'alula', + 'alum', + 'alumina', + 'aluminate', + 'aluminiferous', + 'aluminium', + 'aluminize', + 'aluminothermy', + 'aluminous', + 'aluminum', + 'alumna', + 'alumnus', + 'alumroot', + 'alunite', + 'alveolar', + 'alveolate', + 'alveolus', + 'alvine', + 'always', + 'alyssum', + 'am', + 'amadavat', + 'amadou', + 'amah', + 'amain', + 'amalgam', + 'amalgamate', + 'amalgamation', + 'amandine', + 'amanita', + 'amanuensis', + 'amaranth', + 'amaranthaceous', + 'amaranthine', + 'amarelle', + 'amaryllidaceous', + 'amaryllis', + 'amass', + 'amateur', + 'amateurish', + 'amateurism', + 'amative', + 'amatol', + 'amatory', + 'amaurosis', + 'amaze', + 'amazed', + 'amazement', + 'amazing', + 'amazon', + 'amazonite', + 'ambages', + 'ambagious', + 'ambary', + 'ambassador', + 'ambassadress', + 'amber', + 'ambergris', + 'amberjack', + 'amberoid', + 'ambidexter', + 'ambidexterity', + 'ambidextrous', + 'ambience', + 'ambient', + 'ambiguity', + 'ambiguous', + 'ambit', + 'ambitendency', + 'ambition', + 'ambitious', + 'ambivalence', + 'ambiversion', + 'ambivert', + 'amble', + 'amblygonite', + 'amblyopia', + 'amblyoscope', + 'ambo', + 'amboceptor', + 'ambroid', + 'ambrosia', + 'ambrosial', + 'ambrotype', + 'ambry', + 'ambsace', + 'ambulacrum', + 'ambulance', + 'ambulant', + 'ambulate', + 'ambulator', + 'ambulatory', + 'ambuscade', + 'ambush', + 'ameba', + 'ameer', + 'ameliorate', + 'amelioration', + 'amen', + 'amenable', + 'amend', + 'amendatory', + 'amendment', + 'amends', + 'amenity', + 'ament', + 'amentia', + 'amerce', + 'americium', + 'amesace', + 'amethyst', + 'ametropia', + 'ami', + 'amiable', + 'amianthus', + 'amicable', + 'amice', + 'amid', + 'amidase', + 'amide', + 'amidships', + 'amidst', + 'amie', + 'amigo', + 'amimia', + 'amine', + 'amino', + 'aminoplast', + 'aminopyrine', + 'amir', + 'amiss', + 'amitosis', + 'amity', + 'ammeter', + 'ammine', + 'ammo', + 'ammonal', + 'ammonate', + 'ammonia', + 'ammoniac', + 'ammoniacal', + 'ammoniate', + 'ammonic', + 'ammonify', + 'ammonite', + 'ammonium', + 'ammunition', + 'amnesia', + 'amnesty', + 'amniocentesis', + 'amnion', + 'amoeba', + 'amoebaean', + 'amoebic', + 'amoebocyte', + 'amoeboid', + 'amok', + 'among', + 'amongst', + 'amontillado', + 'amoral', + 'amoretto', + 'amorino', + 'amorist', + 'amoroso', + 'amorous', + 'amorphism', + 'amorphous', + 'amortization', + 'amortize', + 'amortizement', + 'amount', + 'amour', + 'ampelopsis', + 'amperage', + 'ampere', + 'ampersand', + 'amphetamine', + 'amphiarthrosis', + 'amphiaster', + 'amphibian', + 'amphibiotic', + 'amphibious', + 'amphibole', + 'amphibolite', + 'amphibology', + 'amphibolous', + 'amphiboly', + 'amphibrach', + 'amphichroic', + 'amphicoelous', + 'amphictyon', + 'amphictyony', + 'amphidiploid', + 'amphigory', + 'amphimacer', + 'amphimixis', + 'amphioxus', + 'amphipod', + 'amphiprostyle', + 'amphisbaena', + 'amphistylar', + 'amphitheater', + 'amphithecium', + 'amphitropous', + 'amphora', + 'amphoteric', + 'ample', + 'amplexicaul', + 'ampliate', + 'amplification', + 'amplifier', + 'amplify', + 'amplitude', + 'amply', + 'ampoule', + 'ampulla', + 'amputate', + 'amputee', + 'amrita', + 'amu', + 'amuck', + 'amulet', + 'amuse', + 'amused', + 'amusement', + 'amusing', + 'amygdala', + 'amygdalate', + 'amygdalin', + 'amygdaline', + 'amygdaloid', + 'amyl', + 'amylaceous', + 'amylase', + 'amylene', + 'amyloid', + 'amylolysis', + 'amylopectin', + 'amylopsin', + 'amylose', + 'amylum', + 'amyotonia', + 'an', + 'ana', + 'anabaena', + 'anabantid', + 'anabas', + 'anabasis', + 'anabatic', + 'anabiosis', + 'anabolism', + 'anabolite', + 'anabranch', + 'anacardiaceous', + 'anachronism', + 'anachronistic', + 'anachronous', + 'anaclinal', + 'anaclitic', + 'anacoluthia', + 'anacoluthon', + 'anaconda', + 'anacrusis', + 'anadem', + 'anadiplosis', + 'anadromous', + 'anaemia', + 'anaemic', + 'anaerobe', + 'anaerobic', + 'anaesthesia', + 'anaesthesiology', + 'anaesthetize', + 'anaglyph', + 'anagnorisis', + 'anagoge', + 'anagram', + 'anagrammatize', + 'anal', + 'analcite', + 'analects', + 'analemma', + 'analeptic', + 'analgesia', + 'analgesic', + 'analog', + 'analogical', + 'analogize', + 'analogous', + 'analogue', + 'analogy', + 'analphabetic', + 'analysand', + 'analyse', + 'analysis', + 'analyst', + 'analytic', + 'analyze', + 'analyzer', + 'anamnesis', + 'anamorphic', + 'anamorphism', + 'anamorphoscope', + 'anamorphosis', + 'anandrous', + 'ananthous', + 'anapest', + 'anaphase', + 'anaphora', + 'anaphrodisiac', + 'anaphylaxis', + 'anaplastic', + 'anaplasty', + 'anaptyxis', + 'anarch', + 'anarchic', + 'anarchism', + 'anarchist', + 'anarchy', + 'anarthria', + 'anarthrous', + 'anasarca', + 'anastigmat', + 'anastigmatic', + 'anastomose', + 'anastomosis', + 'anastrophe', + 'anatase', + 'anathema', + 'anathematize', + 'anatomical', + 'anatomist', + 'anatomize', + 'anatomy', + 'anatropous', + 'anatto', + 'ancestor', + 'ancestral', + 'ancestress', + 'ancestry', + 'anchor', + 'anchorage', + 'anchoress', + 'anchorite', + 'anchoveta', + 'anchovy', + 'anchusin', + 'anchylose', + 'ancient', + 'anciently', + 'ancilla', + 'ancillary', + 'ancipital', + 'ancon', + 'ancona', + 'ancylostomiasis', + 'and', + 'andalusite', + 'andante', + 'andantino', + 'andesine', + 'andesite', + 'andiron', + 'andradite', + 'androclinium', + 'androecium', + 'androgen', + 'androgyne', + 'androgynous', + 'android', + 'androsphinx', + 'androsterone', + 'ane', + 'anear', + 'anecdotage', + 'anecdotal', + 'anecdote', + 'anecdotic', + 'anecdotist', + 'anechoic', + 'anelace', + 'anele', + 'anemia', + 'anemic', + 'anemochore', + 'anemograph', + 'anemography', + 'anemology', + 'anemometer', + 'anemometry', + 'anemone', + 'anemophilous', + 'anemoscope', + 'anent', + 'anergy', + 'aneroid', + 'aneroidograph', + 'anesthesia', + 'anesthesiologist', + 'anesthesiology', + 'anesthetic', + 'anesthetist', + 'anesthetize', + 'anethole', + 'aneurin', + 'aneurysm', + 'anew', + 'anfractuosity', + 'anfractuous', + 'angary', + 'angel', + 'angelfish', + 'angelic', + 'angelica', + 'angelology', + 'anger', + 'angina', + 'angiology', + 'angioma', + 'angiosperm', + 'angle', + 'angler', + 'anglesite', + 'angleworm', + 'anglicize', + 'angling', + 'angora', + 'angry', + 'angst', + 'angstrom', + 'anguilliform', + 'anguine', + 'anguish', + 'anguished', + 'angular', + 'angularity', + 'angulate', + 'angulation', + 'angwantibo', + 'anhedral', + 'anhinga', + 'anhydride', + 'anhydrite', + 'anhydrous', + 'ani', + 'aniconic', + 'anil', + 'anile', + 'aniline', + 'anility', + 'anima', + 'animadversion', + 'animadvert', + 'animal', + 'animalcule', + 'animalism', + 'animalist', + 'animality', + 'animalize', + 'animate', + 'animated', + 'animation', + 'animatism', + 'animato', + 'animator', + 'animism', + 'animosity', + 'animus', + 'anion', + 'anise', + 'aniseed', + 'aniseikonia', + 'anisette', + 'anisole', + 'anisomerous', + 'anisometric', + 'anisometropia', + 'anisotropic', + 'ankerite', + 'ankh', + 'ankle', + 'anklebone', + 'anklet', + 'ankus', + 'ankylosaur', + 'ankylose', + 'ankylosis', + 'ankylostomiasis', + 'anlace', + 'anlage', + 'anna', + 'annabergite', + 'annal', + 'annalist', + 'annals', + 'annates', + 'annatto', + 'anneal', + 'annelid', + 'annex', + 'annexation', + 'annihilate', + 'annihilation', + 'annihilator', + 'anniversary', + 'annotate', + 'annotation', + 'announce', + 'announcement', + 'announcer', + 'annoy', + 'annoyance', + 'annoying', + 'annual', + 'annuitant', + 'annuity', + 'annul', + 'annular', + 'annulate', + 'annulation', + 'annulet', + 'annulment', + 'annulose', + 'annulus', + 'annunciate', + 'annunciation', + 'annunciator', + 'anoa', + 'anode', + 'anodic', + 'anodize', + 'anodyne', + 'anoint', + 'anole', + 'anomalism', + 'anomalistic', + 'anomalous', + 'anomaly', + 'anomie', + 'anon', + 'anonym', + 'anonymous', + 'anopheles', + 'anorak', + 'anorexia', + 'anorthic', + 'anorthite', + 'anorthosite', + 'anosmia', + 'another', + 'anoxemia', + 'anoxia', + 'ansate', + 'anserine', + 'answer', + 'answerable', + 'ant', + 'anta', + 'antacid', + 'antagonism', + 'antagonist', + 'antagonistic', + 'antagonize', + 'antalkali', + 'antarctic', + 'ante', + 'anteater', + 'antebellum', + 'antecede', + 'antecedence', + 'antecedency', + 'antecedent', + 'antecedents', + 'antechamber', + 'antechoir', + 'antedate', + 'antediluvian', + 'antefix', + 'antelope', + 'antemeridian', + 'antemundane', + 'antenatal', + 'antenna', + 'antennule', + 'antepast', + 'antependium', + 'antepenult', + 'anterior', + 'anteroom', + 'antetype', + 'anteversion', + 'antevert', + 'anthelion', + 'anthelmintic', + 'anthem', + 'anthemion', + 'anther', + 'antheridium', + 'antherozoid', + 'anthesis', + 'anthill', + 'anthocyanin', + 'anthodium', + 'anthologize', + 'anthology', + 'anthophore', + 'anthotaxy', + 'anthozoan', + 'anthracene', + 'anthracite', + 'anthracnose', + 'anthracoid', + 'anthracosilicosis', + 'anthracosis', + 'anthraquinone', + 'anthrax', + 'anthropocentric', + 'anthropogenesis', + 'anthropogeography', + 'anthropography', + 'anthropoid', + 'anthropolatry', + 'anthropologist', + 'anthropology', + 'anthropometry', + 'anthropomorphic', + 'anthropomorphism', + 'anthropomorphize', + 'anthropomorphosis', + 'anthropomorphous', + 'anthropopathy', + 'anthropophagi', + 'anthropophagite', + 'anthropophagy', + 'anthroposophy', + 'anthurium', + 'anti', + 'antiar', + 'antibaryon', + 'antibiosis', + 'antibiotic', + 'antibody', + 'antic', + 'anticatalyst', + 'anticathexis', + 'anticathode', + 'antichlor', + 'anticholinergic', + 'anticipant', + 'anticipate', + 'anticipation', + 'anticipative', + 'anticipatory', + 'anticlastic', + 'anticlerical', + 'anticlimax', + 'anticlinal', + 'anticline', + 'anticlinorium', + 'anticlockwise', + 'anticoagulant', + 'anticyclone', + 'antidepressant', + 'antidisestablishmentarianism', + 'antidote', + 'antidromic', + 'antifebrile', + 'antifouling', + 'antifreeze', + 'antifriction', + 'antigen', + 'antigorite', + 'antihalation', + 'antihelix', + 'antihero', + 'antihistamine', + 'antiknock', + 'antilepton', + 'antilog', + 'antilogarithm', + 'antilogism', + 'antilogy', + 'antimacassar', + 'antimagnetic', + 'antimalarial', + 'antimasque', + 'antimatter', + 'antimere', + 'antimicrobial', + 'antimissile', + 'antimonic', + 'antimonous', + 'antimony', + 'antimonyl', + 'antineutrino', + 'antineutron', + 'anting', + 'antinode', + 'antinomian', + 'antinomy', + 'antinucleon', + 'antioxidant', + 'antiparallel', + 'antiparticle', + 'antipasto', + 'antipathetic', + 'antipathy', + 'antiperiodic', + 'antiperistalsis', + 'antipersonnel', + 'antiperspirant', + 'antiphlogistic', + 'antiphon', + 'antiphonal', + 'antiphonary', + 'antiphony', + 'antiphrasis', + 'antipodal', + 'antipode', + 'antipodes', + 'antipole', + 'antipope', + 'antiproton', + 'antipyretic', + 'antipyrine', + 'antiquarian', + 'antiquary', + 'antiquate', + 'antiquated', + 'antique', + 'antiquity', + 'antirachitic', + 'antirrhinum', + 'antiscorbutic', + 'antisepsis', + 'antiseptic', + 'antisepticize', + 'antiserum', + 'antislavery', + 'antisocial', + 'antispasmodic', + 'antistrophe', + 'antisyphilitic', + 'antitank', + 'antithesis', + 'antitoxic', + 'antitoxin', + 'antitrades', + 'antitragus', + 'antitrust', + 'antitype', + 'antivenin', + 'antiworld', + 'antler', + 'antlia', + 'antlion', + 'antonomasia', + 'antonym', + 'antre', + 'antrorse', + 'antrum', + 'anuran', + 'anuria', + 'anurous', + 'anus', + 'anvil', + 'anxiety', + 'anxious', + 'any', + 'anybody', + 'anyhow', + 'anyone', + 'anyplace', + 'anything', + 'anytime', + 'anyway', + 'anyways', + 'anywhere', + 'anywheres', + 'anywise', + 'aorist', + 'aoristic', + 'aorta', + 'aoudad', + 'apace', + 'apache', + 'apanage', + 'aparejo', + 'apart', + 'apartheid', + 'apartment', + 'apatetic', + 'apathetic', + 'apathy', + 'apatite', + 'ape', + 'apeak', + 'aperient', + 'aperiodic', + 'aperture', + 'apery', + 'apetalous', + 'apex', + 'aphaeresis', + 'aphanite', + 'aphasia', + 'aphasic', + 'aphelion', + 'apheliotropic', + 'aphesis', + 'aphid', + 'aphis', + 'aphonia', + 'aphonic', + 'aphorism', + 'aphoristic', + 'aphorize', + 'aphotic', + 'aphrodisia', + 'aphrodisiac', + 'aphyllous', + 'apian', + 'apiarian', + 'apiarist', + 'apiary', + 'apical', + 'apices', + 'apiculate', + 'apiculture', + 'apiece', + 'apish', + 'apivorous', + 'aplacental', + 'aplanatic', + 'aplanospore', + 'aplasia', + 'aplenty', + 'aplite', + 'aplomb', + 'apnea', + 'apocalypse', + 'apocalyptic', + 'apocarp', + 'apocarpous', + 'apochromatic', + 'apocopate', + 'apocope', + 'apocrine', + 'apocryphal', + 'apocynaceous', + 'apocynthion', + 'apodal', + 'apodictic', + 'apodosis', + 'apoenzyme', + 'apogamy', + 'apogee', + 'apogeotropism', + 'apograph', + 'apolitical', + 'apologete', + 'apologetic', + 'apologetics', + 'apologia', + 'apologist', + 'apologize', + 'apologue', + 'apology', + 'apolune', + 'apomict', + 'apomixis', + 'apomorphine', + 'aponeurosis', + 'apopemptic', + 'apophasis', + 'apophthegm', + 'apophyge', + 'apophyllite', + 'apophysis', + 'apoplectic', + 'apoplexy', + 'aporia', + 'aport', + 'aposematic', + 'aposiopesis', + 'apospory', + 'apostasy', + 'apostate', + 'apostatize', + 'apostil', + 'apostle', + 'apostolate', + 'apostolic', + 'apostrophe', + 'apostrophize', + 'apothecary', + 'apothecium', + 'apothegm', + 'apothem', + 'apotheosis', + 'apotheosize', + 'apotropaic', + 'appal', + 'appall', + 'appalling', + 'appanage', + 'apparatus', + 'apparel', + 'apparent', + 'apparition', + 'apparitor', + 'appassionato', + 'appeal', + 'appealing', + 'appear', + 'appearance', + 'appease', + 'appeasement', + 'appel', + 'appellant', + 'appellate', + 'appellation', + 'appellative', + 'appellee', + 'append', + 'appendage', + 'appendant', + 'appendectomy', + 'appendicectomy', + 'appendicitis', + 'appendicle', + 'appendicular', + 'appendix', + 'apperceive', + 'apperception', + 'appertain', + 'appetence', + 'appetency', + 'appetite', + 'appetitive', + 'appetizer', + 'appetizing', + 'applaud', + 'applause', + 'apple', + 'applecart', + 'applejack', + 'apples', + 'applesauce', + 'appliance', + 'applicable', + 'applicant', + 'application', + 'applicative', + 'applicator', + 'applicatory', + 'applied', + 'applique', + 'apply', + 'appoggiatura', + 'appoint', + 'appointed', + 'appointee', + 'appointive', + 'appointment', + 'appointor', + 'apportion', + 'apportionment', + 'appose', + 'apposite', + 'apposition', + 'appositive', + 'appraisal', + 'appraise', + 'appreciable', + 'appreciate', + 'appreciation', + 'appreciative', + 'apprehend', + 'apprehensible', + 'apprehension', + 'apprehensive', + 'apprentice', + 'appressed', + 'apprise', + 'approach', + 'approachable', + 'approbate', + 'approbation', + 'appropriate', + 'appropriation', + 'approval', + 'approve', + 'approver', + 'approximal', + 'approximate', + 'approximation', + 'appulse', + 'appurtenance', + 'appurtenant', + 'apraxia', + 'apricot', + 'apriorism', + 'apron', + 'apropos', + 'apse', + 'apsis', + 'apt', + 'apteral', + 'apterous', + 'apterygial', + 'apteryx', + 'aptitude', + 'apyretic', + 'aqua', + 'aquacade', + 'aqualung', + 'aquamanile', + 'aquamarine', + 'aquanaut', + 'aquaplane', + 'aquarelle', + 'aquarist', + 'aquarium', + 'aquatic', + 'aquatint', + 'aquavit', + 'aqueduct', + 'aqueous', + 'aquiculture', + 'aquifer', + 'aquilegia', + 'aquiline', + 'aquiver', + 'arabesque', + 'arabinose', + 'arable', + 'araceous', + 'arachnid', + 'arachnoid', + 'aragonite', + 'arak', + 'araliaceous', + 'arapaima', + 'araroba', + 'araucaria', + 'arbalest', + 'arbiter', + 'arbitrage', + 'arbitral', + 'arbitrament', + 'arbitrary', + 'arbitrate', + 'arbitration', + 'arbitrator', + 'arbitress', + 'arbor', + 'arboreal', + 'arboreous', + 'arborescent', + 'arboretum', + 'arboriculture', + 'arborization', + 'arborvitae', + 'arbour', + 'arbutus', + 'arc', + 'arcade', + 'arcane', + 'arcanum', + 'arcature', + 'arch', + 'archaeological', + 'archaeology', + 'archaeopteryx', + 'archaeornis', + 'archaic', + 'archaism', + 'archaize', + 'archangel', + 'archbishop', + 'archbishopric', + 'archdeacon', + 'archdeaconry', + 'archdiocese', + 'archducal', + 'archduchess', + 'archduchy', + 'archduke', + 'arched', + 'archegonium', + 'archenemy', + 'archenteron', + 'archeology', + 'archer', + 'archerfish', + 'archery', + 'archespore', + 'archetype', + 'archfiend', + 'archicarp', + 'archidiaconal', + 'archiepiscopacy', + 'archiepiscopal', + 'archiepiscopate', + 'archil', + 'archimage', + 'archimandrite', + 'archine', + 'arching', + 'archipelago', + 'archiphoneme', + 'archiplasm', + 'architect', + 'architectonic', + 'architectonics', + 'architectural', + 'architecture', + 'architrave', + 'archival', + 'archive', + 'archives', + 'archivist', + 'archivolt', + 'archlute', + 'archon', + 'archoplasm', + 'archpriest', + 'archway', + 'arciform', + 'arcograph', + 'arctic', + 'arcuate', + 'arcuation', + 'ardeb', + 'ardency', + 'ardent', + 'ardor', + 'arduous', + 'are', + 'area', + 'areaway', + 'areca', + 'arena', + 'arenaceous', + 'arenicolous', + 'areola', + 'arethusa', + 'argal', + 'argali', + 'argent', + 'argentic', + 'argentiferous', + 'argentine', + 'argentite', + 'argentous', + 'argentum', + 'argil', + 'argillaceous', + 'argilliferous', + 'argillite', + 'arginine', + 'argol', + 'argon', + 'argosy', + 'argot', + 'arguable', + 'argue', + 'argufy', + 'argument', + 'argumentation', + 'argumentative', + 'argumentum', + 'argyle', + 'aria', + 'arid', + 'ariel', + 'arietta', + 'aright', + 'aril', + 'arillode', + 'ariose', + 'arioso', + 'arise', + 'arista', + 'aristate', + 'aristocracy', + 'aristocrat', + 'aristocratic', + 'arithmetic', + 'arithmetician', + 'arithmomancy', + 'ark', + 'arkose', + 'arm', + 'armada', + 'armadillo', + 'armament', + 'armature', + 'armchair', + 'armed', + 'armet', + 'armful', + 'armhole', + 'armiger', + 'armilla', + 'armillary', + 'arming', + 'armipotent', + 'armistice', + 'armlet', + 'armoire', + 'armor', + 'armored', + 'armorer', + 'armorial', + 'armory', + 'armour', + 'armoured', + 'armourer', + 'armoury', + 'armpit', + 'armrest', + 'arms', + 'armure', + 'army', + 'armyworm', + 'arnica', + 'aroid', + 'aroma', + 'aromatic', + 'aromaticity', + 'aromatize', + 'arose', + 'around', + 'arouse', + 'arpeggio', + 'arpent', + 'arquebus', + 'arrack', + 'arraign', + 'arraignment', + 'arrange', + 'arrangement', + 'arrant', + 'arras', + 'array', + 'arrear', + 'arrearage', + 'arrears', + 'arrest', + 'arrester', + 'arresting', + 'arrestment', + 'arrhythmia', + 'arris', + 'arrival', + 'arrive', + 'arrivederci', + 'arriviste', + 'arroba', + 'arrogance', + 'arrogant', + 'arrogate', + 'arrondissement', + 'arrow', + 'arrowhead', + 'arrowroot', + 'arrowwood', + 'arrowworm', + 'arrowy', + 'arroyo', + 'arse', + 'arsenal', + 'arsenate', + 'arsenic', + 'arsenical', + 'arsenide', + 'arsenious', + 'arsenite', + 'arsenopyrite', + 'arsine', + 'arsis', + 'arson', + 'arsonist', + 'arsphenamine', + 'art', + 'artefact', + 'artel', + 'artemisia', + 'arterial', + 'arterialize', + 'arteriole', + 'arteriosclerosis', + 'arteriotomy', + 'arteriovenous', + 'arteritis', + 'artery', + 'artful', + 'arthralgia', + 'arthritis', + 'arthromere', + 'arthropod', + 'arthrospore', + 'artichoke', + 'article', + 'articular', + 'articulate', + 'articulation', + 'articulator', + 'artifact', + 'artifice', + 'artificer', + 'artificial', + 'artificiality', + 'artillery', + 'artilleryman', + 'artiodactyl', + 'artisan', + 'artist', + 'artiste', + 'artistic', + 'artistry', + 'artless', + 'artwork', + 'arty', + 'arum', + 'arundinaceous', + 'aruspex', + 'arvo', + 'aryl', + 'arytenoid', + 'as', + 'asafetida', + 'asafoetida', + 'asarum', + 'asbestos', + 'asbestosis', + 'ascariasis', + 'ascarid', + 'ascend', + 'ascendancy', + 'ascendant', + 'ascender', + 'ascending', + 'ascension', + 'ascensive', + 'ascent', + 'ascertain', + 'ascetic', + 'asceticism', + 'asci', + 'ascidian', + 'ascidium', + 'ascites', + 'asclepiadaceous', + 'ascocarp', + 'ascogonium', + 'ascomycete', + 'ascospore', + 'ascot', + 'ascribe', + 'ascription', + 'ascus', + 'asdic', + 'aseity', + 'asepsis', + 'aseptic', + 'asexual', + 'ash', + 'ashamed', + 'ashcan', + 'ashen', + 'ashes', + 'ashlar', + 'ashlaring', + 'ashore', + 'ashram', + 'ashtray', + 'ashy', + 'aside', + 'asinine', + 'ask', + 'askance', + 'askew', + 'aslant', + 'asleep', + 'aslope', + 'asocial', + 'asomatous', + 'asp', + 'asparagine', + 'asparagus', + 'aspect', + 'aspectual', + 'aspen', + 'asper', + 'aspergillosis', + 'aspergillum', + 'aspergillus', + 'asperity', + 'asperse', + 'aspersion', + 'aspersorium', + 'asphalt', + 'asphaltite', + 'asphodel', + 'asphyxia', + 'asphyxiant', + 'asphyxiate', + 'aspic', + 'aspidistra', + 'aspirant', + 'aspirate', + 'aspiration', + 'aspirator', + 'aspire', + 'aspirin', + 'asquint', + 'ass', + 'assagai', + 'assai', + 'assail', + 'assailant', + 'assassin', + 'assassinate', + 'assault', + 'assay', + 'assegai', + 'assemblage', + 'assemble', + 'assembled', + 'assembler', + 'assembly', + 'assemblyman', + 'assent', + 'assentation', + 'assentor', + 'assert', + 'asserted', + 'assertion', + 'assertive', + 'assess', + 'assessment', + 'assessor', + 'asset', + 'assets', + 'asseverate', + 'asseveration', + 'assibilate', + 'assiduity', + 'assiduous', + 'assign', + 'assignable', + 'assignat', + 'assignation', + 'assignee', + 'assignment', + 'assignor', + 'assimilable', + 'assimilate', + 'assimilation', + 'assimilative', + 'assist', + 'assistance', + 'assistant', + 'assize', + 'assizes', + 'associate', + 'association', + 'associationism', + 'associative', + 'assoil', + 'assonance', + 'assort', + 'assorted', + 'assortment', + 'assuage', + 'assuasive', + 'assume', + 'assumed', + 'assuming', + 'assumpsit', + 'assumption', + 'assumptive', + 'assurance', + 'assure', + 'assured', + 'assurgent', + 'astatic', + 'astatine', + 'aster', + 'astereognosis', + 'asteriated', + 'asterisk', + 'asterism', + 'astern', + 'asternal', + 'asteroid', + 'asthenia', + 'asthenic', + 'asthenopia', + 'asthenosphere', + 'asthma', + 'asthmatic', + 'astigmatic', + 'astigmatism', + 'astigmia', + 'astilbe', + 'astir', + 'astomatous', + 'astonied', + 'astonish', + 'astonishing', + 'astonishment', + 'astound', + 'astounding', + 'astraddle', + 'astragal', + 'astragalus', + 'astrakhan', + 'astral', + 'astraphobia', + 'astray', + 'astrict', + 'astride', + 'astringent', + 'astrionics', + 'astrobiology', + 'astrodome', + 'astrodynamics', + 'astrogate', + 'astrogation', + 'astrogeology', + 'astrograph', + 'astroid', + 'astrolabe', + 'astrology', + 'astromancy', + 'astrometry', + 'astronaut', + 'astronautics', + 'astronavigation', + 'astronomer', + 'astronomical', + 'astronomy', + 'astrophotography', + 'astrophysics', + 'astrosphere', + 'astute', + 'astylar', + 'asunder', + 'aswarm', + 'asyllabic', + 'asylum', + 'asymmetric', + 'asymmetry', + 'asymptomatic', + 'asymptote', + 'asymptotic', + 'asynchronism', + 'asyndeton', + 'at', + 'ataghan', + 'ataman', + 'ataractic', + 'ataraxia', + 'atavism', + 'atavistic', + 'ataxia', + 'ate', + 'atelectasis', + 'atelier', + 'athanasia', + 'athanor', + 'atheism', + 'atheist', + 'atheistic', + 'atheling', + 'athematic', + 'athenaeum', + 'atheroma', + 'atherosclerosis', + 'athirst', + 'athlete', + 'athletic', + 'athletics', + 'athodyd', + 'athwart', + 'athwartships', + 'atilt', + 'atingle', + 'atiptoe', + 'atlantes', + 'atlas', + 'atman', + 'atmolysis', + 'atmometer', + 'atmosphere', + 'atmospheric', + 'atmospherics', + 'atoll', + 'atom', + 'atomic', + 'atomicity', + 'atomics', + 'atomism', + 'atomize', + 'atomizer', + 'atomy', + 'atonal', + 'atonality', + 'atone', + 'atonement', + 'atonic', + 'atony', + 'atop', + 'atrabilious', + 'atrioventricular', + 'atrip', + 'atrium', + 'atrocious', + 'atrocity', + 'atrophied', + 'atrophy', + 'atropine', + 'attaboy', + 'attach', + 'attached', + 'attachment', + 'attack', + 'attain', + 'attainable', + 'attainder', + 'attainment', + 'attaint', + 'attainture', + 'attar', + 'attemper', + 'attempt', + 'attend', + 'attendance', + 'attendant', + 'attending', + 'attention', + 'attentive', + 'attenuant', + 'attenuate', + 'attenuation', + 'attenuator', + 'attest', + 'attestation', + 'attested', + 'attic', + 'attire', + 'attired', + 'attitude', + 'attitudinarian', + 'attitudinize', + 'attorn', + 'attorney', + 'attract', + 'attractant', + 'attraction', + 'attractive', + 'attrahent', + 'attribute', + 'attribution', + 'attributive', + 'attrition', + 'attune', + 'atween', + 'atwitter', + 'atypical', + 'aubade', + 'auberge', + 'aubergine', + 'auburn', + 'auction', + 'auctioneer', + 'auctorial', + 'audacious', + 'audacity', + 'audible', + 'audience', + 'audient', + 'audile', + 'audio', + 'audiogenic', + 'audiology', + 'audiometer', + 'audiophile', + 'audiovisual', + 'audiphone', + 'audit', + 'audition', + 'auditor', + 'auditorium', + 'auditory', + 'augend', + 'auger', + 'aught', + 'augite', + 'augment', + 'augmentation', + 'augmentative', + 'augmented', + 'augmenter', + 'augur', + 'augury', + 'august', + 'auk', + 'auklet', + 'auld', + 'aulic', + 'aulos', + 'aunt', + 'auntie', + 'aura', + 'aural', + 'auramine', + 'aurar', + 'aureate', + 'aurelia', + 'aureole', + 'aureolin', + 'aureus', + 'auric', + 'auricle', + 'auricula', + 'auricular', + 'auriculate', + 'auriferous', + 'aurify', + 'auriscope', + 'aurist', + 'aurochs', + 'aurora', + 'auroral', + 'aurous', + 'aurum', + 'auscultate', + 'auscultation', + 'auspex', + 'auspicate', + 'auspice', + 'auspicious', + 'austenite', + 'austere', + 'austerity', + 'austral', + 'autacoid', + 'autarch', + 'autarchy', + 'autarky', + 'autecology', + 'auteur', + 'authentic', + 'authenticate', + 'authenticity', + 'author', + 'authoritarian', + 'authoritative', + 'authority', + 'authorization', + 'authorize', + 'authorized', + 'authors', + 'authorship', + 'autism', + 'auto', + 'autobahn', + 'autobiographical', + 'autobiography', + 'autobus', + 'autocade', + 'autocatalysis', + 'autocephalous', + 'autochthon', + 'autochthonous', + 'autoclave', + 'autocorrelation', + 'autocracy', + 'autocrat', + 'autocratic', + 'autodidact', + 'autoerotic', + 'autoeroticism', + 'autoerotism', + 'autogamy', + 'autogenesis', + 'autogenous', + 'autogiro', + 'autograft', + 'autograph', + 'autography', + 'autohypnosis', + 'autoicous', + 'autointoxication', + 'autoionization', + 'autolithography', + 'autolysin', + 'autolysis', + 'automat', + 'automata', + 'automate', + 'automatic', + 'automation', + 'automatism', + 'automatize', + 'automaton', + 'automobile', + 'automotive', + 'autonomic', + 'autonomous', + 'autonomy', + 'autophyte', + 'autopilot', + 'autoplasty', + 'autopsy', + 'autoradiograph', + 'autorotation', + 'autoroute', + 'autosome', + 'autostability', + 'autostrada', + 'autosuggestion', + 'autotomize', + 'autotomy', + 'autotoxin', + 'autotransformer', + 'autotrophic', + 'autotruck', + 'autotype', + 'autoxidation', + 'autumn', + 'autumnal', + 'autunite', + 'auxesis', + 'auxiliaries', + 'auxiliary', + 'auxin', + 'auxochrome', + 'avadavat', + 'avail', + 'availability', + 'available', + 'avalanche', + 'avarice', + 'avaricious', + 'avast', + 'avatar', + 'avaunt', + 'ave', + 'avenge', + 'avens', + 'aventurine', + 'avenue', + 'aver', + 'average', + 'averment', + 'averse', + 'aversion', + 'avert', + 'avian', + 'aviary', + 'aviate', + 'aviation', + 'aviator', + 'aviatrix', + 'aviculture', + 'avid', + 'avidin', + 'avidity', + 'avifauna', + 'avigation', + 'avion', + 'avionics', + 'avirulent', + 'avitaminosis', + 'avocado', + 'avocation', + 'avocet', + 'avoid', + 'avoidance', + 'avoirdupois', + 'avouch', + 'avow', + 'avowal', + 'avowed', + 'avulsion', + 'avuncular', + 'avunculate', + 'aw', + 'await', + 'awake', + 'awaken', + 'awakening', + 'award', + 'aware', + 'awash', + 'away', + 'awe', + 'aweather', + 'awed', + 'aweigh', + 'aweless', + 'awesome', + 'awestricken', + 'awful', + 'awfully', + 'awhile', + 'awhirl', + 'awkward', + 'awl', + 'awlwort', + 'awn', + 'awning', + 'awoke', + 'awry', + 'ax', + 'axe', + 'axenic', + 'axes', + 'axial', + 'axil', + 'axilla', + 'axillary', + 'axinomancy', + 'axiology', + 'axiom', + 'axiomatic', + 'axis', + 'axle', + 'axletree', + 'axolotl', + 'axon', + 'axseed', + 'ay', + 'ayah', + 'aye', + 'ayin', + 'azalea', + 'azan', + 'azedarach', + 'azeotrope', + 'azide', + 'azimuth', + 'azine', + 'azo', + 'azobenzene', + 'azoic', + 'azole', + 'azote', + 'azotemia', + 'azoth', + 'azotic', + 'azotize', + 'azotobacter', + 'azure', + 'azurite', + 'azygous', + 'b', + 'baa', + 'baba', + 'babassu', + 'babbitt', + 'babble', + 'babblement', + 'babbler', + 'babbling', + 'babe', + 'babiche', + 'babirusa', + 'baboon', + 'babu', + 'babul', + 'babushka', + 'baby', + 'baccalaureate', + 'baccarat', + 'baccate', + 'bacchanal', + 'bacchanalia', + 'bacchant', + 'bacchius', + 'bacciferous', + 'bacciform', + 'baccivorous', + 'baccy', + 'bach', + 'bachelor', + 'bachelorism', + 'bacillary', + 'bacillus', + 'bacitracin', + 'back', + 'backache', + 'backbencher', + 'backbend', + 'backbite', + 'backblocks', + 'backboard', + 'backbone', + 'backbreaker', + 'backbreaking', + 'backchat', + 'backcourt', + 'backcross', + 'backdate', + 'backdrop', + 'backed', + 'backer', + 'backfield', + 'backfill', + 'backfire', + 'backflow', + 'backgammon', + 'background', + 'backhand', + 'backhanded', + 'backhander', + 'backhouse', + 'backing', + 'backlash', + 'backlog', + 'backpack', + 'backplate', + 'backrest', + 'backsaw', + 'backscratcher', + 'backset', + 'backsheesh', + 'backside', + 'backsight', + 'backslide', + 'backspace', + 'backspin', + 'backstage', + 'backstairs', + 'backstay', + 'backstitch', + 'backstop', + 'backstretch', + 'backstroke', + 'backswept', + 'backsword', + 'backtrack', + 'backup', + 'backward', + 'backwardation', + 'backwards', + 'backwash', + 'backwater', + 'backwoods', + 'backwoodsman', + 'bacon', + 'bacteria', + 'bactericide', + 'bacterin', + 'bacteriology', + 'bacteriolysis', + 'bacteriophage', + 'bacteriostasis', + 'bacteriostat', + 'bacterium', + 'bacteroid', + 'baculiform', + 'bad', + 'badderlocks', + 'baddie', + 'bade', + 'badge', + 'badger', + 'badinage', + 'badlands', + 'badly', + 'badman', + 'badminton', + 'bael', + 'baffle', + 'bag', + 'bagasse', + 'bagatelle', + 'bagel', + 'baggage', + 'bagging', + 'baggy', + 'baggywrinkle', + 'bagman', + 'bagnio', + 'bagpipe', + 'bagpipes', + 'bags', + 'baguette', + 'baguio', + 'bagwig', + 'bagworm', + 'bah', + 'bahadur', + 'baht', + 'bahuvrihi', + 'bail', + 'bailable', + 'bailee', + 'bailey', + 'bailie', + 'bailiff', + 'bailiwick', + 'bailment', + 'bailor', + 'bailsman', + 'bainite', + 'bairn', + 'bait', + 'baize', + 'bake', + 'bakehouse', + 'baker', + 'bakery', + 'baking', + 'baklava', + 'baksheesh', + 'bal', + 'balalaika', + 'balance', + 'balanced', + 'balancer', + 'balas', + 'balata', + 'balboa', + 'balbriggan', + 'balcony', + 'bald', + 'baldachin', + 'balderdash', + 'baldhead', + 'baldheaded', + 'baldpate', + 'baldric', + 'bale', + 'baleen', + 'balefire', + 'baleful', + 'baler', + 'balk', + 'balky', + 'ball', + 'ballad', + 'ballade', + 'balladeer', + 'balladist', + 'balladmonger', + 'balladry', + 'ballast', + 'ballata', + 'ballerina', + 'ballet', + 'ballflower', + 'ballista', + 'ballistic', + 'ballistics', + 'ballocks', + 'ballon', + 'ballonet', + 'balloon', + 'ballot', + 'ballottement', + 'ballplayer', + 'ballroom', + 'balls', + 'bally', + 'ballyhoo', + 'ballyrag', + 'balm', + 'balmacaan', + 'balmy', + 'balneal', + 'balneology', + 'baloney', + 'balsa', + 'balsam', + 'balsamic', + 'balsamiferous', + 'balsaminaceous', + 'baluster', + 'balustrade', + 'bambino', + 'bamboo', + 'bamboozle', + 'ban', + 'banal', + 'banana', + 'bananas', + 'banausic', + 'banc', + 'band', + 'bandage', + 'bandanna', + 'bandbox', + 'bandeau', + 'banded', + 'banderilla', + 'banderillero', + 'banderole', + 'bandicoot', + 'bandit', + 'banditry', + 'bandmaster', + 'bandog', + 'bandoleer', + 'bandolier', + 'bandoline', + 'bandore', + 'bandsman', + 'bandstand', + 'bandurria', + 'bandwagon', + 'bandwidth', + 'bandy', + 'bane', + 'baneberry', + 'baneful', + 'bang', + 'banger', + 'bangle', + 'bangtail', + 'bani', + 'banian', + 'banish', + 'banister', + 'banjo', + 'bank', + 'bankable', + 'bankbook', + 'banker', + 'banket', + 'banking', + 'bankroll', + 'bankrupt', + 'bankruptcy', + 'banksia', + 'banlieue', + 'banner', + 'banneret', + 'bannerol', + 'bannock', + 'banns', + 'banquet', + 'banquette', + 'bans', + 'banshee', + 'bant', + 'bantam', + 'bantamweight', + 'banter', + 'banting', + 'bantling', + 'banyan', + 'banzai', + 'baobab', + 'baptism', + 'baptistery', + 'baptistry', + 'baptize', + 'bar', + 'barathea', + 'barb', + 'barbarian', + 'barbaric', + 'barbarism', + 'barbarity', + 'barbarize', + 'barbarous', + 'barbate', + 'barbecue', + 'barbed', + 'barbel', + 'barbell', + 'barbellate', + 'barber', + 'barberry', + 'barbershop', + 'barbet', + 'barbette', + 'barbican', + 'barbicel', + 'barbital', + 'barbitone', + 'barbiturate', + 'barbiturism', + 'barbule', + 'barbwire', + 'barcarole', + 'barchan', + 'bard', + 'barde', + 'bare', + 'bareback', + 'barefaced', + 'barefoot', + 'barehanded', + 'bareheaded', + 'barely', + 'baresark', + 'barfly', + 'bargain', + 'barge', + 'bargeboard', + 'bargello', + 'bargeman', + 'barghest', + 'baric', + 'barilla', + 'barite', + 'baritone', + 'barium', + 'bark', + 'barkeeper', + 'barkentine', + 'barker', + 'barley', + 'barleycorn', + 'barm', + 'barmaid', + 'barman', + 'barmy', + 'barn', + 'barnacle', + 'barney', + 'barnstorm', + 'barnyard', + 'barogram', + 'barograph', + 'barometer', + 'barometrograph', + 'barometry', + 'baron', + 'baronage', + 'baroness', + 'baronet', + 'baronetage', + 'baronetcy', + 'barong', + 'baronial', + 'barony', + 'baroque', + 'baroscope', + 'barouche', + 'barque', + 'barquentine', + 'barrack', + 'barracks', + 'barracoon', + 'barracuda', + 'barrage', + 'barramunda', + 'barranca', + 'barrator', + 'barratry', + 'barre', + 'barred', + 'barrel', + 'barrelhouse', + 'barren', + 'barrens', + 'barret', + 'barrette', + 'barretter', + 'barricade', + 'barrier', + 'barring', + 'barrio', + 'barrister', + 'barroom', + 'barrow', + 'bartender', + 'barter', + 'bartizan', + 'barton', + 'barye', + 'baryon', + 'baryta', + 'barytes', + 'baryton', + 'barytone', + 'basal', + 'basalt', + 'basaltware', + 'basanite', + 'bascinet', + 'bascule', + 'base', + 'baseball', + 'baseboard', + 'baseborn', + 'baseburner', + 'baseless', + 'baseline', + 'baseman', + 'basement', + 'bases', + 'bash', + 'bashaw', + 'bashful', + 'bashibazouk', + 'basic', + 'basically', + 'basicity', + 'basidiomycete', + 'basidiospore', + 'basidium', + 'basifixed', + 'basil', + 'basilar', + 'basilica', + 'basilisk', + 'basin', + 'basinet', + 'basion', + 'basipetal', + 'basis', + 'bask', + 'basket', + 'basketball', + 'basketry', + 'basketwork', + 'basophil', + 'bass', + 'bassarisk', + 'basset', + 'bassinet', + 'bassist', + 'basso', + 'bassoon', + 'basswood', + 'bast', + 'bastard', + 'bastardize', + 'bastardy', + 'baste', + 'bastille', + 'bastinado', + 'basting', + 'bastion', + 'bat', + 'batch', + 'bate', + 'bateau', + 'batfish', + 'batfowl', + 'bath', + 'bathe', + 'bathetic', + 'bathhouse', + 'batholith', + 'bathometer', + 'bathos', + 'bathrobe', + 'bathroom', + 'bathtub', + 'bathyal', + 'bathymetry', + 'bathypelagic', + 'bathyscaphe', + 'bathysphere', + 'batik', + 'batiste', + 'batman', + 'baton', + 'batrachian', + 'bats', + 'batsman', + 'batt', + 'battalion', + 'battement', + 'batten', + 'batter', + 'battery', + 'battik', + 'batting', + 'battle', + 'battled', + 'battledore', + 'battlefield', + 'battlement', + 'battleplane', + 'battleship', + 'battologize', + 'battology', + 'battue', + 'batty', + 'batwing', + 'bauble', + 'baud', + 'baudekin', + 'baulk', + 'bauxite', + 'bavardage', + 'bawbee', + 'bawcock', + 'bawd', + 'bawdry', + 'bawdy', + 'bawdyhouse', + 'bawl', + 'bay', + 'bayadere', + 'bayard', + 'bayberry', + 'bayonet', + 'bayou', + 'baywood', + 'bazaar', + 'bazooka', + 'bdellium', + 'be', + 'beach', + 'beachcomber', + 'beachhead', + 'beacon', + 'bead', + 'beaded', + 'beading', + 'beadle', + 'beadledom', + 'beadroll', + 'beadsman', + 'beady', + 'beagle', + 'beak', + 'beaker', + 'beam', + 'beaming', + 'beamy', + 'bean', + 'beanery', + 'beanfeast', + 'beanie', + 'beano', + 'beanpole', + 'beanstalk', + 'bear', + 'bearable', + 'bearberry', + 'bearcat', + 'beard', + 'bearded', + 'beardless', + 'bearer', + 'bearing', + 'bearish', + 'bearskin', + 'bearwood', + 'beast', + 'beastings', + 'beastly', + 'beat', + 'beaten', + 'beater', + 'beatific', + 'beatification', + 'beatify', + 'beating', + 'beatitude', + 'beatnik', + 'beau', + 'beaut', + 'beauteous', + 'beautician', + 'beautiful', + 'beautifully', + 'beautify', + 'beauty', + 'beaux', + 'beaver', + 'beaverette', + 'bebeerine', + 'bebeeru', + 'bebop', + 'becalm', + 'becalmed', + 'became', + 'because', + 'beccafico', + 'bechance', + 'becharm', + 'beck', + 'becket', + 'beckon', + 'becloud', + 'become', + 'becoming', + 'bed', + 'bedabble', + 'bedaub', + 'bedazzle', + 'bedbug', + 'bedchamber', + 'bedclothes', + 'bedcover', + 'bedder', + 'bedding', + 'bedeck', + 'bedel', + 'bedesman', + 'bedevil', + 'bedew', + 'bedfast', + 'bedfellow', + 'bedight', + 'bedim', + 'bedizen', + 'bedlam', + 'bedlamite', + 'bedmate', + 'bedpan', + 'bedplate', + 'bedpost', + 'bedrabble', + 'bedraggle', + 'bedraggled', + 'bedrail', + 'bedridden', + 'bedrock', + 'bedroll', + 'bedroom', + 'bedside', + 'bedsore', + 'bedspread', + 'bedspring', + 'bedstead', + 'bedstraw', + 'bedtime', + 'bedwarmer', + 'bee', + 'beebread', + 'beech', + 'beechnut', + 'beef', + 'beefburger', + 'beefcake', + 'beefeater', + 'beefsteak', + 'beefwood', + 'beefy', + 'beehive', + 'beekeeper', + 'beekeeping', + 'beeline', + 'been', + 'beep', + 'beer', + 'beery', + 'beestings', + 'beeswax', + 'beeswing', + 'beet', + 'beetle', + 'beetroot', + 'beeves', + 'beezer', + 'befall', + 'befit', + 'befitting', + 'befog', + 'befool', + 'before', + 'beforehand', + 'beforetime', + 'befoul', + 'befriend', + 'befuddle', + 'beg', + 'began', + 'begat', + 'beget', + 'beggar', + 'beggarly', + 'beggarweed', + 'beggary', + 'begin', + 'beginner', + 'beginning', + 'begird', + 'begone', + 'begonia', + 'begorra', + 'begot', + 'begotten', + 'begrime', + 'begrudge', + 'beguile', + 'beguine', + 'begum', + 'begun', + 'behalf', + 'behave', + 'behavior', + 'behaviorism', + 'behead', + 'beheld', + 'behemoth', + 'behest', + 'behind', + 'behindhand', + 'behold', + 'beholden', + 'behoof', + 'behoove', + 'beige', + 'being', + 'bejewel', + 'bel', + 'belabor', + 'belated', + 'belaud', + 'belay', + 'belch', + 'beldam', + 'beleaguer', + 'belemnite', + 'belfry', + 'belga', + 'belie', + 'belief', + 'believe', + 'belike', + 'belittle', + 'bell', + 'belladonna', + 'bellarmine', + 'bellbird', + 'bellboy', + 'belle', + 'belletrist', + 'bellflower', + 'bellhop', + 'bellicose', + 'bellied', + 'belligerence', + 'belligerency', + 'belligerent', + 'bellman', + 'bellow', + 'bellows', + 'bellwether', + 'bellwort', + 'belly', + 'bellyache', + 'bellyband', + 'bellybutton', + 'bellyful', + 'belomancy', + 'belong', + 'belonging', + 'belongings', + 'beloved', + 'below', + 'belt', + 'belted', + 'belting', + 'beluga', + 'belvedere', + 'bema', + 'bemean', + 'bemire', + 'bemoan', + 'bemock', + 'bemuse', + 'bemused', + 'ben', + 'bename', + 'bench', + 'bencher', + 'bend', + 'bender', + 'bendwise', + 'bendy', + 'beneath', + 'benedicite', + 'benedict', + 'benediction', + 'benefaction', + 'benefactor', + 'benefactress', + 'benefic', + 'benefice', + 'beneficence', + 'beneficent', + 'beneficial', + 'beneficiary', + 'benefit', + 'benempt', + 'benevolence', + 'benevolent', + 'bengaline', + 'benighted', + 'benign', + 'benignant', + 'benignity', + 'benison', + 'benjamin', + 'benne', + 'bennet', + 'benny', + 'bent', + 'benthos', + 'bentonite', + 'bentwood', + 'benumb', + 'benzaldehyde', + 'benzene', + 'benzidine', + 'benzine', + 'benzoate', + 'benzocaine', + 'benzofuran', + 'benzoic', + 'benzoin', + 'benzol', + 'benzophenone', + 'benzoyl', + 'benzyl', + 'bequeath', + 'bequest', + 'berate', + 'berberidaceous', + 'berberine', + 'berceuse', + 'bereave', + 'bereft', + 'beret', + 'berg', + 'bergamot', + 'bergschrund', + 'beriberi', + 'berkelium', + 'berley', + 'berlin', + 'berm', + 'berretta', + 'berry', + 'bersagliere', + 'berseem', + 'berserk', + 'berserker', + 'berth', + 'bertha', + 'beryl', + 'beryllium', + 'beseech', + 'beseem', + 'beset', + 'besetting', + 'beshrew', + 'beside', + 'besides', + 'besiege', + 'beslobber', + 'besmear', + 'besmirch', + 'besom', + 'besot', + 'besotted', + 'besought', + 'bespangle', + 'bespatter', + 'bespeak', + 'bespectacled', + 'bespoke', + 'bespread', + 'besprent', + 'besprinkle', + 'best', + 'bestead', + 'bestial', + 'bestiality', + 'bestialize', + 'bestiary', + 'bestir', + 'bestow', + 'bestraddle', + 'bestrew', + 'bestride', + 'bet', + 'beta', + 'betaine', + 'betake', + 'betatron', + 'betel', + 'beth', + 'bethel', + 'bethink', + 'bethought', + 'betide', + 'betimes', + 'betoken', + 'betony', + 'betook', + 'betray', + 'betroth', + 'betrothal', + 'betrothed', + 'betta', + 'better', + 'betterment', + 'bettor', + 'betulaceous', + 'between', + 'betweentimes', + 'betweenwhiles', + 'betwixt', + 'bevatron', + 'bevel', + 'beverage', + 'bevy', + 'bewail', + 'beware', + 'bewhiskered', + 'bewilder', + 'bewilderment', + 'bewitch', + 'bewray', + 'bey', + 'beyond', + 'bezant', + 'bezel', + 'bezique', + 'bezoar', + 'bezonian', + 'bhakti', + 'bhang', + 'bharal', + 'bialy', + 'biannual', + 'biannulate', + 'bias', + 'biased', + 'biathlon', + 'biauriculate', + 'biaxial', + 'bib', + 'bibb', + 'bibber', + 'bibcock', + 'bibelot', + 'biblioclast', + 'bibliofilm', + 'bibliogony', + 'bibliographer', + 'bibliography', + 'bibliolatry', + 'bibliology', + 'bibliomancy', + 'bibliomania', + 'bibliopegy', + 'bibliophage', + 'bibliophile', + 'bibliopole', + 'bibliotaph', + 'bibliotheca', + 'bibliotherapy', + 'bibulous', + 'bicameral', + 'bicapsular', + 'bicarb', + 'bicarbonate', + 'bice', + 'bicentenary', + 'bicentennial', + 'bicephalous', + 'biceps', + 'bichloride', + 'bichromate', + 'bicipital', + 'bicker', + 'bickering', + 'bicollateral', + 'bicolor', + 'biconcave', + 'biconvex', + 'bicorn', + 'bicuspid', + 'bicycle', + 'bicyclic', + 'bid', + 'bidarka', + 'biddable', + 'bidden', + 'bidding', + 'biddy', + 'bide', + 'bidentate', + 'bidet', + 'bield', + 'biennial', + 'bier', + 'biestings', + 'bifacial', + 'bifarious', + 'biff', + 'biffin', + 'bifid', + 'bifilar', + 'biflagellate', + 'bifocal', + 'bifocals', + 'bifoliate', + 'bifoliolate', + 'biforate', + 'biforked', + 'biform', + 'bifurcate', + 'big', + 'bigamist', + 'bigamous', + 'bigamy', + 'bigener', + 'bigeye', + 'biggin', + 'bighead', + 'bighorn', + 'bight', + 'bigmouth', + 'bignonia', + 'bignoniaceous', + 'bigot', + 'bigoted', + 'bigotry', + 'bigwig', + 'bijection', + 'bijou', + 'bijouterie', + 'bijugate', + 'bike', + 'bikini', + 'bilabial', + 'bilabiate', + 'bilander', + 'bilateral', + 'bilberry', + 'bilbo', + 'bile', + 'bilection', + 'bilestone', + 'bilge', + 'bilharziasis', + 'biliary', + 'bilinear', + 'bilingual', + 'bilious', + 'bilk', + 'bill', + 'billabong', + 'billboard', + 'billbug', + 'billet', + 'billfish', + 'billfold', + 'billhead', + 'billhook', + 'billiard', + 'billiards', + 'billing', + 'billingsgate', + 'billion', + 'billionaire', + 'billon', + 'billow', + 'billowy', + 'billposter', + 'billy', + 'billycock', + 'bilobate', + 'bilocular', + 'biltong', + 'bimah', + 'bimanous', + 'bimbo', + 'bimestrial', + 'bimetallic', + 'bimetallism', + 'bimolecular', + 'bimonthly', + 'bin', + 'binal', + 'binary', + 'binate', + 'binaural', + 'bind', + 'binder', + 'bindery', + 'binding', + 'bindle', + 'bindweed', + 'bine', + 'binge', + 'binghi', + 'bingle', + 'bingo', + 'binnacle', + 'binocular', + 'binoculars', + 'binomial', + 'binominal', + 'binturong', + 'binucleate', + 'bioastronautics', + 'biocatalyst', + 'biocellate', + 'biochemistry', + 'bioclimatology', + 'biodegradable', + 'biodynamics', + 'bioecology', + 'bioenergetics', + 'biofeedback', + 'biogen', + 'biogenesis', + 'biogeochemistry', + 'biogeography', + 'biographer', + 'biographical', + 'biography', + 'biological', + 'biologist', + 'biology', + 'bioluminescence', + 'biolysis', + 'biomass', + 'biome', + 'biomedicine', + 'biometrics', + 'biometry', + 'bionics', + 'bionomics', + 'biophysics', + 'bioplasm', + 'biopsy', + 'bioscope', + 'bioscopy', + 'biosphere', + 'biostatics', + 'biosynthesis', + 'biota', + 'biotechnology', + 'biotic', + 'biotin', + 'biotite', + 'biotope', + 'biotype', + 'bipack', + 'biparietal', + 'biparous', + 'bipartisan', + 'bipartite', + 'biparty', + 'biped', + 'bipetalous', + 'biphenyl', + 'bipinnate', + 'biplane', + 'bipod', + 'bipolar', + 'bipropellant', + 'biquadrate', + 'biquadratic', + 'biquarterly', + 'biracial', + 'biradial', + 'biramous', + 'birch', + 'bird', + 'birdbath', + 'birdcage', + 'birdhouse', + 'birdie', + 'birdlike', + 'birdlime', + 'birdman', + 'birdseed', + 'birefringence', + 'bireme', + 'biretta', + 'birl', + 'birr', + 'birth', + 'birthday', + 'birthmark', + 'birthplace', + 'birthright', + 'birthroot', + 'birthstone', + 'birthwort', + 'bis', + 'biscuit', + 'bise', + 'bisect', + 'bisector', + 'bisectrix', + 'biserrate', + 'bisexual', + 'bishop', + 'bishopric', + 'bisk', + 'bismuth', + 'bismuthic', + 'bismuthinite', + 'bismuthous', + 'bison', + 'bisque', + 'bissextile', + 'bister', + 'bistort', + 'bistoury', + 'bistre', + 'bistro', + 'bisulcate', + 'bisulfate', + 'bit', + 'bitartrate', + 'bitch', + 'bitchy', + 'bite', + 'biting', + 'bitstock', + 'bitt', + 'bitten', + 'bitter', + 'bitterling', + 'bittern', + 'bitternut', + 'bitterroot', + 'bitters', + 'bittersweet', + 'bitterweed', + 'bitty', + 'bitumen', + 'bituminize', + 'bituminous', + 'bivalent', + 'bivalve', + 'bivouac', + 'biweekly', + 'biyearly', + 'biz', + 'bizarre', + 'blab', + 'blabber', + 'blabbermouth', + 'black', + 'blackamoor', + 'blackball', + 'blackberry', + 'blackbird', + 'blackboard', + 'blackcap', + 'blackcock', + 'blackdamp', + 'blacken', + 'blackface', + 'blackfellow', + 'blackfish', + 'blackguard', + 'blackguardly', + 'blackhead', + 'blackheart', + 'blacking', + 'blackjack', + 'blackleg', + 'blacklist', + 'blackmail', + 'blackness', + 'blackout', + 'blackpoll', + 'blacksmith', + 'blacksnake', + 'blacktail', + 'blackthorn', + 'blacktop', + 'bladder', + 'bladdernose', + 'bladdernut', + 'bladderwort', + 'blade', + 'blaeberry', + 'blague', + 'blah', + 'blain', + 'blamable', + 'blame', + 'blamed', + 'blameful', + 'blameless', + 'blameworthy', + 'blanch', + 'blancmange', + 'bland', + 'blandish', + 'blandishment', + 'blandishments', + 'blank', + 'blankbook', + 'blanket', + 'blanketing', + 'blankly', + 'blare', + 'blarney', + 'blaspheme', + 'blasphemous', + 'blasphemy', + 'blast', + 'blasted', + 'blastema', + 'blasting', + 'blastocoel', + 'blastocyst', + 'blastoderm', + 'blastogenesis', + 'blastomere', + 'blastopore', + 'blastosphere', + 'blastula', + 'blat', + 'blatant', + 'blate', + 'blather', + 'blatherskite', + 'blaubok', + 'blaze', + 'blazer', + 'blazon', + 'blazonry', + 'bleach', + 'bleacher', + 'bleachers', + 'bleak', + 'blear', + 'bleary', + 'bleat', + 'bleb', + 'bleed', + 'bleeder', + 'bleeding', + 'blemish', + 'blench', + 'blend', + 'blende', + 'blender', + 'blennioid', + 'blenny', + 'blent', + 'blepharitis', + 'blesbok', + 'bless', + 'blessed', + 'blessing', + 'blest', + 'blether', + 'blew', + 'blight', + 'blighter', + 'blimey', + 'blimp', + 'blind', + 'blindage', + 'blinders', + 'blindfish', + 'blindfold', + 'blinding', + 'blindly', + 'blindstory', + 'blindworm', + 'blink', + 'blinker', + 'blinkers', + 'blinking', + 'blintz', + 'blintze', + 'blip', + 'bliss', + 'blissful', + 'blister', + 'blistery', + 'blithe', + 'blither', + 'blithering', + 'blithesome', + 'blitz', + 'blitzkrieg', + 'blizzard', + 'bloat', + 'bloated', + 'bloater', + 'blob', + 'bloc', + 'block', + 'blockade', + 'blockage', + 'blockbuster', + 'blockbusting', + 'blocked', + 'blockhead', + 'blockhouse', + 'blocking', + 'blockish', + 'blocky', + 'bloke', + 'blond', + 'blood', + 'bloodcurdling', + 'blooded', + 'bloodfin', + 'bloodhound', + 'bloodless', + 'bloodletting', + 'bloodline', + 'bloodmobile', + 'bloodroot', + 'bloodshed', + 'bloodshot', + 'bloodstain', + 'bloodstained', + 'bloodstock', + 'bloodstone', + 'bloodstream', + 'bloodsucker', + 'bloodthirsty', + 'bloody', + 'bloom', + 'bloomer', + 'bloomers', + 'bloomery', + 'blooming', + 'bloomy', + 'blooper', + 'blossom', + 'blot', + 'blotch', + 'blotchy', + 'blotter', + 'blotto', + 'blouse', + 'blouson', + 'blow', + 'blower', + 'blowfish', + 'blowfly', + 'blowgun', + 'blowhard', + 'blowhole', + 'blowing', + 'blown', + 'blowout', + 'blowpipe', + 'blowsy', + 'blowtorch', + 'blowtube', + 'blowup', + 'blowy', + 'blowzed', + 'blowzy', + 'blub', + 'blubber', + 'blubberhead', + 'blubbery', + 'blucher', + 'bludge', + 'bludgeon', + 'blue', + 'bluebell', + 'blueberry', + 'bluebill', + 'bluebird', + 'bluebonnet', + 'bluebottle', + 'bluecoat', + 'bluefish', + 'bluegill', + 'bluegrass', + 'blueing', + 'bluejacket', + 'blueness', + 'bluenose', + 'bluepoint', + 'blueprint', + 'blues', + 'bluestocking', + 'bluestone', + 'bluet', + 'bluetongue', + 'blueweed', + 'bluey', + 'bluff', + 'bluing', + 'bluish', + 'blunder', + 'blunderbuss', + 'blunge', + 'blunger', + 'blunt', + 'blur', + 'blurb', + 'blurt', + 'blush', + 'bluster', + 'boa', + 'boar', + 'board', + 'boarder', + 'boarding', + 'boardinghouse', + 'boardwalk', + 'boarfish', + 'boarhound', + 'boarish', + 'boart', + 'boast', + 'boaster', + 'boastful', + 'boat', + 'boatbill', + 'boatel', + 'boater', + 'boathouse', + 'boating', + 'boatload', + 'boatman', + 'boatsman', + 'boatswain', + 'boatyard', + 'bob', + 'bobbery', + 'bobbin', + 'bobbinet', + 'bobble', + 'bobby', + 'bobbysocks', + 'bobbysoxer', + 'bobcat', + 'bobolink', + 'bobsled', + 'bobsledding', + 'bobsleigh', + 'bobstay', + 'bobwhite', + 'bocage', + 'boccie', + 'bod', + 'bode', + 'bodega', + 'bodgie', + 'bodice', + 'bodiless', + 'bodily', + 'boding', + 'bodkin', + 'body', + 'bodycheck', + 'bodyguard', + 'bodywork', + 'boffin', + 'bog', + 'bogbean', + 'bogey', + 'bogeyman', + 'boggart', + 'boggle', + 'bogie', + 'bogle', + 'bogtrotter', + 'bogus', + 'bogy', + 'bohunk', + 'boil', + 'boiled', + 'boiler', + 'boilermaker', + 'boiling', + 'boisterous', + 'bola', + 'bold', + 'boldface', + 'bole', + 'bolection', + 'bolero', + 'boletus', + 'bolide', + 'bolivar', + 'boliviano', + 'boll', + 'bollard', + 'bollix', + 'bollworm', + 'bolo', + 'bologna', + 'bolometer', + 'boloney', + 'bolster', + 'bolt', + 'bolter', + 'boltonia', + 'boltrope', + 'bolus', + 'bomb', + 'bombacaceous', + 'bombard', + 'bombardier', + 'bombardon', + 'bombast', + 'bombastic', + 'bombazine', + 'bombe', + 'bomber', + 'bombproof', + 'bombshell', + 'bombsight', + 'bombycid', + 'bonanza', + 'bonbon', + 'bond', + 'bondage', + 'bonded', + 'bondholder', + 'bondmaid', + 'bondman', + 'bondsman', + 'bondstone', + 'bondswoman', + 'bondwoman', + 'bone', + 'boneblack', + 'bonefish', + 'bonehead', + 'boner', + 'boneset', + 'bonesetter', + 'boneyard', + 'bonfire', + 'bongo', + 'bonhomie', + 'bonito', + 'bonkers', + 'bonne', + 'bonnet', + 'bonny', + 'bonnyclabber', + 'bonsai', + 'bonspiel', + 'bontebok', + 'bonus', + 'bony', + 'bonze', + 'bonzer', + 'boo', + 'boob', + 'booby', + 'boodle', + 'boogeyman', + 'boogie', + 'boohoo', + 'book', + 'bookbinder', + 'bookbindery', + 'bookbinding', + 'bookcase', + 'bookcraft', + 'bookie', + 'booking', + 'bookish', + 'bookkeeper', + 'bookkeeping', + 'booklet', + 'booklover', + 'bookmaker', + 'bookman', + 'bookmark', + 'bookmobile', + 'bookplate', + 'bookrack', + 'bookrest', + 'bookseller', + 'bookshelf', + 'bookstack', + 'bookstall', + 'bookstand', + 'bookstore', + 'bookworm', + 'boom', + 'boomer', + 'boomerang', + 'boomkin', + 'boon', + 'boondocks', + 'boondoggle', + 'boong', + 'boor', + 'boorish', + 'boost', + 'booster', + 'boot', + 'bootblack', + 'booted', + 'bootee', + 'bootery', + 'booth', + 'bootie', + 'bootjack', + 'bootlace', + 'bootleg', + 'bootless', + 'bootlick', + 'boots', + 'bootstrap', + 'booty', + 'booze', + 'boozer', + 'boozy', + 'bop', + 'bora', + 'boracic', + 'boracite', + 'borage', + 'boraginaceous', + 'borak', + 'borate', + 'borax', + 'borborygmus', + 'bordello', + 'border', + 'bordereau', + 'borderer', + 'borderland', + 'borderline', + 'bordure', + 'bore', + 'boreal', + 'borecole', + 'boredom', + 'borehole', + 'borer', + 'boresome', + 'boric', + 'boride', + 'boring', + 'born', + 'borne', + 'borneol', + 'bornite', + 'boron', + 'borosilicate', + 'borough', + 'borrow', + 'borrowing', + 'borsch', + 'borscht', + 'borstal', + 'bort', + 'borzoi', + 'boscage', + 'boschbok', + 'boschvark', + 'bosh', + 'bosk', + 'bosket', + 'bosky', + 'bosom', + 'bosomed', + 'bosomy', + 'boson', + 'bosquet', + 'boss', + 'bossism', + 'bossy', + 'bosun', + 'bot', + 'botanical', + 'botanist', + 'botanize', + 'botanomancy', + 'botany', + 'botch', + 'botchy', + 'botel', + 'botfly', + 'both', + 'bother', + 'botheration', + 'bothersome', + 'bothy', + 'botryoidal', + 'bots', + 'bott', + 'bottle', + 'bottleneck', + 'bottom', + 'bottomless', + 'bottommost', + 'bottomry', + 'botulin', + 'botulinus', + 'botulism', + 'boudoir', + 'bouffant', + 'bouffe', + 'bough', + 'boughpot', + 'bought', + 'boughten', + 'bougie', + 'bouillabaisse', + 'bouilli', + 'bouillon', + 'boulder', + 'boule', + 'boulevard', + 'boulevardier', + 'bouleversement', + 'bounce', + 'bouncer', + 'bouncing', + 'bouncy', + 'bound', + 'boundary', + 'bounded', + 'bounden', + 'bounder', + 'boundless', + 'bounds', + 'bounteous', + 'bountiful', + 'bounty', + 'bouquet', + 'bourbon', + 'bourdon', + 'bourg', + 'bourgeois', + 'bourgeoisie', + 'bourgeon', + 'bourn', + 'bourse', + 'bouse', + 'boustrophedon', + 'bout', + 'boutique', + 'boutonniere', + 'bovid', + 'bovine', + 'bow', + 'bowdlerize', + 'bowel', + 'bower', + 'bowerbird', + 'bowery', + 'bowfin', + 'bowhead', + 'bowing', + 'bowknot', + 'bowl', + 'bowlder', + 'bowleg', + 'bowler', + 'bowline', + 'bowling', + 'bowls', + 'bowman', + 'bowse', + 'bowshot', + 'bowsprit', + 'bowstring', + 'bowyer', + 'box', + 'boxberry', + 'boxboard', + 'boxcar', + 'boxer', + 'boxfish', + 'boxhaul', + 'boxing', + 'boxthorn', + 'boxwood', + 'boy', + 'boyar', + 'boycott', + 'boyfriend', + 'boyhood', + 'boyish', + 'boyla', + 'boysenberry', + 'bozo', + 'bra', + 'brabble', + 'brace', + 'bracelet', + 'bracer', + 'braces', + 'brach', + 'brachial', + 'brachiate', + 'brachiopod', + 'brachium', + 'brachycephalic', + 'brachylogy', + 'brachypterous', + 'brachyuran', + 'bracing', + 'bracken', + 'bracket', + 'bracketing', + 'brackish', + 'bract', + 'bracteate', + 'bracteole', + 'brad', + 'bradawl', + 'bradycardia', + 'bradytelic', + 'brae', + 'brag', + 'braggadocio', + 'braggart', + 'braid', + 'braided', + 'braiding', + 'brail', + 'brain', + 'brainchild', + 'brainless', + 'brainpan', + 'brainsick', + 'brainstorm', + 'brainstorming', + 'brainwash', + 'brainwashing', + 'brainwork', + 'brainy', + 'braise', + 'brake', + 'brakeman', + 'brakesman', + 'bramble', + 'brambling', + 'brambly', + 'bran', + 'branch', + 'branchia', + 'branching', + 'branchiopod', + 'brand', + 'brandish', + 'brandling', + 'brandy', + 'branks', + 'branle', + 'branny', + 'brant', + 'brash', + 'brashy', + 'brasier', + 'brasilein', + 'brasilin', + 'brass', + 'brassard', + 'brassbound', + 'brasserie', + 'brassica', + 'brassie', + 'brassiere', + 'brassware', + 'brassy', + 'brat', + 'brattice', + 'brattishing', + 'bratwurst', + 'braunite', + 'bravado', + 'brave', + 'bravery', + 'bravissimo', + 'bravo', + 'bravura', + 'braw', + 'brawl', + 'brawn', + 'brawny', + 'braxy', + 'bray', + 'brayer', + 'braze', + 'brazen', + 'brazier', + 'brazil', + 'brazilein', + 'brazilin', + 'breach', + 'bread', + 'breadbasket', + 'breadboard', + 'breadfruit', + 'breadnut', + 'breadroot', + 'breadstuff', + 'breadth', + 'breadthways', + 'breadwinner', + 'break', + 'breakable', + 'breakage', + 'breakaway', + 'breakdown', + 'breaker', + 'breakfast', + 'breakfront', + 'breaking', + 'breakneck', + 'breakout', + 'breakthrough', + 'breakup', + 'breakwater', + 'bream', + 'breast', + 'breastbone', + 'breastpin', + 'breastplate', + 'breaststroke', + 'breastsummer', + 'breastwork', + 'breath', + 'breathe', + 'breathed', + 'breather', + 'breathing', + 'breathless', + 'breathtaking', + 'breathy', + 'breccia', + 'brecciate', + 'bred', + 'brede', + 'bree', + 'breech', + 'breechblock', + 'breechcloth', + 'breeches', + 'breeching', + 'breechloader', + 'breed', + 'breeder', + 'breeding', + 'breeks', + 'breeze', + 'breezeway', + 'breezy', + 'bregma', + 'brei', + 'bremsstrahlung', + 'brent', + 'brethren', + 'breve', + 'brevet', + 'breviary', + 'brevier', + 'brevity', + 'brew', + 'brewage', + 'brewery', + 'brewhouse', + 'brewing', + 'brewis', + 'brewmaster', + 'briar', + 'briarroot', + 'briarwood', + 'bribe', + 'bribery', + 'brick', + 'brickbat', + 'brickkiln', + 'bricklayer', + 'bricklaying', + 'brickle', + 'brickwork', + 'bricky', + 'brickyard', + 'bricole', + 'bridal', + 'bride', + 'bridegroom', + 'bridesmaid', + 'bridewell', + 'bridge', + 'bridgeboard', + 'bridgehead', + 'bridgework', + 'bridging', + 'bridle', + 'bridlewise', + 'bridoon', + 'brief', + 'briefcase', + 'briefing', + 'briefless', + 'briefs', + 'brier', + 'brierroot', + 'brierwood', + 'brig', + 'brigade', + 'brigadier', + 'brigand', + 'brigandage', + 'brigandine', + 'brigantine', + 'bright', + 'brighten', + 'brightness', + 'brightwork', + 'brill', + 'brilliance', + 'brilliancy', + 'brilliant', + 'brilliantine', + 'brim', + 'brimful', + 'brimmer', + 'brimstone', + 'brindle', + 'brindled', + 'brine', + 'bring', + 'brink', + 'brinkmanship', + 'briny', + 'brio', + 'brioche', + 'briolette', + 'briony', + 'briquet', + 'briquette', + 'brisance', + 'brisk', + 'brisket', + 'brisling', + 'bristle', + 'bristletail', + 'bristling', + 'brit', + 'britches', + 'britska', + 'brittle', + 'britzka', + 'broach', + 'broad', + 'broadax', + 'broadbill', + 'broadbrim', + 'broadcast', + 'broadcaster', + 'broadcasting', + 'broadcloth', + 'broaden', + 'broadleaf', + 'broadloom', + 'broadside', + 'broadsword', + 'broadtail', + 'brocade', + 'brocatel', + 'broccoli', + 'broch', + 'brochette', + 'brochure', + 'brock', + 'brocket', + 'brogan', + 'brogue', + 'broider', + 'broil', + 'broiler', + 'broke', + 'broken', + 'brokenhearted', + 'broker', + 'brokerage', + 'brolly', + 'bromal', + 'bromate', + 'bromeosin', + 'bromic', + 'bromide', + 'bromidic', + 'brominate', + 'bromine', + 'bromism', + 'bromoform', + 'bronchi', + 'bronchia', + 'bronchial', + 'bronchiectasis', + 'bronchiole', + 'bronchitis', + 'bronchopneumonia', + 'bronchoscope', + 'bronchus', + 'bronco', + 'broncobuster', + 'bronze', + 'brooch', + 'brood', + 'brooder', + 'broody', + 'brook', + 'brookite', + 'brooklet', + 'brooklime', + 'brookweed', + 'broom', + 'broomcorn', + 'broomrape', + 'broomstick', + 'brose', + 'broth', + 'brothel', + 'brother', + 'brotherhood', + 'brotherly', + 'brougham', + 'brought', + 'brouhaha', + 'brow', + 'browband', + 'browbeat', + 'brown', + 'brownie', + 'browning', + 'brownout', + 'brownstone', + 'browse', + 'brucellosis', + 'brucine', + 'brucite', + 'bruin', + 'bruise', + 'bruiser', + 'bruit', + 'brumal', + 'brumby', + 'brume', + 'brunch', + 'brunet', + 'brunette', + 'brunt', + 'brush', + 'brushwood', + 'brushwork', + 'brusque', + 'brusquerie', + 'brut', + 'brutal', + 'brutality', + 'brutalize', + 'brute', + 'brutify', + 'brutish', + 'bryology', + 'bryony', + 'bryophyte', + 'bryozoan', + 'bub', + 'bubal', + 'bubaline', + 'bubble', + 'bubbler', + 'bubbly', + 'bubo', + 'bubonocele', + 'buccal', + 'buccaneer', + 'buccinator', + 'bucentaur', + 'buck', + 'buckaroo', + 'buckboard', + 'buckeen', + 'bucket', + 'buckeye', + 'buckhound', + 'buckish', + 'buckjump', + 'buckjumper', + 'buckle', + 'buckler', + 'buckling', + 'bucko', + 'buckra', + 'buckram', + 'bucksaw', + 'buckshee', + 'buckshot', + 'buckskin', + 'buckskins', + 'buckthorn', + 'bucktooth', + 'buckwheat', + 'bucolic', + 'bud', + 'buddhi', + 'buddle', + 'buddleia', + 'buddy', + 'budge', + 'budgerigar', + 'budget', + 'budgie', + 'bueno', + 'buff', + 'buffalo', + 'buffer', + 'buffet', + 'bufflehead', + 'buffo', + 'buffoon', + 'bug', + 'bugaboo', + 'bugbane', + 'bugbear', + 'bugeye', + 'bugger', + 'buggery', + 'buggy', + 'bughouse', + 'bugle', + 'bugleweed', + 'bugloss', + 'bugs', + 'buhl', + 'buhr', + 'buhrstone', + 'build', + 'builder', + 'building', + 'built', + 'bulb', + 'bulbar', + 'bulbiferous', + 'bulbil', + 'bulbous', + 'bulbul', + 'bulge', + 'bulimia', + 'bulk', + 'bulkhead', + 'bulky', + 'bull', + 'bulla', + 'bullace', + 'bullate', + 'bullbat', + 'bulldog', + 'bulldoze', + 'bulldozer', + 'bullet', + 'bulletin', + 'bulletproof', + 'bullfight', + 'bullfighter', + 'bullfinch', + 'bullfrog', + 'bullhead', + 'bullheaded', + 'bullhorn', + 'bullion', + 'bullish', + 'bullnose', + 'bullock', + 'bullpen', + 'bullring', + 'bullshit', + 'bullwhip', + 'bully', + 'bullyboy', + 'bullyrag', + 'bulrush', + 'bulwark', + 'bum', + 'bumbailiff', + 'bumble', + 'bumblebee', + 'bumbledom', + 'bumbling', + 'bumboat', + 'bumf', + 'bumkin', + 'bummalo', + 'bummer', + 'bump', + 'bumper', + 'bumpkin', + 'bumptious', + 'bumpy', + 'bun', + 'bunch', + 'bunchy', + 'bunco', + 'buncombe', + 'bund', + 'bundle', + 'bung', + 'bungalow', + 'bunghole', + 'bungle', + 'bunion', + 'bunk', + 'bunker', + 'bunkhouse', + 'bunkmate', + 'bunko', + 'bunkum', + 'bunny', + 'bunt', + 'bunting', + 'buntline', + 'bunyip', + 'buoy', + 'buoyage', + 'buoyancy', + 'buoyant', + 'buprestid', + 'bur', + 'buran', + 'burble', + 'burbot', + 'burden', + 'burdened', + 'burdensome', + 'burdock', + 'bureau', + 'bureaucracy', + 'bureaucrat', + 'bureaucratic', + 'bureaucratize', + 'burette', + 'burg', + 'burgage', + 'burgee', + 'burgeon', + 'burger', + 'burgess', + 'burgh', + 'burgher', + 'burglar', + 'burglarious', + 'burglarize', + 'burglary', + 'burgle', + 'burgomaster', + 'burgonet', + 'burgoo', + 'burgrave', + 'burial', + 'burier', + 'burin', + 'burka', + 'burke', + 'burl', + 'burlap', + 'burlesque', + 'burletta', + 'burley', + 'burly', + 'burn', + 'burned', + 'burner', + 'burnet', + 'burning', + 'burnish', + 'burnisher', + 'burnoose', + 'burnout', + 'burnsides', + 'burnt', + 'burp', + 'burr', + 'burro', + 'burrow', + 'burrstone', + 'burry', + 'bursa', + 'bursar', + 'bursarial', + 'bursary', + 'burse', + 'burseraceous', + 'bursiform', + 'bursitis', + 'burst', + 'burstone', + 'burthen', + 'burton', + 'burweed', + 'bury', + 'bus', + 'busboy', + 'busby', + 'bush', + 'bushbuck', + 'bushcraft', + 'bushed', + 'bushel', + 'bushelman', + 'bushhammer', + 'bushing', + 'bushman', + 'bushmaster', + 'bushranger', + 'bushtit', + 'bushwa', + 'bushwhack', + 'bushwhacker', + 'bushy', + 'busily', + 'business', + 'businesslike', + 'businessman', + 'businesswoman', + 'busk', + 'buskin', + 'buskined', + 'busload', + 'busman', + 'buss', + 'bust', + 'bustard', + 'bustee', + 'buster', + 'bustle', + 'busty', + 'busy', + 'busybody', + 'busyness', + 'busywork', + 'but', + 'butacaine', + 'butadiene', + 'butane', + 'butanol', + 'butanone', + 'butch', + 'butcher', + 'butcherbird', + 'butchery', + 'butene', + 'butler', + 'butlery', + 'butt', + 'butte', + 'butter', + 'butterball', + 'butterbur', + 'buttercup', + 'butterfat', + 'butterfingers', + 'butterfish', + 'butterflies', + 'butterfly', + 'buttermilk', + 'butternut', + 'butterscotch', + 'butterwort', + 'buttery', + 'buttock', + 'buttocks', + 'button', + 'buttonball', + 'buttonhole', + 'buttonhook', + 'buttons', + 'buttonwood', + 'buttress', + 'butyl', + 'butylene', + 'butyraceous', + 'butyraldehyde', + 'butyrate', + 'butyrin', + 'buxom', + 'buy', + 'buyer', + 'buzz', + 'buzzard', + 'buzzer', + 'bwana', + 'by', + 'bye', + 'byelaw', + 'bygone', + 'bylaw', + 'bypass', + 'bypath', + 'byre', + 'byrnie', + 'byroad', + 'byssinosis', + 'byssus', + 'bystander', + 'bystreet', + 'byte', + 'byway', + 'byword', + 'c', + 'cab', + 'cabal', + 'cabala', + 'cabalism', + 'cabalist', + 'cabalistic', + 'caballero', + 'cabana', + 'cabaret', + 'cabasset', + 'cabbage', + 'cabbagehead', + 'cabbageworm', + 'cabbala', + 'cabby', + 'cabdriver', + 'caber', + 'cabezon', + 'cabin', + 'cabinet', + 'cabinetmaker', + 'cabinetwork', + 'cable', + 'cablegram', + 'cablet', + 'cableway', + 'cabman', + 'cabob', + 'cabochon', + 'caboodle', + 'caboose', + 'cabotage', + 'cabretta', + 'cabrilla', + 'cabriole', + 'cabriolet', + 'cabstand', + 'cacao', + 'cacciatore', + 'cachalot', + 'cache', + 'cachepot', + 'cachet', + 'cachexia', + 'cachinnate', + 'cachou', + 'cachucha', + 'cacique', + 'cackle', + 'cacodemon', + 'cacodyl', + 'cacoepy', + 'cacogenics', + 'cacography', + 'cacology', + 'cacomistle', + 'cacophonous', + 'cacophony', + 'cactus', + 'cacuminal', + 'cad', + 'cadastre', + 'cadaver', + 'cadaverine', + 'cadaverous', + 'caddie', + 'caddis', + 'caddish', + 'caddy', + 'cade', + 'cadelle', + 'cadence', + 'cadency', + 'cadent', + 'cadenza', + 'cadet', + 'cadge', + 'cadi', + 'cadmium', + 'cadre', + 'caduceus', + 'caducity', + 'caducous', + 'caecilian', + 'caecum', + 'caenogenesis', + 'caeoma', + 'caesalpiniaceous', + 'caesium', + 'caespitose', + 'caesura', + 'cafard', + 'cafeteria', + 'caffeine', + 'caftan', + 'cage', + 'cageling', + 'cagey', + 'cahier', + 'cahoot', + 'caiman', + 'cain', + 'caird', + 'cairn', + 'cairngorm', + 'caisson', + 'caitiff', + 'cajeput', + 'cajole', + 'cajolery', + 'cajuput', + 'cake', + 'cakewalk', + 'calabash', + 'calaboose', + 'caladium', + 'calamanco', + 'calamander', + 'calamine', + 'calamint', + 'calamite', + 'calamitous', + 'calamity', + 'calamondin', + 'calamus', + 'calash', + 'calathus', + 'calaverite', + 'calcaneus', + 'calcar', + 'calcareous', + 'calcariferous', + 'calceiform', + 'calceolaria', + 'calces', + 'calcic', + 'calcicole', + 'calciferol', + 'calciferous', + 'calcific', + 'calcification', + 'calcifuge', + 'calcify', + 'calcimine', + 'calcine', + 'calcite', + 'calcium', + 'calculable', + 'calculate', + 'calculated', + 'calculating', + 'calculation', + 'calculator', + 'calculous', + 'calculus', + 'caldarium', + 'caldera', + 'caldron', + 'calefacient', + 'calefaction', + 'calefactory', + 'calendar', + 'calender', + 'calends', + 'calendula', + 'calenture', + 'calf', + 'calfskin', + 'caliber', + 'calibrate', + 'calibre', + 'calices', + 'caliche', + 'calicle', + 'calico', + 'calif', + 'califate', + 'californium', + 'caliginous', + 'calipash', + 'calipee', + 'caliper', + 'caliph', + 'caliphate', + 'calisaya', + 'calisthenics', + 'calix', + 'calk', + 'call', + 'calla', + 'callable', + 'callant', + 'callboy', + 'caller', + 'calligraphy', + 'calling', + 'calliope', + 'calliopsis', + 'callipash', + 'calliper', + 'callipygian', + 'callisthenics', + 'callosity', + 'callous', + 'callow', + 'callus', + 'calm', + 'calmative', + 'calomel', + 'caloric', + 'calorie', + 'calorifacient', + 'calorific', + 'calorimeter', + 'calotte', + 'caloyer', + 'calpac', + 'calque', + 'caltrop', + 'calumet', + 'calumniate', + 'calumniation', + 'calumnious', + 'calumny', + 'calutron', + 'calvaria', + 'calve', + 'calves', + 'calvities', + 'calx', + 'calyces', + 'calycine', + 'calycle', + 'calypso', + 'calyptra', + 'calyptrogen', + 'calyx', + 'cam', + 'camail', + 'camaraderie', + 'camarilla', + 'camass', + 'camber', + 'cambist', + 'cambium', + 'cambogia', + 'camboose', + 'cambrel', + 'cambric', + 'came', + 'camel', + 'camelback', + 'cameleer', + 'camellia', + 'camelopard', + 'cameo', + 'camera', + 'cameral', + 'cameraman', + 'camerlengo', + 'camion', + 'camisado', + 'camise', + 'camisole', + 'camlet', + 'camomile', + 'camouflage', + 'camp', + 'campaign', + 'campanile', + 'campanology', + 'campanula', + 'campanulaceous', + 'campanulate', + 'camper', + 'campestral', + 'campfire', + 'campground', + 'camphene', + 'camphor', + 'camphorate', + 'campion', + 'campo', + 'camporee', + 'campstool', + 'campus', + 'campy', + 'camshaft', + 'can', + 'canaigre', + 'canaille', + 'canakin', + 'canal', + 'canaliculus', + 'canalize', + 'canard', + 'canary', + 'canasta', + 'canaster', + 'cancan', + 'cancel', + 'cancellate', + 'cancellation', + 'cancer', + 'cancroid', + 'candela', + 'candelabra', + 'candelabrum', + 'candent', + 'candescent', + 'candid', + 'candidacy', + 'candidate', + 'candied', + 'candle', + 'candleberry', + 'candlefish', + 'candlelight', + 'candlemaker', + 'candlenut', + 'candlepin', + 'candlepower', + 'candlestand', + 'candlestick', + 'candlewick', + 'candlewood', + 'candor', + 'candy', + 'candytuft', + 'cane', + 'canebrake', + 'canella', + 'canescent', + 'canfield', + 'cangue', + 'canicular', + 'canikin', + 'canine', + 'caning', + 'canister', + 'canker', + 'cankered', + 'cankerous', + 'cankerworm', + 'canna', + 'cannabin', + 'cannabis', + 'canned', + 'cannelloni', + 'canner', + 'cannery', + 'cannibal', + 'cannibalism', + 'cannibalize', + 'cannikin', + 'canning', + 'cannon', + 'cannonade', + 'cannonball', + 'cannoneer', + 'cannonry', + 'cannot', + 'cannula', + 'cannular', + 'canny', + 'canoe', + 'canoewood', + 'canon', + 'canoness', + 'canonical', + 'canonicals', + 'canonicate', + 'canonicity', + 'canonist', + 'canonize', + 'canonry', + 'canoodle', + 'canopy', + 'canorous', + 'canso', + 'canst', + 'cant', + 'cantabile', + 'cantaloupe', + 'cantankerous', + 'cantata', + 'cantatrice', + 'canteen', + 'canter', + 'cantharides', + 'canthus', + 'canticle', + 'cantilena', + 'cantilever', + 'cantillate', + 'cantina', + 'cantle', + 'canto', + 'canton', + 'cantonment', + 'cantor', + 'cantoris', + 'cantrip', + 'cantus', + 'canty', + 'canula', + 'canvas', + 'canvasback', + 'canvass', + 'canyon', + 'canzona', + 'canzone', + 'canzonet', + 'caoutchouc', + 'cap', + 'capability', + 'capable', + 'capacious', + 'capacitance', + 'capacitate', + 'capacitor', + 'capacity', + 'caparison', + 'cape', + 'capelin', + 'caper', + 'capercaillie', + 'capeskin', + 'capful', + 'capias', + 'capillaceous', + 'capillarity', + 'capillary', + 'capita', + 'capital', + 'capitalism', + 'capitalist', + 'capitalistic', + 'capitalization', + 'capitalize', + 'capitally', + 'capitate', + 'capitation', + 'capitol', + 'capitular', + 'capitulary', + 'capitulate', + 'capitulation', + 'capitulum', + 'caplin', + 'capo', + 'capon', + 'caponize', + 'caporal', + 'capote', + 'capparidaceous', + 'capper', + 'capping', + 'cappuccino', + 'capreolate', + 'capriccio', + 'capriccioso', + 'caprice', + 'capricious', + 'caprification', + 'caprifig', + 'caprifoliaceous', + 'caprine', + 'capriole', + 'capsaicin', + 'capsicum', + 'capsize', + 'capstan', + 'capstone', + 'capsular', + 'capsulate', + 'capsule', + 'capsulize', + 'captain', + 'captainship', + 'caption', + 'captious', + 'captivate', + 'captive', + 'captivity', + 'captor', + 'capture', + 'capuche', + 'capuchin', + 'caput', + 'capybara', + 'car', + 'carabao', + 'carabin', + 'carabineer', + 'carabiniere', + 'caracal', + 'caracara', + 'caracole', + 'caracul', + 'carafe', + 'caramel', + 'caramelize', + 'carangid', + 'carapace', + 'carat', + 'caravan', + 'caravansary', + 'caravel', + 'caraway', + 'carbamate', + 'carbamidine', + 'carbarn', + 'carbazole', + 'carbide', + 'carbine', + 'carbineer', + 'carbohydrate', + 'carbolated', + 'carbolize', + 'carbon', + 'carbonaceous', + 'carbonado', + 'carbonate', + 'carbonation', + 'carbonic', + 'carboniferous', + 'carbonization', + 'carbonize', + 'carbonous', + 'carbonyl', + 'carboxylase', + 'carboxylate', + 'carboy', + 'carbuncle', + 'carburet', + 'carburetor', + 'carburize', + 'carbylamine', + 'carcajou', + 'carcanet', + 'carcass', + 'carcinogen', + 'carcinoma', + 'carcinomatosis', + 'card', + 'cardamom', + 'cardboard', + 'cardholder', + 'cardiac', + 'cardialgia', + 'cardigan', + 'cardinal', + 'cardinalate', + 'carding', + 'cardiogram', + 'cardiograph', + 'cardioid', + 'cardiology', + 'cardiomegaly', + 'cardiovascular', + 'carditis', + 'cardoon', + 'cards', + 'cardsharp', + 'carduaceous', + 'care', + 'careen', + 'career', + 'careerism', + 'careerist', + 'carefree', + 'careful', + 'careless', + 'caress', + 'caressive', + 'caret', + 'caretaker', + 'careworn', + 'carfare', + 'cargo', + 'carhop', + 'caribou', + 'caricature', + 'caries', + 'carillon', + 'carillonneur', + 'carina', + 'carinate', + 'carioca', + 'cariole', + 'carious', + 'caritas', + 'cark', + 'carl', + 'carline', + 'carling', + 'carload', + 'carmagnole', + 'carman', + 'carminative', + 'carmine', + 'carnage', + 'carnal', + 'carnallite', + 'carnassial', + 'carnation', + 'carnauba', + 'carnelian', + 'carnet', + 'carnify', + 'carnival', + 'carnivore', + 'carnivorous', + 'carnotite', + 'carny', + 'carob', + 'caroche', + 'carol', + 'carolus', + 'carom', + 'carotene', + 'carotenoid', + 'carotid', + 'carousal', + 'carouse', + 'carousel', + 'carp', + 'carpal', + 'carpel', + 'carpenter', + 'carpentry', + 'carpet', + 'carpetbag', + 'carpetbagger', + 'carpeting', + 'carpi', + 'carping', + 'carpogonium', + 'carpology', + 'carpometacarpus', + 'carpophagous', + 'carpophore', + 'carport', + 'carpospore', + 'carpus', + 'carrack', + 'carrageen', + 'carrefour', + 'carrel', + 'carriage', + 'carrier', + 'carriole', + 'carrion', + 'carronade', + 'carrot', + 'carroty', + 'carrousel', + 'carry', + 'carryall', + 'carse', + 'carsick', + 'cart', + 'cartage', + 'carte', + 'cartel', + 'cartelize', + 'cartilage', + 'cartilaginous', + 'cartload', + 'cartogram', + 'cartography', + 'cartomancy', + 'carton', + 'cartoon', + 'cartouche', + 'cartridge', + 'cartulary', + 'cartwheel', + 'caruncle', + 'carve', + 'carvel', + 'carven', + 'carving', + 'caryatid', + 'caryophyllaceous', + 'caryopsis', + 'casa', + 'casaba', + 'cascabel', + 'cascade', + 'cascara', + 'cascarilla', + 'case', + 'casease', + 'caseate', + 'caseation', + 'casebook', + 'casebound', + 'casefy', + 'casein', + 'caseinogen', + 'casemaker', + 'casemate', + 'casement', + 'caseose', + 'caseous', + 'casern', + 'casework', + 'caseworm', + 'cash', + 'cashbook', + 'cashbox', + 'cashew', + 'cashier', + 'cashmere', + 'casing', + 'casino', + 'cask', + 'casket', + 'casque', + 'cassaba', + 'cassareep', + 'cassation', + 'cassava', + 'casserole', + 'cassette', + 'cassia', + 'cassimere', + 'cassino', + 'cassis', + 'cassiterite', + 'cassock', + 'cassoulet', + 'cassowary', + 'cast', + 'castanets', + 'castaway', + 'caste', + 'castellan', + 'castellany', + 'castellated', + 'castellatus', + 'caster', + 'castigate', + 'casting', + 'castle', + 'castled', + 'castoff', + 'castor', + 'castrate', + 'castrato', + 'casual', + 'casualty', + 'casuist', + 'casuistry', + 'cat', + 'catabasis', + 'catabolism', + 'catabolite', + 'catacaustic', + 'catachresis', + 'cataclinal', + 'cataclysm', + 'cataclysmic', + 'catacomb', + 'catadromous', + 'catafalque', + 'catalase', + 'catalectic', + 'catalepsy', + 'catalo', + 'catalog', + 'catalogue', + 'catalpa', + 'catalysis', + 'catalyst', + 'catalyze', + 'catamaran', + 'catamenia', + 'catamite', + 'catamnesis', + 'catamount', + 'cataphoresis', + 'cataphyll', + 'cataplasia', + 'cataplasm', + 'cataplexy', + 'catapult', + 'cataract', + 'catarrh', + 'catarrhine', + 'catastrophe', + 'catastrophism', + 'catatonia', + 'catbird', + 'catboat', + 'catcall', + 'catch', + 'catchall', + 'catcher', + 'catchfly', + 'catching', + 'catchment', + 'catchpenny', + 'catchpole', + 'catchup', + 'catchweight', + 'catchword', + 'catchy', + 'cate', + 'catechetical', + 'catechin', + 'catechism', + 'catechist', + 'catechize', + 'catechol', + 'catechu', + 'catechumen', + 'categorical', + 'categorize', + 'category', + 'catena', + 'catenane', + 'catenary', + 'catenate', + 'catenoid', + 'cater', + 'cateran', + 'catercorner', + 'caterer', + 'catering', + 'caterpillar', + 'caterwaul', + 'catfall', + 'catfish', + 'catgut', + 'catharsis', + 'cathartic', + 'cathead', + 'cathedral', + 'cathepsin', + 'catheter', + 'catheterize', + 'cathexis', + 'cathode', + 'cathodoluminescence', + 'catholic', + 'catholicity', + 'catholicize', + 'catholicon', + 'cathouse', + 'cation', + 'catkin', + 'catlike', + 'catling', + 'catmint', + 'catnap', + 'catnip', + 'catoptrics', + 'catsup', + 'cattail', + 'cattalo', + 'cattery', + 'cattish', + 'cattle', + 'cattleman', + 'cattleya', + 'catty', + 'catwalk', + 'caucus', + 'cauda', + 'caudad', + 'caudal', + 'caudate', + 'caudex', + 'caudillo', + 'caudle', + 'caught', + 'caul', + 'cauldron', + 'caulescent', + 'caulicle', + 'cauliflower', + 'cauline', + 'caulis', + 'caulk', + 'causal', + 'causalgia', + 'causality', + 'causation', + 'causative', + 'cause', + 'causerie', + 'causeuse', + 'causeway', + 'causey', + 'caustic', + 'cauterant', + 'cauterize', + 'cautery', + 'caution', + 'cautionary', + 'cautious', + 'cavalcade', + 'cavalier', + 'cavalierly', + 'cavalla', + 'cavalry', + 'cavalryman', + 'cavatina', + 'cave', + 'caveat', + 'caveator', + 'cavefish', + 'caveman', + 'cavendish', + 'cavern', + 'cavernous', + 'cavesson', + 'cavetto', + 'caviar', + 'cavicorn', + 'cavie', + 'cavil', + 'cavitation', + 'cavity', + 'cavort', + 'cavy', + 'caw', + 'cay', + 'cayenne', + 'cayman', + 'cayuse', + 'cd', + 'cease', + 'ceaseless', + 'cecity', + 'cecum', + 'cedar', + 'cede', + 'cedilla', + 'ceiba', + 'ceil', + 'ceilidh', + 'ceiling', + 'ceilometer', + 'celadon', + 'celandine', + 'celebrant', + 'celebrate', + 'celebrated', + 'celebration', + 'celebrity', + 'celeriac', + 'celerity', + 'celery', + 'celesta', + 'celestial', + 'celestite', + 'celiac', + 'celibacy', + 'celibate', + 'celiotomy', + 'cell', + 'cella', + 'cellar', + 'cellarage', + 'cellarer', + 'cellaret', + 'cellist', + 'cello', + 'cellobiose', + 'celloidin', + 'cellophane', + 'cellular', + 'cellule', + 'cellulitis', + 'cellulose', + 'cellulosic', + 'cellulous', + 'celom', + 'celt', + 'celtuce', + 'cembalo', + 'cement', + 'cementation', + 'cementite', + 'cementum', + 'cemetery', + 'cenacle', + 'cenesthesia', + 'cenobite', + 'cenogenesis', + 'cenotaph', + 'cense', + 'censer', + 'censor', + 'censorious', + 'censorship', + 'censurable', + 'censure', + 'census', + 'cent', + 'cental', + 'centare', + 'centaur', + 'centaury', + 'centavo', + 'centenarian', + 'centenary', + 'centennial', + 'center', + 'centerboard', + 'centering', + 'centerpiece', + 'centesimal', + 'centesimo', + 'centiare', + 'centigrade', + 'centigram', + 'centiliter', + 'centillion', + 'centime', + 'centimeter', + 'centipede', + 'centipoise', + 'centistere', + 'centner', + 'cento', + 'centra', + 'central', + 'centralism', + 'centrality', + 'centralization', + 'centralize', + 'centre', + 'centreboard', + 'centrepiece', + 'centric', + 'centrifugal', + 'centrifugate', + 'centrifuge', + 'centring', + 'centriole', + 'centripetal', + 'centrist', + 'centrobaric', + 'centroclinal', + 'centroid', + 'centromere', + 'centrosome', + 'centrosphere', + 'centrosymmetric', + 'centrum', + 'centum', + 'centuple', + 'centuplicate', + 'centurial', + 'centurion', + 'century', + 'ceorl', + 'cephalad', + 'cephalalgia', + 'cephalic', + 'cephalization', + 'cephalochordate', + 'cephalometer', + 'cephalopod', + 'cephalothorax', + 'ceraceous', + 'ceramal', + 'ceramic', + 'ceramics', + 'ceramist', + 'cerargyrite', + 'cerate', + 'cerated', + 'ceratodus', + 'ceratoid', + 'cercaria', + 'cercus', + 'cere', + 'cereal', + 'cerebellum', + 'cerebral', + 'cerebrate', + 'cerebration', + 'cerebritis', + 'cerebroside', + 'cerebrospinal', + 'cerebrovascular', + 'cerebrum', + 'cerecloth', + 'cerement', + 'ceremonial', + 'ceremonious', + 'ceremony', + 'ceresin', + 'cereus', + 'ceria', + 'ceric', + 'cerise', + 'cerium', + 'cermet', + 'cernuous', + 'cero', + 'cerography', + 'ceroplastic', + 'ceroplastics', + 'cerotype', + 'cerous', + 'certain', + 'certainly', + 'certainty', + 'certes', + 'certifiable', + 'certificate', + 'certification', + 'certified', + 'certify', + 'certiorari', + 'certitude', + 'cerulean', + 'cerumen', + 'ceruse', + 'cerussite', + 'cervelat', + 'cervical', + 'cervicitis', + 'cervine', + 'cervix', + 'cesium', + 'cespitose', + 'cess', + 'cessation', + 'cession', + 'cessionary', + 'cesspool', + 'cesta', + 'cestode', + 'cestoid', + 'cestus', + 'cesura', + 'cetacean', + 'cetane', + 'cetology', + 'chabazite', + 'chacma', + 'chaconne', + 'chad', + 'chaeta', + 'chaetognath', + 'chaetopod', + 'chafe', + 'chafer', + 'chaff', + 'chaffer', + 'chaffinch', + 'chagrin', + 'chain', + 'chainman', + 'chainplate', + 'chair', + 'chairborne', + 'chairman', + 'chairmanship', + 'chairwoman', + 'chaise', + 'chalaza', + 'chalcanthite', + 'chalcedony', + 'chalcocite', + 'chalcography', + 'chalcopyrite', + 'chaldron', + 'chalet', + 'chalice', + 'chalk', + 'chalkboard', + 'chalkstone', + 'chalky', + 'challah', + 'challenge', + 'challenging', + 'challis', + 'chalone', + 'chalutz', + 'chalybeate', + 'chalybite', + 'cham', + 'chamade', + 'chamber', + 'chamberlain', + 'chambermaid', + 'chambers', + 'chambray', + 'chameleon', + 'chamfer', + 'chamfron', + 'chammy', + 'chamois', + 'chamomile', + 'champ', + 'champac', + 'champagne', + 'champaign', + 'champerty', + 'champignon', + 'champion', + 'championship', + 'chance', + 'chancel', + 'chancellery', + 'chancellor', + 'chancellorship', + 'chancery', + 'chancre', + 'chancroid', + 'chancy', + 'chandelier', + 'chandelle', + 'chandler', + 'chandlery', + 'change', + 'changeable', + 'changeful', + 'changeless', + 'changeling', + 'changeover', + 'channel', + 'channelize', + 'chanson', + 'chant', + 'chanter', + 'chanterelle', + 'chanteuse', + 'chantey', + 'chanticleer', + 'chantress', + 'chantry', + 'chanty', + 'chaos', + 'chaotic', + 'chap', + 'chaparajos', + 'chaparral', + 'chapatti', + 'chapbook', + 'chape', + 'chapeau', + 'chapel', + 'chaperon', + 'chaperone', + 'chapfallen', + 'chapiter', + 'chaplain', + 'chaplet', + 'chapman', + 'chappie', + 'chaps', + 'chapter', + 'chaqueta', + 'char', + 'charabanc', + 'character', + 'characteristic', + 'characteristically', + 'characterization', + 'characterize', + 'charactery', + 'charade', + 'charades', + 'charcoal', + 'charcuterie', + 'chard', + 'chare', + 'charge', + 'chargeable', + 'charged', + 'charger', + 'charily', + 'chariness', + 'chariot', + 'charioteer', + 'charisma', + 'charismatic', + 'charitable', + 'charity', + 'charivari', + 'charkha', + 'charlady', + 'charlatan', + 'charlatanism', + 'charlatanry', + 'charlock', + 'charlotte', + 'charm', + 'charmer', + 'charmeuse', + 'charming', + 'charnel', + 'charpoy', + 'charqui', + 'charr', + 'chart', + 'charter', + 'chartist', + 'chartography', + 'chartreuse', + 'chartulary', + 'charwoman', + 'chary', + 'chase', + 'chaser', + 'chasing', + 'chasm', + 'chassepot', + 'chasseur', + 'chassis', + 'chaste', + 'chasten', + 'chastise', + 'chastity', + 'chasuble', + 'chat', + 'chateau', + 'chatelain', + 'chatelaine', + 'chatoyant', + 'chattel', + 'chatter', + 'chatterbox', + 'chatterer', + 'chatty', + 'chaudfroid', + 'chauffer', + 'chauffeur', + 'chaulmoogra', + 'chaunt', + 'chausses', + 'chaussure', + 'chauvinism', + 'chaw', + 'chayote', + 'chazan', + 'cheap', + 'cheapen', + 'cheapskate', + 'cheat', + 'cheater', + 'check', + 'checkbook', + 'checked', + 'checker', + 'checkerberry', + 'checkerbloom', + 'checkerboard', + 'checkered', + 'checkers', + 'checkerwork', + 'checklist', + 'checkmate', + 'checkoff', + 'checkpoint', + 'checkrein', + 'checkroom', + 'checkrow', + 'checkup', + 'checky', + 'cheddite', + 'cheder', + 'cheek', + 'cheekbone', + 'cheekpiece', + 'cheeky', + 'cheep', + 'cheer', + 'cheerful', + 'cheerio', + 'cheerleader', + 'cheerless', + 'cheerly', + 'cheery', + 'cheese', + 'cheeseburger', + 'cheesecake', + 'cheesecloth', + 'cheeseparing', + 'cheesewood', + 'cheesy', + 'cheetah', + 'chef', + 'chela', + 'chelate', + 'chelicera', + 'cheliform', + 'cheloid', + 'chelonian', + 'chemical', + 'chemiluminescence', + 'chemise', + 'chemisette', + 'chemism', + 'chemisorb', + 'chemisorption', + 'chemist', + 'chemistry', + 'chemmy', + 'chemoprophylaxis', + 'chemoreceptor', + 'chemosmosis', + 'chemosphere', + 'chemosynthesis', + 'chemotaxis', + 'chemotherapy', + 'chemotropism', + 'chemurgy', + 'chenille', + 'chenopod', + 'cheongsam', + 'cheque', + 'chequer', + 'chequerboard', + 'chequered', + 'cherimoya', + 'cherish', + 'cheroot', + 'cherry', + 'chersonese', + 'chert', + 'cherub', + 'chervil', + 'chervonets', + 'chess', + 'chessboard', + 'chessman', + 'chest', + 'chesterfield', + 'chestnut', + 'chesty', + 'chetah', + 'chevalier', + 'chevet', + 'cheviot', + 'chevrette', + 'chevron', + 'chevrotain', + 'chevy', + 'chew', + 'chewink', + 'chewy', + 'chez', + 'chi', + 'chiack', + 'chiao', + 'chiaroscuro', + 'chiasma', + 'chiasmus', + 'chiastic', + 'chiastolite', + 'chibouk', + 'chic', + 'chicalote', + 'chicane', + 'chicanery', + 'chiccory', + 'chichi', + 'chick', + 'chickabiddy', + 'chickadee', + 'chickaree', + 'chicken', + 'chickenhearted', + 'chickpea', + 'chickweed', + 'chicle', + 'chico', + 'chicory', + 'chide', + 'chief', + 'chiefly', + 'chieftain', + 'chiffchaff', + 'chiffon', + 'chiffonier', + 'chifforobe', + 'chigetai', + 'chigger', + 'chignon', + 'chigoe', + 'chilblain', + 'child', + 'childbearing', + 'childbed', + 'childbirth', + 'childe', + 'childhood', + 'childish', + 'childlike', + 'children', + 'chile', + 'chili', + 'chiliad', + 'chiliarch', + 'chiliasm', + 'chill', + 'chiller', + 'chilli', + 'chilly', + 'chilopod', + 'chimaera', + 'chimb', + 'chime', + 'chimera', + 'chimere', + 'chimerical', + 'chimney', + 'chimp', + 'chimpanzee', + 'chin', + 'china', + 'chinaberry', + 'chinaware', + 'chincapin', + 'chinch', + 'chinchilla', + 'chinchy', + 'chine', + 'chinfest', + 'chink', + 'chinkapin', + 'chino', + 'chinoiserie', + 'chinook', + 'chinquapin', + 'chintz', + 'chintzy', + 'chip', + 'chipboard', + 'chipmunk', + 'chipper', + 'chippy', + 'chirk', + 'chirm', + 'chirography', + 'chiromancy', + 'chiropodist', + 'chiropody', + 'chiropractic', + 'chiropractor', + 'chiropteran', + 'chirp', + 'chirpy', + 'chirr', + 'chirrup', + 'chirrupy', + 'chirurgeon', + 'chisel', + 'chiseler', + 'chit', + 'chitarrone', + 'chitchat', + 'chitin', + 'chiton', + 'chitter', + 'chitterlings', + 'chivalric', + 'chivalrous', + 'chivalry', + 'chivaree', + 'chive', + 'chivy', + 'chlamydate', + 'chlamydeous', + 'chlamydospore', + 'chlamys', + 'chloral', + 'chloramine', + 'chloramphenicol', + 'chlorate', + 'chlordane', + 'chlorella', + 'chlorenchyma', + 'chloric', + 'chloride', + 'chlorinate', + 'chlorine', + 'chlorite', + 'chlorobenzene', + 'chloroform', + 'chlorohydrin', + 'chlorophyll', + 'chloropicrin', + 'chloroplast', + 'chloroprene', + 'chlorosis', + 'chlorothiazide', + 'chlorous', + 'chlorpromazine', + 'chlortetracycline', + 'choanocyte', + 'chock', + 'chocolate', + 'choice', + 'choir', + 'choirboy', + 'choirmaster', + 'choke', + 'chokeberry', + 'chokebore', + 'chokecherry', + 'chokedamp', + 'choker', + 'choking', + 'cholecalciferol', + 'cholecyst', + 'cholecystectomy', + 'cholecystitis', + 'cholecystotomy', + 'cholent', + 'choler', + 'cholera', + 'choleric', + 'cholesterol', + 'choli', + 'choline', + 'cholinesterase', + 'cholla', + 'chomp', + 'chon', + 'chondriosome', + 'chondrite', + 'chondroma', + 'chondrule', + 'chook', + 'choose', + 'choosey', + 'choosy', + 'chop', + 'chopfallen', + 'chophouse', + 'chopine', + 'choplogic', + 'chopper', + 'chopping', + 'choppy', + 'chops', + 'chopstick', + 'choragus', + 'choral', + 'chorale', + 'chord', + 'chordate', + 'chordophone', + 'chore', + 'chorea', + 'choreodrama', + 'choreograph', + 'choreographer', + 'choreography', + 'choriamb', + 'choric', + 'choriocarcinoma', + 'chorion', + 'chorister', + 'chorizo', + 'chorography', + 'choroid', + 'choroiditis', + 'chortle', + 'chorus', + 'chose', + 'chosen', + 'chou', + 'chough', + 'chow', + 'chowder', + 'chrestomathy', + 'chrism', + 'chrismatory', + 'chrisom', + 'christcross', + 'christen', + 'christening', + 'chroma', + 'chromate', + 'chromatic', + 'chromaticity', + 'chromaticness', + 'chromatics', + 'chromatid', + 'chromatin', + 'chromatism', + 'chromatogram', + 'chromatograph', + 'chromatography', + 'chromatology', + 'chromatolysis', + 'chromatophore', + 'chrome', + 'chromic', + 'chrominance', + 'chromite', + 'chromium', + 'chromo', + 'chromogen', + 'chromogenic', + 'chromolithograph', + 'chromolithography', + 'chromomere', + 'chromonema', + 'chromophore', + 'chromoplast', + 'chromoprotein', + 'chromosome', + 'chromosphere', + 'chromous', + 'chromyl', + 'chronaxie', + 'chronic', + 'chronicle', + 'chronogram', + 'chronograph', + 'chronological', + 'chronologist', + 'chronology', + 'chronometer', + 'chronometry', + 'chronon', + 'chronopher', + 'chronoscope', + 'chrysalid', + 'chrysalis', + 'chrysanthemum', + 'chrysarobin', + 'chryselephantine', + 'chrysoberyl', + 'chrysolite', + 'chrysoprase', + 'chrysotile', + 'chthonian', + 'chub', + 'chubby', + 'chuck', + 'chuckhole', + 'chuckle', + 'chucklehead', + 'chuckwalla', + 'chuddar', + 'chufa', + 'chuff', + 'chuffy', + 'chug', + 'chukar', + 'chukker', + 'chum', + 'chummy', + 'chump', + 'chunk', + 'chunky', + 'chuppah', + 'church', + 'churchgoer', + 'churchless', + 'churchlike', + 'churchly', + 'churchman', + 'churchwarden', + 'churchwoman', + 'churchy', + 'churchyard', + 'churinga', + 'churl', + 'churlish', + 'churn', + 'churning', + 'churr', + 'churrigueresque', + 'chute', + 'chutney', + 'chutzpah', + 'chyack', + 'chyle', + 'chyme', + 'chymotrypsin', + 'ciao', + 'ciborium', + 'cicada', + 'cicala', + 'cicatrix', + 'cicatrize', + 'cicely', + 'cicero', + 'cicerone', + 'cichlid', + 'cicisbeo', + 'cider', + 'cig', + 'cigar', + 'cigarette', + 'cigarillo', + 'cilia', + 'ciliary', + 'ciliate', + 'cilice', + 'ciliolate', + 'cilium', + 'cimbalom', + 'cimex', + 'cinch', + 'cinchona', + 'cinchonidine', + 'cinchonine', + 'cinchonism', + 'cinchonize', + 'cincture', + 'cinder', + 'cineaste', + 'cinema', + 'cinematograph', + 'cinematography', + 'cineraria', + 'cinerarium', + 'cinerary', + 'cinerator', + 'cinereous', + 'cingulum', + 'cinnabar', + 'cinnamon', + 'cinquain', + 'cinque', + 'cinquecento', + 'cinquefoil', + 'cipher', + 'cipolin', + 'circa', + 'circadian', + 'circinate', + 'circle', + 'circlet', + 'circuit', + 'circuitous', + 'circuitry', + 'circuity', + 'circular', + 'circularize', + 'circulate', + 'circulation', + 'circumambient', + 'circumambulate', + 'circumbendibus', + 'circumcise', + 'circumcision', + 'circumference', + 'circumferential', + 'circumflex', + 'circumfluent', + 'circumfluous', + 'circumfuse', + 'circumgyration', + 'circumjacent', + 'circumlocution', + 'circumlunar', + 'circumnavigate', + 'circumnutate', + 'circumpolar', + 'circumrotate', + 'circumscissile', + 'circumscribe', + 'circumscription', + 'circumsolar', + 'circumspect', + 'circumspection', + 'circumstance', + 'circumstantial', + 'circumstantiality', + 'circumstantiate', + 'circumvallate', + 'circumvent', + 'circumvolution', + 'circus', + 'cirque', + 'cirrate', + 'cirrhosis', + 'cirrocumulus', + 'cirrose', + 'cirrostratus', + 'cirrus', + 'cirsoid', + 'cisalpine', + 'cisco', + 'cislunar', + 'cismontane', + 'cispadane', + 'cissoid', + 'cist', + 'cistaceous', + 'cistern', + 'cisterna', + 'citadel', + 'citation', + 'cite', + 'cithara', + 'cither', + 'citified', + 'citify', + 'citizen', + 'citizenry', + 'citizenship', + 'citole', + 'citral', + 'citrange', + 'citrate', + 'citreous', + 'citric', + 'citriculture', + 'citrin', + 'citrine', + 'citron', + 'citronella', + 'citronellal', + 'citrus', + 'cittern', + 'city', + 'cityscape', + 'civet', + 'civic', + 'civics', + 'civies', + 'civil', + 'civilian', + 'civility', + 'civilization', + 'civilize', + 'civilized', + 'civilly', + 'civism', + 'civvies', + 'clabber', + 'clachan', + 'clack', + 'clad', + 'cladding', + 'cladoceran', + 'cladophyll', + 'claim', + 'claimant', + 'clairaudience', + 'clairvoyance', + 'clairvoyant', + 'clam', + 'clamant', + 'clamatorial', + 'clambake', + 'clamber', + 'clammy', + 'clamor', + 'clamorous', + 'clamp', + 'clamper', + 'clamshell', + 'clamworm', + 'clan', + 'clandestine', + 'clang', + 'clangor', + 'clank', + 'clannish', + 'clansman', + 'clap', + 'clapboard', + 'clapper', + 'clapperclaw', + 'claptrap', + 'claque', + 'claqueur', + 'clarabella', + 'clarence', + 'claret', + 'clarify', + 'clarinet', + 'clarino', + 'clarion', + 'clarity', + 'clarkia', + 'claro', + 'clarsach', + 'clary', + 'clash', + 'clasp', + 'clasping', + 'class', + 'classic', + 'classical', + 'classicism', + 'classicist', + 'classicize', + 'classics', + 'classification', + 'classified', + 'classify', + 'classis', + 'classless', + 'classmate', + 'classroom', + 'classy', + 'clastic', + 'clathrate', + 'clatter', + 'claudicant', + 'claudication', + 'clause', + 'claustral', + 'claustrophobia', + 'clavate', + 'clave', + 'claver', + 'clavicembalo', + 'clavichord', + 'clavicle', + 'clavicorn', + 'clavicytherium', + 'clavier', + 'claviform', + 'clavus', + 'claw', + 'clay', + 'claybank', + 'claymore', + 'claypan', + 'claytonia', + 'clean', + 'cleaner', + 'cleaning', + 'cleanly', + 'cleanse', + 'cleanser', + 'cleanup', + 'clear', + 'clearance', + 'clearcole', + 'clearheaded', + 'clearing', + 'clearly', + 'clearness', + 'clearstory', + 'clearway', + 'clearwing', + 'cleat', + 'cleavable', + 'cleavage', + 'cleave', + 'cleaver', + 'cleavers', + 'cleek', + 'clef', + 'cleft', + 'cleistogamy', + 'clem', + 'clematis', + 'clemency', + 'clement', + 'clench', + 'cleome', + 'clepe', + 'clepsydra', + 'cleptomania', + 'clerestory', + 'clergy', + 'clergyman', + 'cleric', + 'clerical', + 'clericalism', + 'clericals', + 'clerihew', + 'clerk', + 'clerkly', + 'cleromancy', + 'cleruchy', + 'cleveite', + 'clever', + 'clevis', + 'clew', + 'click', + 'clicker', + 'client', + 'clientage', + 'clientele', + 'cliff', + 'climacteric', + 'climactic', + 'climate', + 'climatology', + 'climax', + 'climb', + 'climber', + 'clime', + 'clinandrium', + 'clinch', + 'clincher', + 'cline', + 'cling', + 'clingfish', + 'clingstone', + 'clingy', + 'clinic', + 'clinical', + 'clinician', + 'clink', + 'clinker', + 'clinkstone', + 'clinometer', + 'clinquant', + 'clintonia', + 'clip', + 'clipboard', + 'clipped', + 'clipper', + 'clippers', + 'clipping', + 'clique', + 'cliquish', + 'clishmaclaver', + 'clitoris', + 'cloaca', + 'cloak', + 'cloakroom', + 'clobber', + 'cloche', + 'clock', + 'clockmaker', + 'clockwise', + 'clockwork', + 'clod', + 'cloddish', + 'clodhopper', + 'clodhopping', + 'clog', + 'cloison', + 'cloister', + 'cloistered', + 'cloistral', + 'clomb', + 'clomp', + 'clone', + 'clonus', + 'clop', + 'clos', + 'close', + 'closed', + 'closefisted', + 'closemouthed', + 'closer', + 'closet', + 'closing', + 'clostridium', + 'closure', + 'clot', + 'cloth', + 'clothbound', + 'clothe', + 'clothes', + 'clothesbasket', + 'clotheshorse', + 'clothesline', + 'clothespin', + 'clothespress', + 'clothier', + 'clothing', + 'cloture', + 'cloud', + 'cloudberry', + 'cloudburst', + 'clouded', + 'cloudland', + 'cloudless', + 'cloudlet', + 'cloudscape', + 'cloudy', + 'clough', + 'clout', + 'clove', + 'cloven', + 'clover', + 'cloverleaf', + 'clown', + 'clownery', + 'cloy', + 'cloying', + 'club', + 'clubbable', + 'clubby', + 'clubfoot', + 'clubhaul', + 'clubhouse', + 'clubman', + 'clubwoman', + 'cluck', + 'clue', + 'clueless', + 'clump', + 'clumsy', + 'clung', + 'clunk', + 'clupeid', + 'clupeoid', + 'cluster', + 'clustered', + 'clutch', + 'clutter', + 'clypeate', + 'clypeus', + 'clyster', + 'cm', + 'cnemis', + 'cnidoblast', + 'coacervate', + 'coach', + 'coacher', + 'coachman', + 'coachwhip', + 'coachwork', + 'coact', + 'coaction', + 'coactive', + 'coadjutant', + 'coadjutor', + 'coadjutress', + 'coadjutrix', + 'coadunate', + 'coagulant', + 'coagulase', + 'coagulate', + 'coagulum', + 'coal', + 'coaler', + 'coalesce', + 'coalfield', + 'coalfish', + 'coalition', + 'coaly', + 'coaming', + 'coaptation', + 'coarctate', + 'coarse', + 'coarsen', + 'coast', + 'coastal', + 'coaster', + 'coastguardsman', + 'coastland', + 'coastline', + 'coastward', + 'coastwise', + 'coat', + 'coated', + 'coatee', + 'coati', + 'coating', + 'coattail', + 'coauthor', + 'coax', + 'coaxial', + 'cob', + 'cobalt', + 'cobaltic', + 'cobaltite', + 'cobaltous', + 'cobber', + 'cobble', + 'cobbler', + 'cobblestone', + 'cobelligerent', + 'cobia', + 'coble', + 'cobnut', + 'cobra', + 'coburg', + 'cobweb', + 'cobwebby', + 'coca', + 'cocaine', + 'cocainism', + 'cocainize', + 'cocci', + 'coccid', + 'coccidioidomycosis', + 'coccidiosis', + 'coccus', + 'coccyx', + 'cochineal', + 'cochlea', + 'cochleate', + 'cock', + 'cockade', + 'cockalorum', + 'cockatiel', + 'cockatoo', + 'cockatrice', + 'cockboat', + 'cockchafer', + 'cockcrow', + 'cocker', + 'cockerel', + 'cockeye', + 'cockeyed', + 'cockfight', + 'cockhorse', + 'cockiness', + 'cockle', + 'cockleboat', + 'cocklebur', + 'cockleshell', + 'cockloft', + 'cockney', + 'cockneyfy', + 'cockneyism', + 'cockpit', + 'cockroach', + 'cockscomb', + 'cockshy', + 'cockspur', + 'cocksure', + 'cockswain', + 'cocktail', + 'cockup', + 'cocky', + 'coco', + 'cocoa', + 'coconut', + 'cocoon', + 'cocotte', + 'cod', + 'coda', + 'coddle', + 'code', + 'codeclination', + 'codeine', + 'codex', + 'codfish', + 'codger', + 'codices', + 'codicil', + 'codification', + 'codify', + 'codling', + 'codon', + 'codpiece', + 'coeducation', + 'coefficient', + 'coelacanth', + 'coelenterate', + 'coelenteron', + 'coeliac', + 'coelom', + 'coelostat', + 'coenesthesia', + 'coenobite', + 'coenocyte', + 'coenosarc', + 'coenurus', + 'coenzyme', + 'coequal', + 'coerce', + 'coercion', + 'coercive', + 'coessential', + 'coetaneous', + 'coeternal', + 'coeternity', + 'coeval', + 'coexecutor', + 'coexist', + 'coextend', + 'coextensive', + 'coff', + 'coffee', + 'coffeehouse', + 'coffeepot', + 'coffer', + 'cofferdam', + 'coffin', + 'coffle', + 'cog', + 'cogency', + 'cogent', + 'cogitable', + 'cogitate', + 'cogitation', + 'cogitative', + 'cognac', + 'cognate', + 'cognation', + 'cognition', + 'cognizable', + 'cognizance', + 'cognizant', + 'cognize', + 'cognomen', + 'cognoscenti', + 'cogon', + 'cogwheel', + 'cohabit', + 'coheir', + 'cohere', + 'coherence', + 'coherent', + 'cohesion', + 'cohesive', + 'cohobate', + 'cohort', + 'cohosh', + 'cohune', + 'coif', + 'coiffeur', + 'coiffure', + 'coign', + 'coil', + 'coin', + 'coinage', + 'coincide', + 'coincidence', + 'coincident', + 'coincidental', + 'coincidentally', + 'coinstantaneous', + 'coinsurance', + 'coinsure', + 'coir', + 'coition', + 'coitus', + 'coke', + 'col', + 'cola', + 'colander', + 'colatitude', + 'colcannon', + 'colchicine', + 'colchicum', + 'colcothar', + 'cold', + 'cole', + 'colectomy', + 'colemanite', + 'coleopteran', + 'coleoptile', + 'coleorhiza', + 'coleslaw', + 'coleus', + 'colewort', + 'colic', + 'colicroot', + 'colicweed', + 'coliseum', + 'colitis', + 'collaborate', + 'collaboration', + 'collaborationist', + 'collaborative', + 'collage', + 'collagen', + 'collapse', + 'collar', + 'collarbone', + 'collard', + 'collate', + 'collateral', + 'collation', + 'collative', + 'collator', + 'colleague', + 'collect', + 'collectanea', + 'collected', + 'collection', + 'collective', + 'collectivism', + 'collectivity', + 'collectivize', + 'collector', + 'colleen', + 'college', + 'collegian', + 'collegiate', + 'collegium', + 'collenchyma', + 'collet', + 'collide', + 'collie', + 'collier', + 'colliery', + 'colligate', + 'collimate', + 'collimator', + 'collinear', + 'collins', + 'collinsia', + 'collision', + 'collocate', + 'collocation', + 'collocutor', + 'collodion', + 'collogue', + 'colloid', + 'colloidal', + 'collop', + 'colloquial', + 'colloquialism', + 'colloquium', + 'colloquy', + 'collotype', + 'collude', + 'collusion', + 'collusive', + 'colly', + 'collyrium', + 'collywobbles', + 'colobus', + 'colocynth', + 'cologarithm', + 'cologne', + 'colon', + 'colonel', + 'colonial', + 'colonialism', + 'colonic', + 'colonist', + 'colonize', + 'colonnade', + 'colony', + 'colophon', + 'colophony', + 'coloquintida', + 'color', + 'colorable', + 'colorado', + 'colorant', + 'coloration', + 'coloratura', + 'colorcast', + 'colored', + 'colorfast', + 'colorful', + 'colorific', + 'colorimeter', + 'coloring', + 'colorist', + 'colorless', + 'colossal', + 'colosseum', + 'colossus', + 'colostomy', + 'colostrum', + 'colotomy', + 'colour', + 'colourable', + 'colpitis', + 'colporteur', + 'colpotomy', + 'colt', + 'colter', + 'coltish', + 'coltsfoot', + 'colubrid', + 'colubrine', + 'colugo', + 'columbarium', + 'columbary', + 'columbic', + 'columbine', + 'columbite', + 'columbium', + 'columbous', + 'columella', + 'columelliform', + 'column', + 'columnar', + 'columniation', + 'columnist', + 'colure', + 'coly', + 'colza', + 'coma', + 'comate', + 'comatose', + 'comatulid', + 'comb', + 'combat', + 'combatant', + 'combative', + 'combe', + 'comber', + 'combination', + 'combinative', + 'combine', + 'combined', + 'combings', + 'combo', + 'combust', + 'combustible', + 'combustion', + 'combustor', + 'come', + 'comeback', + 'comedian', + 'comedic', + 'comedienne', + 'comedietta', + 'comedo', + 'comedown', + 'comedy', + 'comely', + 'comer', + 'comestible', + 'comet', + 'comeuppance', + 'comfit', + 'comfort', + 'comfortable', + 'comforter', + 'comfrey', + 'comfy', + 'comic', + 'comical', + 'coming', + 'comitative', + 'comitia', + 'comity', + 'comma', + 'command', + 'commandant', + 'commandeer', + 'commander', + 'commanding', + 'commandment', + 'commando', + 'commeasure', + 'commemorate', + 'commemoration', + 'commemorative', + 'commence', + 'commencement', + 'commend', + 'commendam', + 'commendation', + 'commendatory', + 'commensal', + 'commensurable', + 'commensurate', + 'comment', + 'commentary', + 'commentate', + 'commentative', + 'commentator', + 'commerce', + 'commercial', + 'commercialism', + 'commercialize', + 'commie', + 'comminate', + 'commination', + 'commingle', + 'comminute', + 'commiserate', + 'commissar', + 'commissariat', + 'commissary', + 'commission', + 'commissionaire', + 'commissioner', + 'commissure', + 'commit', + 'commitment', + 'committal', + 'committee', + 'committeeman', + 'committeewoman', + 'commix', + 'commixture', + 'commode', + 'commodious', + 'commodity', + 'commodore', + 'common', + 'commonable', + 'commonage', + 'commonality', + 'commonalty', + 'commoner', + 'commonly', + 'commonplace', + 'commons', + 'commonweal', + 'commonwealth', + 'commorancy', + 'commorant', + 'commotion', + 'commove', + 'communal', + 'communalism', + 'communalize', + 'commune', + 'communicable', + 'communicant', + 'communicate', + 'communication', + 'communicative', + 'communion', + 'communism', + 'communist', + 'communistic', + 'communitarian', + 'community', + 'communize', + 'commutable', + 'commutate', + 'commutation', + 'commutative', + 'commutator', + 'commute', + 'commuter', + 'commutual', + 'comose', + 'comp', + 'compact', + 'compaction', + 'compagnie', + 'compander', + 'companion', + 'companionable', + 'companionate', + 'companionship', + 'companionway', + 'company', + 'comparable', + 'comparative', + 'comparator', + 'compare', + 'comparison', + 'compartment', + 'compartmentalize', + 'compass', + 'compassion', + 'compassionate', + 'compatible', + 'compatriot', + 'compeer', + 'compel', + 'compellation', + 'compelling', + 'compendious', + 'compendium', + 'compensable', + 'compensate', + 'compensation', + 'compensatory', + 'compete', + 'competence', + 'competency', + 'competent', + 'competition', + 'competitive', + 'competitor', + 'compilation', + 'compile', + 'compiler', + 'complacence', + 'complacency', + 'complacent', + 'complain', + 'complainant', + 'complaint', + 'complaisance', + 'complaisant', + 'complect', + 'complected', + 'complement', + 'complemental', + 'complementary', + 'complete', + 'completion', + 'complex', + 'complexion', + 'complexioned', + 'complexity', + 'compliance', + 'compliancy', + 'compliant', + 'complicacy', + 'complicate', + 'complicated', + 'complication', + 'complice', + 'complicity', + 'compliment', + 'complimentary', + 'compline', + 'complot', + 'comply', + 'compo', + 'component', + 'compony', + 'comport', + 'comportment', + 'compose', + 'composed', + 'composer', + 'composite', + 'composition', + 'compositor', + 'compossible', + 'compost', + 'composure', + 'compotation', + 'compote', + 'compound', + 'comprador', + 'comprehend', + 'comprehensible', + 'comprehension', + 'comprehensive', + 'compress', + 'compressed', + 'compressibility', + 'compression', + 'compressive', + 'compressor', + 'comprise', + 'compromise', + 'comptroller', + 'compulsion', + 'compulsive', + 'compulsory', + 'compunction', + 'compurgation', + 'computation', + 'compute', + 'computer', + 'computerize', + 'comrade', + 'comradery', + 'comstockery', + 'con', + 'conation', + 'conative', + 'conatus', + 'concatenate', + 'concatenation', + 'concave', + 'concavity', + 'conceal', + 'concealment', + 'concede', + 'conceit', + 'conceited', + 'conceivable', + 'conceive', + 'concelebrate', + 'concent', + 'concenter', + 'concentrate', + 'concentrated', + 'concentration', + 'concentre', + 'concentric', + 'concept', + 'conceptacle', + 'conception', + 'conceptual', + 'conceptualism', + 'conceptualize', + 'concern', + 'concerned', + 'concerning', + 'concernment', + 'concert', + 'concertante', + 'concerted', + 'concertgoer', + 'concertina', + 'concertino', + 'concertize', + 'concertmaster', + 'concerto', + 'concession', + 'concessionaire', + 'concessive', + 'conch', + 'concha', + 'conchie', + 'conchiferous', + 'conchiolin', + 'conchoid', + 'conchoidal', + 'conchology', + 'concierge', + 'conciliar', + 'conciliate', + 'conciliator', + 'conciliatory', + 'concinnate', + 'concinnity', + 'concinnous', + 'concise', + 'conciseness', + 'concision', + 'conclave', + 'conclude', + 'conclusion', + 'conclusive', + 'concoct', + 'concoction', + 'concomitance', + 'concomitant', + 'concord', + 'concordance', + 'concordant', + 'concordat', + 'concourse', + 'concrescence', + 'concrete', + 'concretion', + 'concretize', + 'concubinage', + 'concubine', + 'concupiscence', + 'concupiscent', + 'concur', + 'concurrence', + 'concurrent', + 'concuss', + 'concussion', + 'condemn', + 'condemnation', + 'condemnatory', + 'condensable', + 'condensate', + 'condensation', + 'condense', + 'condensed', + 'condenser', + 'condescend', + 'condescendence', + 'condescending', + 'condescension', + 'condign', + 'condiment', + 'condition', + 'conditional', + 'conditioned', + 'conditioner', + 'conditioning', + 'condole', + 'condolence', + 'condolent', + 'condom', + 'condominium', + 'condonation', + 'condone', + 'condor', + 'condottiere', + 'conduce', + 'conducive', + 'conduct', + 'conductance', + 'conduction', + 'conductive', + 'conductivity', + 'conductor', + 'conduit', + 'conduplicate', + 'condyle', + 'condyloid', + 'condyloma', + 'cone', + 'coneflower', + 'coney', + 'confab', + 'confabulate', + 'confabulation', + 'confect', + 'confection', + 'confectionary', + 'confectioner', + 'confectionery', + 'confederacy', + 'confederate', + 'confederation', + 'confer', + 'conferee', + 'conference', + 'conferral', + 'conferva', + 'confess', + 'confessedly', + 'confession', + 'confessional', + 'confessor', + 'confetti', + 'confidant', + 'confidante', + 'confide', + 'confidence', + 'confident', + 'confidential', + 'confiding', + 'configuration', + 'configurationism', + 'confine', + 'confined', + 'confinement', + 'confirm', + 'confirmand', + 'confirmation', + 'confirmatory', + 'confirmed', + 'confiscable', + 'confiscate', + 'confiscatory', + 'confiture', + 'conflagrant', + 'conflagration', + 'conflation', + 'conflict', + 'confluence', + 'confluent', + 'conflux', + 'confocal', + 'conform', + 'conformable', + 'conformal', + 'conformance', + 'conformation', + 'conformist', + 'conformity', + 'confound', + 'confounded', + 'confraternity', + 'confrere', + 'confront', + 'confuse', + 'confusion', + 'confutation', + 'confute', + 'conga', + 'congeal', + 'congelation', + 'congener', + 'congeneric', + 'congenial', + 'congenital', + 'conger', + 'congeries', + 'congest', + 'congius', + 'conglobate', + 'conglomerate', + 'conglomeration', + 'conglutinate', + 'congou', + 'congratulant', + 'congratulate', + 'congratulation', + 'congratulatory', + 'congregate', + 'congregation', + 'congregational', + 'congress', + 'congressional', + 'congressman', + 'congresswoman', + 'congruence', + 'congruency', + 'congruent', + 'congruity', + 'congruous', + 'conic', + 'conics', + 'conidiophore', + 'conidium', + 'conifer', + 'coniferous', + 'coniine', + 'coniology', + 'conium', + 'conjectural', + 'conjecture', + 'conjoin', + 'conjoined', + 'conjoint', + 'conjugal', + 'conjugate', + 'conjugated', + 'conjugation', + 'conjunct', + 'conjunction', + 'conjunctiva', + 'conjunctive', + 'conjunctivitis', + 'conjuncture', + 'conjuration', + 'conjure', + 'conjurer', + 'conk', + 'conker', + 'conn', + 'connate', + 'connatural', + 'connect', + 'connected', + 'connection', + 'connective', + 'conniption', + 'connivance', + 'connive', + 'connivent', + 'connoisseur', + 'connotation', + 'connotative', + 'connote', + 'connubial', + 'conoid', + 'conoscenti', + 'conquer', + 'conqueror', + 'conquest', + 'conquian', + 'conquistador', + 'cons', + 'consanguineous', + 'consanguinity', + 'conscience', + 'conscientious', + 'conscionable', + 'conscious', + 'consciousness', + 'conscript', + 'conscription', + 'consecrate', + 'consecration', + 'consecution', + 'consecutive', + 'consensual', + 'consensus', + 'consent', + 'consentaneous', + 'consentient', + 'consequence', + 'consequent', + 'consequential', + 'consequently', + 'conservancy', + 'conservation', + 'conservationist', + 'conservatism', + 'conservative', + 'conservatoire', + 'conservator', + 'conservatory', + 'conserve', + 'consider', + 'considerable', + 'considerate', + 'consideration', + 'considered', + 'considering', + 'consign', + 'consignee', + 'consignment', + 'consignor', + 'consist', + 'consistence', + 'consistency', + 'consistent', + 'consistory', + 'consociate', + 'consol', + 'consolation', + 'consolatory', + 'console', + 'consolidate', + 'consolidation', + 'consols', + 'consolute', + 'consonance', + 'consonant', + 'consonantal', + 'consort', + 'consortium', + 'conspecific', + 'conspectus', + 'conspicuous', + 'conspiracy', + 'conspire', + 'constable', + 'constabulary', + 'constancy', + 'constant', + 'constantan', + 'constellate', + 'constellation', + 'consternate', + 'consternation', + 'constipate', + 'constipation', + 'constituency', + 'constituent', + 'constitute', + 'constitution', + 'constitutional', + 'constitutionalism', + 'constitutionality', + 'constitutionally', + 'constitutive', + 'constrain', + 'constrained', + 'constraint', + 'constrict', + 'constriction', + 'constrictive', + 'constrictor', + 'constringe', + 'constringent', + 'construct', + 'construction', + 'constructionist', + 'constructive', + 'constructivism', + 'construe', + 'consubstantial', + 'consubstantiate', + 'consubstantiation', + 'consuetude', + 'consuetudinary', + 'consul', + 'consulate', + 'consult', + 'consultant', + 'consultation', + 'consultative', + 'consumable', + 'consume', + 'consumedly', + 'consumer', + 'consumerism', + 'consummate', + 'consummation', + 'consumption', + 'consumptive', + 'contact', + 'contactor', + 'contagion', + 'contagious', + 'contagium', + 'contain', + 'container', + 'containerize', + 'containment', + 'contaminant', + 'contaminate', + 'contamination', + 'contango', + 'conte', + 'contemn', + 'contemplate', + 'contemplation', + 'contemplative', + 'contemporaneous', + 'contemporary', + 'contemporize', + 'contempt', + 'contemptible', + 'contemptuous', + 'contend', + 'content', + 'contented', + 'contention', + 'contentious', + 'contentment', + 'conterminous', + 'contest', + 'contestant', + 'contestation', + 'context', + 'contextual', + 'contexture', + 'contiguity', + 'contiguous', + 'continence', + 'continent', + 'continental', + 'contingence', + 'contingency', + 'contingent', + 'continual', + 'continually', + 'continuance', + 'continuant', + 'continuate', + 'continuation', + 'continuative', + 'continuator', + 'continue', + 'continuity', + 'continuo', + 'continuous', + 'continuum', + 'conto', + 'contort', + 'contorted', + 'contortion', + 'contortionist', + 'contortive', + 'contour', + 'contra', + 'contraband', + 'contrabandist', + 'contrabass', + 'contrabassoon', + 'contraception', + 'contraceptive', + 'contract', + 'contracted', + 'contractile', + 'contraction', + 'contractive', + 'contractor', + 'contractual', + 'contracture', + 'contradance', + 'contradict', + 'contradiction', + 'contradictory', + 'contradistinction', + 'contradistinguish', + 'contrail', + 'contraindicate', + 'contralto', + 'contraoctave', + 'contrapose', + 'contraposition', + 'contrapositive', + 'contraption', + 'contrapuntal', + 'contrapuntist', + 'contrariety', + 'contrarily', + 'contrarious', + 'contrariwise', + 'contrary', + 'contrast', + 'contrastive', + 'contrasty', + 'contravallation', + 'contravene', + 'contravention', + 'contrayerva', + 'contrecoup', + 'contredanse', + 'contretemps', + 'contribute', + 'contribution', + 'contributor', + 'contributory', + 'contrite', + 'contrition', + 'contrivance', + 'contrive', + 'contrived', + 'control', + 'controller', + 'controversial', + 'controversy', + 'controvert', + 'contumacious', + 'contumacy', + 'contumelious', + 'contumely', + 'contuse', + 'contusion', + 'conundrum', + 'conurbation', + 'conure', + 'convalesce', + 'convalescence', + 'convalescent', + 'convection', + 'convector', + 'convenance', + 'convene', + 'convenience', + 'convenient', + 'convent', + 'conventicle', + 'convention', + 'conventional', + 'conventionalism', + 'conventionality', + 'conventionalize', + 'conventioneer', + 'conventioner', + 'conventual', + 'converge', + 'convergence', + 'convergent', + 'conversable', + 'conversant', + 'conversation', + 'conversational', + 'conversationalist', + 'conversazione', + 'converse', + 'conversion', + 'convert', + 'converted', + 'converter', + 'convertible', + 'convertiplane', + 'convertite', + 'convex', + 'convexity', + 'convey', + 'conveyance', + 'conveyancer', + 'conveyancing', + 'conveyor', + 'convict', + 'conviction', + 'convince', + 'convincing', + 'convivial', + 'convocation', + 'convoke', + 'convolute', + 'convoluted', + 'convolution', + 'convolve', + 'convolvulaceous', + 'convolvulus', + 'convoy', + 'convulsant', + 'convulse', + 'convulsion', + 'convulsive', + 'cony', + 'coo', + 'cooee', + 'cook', + 'cookbook', + 'cooker', + 'cookery', + 'cookhouse', + 'cookie', + 'cooking', + 'cookout', + 'cookshop', + 'cookstove', + 'cooky', + 'cool', + 'coolant', + 'cooler', + 'coolie', + 'coolish', + 'coolth', + 'coom', + 'coomb', + 'coon', + 'cooncan', + 'coonhound', + 'coonskin', + 'coontie', + 'coop', + 'cooper', + 'cooperage', + 'cooperate', + 'cooperation', + 'cooperative', + 'coopery', + 'coordinate', + 'coordination', + 'coot', + 'cootch', + 'cootie', + 'cop', + 'copacetic', + 'copaiba', + 'copal', + 'copalite', + 'copalm', + 'coparcenary', + 'coparcener', + 'copartner', + 'cope', + 'copeck', + 'copepod', + 'coper', + 'copestone', + 'copier', + 'copilot', + 'coping', + 'copious', + 'coplanar', + 'copolymer', + 'copolymerize', + 'copper', + 'copperas', + 'copperhead', + 'copperplate', + 'coppersmith', + 'coppery', + 'coppice', + 'copra', + 'coprolalia', + 'coprolite', + 'coprology', + 'coprophagous', + 'coprophilia', + 'coprophilous', + 'copse', + 'copter', + 'copula', + 'copulate', + 'copulation', + 'copulative', + 'copy', + 'copybook', + 'copyboy', + 'copycat', + 'copyhold', + 'copyholder', + 'copyist', + 'copyread', + 'copyreader', + 'copyright', + 'copywriter', + 'coquelicot', + 'coquet', + 'coquetry', + 'coquette', + 'coquillage', + 'coquille', + 'coquina', + 'coquito', + 'coraciiform', + 'coracle', + 'coracoid', + 'coral', + 'coralline', + 'corallite', + 'coralloid', + 'coranto', + 'corban', + 'corbeil', + 'corbel', + 'corbicula', + 'corbie', + 'cord', + 'cordage', + 'cordate', + 'corded', + 'cordial', + 'cordiality', + 'cordierite', + 'cordiform', + 'cordillera', + 'cording', + 'cordite', + 'cordless', + 'cordoba', + 'cordon', + 'cordovan', + 'cords', + 'corduroy', + 'corduroys', + 'cordwain', + 'cordwainer', + 'cordwood', + 'core', + 'corelation', + 'corelative', + 'coreligionist', + 'coremaker', + 'coreopsis', + 'corespondent', + 'corf', + 'corgi', + 'coriaceous', + 'coriander', + 'corium', + 'cork', + 'corkage', + 'corkboard', + 'corked', + 'corker', + 'corking', + 'corkscrew', + 'corkwood', + 'corky', + 'corm', + 'cormophyte', + 'cormorant', + 'corn', + 'cornaceous', + 'corncob', + 'corncrib', + 'cornea', + 'corned', + 'cornel', + 'cornelian', + 'cornemuse', + 'corneous', + 'corner', + 'cornered', + 'cornerstone', + 'cornerwise', + 'cornet', + 'cornetcy', + 'cornetist', + 'cornett', + 'cornfield', + 'cornflakes', + 'cornflower', + 'cornhusk', + 'cornhusking', + 'cornice', + 'corniculate', + 'cornstalk', + 'cornstarch', + 'cornu', + 'cornucopia', + 'cornute', + 'cornuted', + 'corny', + 'corody', + 'corolla', + 'corollaceous', + 'corollary', + 'corona', + 'coronach', + 'coronagraph', + 'coronal', + 'coronary', + 'coronation', + 'coroner', + 'coronet', + 'coroneted', + 'coronograph', + 'corpora', + 'corporal', + 'corporate', + 'corporation', + 'corporative', + 'corporator', + 'corporeal', + 'corporeity', + 'corposant', + 'corps', + 'corpse', + 'corpsman', + 'corpulence', + 'corpulent', + 'corpus', + 'corpuscle', + 'corrade', + 'corral', + 'corrasion', + 'correct', + 'correction', + 'correctitude', + 'corrective', + 'correlate', + 'correlation', + 'correlative', + 'correspond', + 'correspondence', + 'correspondent', + 'corrida', + 'corridor', + 'corrie', + 'corrigendum', + 'corrigible', + 'corrival', + 'corroborant', + 'corroborate', + 'corroboration', + 'corroboree', + 'corrode', + 'corrody', + 'corrosion', + 'corrosive', + 'corrugate', + 'corrugation', + 'corrupt', + 'corruptible', + 'corruption', + 'corsage', + 'corsair', + 'corse', + 'corselet', + 'corset', + 'cortege', + 'cortex', + 'cortical', + 'corticate', + 'corticosteroid', + 'corticosterone', + 'cortisol', + 'cortisone', + 'corundum', + 'coruscate', + 'coruscation', + 'corves', + 'corvette', + 'corvine', + 'corybantic', + 'corydalis', + 'corymb', + 'coryphaeus', + 'coryza', + 'cos', + 'cosec', + 'cosecant', + 'coseismal', + 'coset', + 'cosh', + 'cosher', + 'cosignatory', + 'cosine', + 'cosmetic', + 'cosmetician', + 'cosmic', + 'cosmism', + 'cosmogony', + 'cosmography', + 'cosmology', + 'cosmonaut', + 'cosmonautics', + 'cosmopolis', + 'cosmopolitan', + 'cosmopolite', + 'cosmorama', + 'cosmos', + 'coss', + 'cosset', + 'cost', + 'costa', + 'costard', + 'costate', + 'costermonger', + 'costive', + 'costly', + 'costmary', + 'costotomy', + 'costrel', + 'costume', + 'costumer', + 'costumier', + 'cosy', + 'cot', + 'cotangent', + 'cote', + 'cotemporary', + 'cotenant', + 'coterie', + 'coterminous', + 'coth', + 'cothurnus', + 'cotidal', + 'cotillion', + 'cotinga', + 'cotoneaster', + 'cotquean', + 'cotta', + 'cottage', + 'cottager', + 'cottar', + 'cotter', + 'cottier', + 'cotton', + 'cottonade', + 'cottonmouth', + 'cottonseed', + 'cottontail', + 'cottonweed', + 'cottonwood', + 'cottony', + 'cotyledon', + 'coucal', + 'couch', + 'couchant', + 'couching', + 'cougar', + 'cough', + 'could', + 'couldst', + 'coulee', + 'coulisse', + 'couloir', + 'coulomb', + 'coulometer', + 'coulter', + 'coumarin', + 'coumarone', + 'council', + 'councillor', + 'councilman', + 'councilor', + 'councilwoman', + 'counsel', + 'counsellor', + 'counselor', + 'count', + 'countable', + 'countdown', + 'countenance', + 'counter', + 'counteraccusation', + 'counteract', + 'counterattack', + 'counterattraction', + 'counterbalance', + 'counterblast', + 'counterblow', + 'counterchange', + 'countercharge', + 'countercheck', + 'counterclaim', + 'counterclockwise', + 'countercurrent', + 'counterespionage', + 'counterfactual', + 'counterfeit', + 'counterfoil', + 'counterforce', + 'counterglow', + 'counterinsurgency', + 'counterintelligence', + 'counterirritant', + 'counterman', + 'countermand', + 'countermarch', + 'countermark', + 'countermeasure', + 'countermine', + 'countermove', + 'counteroffensive', + 'counterpane', + 'counterpart', + 'counterplot', + 'counterpoint', + 'counterpoise', + 'counterpoison', + 'counterpressure', + 'counterproductive', + 'counterproof', + 'counterproposal', + 'counterpunch', + 'counterreply', + 'counterrevolution', + 'counterscarp', + 'countershading', + 'countershaft', + 'countersign', + 'countersignature', + 'countersink', + 'counterspy', + 'counterstamp', + 'counterstatement', + 'counterstroke', + 'countersubject', + 'countertenor', + 'countertype', + 'countervail', + 'counterweigh', + 'counterweight', + 'counterword', + 'counterwork', + 'countess', + 'countless', + 'countrified', + 'country', + 'countryfied', + 'countryman', + 'countryside', + 'countrywoman', + 'county', + 'coup', + 'coupe', + 'couple', + 'coupler', + 'couplet', + 'coupling', + 'coupon', + 'courage', + 'courageous', + 'courante', + 'courier', + 'courlan', + 'course', + 'courser', + 'courses', + 'coursing', + 'court', + 'courteous', + 'courtesan', + 'courtesy', + 'courthouse', + 'courtier', + 'courtly', + 'courtroom', + 'courtship', + 'courtyard', + 'couscous', + 'cousin', + 'couteau', + 'couthie', + 'couture', + 'couturier', + 'couvade', + 'covalence', + 'covariance', + 'cove', + 'coven', + 'covenant', + 'covenantee', + 'covenanter', + 'covenantor', + 'cover', + 'coverage', + 'coverall', + 'covered', + 'covering', + 'coverlet', + 'covert', + 'coverture', + 'covet', + 'covetous', + 'covey', + 'covin', + 'cow', + 'cowage', + 'coward', + 'cowardice', + 'cowardly', + 'cowbane', + 'cowbell', + 'cowberry', + 'cowbind', + 'cowbird', + 'cowboy', + 'cowcatcher', + 'cower', + 'cowfish', + 'cowgirl', + 'cowherb', + 'cowherd', + 'cowhide', + 'cowitch', + 'cowl', + 'cowled', + 'cowlick', + 'cowling', + 'cowman', + 'cowpea', + 'cowpoke', + 'cowpox', + 'cowpuncher', + 'cowrie', + 'cowry', + 'cowshed', + 'cowskin', + 'cowslip', + 'cox', + 'coxa', + 'coxalgia', + 'coxcomb', + 'coxcombry', + 'coxswain', + 'coy', + 'coyote', + 'coyotillo', + 'coypu', + 'coz', + 'coze', + 'cozen', + 'cozenage', + 'cozy', + 'craal', + 'crab', + 'crabbed', + 'crabber', + 'crabbing', + 'crabby', + 'crabstick', + 'crabwise', + 'crack', + 'crackbrain', + 'crackbrained', + 'crackdown', + 'cracked', + 'cracker', + 'crackerjack', + 'cracking', + 'crackle', + 'crackleware', + 'crackling', + 'cracknel', + 'crackpot', + 'cracksman', + 'cradle', + 'cradlesong', + 'cradling', + 'craft', + 'craftsman', + 'craftwork', + 'crafty', + 'crag', + 'craggy', + 'cragsman', + 'crake', + 'cram', + 'crambo', + 'crammer', + 'cramoisy', + 'cramp', + 'cramped', + 'crampon', + 'cranage', + 'cranberry', + 'crane', + 'cranial', + 'craniate', + 'craniology', + 'craniometer', + 'craniometry', + 'craniotomy', + 'cranium', + 'crank', + 'crankcase', + 'crankle', + 'crankpin', + 'crankshaft', + 'cranky', + 'crannog', + 'cranny', + 'crap', + 'crape', + 'crappie', + 'craps', + 'crapshooter', + 'crapulent', + 'crapulous', + 'craquelure', + 'crash', + 'crashing', + 'crasis', + 'crass', + 'crassulaceous', + 'cratch', + 'crate', + 'crater', + 'craunch', + 'cravat', + 'crave', + 'craven', + 'craving', + 'craw', + 'crawfish', + 'crawl', + 'crawler', + 'crawly', + 'crayfish', + 'crayon', + 'craze', + 'crazed', + 'crazy', + 'crazyweed', + 'creak', + 'creaky', + 'cream', + 'creamcups', + 'creamer', + 'creamery', + 'creamy', + 'crease', + 'create', + 'creatine', + 'creatinine', + 'creation', + 'creationism', + 'creative', + 'creativity', + 'creator', + 'creatural', + 'creature', + 'creaturely', + 'credence', + 'credendum', + 'credent', + 'credential', + 'credenza', + 'credible', + 'credit', + 'creditable', + 'creditor', + 'credits', + 'credo', + 'credulity', + 'credulous', + 'creed', + 'creek', + 'creel', + 'creep', + 'creeper', + 'creepie', + 'creeps', + 'creepy', + 'creese', + 'cremate', + 'cremator', + 'crematorium', + 'crematory', + 'crenate', + 'crenation', + 'crenel', + 'crenelate', + 'crenelation', + 'crenellate', + 'crenulate', + 'crenulation', + 'creodont', + 'creole', + 'creolized', + 'creosol', + 'creosote', + 'crepe', + 'crepitate', + 'crept', + 'crepuscular', + 'crepuscule', + 'crescendo', + 'crescent', + 'crescentic', + 'cresol', + 'cress', + 'cresset', + 'crest', + 'crestfallen', + 'cresting', + 'cretaceous', + 'cretic', + 'cretin', + 'cretinism', + 'cretonne', + 'crevasse', + 'crevice', + 'crew', + 'crewel', + 'crewelwork', + 'crib', + 'cribbage', + 'cribbing', + 'cribble', + 'cribriform', + 'cribwork', + 'crick', + 'cricket', + 'cricoid', + 'crier', + 'crime', + 'criminal', + 'criminality', + 'criminate', + 'criminology', + 'crimmer', + 'crimp', + 'crimple', + 'crimpy', + 'crimson', + 'crine', + 'cringe', + 'cringle', + 'crinite', + 'crinkle', + 'crinkleroot', + 'crinkly', + 'crinoid', + 'crinoline', + 'crinose', + 'crinum', + 'criollo', + 'cripple', + 'crippling', + 'crisis', + 'crisp', + 'crispate', + 'crispation', + 'crisper', + 'crispy', + 'crisscross', + 'crissum', + 'crista', + 'cristate', + 'cristobalite', + 'criterion', + 'critic', + 'critical', + 'criticaster', + 'criticism', + 'criticize', + 'critique', + 'critter', + 'croak', + 'croaker', + 'croaky', + 'crocein', + 'crochet', + 'crocidolite', + 'crock', + 'crocked', + 'crockery', + 'crocket', + 'crocodile', + 'crocodilian', + 'crocoite', + 'crocus', + 'croft', + 'crofter', + 'croissant', + 'cromlech', + 'cromorne', + 'crone', + 'cronk', + 'crony', + 'cronyism', + 'crook', + 'crookback', + 'crooked', + 'croon', + 'crop', + 'cropland', + 'cropper', + 'croquet', + 'croquette', + 'crore', + 'crosier', + 'cross', + 'crossarm', + 'crossbar', + 'crossbeam', + 'crossbill', + 'crossbones', + 'crossbow', + 'crossbred', + 'crossbreed', + 'crosscurrent', + 'crosscut', + 'crosse', + 'crossed', + 'crosshatch', + 'crosshead', + 'crossing', + 'crossjack', + 'crosslet', + 'crossly', + 'crossness', + 'crossopterygian', + 'crossover', + 'crosspatch', + 'crosspiece', + 'crossroad', + 'crossroads', + 'crossruff', + 'crosstie', + 'crosstree', + 'crosswalk', + 'crossway', + 'crossways', + 'crosswind', + 'crosswise', + 'crotch', + 'crotchet', + 'crotchety', + 'croton', + 'crouch', + 'croup', + 'croupier', + 'crouse', + 'crouton', + 'crow', + 'crowbar', + 'crowberry', + 'crowboot', + 'crowd', + 'crowded', + 'crowfoot', + 'crown', + 'crowned', + 'crowning', + 'crownpiece', + 'crownwork', + 'croze', + 'crozier', + 'cru', + 'cruces', + 'crucial', + 'cruciate', + 'crucible', + 'crucifer', + 'cruciferous', + 'crucifix', + 'crucifixion', + 'cruciform', + 'crucify', + 'cruck', + 'crud', + 'crude', + 'crudity', + 'cruel', + 'cruelty', + 'cruet', + 'cruise', + 'cruiser', + 'cruiserweight', + 'cruller', + 'crumb', + 'crumble', + 'crumbly', + 'crumby', + 'crumhorn', + 'crummy', + 'crump', + 'crumpet', + 'crumple', + 'crumpled', + 'crunch', + 'crunode', + 'crupper', + 'crural', + 'crus', + 'crusade', + 'crusado', + 'cruse', + 'crush', + 'crushing', + 'crust', + 'crustacean', + 'crustaceous', + 'crustal', + 'crusted', + 'crusty', + 'crutch', + 'crux', + 'cruzado', + 'cruzeiro', + 'crwth', + 'cry', + 'crybaby', + 'crying', + 'crymotherapy', + 'cryobiology', + 'cryogen', + 'cryogenics', + 'cryohydrate', + 'cryolite', + 'cryology', + 'cryometer', + 'cryoscope', + 'cryoscopy', + 'cryostat', + 'cryosurgery', + 'cryotherapy', + 'crypt', + 'cryptanalysis', + 'cryptic', + 'cryptoanalysis', + 'cryptoclastic', + 'cryptocrystalline', + 'cryptogam', + 'cryptogenic', + 'cryptogram', + 'cryptograph', + 'cryptography', + 'cryptology', + 'cryptomeria', + 'cryptonym', + 'cryptonymous', + 'cryptozoic', + 'cryptozoite', + 'crystal', + 'crystalline', + 'crystallite', + 'crystallization', + 'crystallize', + 'crystallography', + 'crystalloid', + 'csc', + 'csch', + 'ctenidium', + 'ctenoid', + 'ctenophore', + 'ctn', + 'cub', + 'cubage', + 'cubature', + 'cubby', + 'cubbyhole', + 'cube', + 'cubeb', + 'cubic', + 'cubical', + 'cubicle', + 'cubiculum', + 'cubiform', + 'cubism', + 'cubit', + 'cubital', + 'cubitiere', + 'cuboid', + 'cuckold', + 'cuckoo', + 'cuckooflower', + 'cuckoopint', + 'cuculiform', + 'cucullate', + 'cucumber', + 'cucurbit', + 'cud', + 'cudbear', + 'cuddle', + 'cuddy', + 'cudgel', + 'cudweed', + 'cue', + 'cuesta', + 'cuff', + 'cuffs', + 'cuirass', + 'cuirassier', + 'cuisine', + 'cuisse', + 'culch', + 'culet', + 'culex', + 'culicid', + 'culinarian', + 'culinary', + 'cull', + 'cullender', + 'cullet', + 'cullis', + 'cully', + 'culm', + 'culmiferous', + 'culminant', + 'culminate', + 'culmination', + 'culottes', + 'culpa', + 'culpable', + 'culprit', + 'cult', + 'cultch', + 'cultigen', + 'cultism', + 'cultivable', + 'cultivar', + 'cultivate', + 'cultivated', + 'cultivation', + 'cultivator', + 'cultrate', + 'cultural', + 'culture', + 'cultured', + 'cultus', + 'culver', + 'culverin', + 'culvert', + 'cum', + 'cumber', + 'cumbersome', + 'cumbrance', + 'cumbrous', + 'cumin', + 'cummerbund', + 'cumquat', + 'cumshaw', + 'cumulate', + 'cumulation', + 'cumulative', + 'cumuliform', + 'cumulonimbus', + 'cumulostratus', + 'cumulous', + 'cumulus', + 'cunctation', + 'cuneal', + 'cuneate', + 'cuneiform', + 'cunnilingus', + 'cunning', + 'cup', + 'cupbearer', + 'cupboard', + 'cupcake', + 'cupel', + 'cupellation', + 'cupid', + 'cupidity', + 'cupola', + 'cupped', + 'cupping', + 'cupreous', + 'cupric', + 'cupriferous', + 'cuprite', + 'cupronickel', + 'cuprous', + 'cuprum', + 'cupulate', + 'cupule', + 'cur', + 'curable', + 'curacy', + 'curagh', + 'curare', + 'curarize', + 'curassow', + 'curate', + 'curative', + 'curator', + 'curb', + 'curbing', + 'curbstone', + 'curch', + 'curculio', + 'curcuma', + 'curd', + 'curdle', + 'cure', + 'curet', + 'curettage', + 'curfew', + 'curia', + 'curie', + 'curio', + 'curiosa', + 'curiosity', + 'curious', + 'curium', + 'curl', + 'curler', + 'curlew', + 'curlicue', + 'curling', + 'curlpaper', + 'curly', + 'curmudgeon', + 'currajong', + 'currant', + 'currency', + 'current', + 'curricle', + 'curriculum', + 'currier', + 'curriery', + 'currish', + 'curry', + 'currycomb', + 'curse', + 'cursed', + 'cursive', + 'cursor', + 'cursorial', + 'cursory', + 'curst', + 'curt', + 'curtail', + 'curtain', + 'curtal', + 'curtate', + 'curtilage', + 'curtsey', + 'curtsy', + 'curule', + 'curvaceous', + 'curvature', + 'curve', + 'curvet', + 'curvilinear', + 'curvy', + 'cusec', + 'cushat', + 'cushion', + 'cushiony', + 'cushy', + 'cusk', + 'cusp', + 'cusped', + 'cuspid', + 'cuspidate', + 'cuspidation', + 'cuspidor', + 'cuss', + 'cussed', + 'cussedness', + 'custard', + 'custodial', + 'custodian', + 'custody', + 'custom', + 'customable', + 'customary', + 'customer', + 'customhouse', + 'customs', + 'custos', + 'custumal', + 'cut', + 'cutaneous', + 'cutaway', + 'cutback', + 'cutch', + 'cutcherry', + 'cute', + 'cuticle', + 'cuticula', + 'cutie', + 'cutin', + 'cutinize', + 'cutis', + 'cutlass', + 'cutler', + 'cutlery', + 'cutlet', + 'cutoff', + 'cutout', + 'cutpurse', + 'cutter', + 'cutthroat', + 'cutting', + 'cuttle', + 'cuttlebone', + 'cuttlefish', + 'cutty', + 'cutup', + 'cutwater', + 'cutwork', + 'cutworm', + 'cuvette', + 'cwm', + 'cyan', + 'cyanamide', + 'cyanate', + 'cyaneous', + 'cyanic', + 'cyanide', + 'cyanine', + 'cyanite', + 'cyanocobalamin', + 'cyanogen', + 'cyanohydrin', + 'cyanosis', + 'cyanotype', + 'cyathus', + 'cybernetics', + 'cycad', + 'cyclamate', + 'cyclamen', + 'cycle', + 'cyclic', + 'cycling', + 'cyclist', + 'cyclograph', + 'cyclohexane', + 'cycloid', + 'cyclometer', + 'cyclone', + 'cyclonite', + 'cycloparaffin', + 'cyclopedia', + 'cyclopentane', + 'cycloplegia', + 'cyclopropane', + 'cyclorama', + 'cyclosis', + 'cyclostome', + 'cyclostyle', + 'cyclothymia', + 'cyclotron', + 'cyder', + 'cygnet', + 'cylinder', + 'cylindrical', + 'cylindroid', + 'cylix', + 'cyma', + 'cymar', + 'cymatium', + 'cymbal', + 'cymbiform', + 'cyme', + 'cymene', + 'cymogene', + 'cymograph', + 'cymoid', + 'cymophane', + 'cymose', + 'cynic', + 'cynical', + 'cynicism', + 'cynosure', + 'cyperaceous', + 'cypher', + 'cypress', + 'cyprinid', + 'cyprinodont', + 'cyprinoid', + 'cypripedium', + 'cypsela', + 'cyst', + 'cystectomy', + 'cysteine', + 'cystic', + 'cysticercoid', + 'cysticercus', + 'cystine', + 'cystitis', + 'cystocarp', + 'cystocele', + 'cystoid', + 'cystolith', + 'cystoscope', + 'cystotomy', + 'cytaster', + 'cytochemistry', + 'cytochrome', + 'cytogenesis', + 'cytogenetics', + 'cytokinesis', + 'cytologist', + 'cytology', + 'cytolysin', + 'cytolysis', + 'cyton', + 'cytoplasm', + 'cytoplast', + 'cytosine', + 'cytotaxonomy', + 'czar', + 'czardas', + 'czardom', + 'czarevitch', + 'czarevna', + 'czarina', + 'czarism', + 'czarist', + 'd', + 'dab', + 'dabber', + 'dabble', + 'dabchick', + 'dabster', + 'dace', + 'dacha', + 'dachshund', + 'dacoit', + 'dacoity', + 'dactyl', + 'dactylic', + 'dactylogram', + 'dactylography', + 'dactylology', + 'dad', + 'daddy', + 'dado', + 'daedal', + 'daemon', + 'daff', + 'daffodil', + 'daffy', + 'daft', + 'dag', + 'dagger', + 'daggerboard', + 'daglock', + 'dago', + 'dagoba', + 'daguerreotype', + 'dah', + 'dahabeah', + 'dahlia', + 'daily', + 'daimon', + 'daimyo', + 'dainty', + 'daiquiri', + 'dairy', + 'dairying', + 'dairymaid', + 'dairyman', + 'dais', + 'daisy', + 'dak', + 'dale', + 'dalesman', + 'daleth', + 'dalliance', + 'dally', + 'dalmatic', + 'daltonism', + 'dam', + 'damage', + 'damages', + 'damaging', + 'daman', + 'damar', + 'damascene', + 'damask', + 'dame', + 'dammar', + 'damn', + 'damnable', + 'damnation', + 'damnatory', + 'damned', + 'damnedest', + 'damnify', + 'damning', + 'damoiselle', + 'damp', + 'dampen', + 'damper', + 'dampproof', + 'damsel', + 'damselfish', + 'damselfly', + 'damson', + 'dance', + 'dancer', + 'dancette', + 'dandelion', + 'dander', + 'dandify', + 'dandiprat', + 'dandle', + 'dandruff', + 'dandy', + 'dang', + 'danged', + 'danger', + 'dangerous', + 'dangle', + 'danio', + 'dank', + 'danseur', + 'danseuse', + 'dap', + 'daphne', + 'dapper', + 'dapple', + 'dappled', + 'darbies', + 'dare', + 'daredevil', + 'daredeviltry', + 'daresay', + 'darg', + 'daric', + 'daring', + 'dariole', + 'dark', + 'darken', + 'darkish', + 'darkle', + 'darkling', + 'darkness', + 'darkroom', + 'darksome', + 'darky', + 'darling', + 'darn', + 'darned', + 'darnel', + 'darner', + 'dart', + 'dartboard', + 'darter', + 'dash', + 'dashboard', + 'dashed', + 'dasheen', + 'dasher', + 'dashing', + 'dashpot', + 'dastard', + 'dastardly', + 'dasyure', + 'data', + 'datary', + 'datcha', + 'date', + 'dated', + 'dateless', + 'dateline', + 'dative', + 'dato', + 'datolite', + 'datum', + 'datura', + 'daub', + 'daube', + 'daubery', + 'daughter', + 'daughterly', + 'daunt', + 'dauntless', + 'dauphin', + 'dauphine', + 'davenport', + 'davit', + 'daw', + 'dawdle', + 'dawn', + 'day', + 'daybook', + 'daybreak', + 'daydream', + 'dayflower', + 'dayfly', + 'daylight', + 'daylong', + 'days', + 'dayspring', + 'daystar', + 'daytime', + 'daze', + 'dazzle', + 'de', + 'deacon', + 'deaconess', + 'deaconry', + 'deactivate', + 'dead', + 'deadbeat', + 'deaden', + 'deadening', + 'deadeye', + 'deadfall', + 'deadhead', + 'deadlight', + 'deadline', + 'deadlock', + 'deadly', + 'deadpan', + 'deadweight', + 'deadwood', + 'deaf', + 'deafen', + 'deafening', + 'deal', + 'dealate', + 'dealer', + 'dealfish', + 'dealing', + 'dealings', + 'dealt', + 'deaminate', + 'dean', + 'deanery', + 'dear', + 'dearly', + 'dearth', + 'deary', + 'death', + 'deathbed', + 'deathblow', + 'deathday', + 'deathful', + 'deathless', + 'deathlike', + 'deathly', + 'deathtrap', + 'deathwatch', + 'deb', + 'debacle', + 'debag', + 'debar', + 'debark', + 'debase', + 'debatable', + 'debate', + 'debauch', + 'debauched', + 'debauchee', + 'debauchery', + 'debenture', + 'debilitate', + 'debility', + 'debit', + 'debonair', + 'debouch', + 'debouchment', + 'debrief', + 'debris', + 'debt', + 'debtor', + 'debug', + 'debunk', + 'debus', + 'debut', + 'debutant', + 'debutante', + 'decade', + 'decadence', + 'decadent', + 'decaffeinate', + 'decagon', + 'decagram', + 'decahedron', + 'decal', + 'decalcify', + 'decalcomania', + 'decalescence', + 'decaliter', + 'decalogue', + 'decameter', + 'decamp', + 'decanal', + 'decane', + 'decani', + 'decant', + 'decanter', + 'decapitate', + 'decapod', + 'decarbonate', + 'decarbonize', + 'decarburize', + 'decare', + 'decastere', + 'decastyle', + 'decasyllabic', + 'decasyllable', + 'decathlon', + 'decay', + 'decease', + 'deceased', + 'decedent', + 'deceit', + 'deceitful', + 'deceive', + 'decelerate', + 'deceleron', + 'decemvir', + 'decemvirate', + 'decencies', + 'decency', + 'decennary', + 'decennial', + 'decennium', + 'decent', + 'decentralization', + 'decentralize', + 'deception', + 'deceptive', + 'decerebrate', + 'decern', + 'deciare', + 'decibel', + 'decide', + 'decided', + 'decidua', + 'deciduous', + 'decigram', + 'decile', + 'deciliter', + 'decillion', + 'decimal', + 'decimalize', + 'decimate', + 'decimeter', + 'decipher', + 'decision', + 'decisive', + 'deck', + 'deckhand', + 'deckhouse', + 'deckle', + 'declaim', + 'declamation', + 'declamatory', + 'declarant', + 'declaration', + 'declarative', + 'declaratory', + 'declare', + 'declared', + 'declarer', + 'declass', + 'declassify', + 'declension', + 'declinate', + 'declination', + 'declinatory', + 'declinature', + 'decline', + 'declinometer', + 'declivitous', + 'declivity', + 'declivous', + 'decoct', + 'decoction', + 'decode', + 'decoder', + 'decollate', + 'decolonize', + 'decolorant', + 'decolorize', + 'decommission', + 'decompensation', + 'decompose', + 'decomposed', + 'decomposer', + 'decomposition', + 'decompound', + 'decompress', + 'decongestant', + 'deconsecrate', + 'decontaminate', + 'decontrol', + 'decor', + 'decorate', + 'decoration', + 'decorative', + 'decorator', + 'decorous', + 'decorticate', + 'decortication', + 'decorum', + 'decoupage', + 'decoy', + 'decrease', + 'decreasing', + 'decree', + 'decrement', + 'decrepit', + 'decrepitate', + 'decrepitude', + 'decrescendo', + 'decrescent', + 'decretal', + 'decretive', + 'decretory', + 'decrial', + 'decry', + 'decrypt', + 'decumbent', + 'decuple', + 'decurion', + 'decurrent', + 'decurved', + 'decury', + 'decussate', + 'dedal', + 'dedans', + 'dedicate', + 'dedicated', + 'dedication', + 'dedifferentiation', + 'deduce', + 'deduct', + 'deductible', + 'deduction', + 'deductive', + 'deed', + 'deejay', + 'deem', + 'deemster', + 'deep', + 'deepen', + 'deeply', + 'deer', + 'deerhound', + 'deerskin', + 'deerstalker', + 'deface', + 'defalcate', + 'defalcation', + 'defamation', + 'defamatory', + 'defame', + 'default', + 'defaulter', + 'defeasance', + 'defeasible', + 'defeat', + 'defeatism', + 'defeatist', + 'defecate', + 'defect', + 'defection', + 'defective', + 'defector', + 'defence', + 'defend', + 'defendant', + 'defenestration', + 'defense', + 'defensible', + 'defensive', + 'defer', + 'deference', + 'deferent', + 'deferential', + 'deferment', + 'deferral', + 'deferred', + 'defiance', + 'defiant', + 'defibrillator', + 'deficiency', + 'deficient', + 'deficit', + 'defilade', + 'defile', + 'define', + 'definiendum', + 'definiens', + 'definite', + 'definitely', + 'definition', + 'definitive', + 'deflagrate', + 'deflate', + 'deflation', + 'deflect', + 'deflected', + 'deflection', + 'deflective', + 'deflexed', + 'deflocculate', + 'defloration', + 'deflower', + 'defluxion', + 'defoliant', + 'defoliate', + 'deforce', + 'deforest', + 'deform', + 'deformation', + 'deformed', + 'deformity', + 'defraud', + 'defray', + 'defrayal', + 'defrock', + 'defrost', + 'defroster', + 'deft', + 'defunct', + 'defy', + 'degas', + 'degauss', + 'degeneracy', + 'degenerate', + 'degeneration', + 'deglutinate', + 'deglutition', + 'degradable', + 'degradation', + 'degrade', + 'degraded', + 'degrading', + 'degrease', + 'degree', + 'degression', + 'degust', + 'dehisce', + 'dehiscence', + 'dehiscent', + 'dehorn', + 'dehumanize', + 'dehumidifier', + 'dehumidify', + 'dehydrate', + 'dehydrogenase', + 'dehydrogenate', + 'dehypnotize', + 'deice', + 'deicer', + 'deicide', + 'deictic', + 'deific', + 'deification', + 'deiform', + 'deify', + 'deign', + 'deil', + 'deipnosophist', + 'deism', + 'deist', + 'deity', + 'deject', + 'dejecta', + 'dejected', + 'dejection', + 'dekaliter', + 'dekameter', + 'dekko', + 'delaine', + 'delaminate', + 'delamination', + 'delate', + 'delative', + 'delay', + 'dele', + 'delectable', + 'delectate', + 'delectation', + 'delegacy', + 'delegate', + 'delegation', + 'delete', + 'deleterious', + 'deletion', + 'delft', + 'delftware', + 'deli', + 'deliberate', + 'deliberation', + 'deliberative', + 'delicacy', + 'delicate', + 'delicatessen', + 'delicious', + 'delict', + 'delight', + 'delighted', + 'delightful', + 'delimit', + 'delimitate', + 'delineate', + 'delineation', + 'delineator', + 'delinquency', + 'delinquent', + 'deliquesce', + 'deliquescence', + 'delirious', + 'delirium', + 'delitescence', + 'delitescent', + 'deliver', + 'deliverance', + 'delivery', + 'dell', + 'delocalize', + 'delouse', + 'delphinium', + 'delta', + 'deltaic', + 'deltoid', + 'delubrum', + 'delude', + 'deluge', + 'delusion', + 'delusive', + 'deluxe', + 'delve', + 'demagnetize', + 'demagogic', + 'demagogue', + 'demagoguery', + 'demagogy', + 'demand', + 'demandant', + 'demanding', + 'demantoid', + 'demarcate', + 'demarcation', + 'demarche', + 'demark', + 'demasculinize', + 'dematerialize', + 'deme', + 'demean', + 'demeanor', + 'dement', + 'demented', + 'dementia', + 'demerit', + 'demesne', + 'demibastion', + 'demicanton', + 'demigod', + 'demijohn', + 'demilitarize', + 'demilune', + 'demimondaine', + 'demimonde', + 'demineralize', + 'demirelief', + 'demirep', + 'demise', + 'demisemiquaver', + 'demission', + 'demit', + 'demitasse', + 'demiurge', + 'demivolt', + 'demo', + 'demob', + 'demobilize', + 'democracy', + 'democrat', + 'democratic', + 'democratize', + 'demodulate', + 'demodulation', + 'demodulator', + 'demography', + 'demoiselle', + 'demolish', + 'demolition', + 'demon', + 'demonetize', + 'demoniac', + 'demonic', + 'demonism', + 'demonize', + 'demonography', + 'demonolater', + 'demonolatry', + 'demonology', + 'demonstrable', + 'demonstrate', + 'demonstration', + 'demonstrative', + 'demonstrator', + 'demoralize', + 'demos', + 'demote', + 'demotic', + 'demount', + 'dempster', + 'demulcent', + 'demulsify', + 'demur', + 'demure', + 'demurrage', + 'demurral', + 'demurrer', + 'demy', + 'demythologize', + 'den', + 'denarius', + 'denary', + 'denationalize', + 'denaturalize', + 'denature', + 'denazify', + 'dendriform', + 'dendrite', + 'dendritic', + 'dendrochronology', + 'dendroid', + 'dendrology', + 'dene', + 'denegation', + 'dengue', + 'deniable', + 'denial', + 'denier', + 'denigrate', + 'denim', + 'denims', + 'denitrate', + 'denitrify', + 'denizen', + 'denominate', + 'denomination', + 'denominational', + 'denominationalism', + 'denominative', + 'denominator', + 'denotation', + 'denotative', + 'denote', + 'denouement', + 'denounce', + 'dense', + 'densify', + 'densimeter', + 'densitometer', + 'density', + 'dent', + 'dental', + 'dentalium', + 'dentate', + 'dentation', + 'dentelle', + 'denticle', + 'denticulate', + 'denticulation', + 'dentiform', + 'dentifrice', + 'dentil', + 'dentilabial', + 'dentilingual', + 'dentist', + 'dentistry', + 'dentition', + 'dentoid', + 'denture', + 'denudate', + 'denudation', + 'denude', + 'denumerable', + 'denunciate', + 'denunciation', + 'denunciatory', + 'deny', + 'deodand', + 'deodar', + 'deodorant', + 'deodorize', + 'deontology', + 'deoxidize', + 'deoxygenate', + 'deoxyribonuclease', + 'deoxyribose', + 'depart', + 'departed', + 'department', + 'departmentalism', + 'departmentalize', + 'departure', + 'depend', + 'dependable', + 'dependence', + 'dependency', + 'dependent', + 'depersonalization', + 'depersonalize', + 'depict', + 'depicture', + 'depilate', + 'depilatory', + 'deplane', + 'deplete', + 'deplorable', + 'deplore', + 'deploy', + 'deplume', + 'depolarize', + 'depolymerize', + 'depone', + 'deponent', + 'depopulate', + 'deport', + 'deportation', + 'deportee', + 'deportment', + 'deposal', + 'depose', + 'deposit', + 'depositary', + 'deposition', + 'depositor', + 'depository', + 'depot', + 'deprave', + 'depraved', + 'depravity', + 'deprecate', + 'deprecative', + 'deprecatory', + 'depreciable', + 'depreciate', + 'depreciation', + 'depreciatory', + 'depredate', + 'depredation', + 'depress', + 'depressant', + 'depressed', + 'depression', + 'depressive', + 'depressomotor', + 'depressor', + 'deprivation', + 'deprive', + 'deprived', + 'depside', + 'depth', + 'depurate', + 'depurative', + 'deputation', + 'depute', + 'deputize', + 'deputy', + 'deracinate', + 'deraign', + 'derail', + 'derange', + 'deranged', + 'derangement', + 'deration', + 'derby', + 'dereism', + 'derelict', + 'dereliction', + 'deride', + 'derisible', + 'derision', + 'derisive', + 'derivation', + 'derivative', + 'derive', + 'derma', + 'dermal', + 'dermatitis', + 'dermatogen', + 'dermatoglyphics', + 'dermatoid', + 'dermatologist', + 'dermatology', + 'dermatome', + 'dermatophyte', + 'dermatoplasty', + 'dermatosis', + 'dermis', + 'dermoid', + 'derogate', + 'derogative', + 'derogatory', + 'derrick', + 'derringer', + 'derris', + 'derry', + 'dervish', + 'desalinate', + 'descant', + 'descend', + 'descendant', + 'descendent', + 'descender', + 'descendible', + 'descent', + 'describe', + 'description', + 'descriptive', + 'descry', + 'desecrate', + 'desegregate', + 'desensitize', + 'desert', + 'deserted', + 'desertion', + 'deserve', + 'deserved', + 'deservedly', + 'deserving', + 'desex', + 'desexualize', + 'deshabille', + 'desiccant', + 'desiccate', + 'desiccated', + 'desiccator', + 'desiderata', + 'desiderate', + 'desiderative', + 'desideratum', + 'design', + 'designate', + 'designation', + 'designed', + 'designedly', + 'designer', + 'designing', + 'desinence', + 'desirable', + 'desire', + 'desired', + 'desirous', + 'desist', + 'desk', + 'desman', + 'desmid', + 'desmoid', + 'desolate', + 'desolation', + 'desorb', + 'despair', + 'despairing', + 'despatch', + 'desperado', + 'desperate', + 'desperation', + 'despicable', + 'despise', + 'despite', + 'despiteful', + 'despoil', + 'despoliation', + 'despond', + 'despondency', + 'despondent', + 'despot', + 'despotic', + 'despotism', + 'despumate', + 'desquamate', + 'dessert', + 'dessertspoon', + 'dessiatine', + 'destination', + 'destine', + 'destined', + 'destiny', + 'destitute', + 'destitution', + 'destrier', + 'destroy', + 'destroyer', + 'destruct', + 'destructible', + 'destruction', + 'destructionist', + 'destructive', + 'destructor', + 'desuetude', + 'desulphurize', + 'desultory', + 'detach', + 'detached', + 'detachment', + 'detail', + 'detailed', + 'detain', + 'detainer', + 'detect', + 'detection', + 'detective', + 'detector', + 'detent', + 'detention', + 'deter', + 'deterge', + 'detergency', + 'detergent', + 'deteriorate', + 'deterioration', + 'determinable', + 'determinant', + 'determinate', + 'determination', + 'determinative', + 'determine', + 'determined', + 'determiner', + 'determinism', + 'deterrence', + 'deterrent', + 'detest', + 'detestable', + 'detestation', + 'dethrone', + 'detinue', + 'detonate', + 'detonation', + 'detonator', + 'detour', + 'detoxicate', + 'detoxify', + 'detract', + 'detraction', + 'detrain', + 'detribalize', + 'detriment', + 'detrimental', + 'detrital', + 'detrition', + 'detritus', + 'detrude', + 'detruncate', + 'detrusion', + 'detumescence', + 'deuce', + 'deuced', + 'deuteragonist', + 'deuteranope', + 'deuteranopia', + 'deuterium', + 'deuterogamy', + 'deuteron', + 'deutoplasm', + 'deutzia', + 'deva', + 'devaluate', + 'devaluation', + 'devalue', + 'devastate', + 'devastating', + 'devastation', + 'develop', + 'developer', + 'developing', + 'development', + 'devest', + 'deviant', + 'deviate', + 'deviation', + 'deviationism', + 'device', + 'devil', + 'deviled', + 'devilfish', + 'devilish', + 'devilkin', + 'devilment', + 'devilry', + 'deviltry', + 'devious', + 'devisable', + 'devisal', + 'devise', + 'devisee', + 'devisor', + 'devitalize', + 'devitrify', + 'devoice', + 'devoid', + 'devoir', + 'devoirs', + 'devolution', + 'devolve', + 'devote', + 'devoted', + 'devotee', + 'devotion', + 'devotional', + 'devour', + 'devout', + 'dew', + 'dewan', + 'dewberry', + 'dewclaw', + 'dewdrop', + 'dewlap', + 'dewy', + 'dexamethasone', + 'dexter', + 'dexterity', + 'dexterous', + 'dextrad', + 'dextral', + 'dextrality', + 'dextran', + 'dextrin', + 'dextro', + 'dextroamphetamine', + 'dextrocular', + 'dextroglucose', + 'dextrogyrate', + 'dextrorotation', + 'dextrorse', + 'dextrose', + 'dextrosinistral', + 'dextrous', + 'dey', + 'dg', + 'dharana', + 'dharma', + 'dharna', + 'dhobi', + 'dhole', + 'dhoti', + 'dhow', + 'dhyana', + 'diabase', + 'diabetes', + 'diabetic', + 'diablerie', + 'diabolic', + 'diabolism', + 'diabolize', + 'diabolo', + 'diacaustic', + 'diacetylmorphine', + 'diachronic', + 'diacid', + 'diaconal', + 'diaconate', + 'diaconicon', + 'diaconicum', + 'diacritic', + 'diacritical', + 'diactinic', + 'diadelphous', + 'diadem', + 'diadromous', + 'diaeresis', + 'diagenesis', + 'diageotropism', + 'diagnose', + 'diagnosis', + 'diagnostic', + 'diagnostician', + 'diagnostics', + 'diagonal', + 'diagram', + 'diagraph', + 'diakinesis', + 'dial', + 'dialect', + 'dialectal', + 'dialectic', + 'dialectical', + 'dialectician', + 'dialecticism', + 'dialectics', + 'dialectologist', + 'dialectology', + 'diallage', + 'dialogism', + 'dialogist', + 'dialogize', + 'dialogue', + 'dialyse', + 'dialyser', + 'dialysis', + 'dialytic', + 'dialyze', + 'diamagnet', + 'diamagnetic', + 'diamagnetism', + 'diameter', + 'diametral', + 'diametrically', + 'diamine', + 'diamond', + 'diamondback', + 'diandrous', + 'dianetics', + 'dianoetic', + 'dianoia', + 'dianthus', + 'diapason', + 'diapause', + 'diapedesis', + 'diaper', + 'diaphane', + 'diaphaneity', + 'diaphanous', + 'diaphone', + 'diaphony', + 'diaphoresis', + 'diaphoretic', + 'diaphragm', + 'diaphysis', + 'diapophysis', + 'diapositive', + 'diarchy', + 'diarist', + 'diarrhea', + 'diarrhoea', + 'diarthrosis', + 'diary', + 'diaspore', + 'diastase', + 'diastasis', + 'diastema', + 'diaster', + 'diastole', + 'diastrophism', + 'diastyle', + 'diatessaron', + 'diathermic', + 'diathermy', + 'diathesis', + 'diatom', + 'diatomaceous', + 'diatomic', + 'diatomite', + 'diatonic', + 'diatribe', + 'diatropism', + 'diazine', + 'diazo', + 'diazole', + 'diazomethane', + 'diazonium', + 'diazotize', + 'dib', + 'dibasic', + 'dibble', + 'dibbuk', + 'dibranchiate', + 'dibromide', + 'dibs', + 'dibucaine', + 'dicast', + 'dice', + 'dicentra', + 'dicephalous', + 'dichasium', + 'dichlamydeous', + 'dichloride', + 'dichlorodifluoromethane', + 'dichlorodiphenyltrichloroethane', + 'dichogamy', + 'dichotomize', + 'dichotomous', + 'dichotomy', + 'dichroic', + 'dichroism', + 'dichroite', + 'dichromate', + 'dichromatic', + 'dichromaticism', + 'dichromatism', + 'dichromic', + 'dichroscope', + 'dick', + 'dickens', + 'dicker', + 'dickey', + 'dicky', + 'diclinous', + 'dicot', + 'dicotyledon', + 'dicrotic', + 'dicta', + 'dictate', + 'dictation', + 'dictator', + 'dictatorial', + 'dictatorship', + 'diction', + 'dictionary', + 'dictum', + 'did', + 'didactic', + 'didactics', + 'diddle', + 'dido', + 'didst', + 'didymium', + 'didymous', + 'didynamous', + 'die', + 'dieback', + 'diecious', + 'diehard', + 'dieldrin', + 'dielectric', + 'diencephalon', + 'dieresis', + 'diesel', + 'diesis', + 'diestock', + 'diet', + 'dietary', + 'dietetic', + 'dietetics', + 'dietitian', + 'differ', + 'difference', + 'different', + 'differentia', + 'differentiable', + 'differential', + 'differentiate', + 'differentiation', + 'difficile', + 'difficult', + 'difficulty', + 'diffidence', + 'diffident', + 'diffluent', + 'diffract', + 'diffraction', + 'diffractive', + 'diffractometer', + 'diffuse', + 'diffuser', + 'diffusion', + 'diffusive', + 'diffusivity', + 'dig', + 'digamma', + 'digamy', + 'digastric', + 'digenesis', + 'digest', + 'digestant', + 'digester', + 'digestible', + 'digestif', + 'digestion', + 'digestive', + 'digged', + 'digger', + 'diggings', + 'dight', + 'digit', + 'digital', + 'digitalin', + 'digitalis', + 'digitalism', + 'digitalize', + 'digitate', + 'digitiform', + 'digitigrade', + 'digitize', + 'digitoxin', + 'diglot', + 'dignified', + 'dignify', + 'dignitary', + 'dignity', + 'digraph', + 'digress', + 'digression', + 'digressive', + 'dihedral', + 'dihedron', + 'dihybrid', + 'dihydric', + 'dihydrostreptomycin', + 'dike', + 'dilapidate', + 'dilapidated', + 'dilapidation', + 'dilatant', + 'dilatation', + 'dilate', + 'dilation', + 'dilative', + 'dilatometer', + 'dilator', + 'dilatory', + 'dildo', + 'dilemma', + 'dilettante', + 'dilettantism', + 'diligence', + 'diligent', + 'dill', + 'dilly', + 'dillydally', + 'diluent', + 'dilute', + 'dilution', + 'diluvial', + 'diluvium', + 'dim', + 'dime', + 'dimenhydrinate', + 'dimension', + 'dimer', + 'dimercaprol', + 'dimerous', + 'dimeter', + 'dimetric', + 'dimidiate', + 'diminish', + 'diminished', + 'diminuendo', + 'diminution', + 'diminutive', + 'dimissory', + 'dimity', + 'dimmer', + 'dimorph', + 'dimorphism', + 'dimorphous', + 'dimple', + 'dimwit', + 'din', + 'dinar', + 'dine', + 'diner', + 'dineric', + 'dinette', + 'ding', + 'dingbat', + 'dinge', + 'dinghy', + 'dingle', + 'dingo', + 'dingus', + 'dingy', + 'dinitrobenzene', + 'dink', + 'dinky', + 'dinner', + 'dinnerware', + 'dinoflagellate', + 'dinosaur', + 'dinosaurian', + 'dinothere', + 'dint', + 'diocesan', + 'diocese', + 'diode', + 'dioecious', + 'diopside', + 'dioptase', + 'dioptometer', + 'dioptric', + 'dioptrics', + 'diorama', + 'diorite', + 'dioxide', + 'dip', + 'dipeptide', + 'dipetalous', + 'diphase', + 'diphenyl', + 'diphenylamine', + 'diphenylhydantoin', + 'diphosgene', + 'diphtheria', + 'diphthong', + 'diphthongize', + 'diphyllous', + 'diphyodont', + 'diplegia', + 'diplex', + 'diploblastic', + 'diplocardiac', + 'diplococcus', + 'diplodocus', + 'diploid', + 'diploma', + 'diplomacy', + 'diplomat', + 'diplomate', + 'diplomatic', + 'diplomatics', + 'diplomatist', + 'diplopia', + 'diplopod', + 'diplosis', + 'diplostemonous', + 'dipnoan', + 'dipody', + 'dipole', + 'dipper', + 'dippy', + 'dipsomania', + 'dipsomaniac', + 'dipstick', + 'dipteral', + 'dipteran', + 'dipterocarpaceous', + 'dipterous', + 'diptych', + 'dire', + 'direct', + 'directed', + 'direction', + 'directional', + 'directions', + 'directive', + 'directly', + 'director', + 'directorate', + 'directorial', + 'directory', + 'directrix', + 'direful', + 'dirge', + 'dirham', + 'dirigible', + 'dirk', + 'dirndl', + 'dirt', + 'dirty', + 'disability', + 'disable', + 'disabled', + 'disabuse', + 'disaccharide', + 'disaccord', + 'disaccredit', + 'disaccustom', + 'disadvantage', + 'disadvantaged', + 'disadvantageous', + 'disaffect', + 'disaffection', + 'disaffiliate', + 'disaffirm', + 'disafforest', + 'disagree', + 'disagreeable', + 'disagreement', + 'disallow', + 'disannul', + 'disappear', + 'disappearance', + 'disappoint', + 'disappointed', + 'disappointment', + 'disapprobation', + 'disapproval', + 'disapprove', + 'disarm', + 'disarmament', + 'disarming', + 'disarrange', + 'disarray', + 'disarticulate', + 'disassemble', + 'disassembly', + 'disassociate', + 'disaster', + 'disastrous', + 'disavow', + 'disavowal', + 'disband', + 'disbar', + 'disbelief', + 'disbelieve', + 'disbranch', + 'disbud', + 'disburden', + 'disburse', + 'disbursement', + 'disc', + 'discalced', + 'discant', + 'discard', + 'discarnate', + 'discern', + 'discernible', + 'discerning', + 'discernment', + 'discharge', + 'disciple', + 'disciplinant', + 'disciplinarian', + 'disciplinary', + 'discipline', + 'disclaim', + 'disclaimer', + 'disclamation', + 'disclimax', + 'disclose', + 'disclosure', + 'discobolus', + 'discography', + 'discoid', + 'discolor', + 'discoloration', + 'discombobulate', + 'discomfit', + 'discomfiture', + 'discomfort', + 'discomfortable', + 'discommend', + 'discommode', + 'discommodity', + 'discommon', + 'discompose', + 'discomposure', + 'disconcert', + 'disconcerted', + 'disconformity', + 'disconnect', + 'disconnected', + 'disconnection', + 'disconsider', + 'disconsolate', + 'discontent', + 'discontented', + 'discontinuance', + 'discontinuation', + 'discontinue', + 'discontinuity', + 'discontinuous', + 'discophile', + 'discord', + 'discordance', + 'discordancy', + 'discordant', + 'discotheque', + 'discount', + 'discountenance', + 'discounter', + 'discourage', + 'discouragement', + 'discourse', + 'discourteous', + 'discourtesy', + 'discover', + 'discoverer', + 'discovert', + 'discovery', + 'discredit', + 'discreditable', + 'discreet', + 'discrepancy', + 'discrepant', + 'discrete', + 'discretion', + 'discretional', + 'discretionary', + 'discriminant', + 'discriminate', + 'discriminating', + 'discrimination', + 'discriminative', + 'discriminator', + 'discriminatory', + 'discrown', + 'discursion', + 'discursive', + 'discus', + 'discuss', + 'discussant', + 'discussion', + 'disdain', + 'disdainful', + 'disease', + 'diseased', + 'disembark', + 'disembarrass', + 'disembodied', + 'disembody', + 'disembogue', + 'disembowel', + 'disembroil', + 'disenable', + 'disenchant', + 'disencumber', + 'disendow', + 'disenfranchise', + 'disengage', + 'disengagement', + 'disentail', + 'disentangle', + 'disenthral', + 'disenthrall', + 'disenthrone', + 'disentitle', + 'disentomb', + 'disentwine', + 'disepalous', + 'disequilibrium', + 'disestablish', + 'disesteem', + 'diseur', + 'diseuse', + 'disfavor', + 'disfeature', + 'disfigure', + 'disfigurement', + 'disforest', + 'disfranchise', + 'disfrock', + 'disgorge', + 'disgrace', + 'disgraceful', + 'disgruntle', + 'disguise', + 'disgust', + 'disgusting', + 'dish', + 'dishabille', + 'disharmonious', + 'disharmony', + 'dishcloth', + 'dishearten', + 'dished', + 'disherison', + 'dishevel', + 'disheveled', + 'dishonest', + 'dishonesty', + 'dishonor', + 'dishonorable', + 'dishpan', + 'dishrag', + 'dishtowel', + 'dishwasher', + 'dishwater', + 'disillusion', + 'disillusionize', + 'disincentive', + 'disinclination', + 'disincline', + 'disinclined', + 'disinfect', + 'disinfectant', + 'disinfection', + 'disinfest', + 'disingenuous', + 'disinherit', + 'disintegrate', + 'disintegration', + 'disinter', + 'disinterest', + 'disinterested', + 'disject', + 'disjoin', + 'disjoined', + 'disjoint', + 'disjointed', + 'disjunct', + 'disjunction', + 'disjunctive', + 'disjuncture', + 'disk', + 'dislike', + 'dislimn', + 'dislocate', + 'dislocation', + 'dislodge', + 'disloyal', + 'disloyalty', + 'dismal', + 'dismantle', + 'dismast', + 'dismay', + 'dismember', + 'dismiss', + 'dismissal', + 'dismissive', + 'dismount', + 'disobedience', + 'disobedient', + 'disobey', + 'disoblige', + 'disoperation', + 'disorder', + 'disordered', + 'disorderly', + 'disorganization', + 'disorganize', + 'disorient', + 'disorientate', + 'disown', + 'disparage', + 'disparagement', + 'disparate', + 'disparity', + 'dispart', + 'dispassion', + 'dispassionate', + 'dispatch', + 'dispatcher', + 'dispel', + 'dispend', + 'dispensable', + 'dispensary', + 'dispensation', + 'dispensatory', + 'dispense', + 'dispenser', + 'dispeople', + 'dispermous', + 'dispersal', + 'dispersant', + 'disperse', + 'dispersion', + 'dispersive', + 'dispersoid', + 'dispirit', + 'dispirited', + 'displace', + 'displacement', + 'displant', + 'display', + 'displayed', + 'displease', + 'displeasure', + 'displode', + 'displume', + 'disport', + 'disposable', + 'disposal', + 'dispose', + 'disposed', + 'disposition', + 'dispossess', + 'disposure', + 'dispraise', + 'dispread', + 'disprize', + 'disproof', + 'disproportion', + 'disproportionate', + 'disproportionation', + 'disprove', + 'disputable', + 'disputant', + 'disputation', + 'disputatious', + 'dispute', + 'disqualification', + 'disqualify', + 'disquiet', + 'disquieting', + 'disquietude', + 'disquisition', + 'disrate', + 'disregard', + 'disregardful', + 'disrelish', + 'disremember', + 'disrepair', + 'disreputable', + 'disrepute', + 'disrespect', + 'disrespectable', + 'disrespectful', + 'disrobe', + 'disrupt', + 'disruption', + 'disruptive', + 'dissatisfaction', + 'dissatisfactory', + 'dissatisfied', + 'dissatisfy', + 'dissect', + 'dissected', + 'dissection', + 'disseise', + 'disseisin', + 'dissemblance', + 'dissemble', + 'disseminate', + 'disseminule', + 'dissension', + 'dissent', + 'dissenter', + 'dissentient', + 'dissentious', + 'dissepiment', + 'dissert', + 'dissertate', + 'dissertation', + 'disserve', + 'disservice', + 'dissever', + 'dissidence', + 'dissident', + 'dissimilar', + 'dissimilarity', + 'dissimilate', + 'dissimilation', + 'dissimilitude', + 'dissimulate', + 'dissimulation', + 'dissipate', + 'dissipated', + 'dissipation', + 'dissociable', + 'dissociate', + 'dissociation', + 'dissogeny', + 'dissoluble', + 'dissolute', + 'dissolution', + 'dissolve', + 'dissolvent', + 'dissonance', + 'dissonancy', + 'dissonant', + 'dissuade', + 'dissuasion', + 'dissuasive', + 'dissyllable', + 'dissymmetry', + 'distaff', + 'distal', + 'distance', + 'distant', + 'distaste', + 'distasteful', + 'distemper', + 'distend', + 'distended', + 'distich', + 'distichous', + 'distil', + 'distill', + 'distillate', + 'distillation', + 'distilled', + 'distiller', + 'distillery', + 'distinct', + 'distinction', + 'distinctive', + 'distinctly', + 'distinguish', + 'distinguished', + 'distinguishing', + 'distort', + 'distorted', + 'distortion', + 'distract', + 'distracted', + 'distraction', + 'distrain', + 'distraint', + 'distrait', + 'distraught', + 'distress', + 'distressed', + 'distressful', + 'distributary', + 'distribute', + 'distributee', + 'distribution', + 'distributive', + 'distributor', + 'district', + 'distrust', + 'distrustful', + 'disturb', + 'disturbance', + 'disturbed', + 'disturbing', + 'disulfide', + 'disulfiram', + 'disunion', + 'disunite', + 'disunity', + 'disuse', + 'disused', + 'disvalue', + 'disyllable', + 'dit', + 'ditch', + 'ditchwater', + 'ditheism', + 'dither', + 'dithionite', + 'dithyramb', + 'dithyrambic', + 'dittany', + 'ditto', + 'dittography', + 'ditty', + 'diuresis', + 'diuretic', + 'diurnal', + 'diva', + 'divagate', + 'divalent', + 'divan', + 'divaricate', + 'dive', + 'diver', + 'diverge', + 'divergence', + 'divergency', + 'divergent', + 'divers', + 'diverse', + 'diversification', + 'diversified', + 'diversiform', + 'diversify', + 'diversion', + 'diversity', + 'divert', + 'diverticulitis', + 'diverticulosis', + 'diverticulum', + 'divertimento', + 'diverting', + 'divertissement', + 'divest', + 'divestiture', + 'divide', + 'divided', + 'dividend', + 'divider', + 'dividers', + 'divination', + 'divine', + 'diviner', + 'divinity', + 'divinize', + 'divisibility', + 'divisible', + 'division', + 'divisionism', + 'divisive', + 'divisor', + 'divorce', + 'divorcee', + 'divorcement', + 'divot', + 'divulgate', + 'divulge', + 'divulgence', + 'divulsion', + 'divvy', + 'diwan', + 'dixie', + 'dizen', + 'dizzy', + 'djebel', + 'dkl', + 'dm', + 'do', + 'doable', + 'dobbin', + 'dobby', + 'dobla', + 'dobsonfly', + 'doc', + 'docent', + 'docile', + 'dock', + 'dockage', + 'docker', + 'docket', + 'dockhand', + 'dockyard', + 'doctor', + 'doctorate', + 'doctrinaire', + 'doctrinal', + 'doctrine', + 'document', + 'documentary', + 'documentation', + 'dodder', + 'doddered', + 'doddering', + 'dodecagon', + 'dodecahedron', + 'dodecasyllable', + 'dodge', + 'dodger', + 'dodo', + 'doe', + 'doer', + 'does', + 'doeskin', + 'doff', + 'dog', + 'dogbane', + 'dogberry', + 'dogcart', + 'dogcatcher', + 'doge', + 'dogface', + 'dogfight', + 'dogfish', + 'dogged', + 'dogger', + 'doggerel', + 'doggery', + 'doggish', + 'doggo', + 'doggone', + 'doggoned', + 'doggy', + 'doghouse', + 'dogie', + 'dogleg', + 'doglike', + 'dogma', + 'dogmatic', + 'dogmatics', + 'dogmatism', + 'dogmatist', + 'dogmatize', + 'dogtooth', + 'dogtrot', + 'dogvane', + 'dogwatch', + 'dogwood', + 'dogy', + 'doily', + 'doing', + 'doings', + 'doit', + 'doited', + 'dol', + 'dolabriform', + 'dolce', + 'doldrums', + 'dole', + 'doleful', + 'dolerite', + 'dolichocephalic', + 'doll', + 'dollar', + 'dollarbird', + 'dollarfish', + 'dollhouse', + 'dollop', + 'dolly', + 'dolman', + 'dolmen', + 'dolomite', + 'dolor', + 'dolorimetry', + 'doloroso', + 'dolorous', + 'dolphin', + 'dolt', + 'dom', + 'domain', + 'dome', + 'domesday', + 'domestic', + 'domesticate', + 'domesticity', + 'domicile', + 'domiciliary', + 'domiciliate', + 'dominance', + 'dominant', + 'dominate', + 'domination', + 'dominations', + 'domineer', + 'domineering', + 'dominical', + 'dominie', + 'dominion', + 'dominions', + 'dominium', + 'domino', + 'dominoes', + 'don', + 'dona', + 'donate', + 'donation', + 'donative', + 'done', + 'donee', + 'dong', + 'donga', + 'donjon', + 'donkey', + 'donna', + 'donnish', + 'donnybrook', + 'donor', + 'doodad', + 'doodle', + 'doodlebug', + 'doodlesack', + 'doolie', + 'doom', + 'doomsday', + 'door', + 'doorbell', + 'doorframe', + 'doorjamb', + 'doorkeeper', + 'doorknob', + 'doorman', + 'doormat', + 'doornail', + 'doorplate', + 'doorpost', + 'doorsill', + 'doorstep', + 'doorstone', + 'doorstop', + 'doorway', + 'dooryard', + 'dope', + 'dopester', + 'dopey', + 'dor', + 'dorado', + 'dorm', + 'dormancy', + 'dormant', + 'dormer', + 'dormeuse', + 'dormie', + 'dormitory', + 'dormouse', + 'dornick', + 'doronicum', + 'dorp', + 'dorsad', + 'dorsal', + 'dorser', + 'dorsiferous', + 'dorsiventral', + 'dorsoventral', + 'dorsum', + 'dorty', + 'dory', + 'dosage', + 'dose', + 'dosimeter', + 'doss', + 'dossal', + 'dosser', + 'dossier', + 'dost', + 'dot', + 'dotage', + 'dotard', + 'dotation', + 'dote', + 'doth', + 'doting', + 'dotted', + 'dotterel', + 'dottle', + 'dotty', + 'double', + 'doubleganger', + 'doubleheader', + 'doubleness', + 'doubles', + 'doublet', + 'doublethink', + 'doubleton', + 'doubletree', + 'doubling', + 'doubloon', + 'doublure', + 'doubly', + 'doubt', + 'doubtful', + 'doubtless', + 'douce', + 'douceur', + 'douche', + 'dough', + 'doughboy', + 'doughnut', + 'doughty', + 'doughy', + 'douma', + 'dour', + 'doura', + 'dourine', + 'douse', + 'douzepers', + 'dove', + 'dovecote', + 'dovekie', + 'dovelike', + 'dovetail', + 'dovetailed', + 'dow', + 'dowable', + 'dowager', + 'dowdy', + 'dowel', + 'dower', + 'dowery', + 'dowie', + 'dowitcher', + 'down', + 'downbeat', + 'downcast', + 'downcome', + 'downcomer', + 'downdraft', + 'downfall', + 'downgrade', + 'downhaul', + 'downhearted', + 'downhill', + 'downpipe', + 'downpour', + 'downrange', + 'downright', + 'downs', + 'downspout', + 'downstage', + 'downstairs', + 'downstate', + 'downstream', + 'downstroke', + 'downswing', + 'downthrow', + 'downtime', + 'downtown', + 'downtrend', + 'downtrodden', + 'downturn', + 'downward', + 'downwards', + 'downwash', + 'downwind', + 'downy', + 'dowry', + 'dowsabel', + 'dowse', + 'dowser', + 'doxology', + 'doxy', + 'doyen', + 'doyenne', + 'doyley', + 'doze', + 'dozen', + 'dozer', + 'dozy', + 'dr', + 'drab', + 'drabbet', + 'drabble', + 'dracaena', + 'drachm', + 'drachma', + 'draconic', + 'draff', + 'draft', + 'draftee', + 'draftsman', + 'drafty', + 'drag', + 'dragging', + 'draggle', + 'draggletailed', + 'draghound', + 'dragline', + 'dragnet', + 'dragoman', + 'dragon', + 'dragonet', + 'dragonfly', + 'dragonhead', + 'dragonnade', + 'dragonroot', + 'dragoon', + 'dragrope', + 'dragster', + 'drain', + 'drainage', + 'drainpipe', + 'drake', + 'dram', + 'drama', + 'dramatic', + 'dramatics', + 'dramatist', + 'dramatization', + 'dramatize', + 'dramaturge', + 'dramaturgy', + 'dramshop', + 'drank', + 'drape', + 'draper', + 'drapery', + 'drastic', + 'drat', + 'dratted', + 'draught', + 'draughtboard', + 'draughts', + 'draughtsman', + 'draughty', + 'draw', + 'drawback', + 'drawbar', + 'drawbridge', + 'drawee', + 'drawer', + 'drawers', + 'drawing', + 'drawknife', + 'drawl', + 'drawn', + 'drawplate', + 'drawshave', + 'drawstring', + 'drawtube', + 'dray', + 'drayage', + 'drayman', + 'dread', + 'dreadful', + 'dreadfully', + 'dreadnought', + 'dream', + 'dreamer', + 'dreamland', + 'dreamworld', + 'dreamy', + 'drear', + 'dreary', + 'dredge', + 'dredger', + 'dree', + 'dreg', + 'dregs', + 'drench', + 'dress', + 'dressage', + 'dresser', + 'dressing', + 'dressmaker', + 'dressy', + 'drew', + 'dribble', + 'driblet', + 'dried', + 'drier', + 'driest', + 'drift', + 'driftage', + 'drifter', + 'driftwood', + 'drill', + 'drilling', + 'drillmaster', + 'drillstock', + 'drily', + 'drink', + 'drinkable', + 'drinker', + 'drinking', + 'drip', + 'dripping', + 'drippy', + 'dripstone', + 'drive', + 'drivel', + 'driven', + 'driver', + 'driveway', + 'driving', + 'drizzle', + 'drogue', + 'droit', + 'droll', + 'drollery', + 'dromedary', + 'dromond', + 'drone', + 'drongo', + 'drool', + 'droop', + 'droopy', + 'drop', + 'droplet', + 'droplight', + 'dropline', + 'dropout', + 'dropper', + 'dropping', + 'droppings', + 'drops', + 'dropsical', + 'dropsonde', + 'dropsy', + 'dropwort', + 'droshky', + 'drosophila', + 'dross', + 'drought', + 'droughty', + 'drove', + 'drover', + 'drown', + 'drowse', + 'drowsy', + 'drub', + 'drubbing', + 'drudge', + 'drudgery', + 'drug', + 'drugget', + 'druggist', + 'drugstore', + 'druid', + 'drum', + 'drumbeat', + 'drumfire', + 'drumfish', + 'drumhead', + 'drumlin', + 'drummer', + 'drumstick', + 'drunk', + 'drunkard', + 'drunken', + 'drunkometer', + 'drupe', + 'drupelet', + 'druse', + 'dry', + 'dryad', + 'dryasdust', + 'dryer', + 'drying', + 'dryly', + 'drypoint', + 'drysalter', + 'duad', + 'dual', + 'dualism', + 'dualistic', + 'duality', + 'duarchy', + 'dub', + 'dubbin', + 'dubbing', + 'dubiety', + 'dubious', + 'dubitable', + 'dubitation', + 'ducal', + 'ducat', + 'duce', + 'duchess', + 'duchy', + 'duck', + 'duckbill', + 'duckboard', + 'duckling', + 'duckpin', + 'ducks', + 'ducktail', + 'duckweed', + 'ducky', + 'duct', + 'ductile', + 'dud', + 'dude', + 'dudeen', + 'dudgeon', + 'duds', + 'due', + 'duel', + 'duelist', + 'duello', + 'duenna', + 'dues', + 'duet', + 'duff', + 'duffel', + 'duffer', + 'dug', + 'dugong', + 'dugout', + 'duiker', + 'duke', + 'dukedom', + 'dulcet', + 'dulciana', + 'dulcify', + 'dulcimer', + 'dulcinea', + 'dulia', + 'dull', + 'dullard', + 'dullish', + 'dulosis', + 'dulse', + 'duly', + 'duma', + 'dumb', + 'dumbbell', + 'dumbfound', + 'dumbhead', + 'dumbstruck', + 'dumbwaiter', + 'dumdum', + 'dumfound', + 'dummy', + 'dumortierite', + 'dump', + 'dumpcart', + 'dumpish', + 'dumpling', + 'dumps', + 'dumpy', + 'dun', + 'dunce', + 'dunderhead', + 'dune', + 'dung', + 'dungaree', + 'dungeon', + 'dunghill', + 'dunite', + 'dunk', + 'dunlin', + 'dunnage', + 'dunnite', + 'dunno', + 'dunnock', + 'dunt', + 'duo', + 'duodecillion', + 'duodecimal', + 'duodecimo', + 'duodenal', + 'duodenary', + 'duodenitis', + 'duodenum', + 'duodiode', + 'duologue', + 'duomo', + 'duotone', + 'dup', + 'dupe', + 'dupery', + 'dupion', + 'duple', + 'duplet', + 'duplex', + 'duplicate', + 'duplication', + 'duplicator', + 'duplicature', + 'duplicity', + 'dupondius', + 'duppy', + 'durable', + 'duramen', + 'durance', + 'duration', + 'durative', + 'durbar', + 'duress', + 'durian', + 'during', + 'durmast', + 'duro', + 'durra', + 'durst', + 'dusk', + 'dusky', + 'dust', + 'dustcloth', + 'duster', + 'dustheap', + 'dustman', + 'dustpan', + 'dustproof', + 'dustup', + 'dusty', + 'duteous', + 'dutiable', + 'dutiful', + 'duty', + 'duumvir', + 'duumvirate', + 'duvetyn', + 'dux', + 'dvandva', + 'dwarf', + 'dwarfish', + 'dwarfism', + 'dwell', + 'dwelling', + 'dwelt', + 'dwindle', + 'dwt', + 'dyad', + 'dyadic', + 'dyarchy', + 'dybbuk', + 'dye', + 'dyeing', + 'dyeline', + 'dyestuff', + 'dyewood', + 'dying', + 'dyke', + 'dynameter', + 'dynamic', + 'dynamics', + 'dynamism', + 'dynamite', + 'dynamiter', + 'dynamo', + 'dynamoelectric', + 'dynamometer', + 'dynamometry', + 'dynamotor', + 'dynast', + 'dynasty', + 'dynatron', + 'dyne', + 'dynode', + 'dysarthria', + 'dyscrasia', + 'dysentery', + 'dysfunction', + 'dysgenic', + 'dysgenics', + 'dysgraphia', + 'dyslalia', + 'dyslexia', + 'dyslogia', + 'dyslogistic', + 'dyspepsia', + 'dyspeptic', + 'dysphagia', + 'dysphasia', + 'dysphemia', + 'dysphemism', + 'dysphonia', + 'dysphoria', + 'dysplasia', + 'dyspnea', + 'dysprosium', + 'dysteleology', + 'dysthymia', + 'dystopia', + 'dystrophy', + 'dysuria', + 'dziggetai', + 'e', + 'each', + 'eager', + 'eagle', + 'eaglestone', + 'eaglet', + 'eaglewood', + 'eagre', + 'ealdorman', + 'ear', + 'earache', + 'eardrop', + 'eardrum', + 'eared', + 'earflap', + 'earful', + 'earing', + 'earl', + 'earlap', + 'earldom', + 'early', + 'earmark', + 'earmuff', + 'earn', + 'earnest', + 'earnings', + 'earphone', + 'earpiece', + 'earplug', + 'earreach', + 'earring', + 'earshot', + 'earth', + 'earthborn', + 'earthbound', + 'earthen', + 'earthenware', + 'earthiness', + 'earthlight', + 'earthling', + 'earthly', + 'earthman', + 'earthnut', + 'earthquake', + 'earthshaker', + 'earthshaking', + 'earthshine', + 'earthstar', + 'earthward', + 'earthwork', + 'earthworm', + 'earthy', + 'earwax', + 'earwig', + 'earwitness', + 'ease', + 'easeful', + 'easel', + 'easement', + 'easily', + 'easiness', + 'easing', + 'east', + 'eastbound', + 'easterly', + 'eastern', + 'easternmost', + 'easting', + 'eastward', + 'eastwardly', + 'eastwards', + 'easy', + 'easygoing', + 'eat', + 'eatable', + 'eatables', + 'eatage', + 'eaten', + 'eating', + 'eats', + 'eau', + 'eaves', + 'eavesdrop', + 'ebb', + 'ebon', + 'ebonite', + 'ebonize', + 'ebony', + 'ebracteate', + 'ebullience', + 'ebullient', + 'ebullition', + 'eburnation', + 'ecbolic', + 'eccentric', + 'eccentricity', + 'ecchymosis', + 'ecclesia', + 'ecclesiastic', + 'ecclesiastical', + 'ecclesiasticism', + 'ecclesiolatry', + 'ecclesiology', + 'eccrine', + 'eccrinology', + 'ecdysiast', + 'ecdysis', + 'ecesis', + 'echelon', + 'echidna', + 'echinate', + 'echinoderm', + 'echinoid', + 'echinus', + 'echo', + 'echoic', + 'echoism', + 'echolalia', + 'echolocation', + 'echopraxia', + 'echovirus', + 'echt', + 'eclair', + 'eclampsia', + 'eclat', + 'eclectic', + 'eclecticism', + 'eclipse', + 'ecliptic', + 'eclogite', + 'eclogue', + 'eclosion', + 'ecology', + 'econometrics', + 'economic', + 'economical', + 'economically', + 'economics', + 'economist', + 'economize', + 'economizer', + 'economy', + 'ecospecies', + 'ecosphere', + 'ecosystem', + 'ecotone', + 'ecotype', + 'ecphonesis', + 'ecru', + 'ecstasy', + 'ecstatic', + 'ecstatics', + 'ecthyma', + 'ectoblast', + 'ectoderm', + 'ectoenzyme', + 'ectogenous', + 'ectomere', + 'ectomorph', + 'ectoparasite', + 'ectophyte', + 'ectopia', + 'ectoplasm', + 'ectosarc', + 'ectropion', + 'ectype', + 'ecumenical', + 'ecumenicalism', + 'ecumenicism', + 'ecumenicist', + 'ecumenicity', + 'ecumenism', + 'eczema', + 'edacious', + 'edacity', + 'edaphic', + 'eddo', + 'eddy', + 'edelweiss', + 'edema', + 'edentate', + 'edge', + 'edgebone', + 'edger', + 'edgeways', + 'edgewise', + 'edging', + 'edgy', + 'edh', + 'edible', + 'edibles', + 'edict', + 'edification', + 'edifice', + 'edify', + 'edile', + 'edit', + 'edition', + 'editor', + 'editorial', + 'editorialize', + 'educable', + 'educate', + 'educated', + 'educatee', + 'education', + 'educational', + 'educationist', + 'educative', + 'educator', + 'educatory', + 'educe', + 'educt', + 'eduction', + 'eductive', + 'edulcorate', + 'eel', + 'eelgrass', + 'eellike', + 'eelpout', + 'eelworm', + 'eerie', + 'effable', + 'efface', + 'effect', + 'effective', + 'effector', + 'effects', + 'effectual', + 'effectually', + 'effectuate', + 'effeminacy', + 'effeminate', + 'effeminize', + 'effendi', + 'efferent', + 'effervesce', + 'effervescent', + 'effete', + 'efficacious', + 'efficacy', + 'efficiency', + 'efficient', + 'effigy', + 'effloresce', + 'efflorescence', + 'efflorescent', + 'effluence', + 'effluent', + 'effluvium', + 'efflux', + 'effort', + 'effortful', + 'effortless', + 'effrontery', + 'effulgence', + 'effulgent', + 'effuse', + 'effusion', + 'effusive', + 'eft', + 'egad', + 'egalitarian', + 'egest', + 'egesta', + 'egestion', + 'egg', + 'eggbeater', + 'eggcup', + 'egger', + 'egghead', + 'eggnog', + 'eggplant', + 'eggshell', + 'egis', + 'eglantine', + 'ego', + 'egocentric', + 'egocentrism', + 'egoism', + 'egoist', + 'egomania', + 'egotism', + 'egotist', + 'egregious', + 'egress', + 'egression', + 'egret', + 'eh', + 'eider', + 'eiderdown', + 'eidetic', + 'eidolon', + 'eigenfunction', + 'eigenvalue', + 'eight', + 'eighteen', + 'eighteenmo', + 'eighteenth', + 'eightfold', + 'eighth', + 'eightieth', + 'eighty', + 'eikon', + 'einkorn', + 'einsteinium', + 'eisegesis', + 'eisteddfod', + 'either', + 'ejaculate', + 'ejaculation', + 'ejaculatory', + 'eject', + 'ejecta', + 'ejection', + 'ejective', + 'ejectment', + 'ejector', + 'eke', + 'el', + 'elaborate', + 'elaboration', + 'elaeoptene', + 'elan', + 'eland', + 'elapid', + 'elapse', + 'elasmobranch', + 'elastance', + 'elastic', + 'elasticity', + 'elasticize', + 'elastin', + 'elastomer', + 'elate', + 'elated', + 'elater', + 'elaterid', + 'elaterin', + 'elaterite', + 'elaterium', + 'elation', + 'elative', + 'elbow', + 'elbowroom', + 'eld', + 'elder', + 'elderberry', + 'elderly', + 'eldest', + 'eldritch', + 'elecampane', + 'elect', + 'election', + 'electioneer', + 'elective', + 'elector', + 'electoral', + 'electorate', + 'electret', + 'electric', + 'electrical', + 'electrician', + 'electricity', + 'electrify', + 'electro', + 'electroacoustics', + 'electroanalysis', + 'electroballistics', + 'electrobiology', + 'electrocardiogram', + 'electrocardiograph', + 'electrocautery', + 'electrochemistry', + 'electrocorticogram', + 'electrocute', + 'electrode', + 'electrodeposit', + 'electrodialysis', + 'electrodynamic', + 'electrodynamics', + 'electrodynamometer', + 'electroencephalogram', + 'electroencephalograph', + 'electroform', + 'electrograph', + 'electrojet', + 'electrokinetic', + 'electrokinetics', + 'electrolier', + 'electroluminescence', + 'electrolyse', + 'electrolysis', + 'electrolyte', + 'electrolytic', + 'electrolyze', + 'electromagnet', + 'electromagnetic', + 'electromagnetism', + 'electromechanical', + 'electrometallurgy', + 'electrometer', + 'electromotive', + 'electromotor', + 'electromyography', + 'electron', + 'electronarcosis', + 'electronegative', + 'electronic', + 'electronics', + 'electrophilic', + 'electrophone', + 'electrophoresis', + 'electrophorus', + 'electrophotography', + 'electrophysiology', + 'electroplate', + 'electropositive', + 'electroscope', + 'electroshock', + 'electrostatic', + 'electrostatics', + 'electrostriction', + 'electrosurgery', + 'electrotechnics', + 'electrotechnology', + 'electrotherapeutics', + 'electrotherapy', + 'electrothermal', + 'electrothermics', + 'electrotonus', + 'electrotype', + 'electrum', + 'electuary', + 'eleemosynary', + 'elegance', + 'elegancy', + 'elegant', + 'elegiac', + 'elegist', + 'elegit', + 'elegize', + 'elegy', + 'element', + 'elemental', + 'elementary', + 'elemi', + 'elenchus', + 'eleoptene', + 'elephant', + 'elephantiasis', + 'elephantine', + 'elevate', + 'elevated', + 'elevation', + 'elevator', + 'eleven', + 'elevenses', + 'eleventh', + 'elevon', + 'elf', + 'elfin', + 'elfish', + 'elfland', + 'elflock', + 'elicit', + 'elide', + 'eligibility', + 'eligible', + 'eliminate', + 'elimination', + 'elision', + 'elite', + 'elitism', + 'elixir', + 'elk', + 'elkhound', + 'ell', + 'ellipse', + 'ellipsis', + 'ellipsoid', + 'ellipticity', + 'elm', + 'elocution', + 'eloign', + 'elongate', + 'elongation', + 'elope', + 'eloquence', + 'eloquent', + 'else', + 'elsewhere', + 'elucidate', + 'elude', + 'elusion', + 'elusive', + 'elute', + 'elutriate', + 'eluviation', + 'eluvium', + 'elver', + 'elves', + 'elvish', + 'elytron', + 'em', + 'emaciate', + 'emaciated', + 'emaciation', + 'emanate', + 'emanation', + 'emanative', + 'emancipate', + 'emancipated', + 'emancipation', + 'emancipator', + 'emarginate', + 'emasculate', + 'embalm', + 'embank', + 'embankment', + 'embargo', + 'embark', + 'embarkation', + 'embarkment', + 'embarrass', + 'embarrassment', + 'embassy', + 'embattle', + 'embattled', + 'embay', + 'embayment', + 'embed', + 'embellish', + 'embellishment', + 'ember', + 'embezzle', + 'embitter', + 'emblaze', + 'emblazon', + 'emblazonment', + 'emblazonry', + 'emblem', + 'emblematize', + 'emblements', + 'embodiment', + 'embody', + 'embolden', + 'embolectomy', + 'embolic', + 'embolism', + 'embolus', + 'emboly', + 'embonpoint', + 'embosom', + 'emboss', + 'embosser', + 'embouchure', + 'embow', + 'embowed', + 'embowel', + 'embower', + 'embrace', + 'embraceor', + 'embracery', + 'embranchment', + 'embrangle', + 'embrasure', + 'embrocate', + 'embrocation', + 'embroider', + 'embroideress', + 'embroidery', + 'embroil', + 'embrue', + 'embryectomy', + 'embryo', + 'embryogeny', + 'embryologist', + 'embryology', + 'embryonic', + 'embryotomy', + 'embus', + 'emcee', + 'emend', + 'emendate', + 'emendation', + 'emerald', + 'emerge', + 'emergence', + 'emergency', + 'emergent', + 'emeritus', + 'emersed', + 'emersion', + 'emery', + 'emesis', + 'emetic', + 'emetine', + 'emf', + 'emigrant', + 'emigrate', + 'emigration', + 'eminence', + 'eminent', + 'emir', + 'emirate', + 'emissary', + 'emission', + 'emissive', + 'emissivity', + 'emit', + 'emitter', + 'emmenagogue', + 'emmer', + 'emmet', + 'emmetropia', + 'emollient', + 'emolument', + 'emote', + 'emotion', + 'emotional', + 'emotionalism', + 'emotionality', + 'emotionalize', + 'emotive', + 'empale', + 'empanel', + 'empathic', + 'empathize', + 'empathy', + 'empennage', + 'emperor', + 'empery', + 'emphasis', + 'emphasize', + 'emphatic', + 'emphysema', + 'empire', + 'empiric', + 'empirical', + 'empiricism', + 'emplace', + 'emplacement', + 'emplane', + 'employ', + 'employee', + 'employer', + 'employment', + 'empoison', + 'emporium', + 'empoverish', + 'empower', + 'empress', + 'empressement', + 'emprise', + 'emptor', + 'empty', + 'empurple', + 'empyema', + 'empyreal', + 'empyrean', + 'emu', + 'emulate', + 'emulation', + 'emulous', + 'emulsifier', + 'emulsify', + 'emulsion', + 'emulsoid', + 'emunctory', + 'en', + 'enable', + 'enabling', + 'enact', + 'enactment', + 'enallage', + 'enamel', + 'enameling', + 'enamelware', + 'enamor', + 'enamour', + 'enantiomorph', + 'enarthrosis', + 'enate', + 'encaenia', + 'encage', + 'encamp', + 'encampment', + 'encapsulate', + 'encarnalize', + 'encase', + 'encasement', + 'encaustic', + 'enceinte', + 'encephalic', + 'encephalitis', + 'encephalogram', + 'encephalograph', + 'encephalography', + 'encephaloma', + 'encephalomyelitis', + 'encephalon', + 'enchain', + 'enchant', + 'enchanter', + 'enchanting', + 'enchantment', + 'enchantress', + 'enchase', + 'enchilada', + 'enchiridion', + 'enchondroma', + 'enchorial', + 'encincture', + 'encipher', + 'encircle', + 'enclasp', + 'enclave', + 'enclitic', + 'enclose', + 'enclosure', + 'encode', + 'encomiast', + 'encomiastic', + 'encomium', + 'encompass', + 'encore', + 'encounter', + 'encourage', + 'encouragement', + 'encrimson', + 'encrinite', + 'encroach', + 'encroachment', + 'encrust', + 'enculturation', + 'encumber', + 'encumbrance', + 'encumbrancer', + 'encyclical', + 'encyclopedia', + 'encyclopedic', + 'encyclopedist', + 'encyst', + 'end', + 'endamage', + 'endamoeba', + 'endanger', + 'endarch', + 'endbrain', + 'endear', + 'endearment', + 'endeavor', + 'endemic', + 'endermic', + 'endgame', + 'ending', + 'endive', + 'endless', + 'endlong', + 'endmost', + 'endoblast', + 'endocardial', + 'endocarditis', + 'endocardium', + 'endocarp', + 'endocentric', + 'endocranium', + 'endocrine', + 'endocrinology', + 'endocrinotherapy', + 'endoderm', + 'endodermis', + 'endodontics', + 'endodontist', + 'endoenzyme', + 'endoergic', + 'endogamy', + 'endogen', + 'endogenous', + 'endolymph', + 'endometriosis', + 'endometrium', + 'endomorph', + 'endomorphic', + 'endomorphism', + 'endoparasite', + 'endopeptidase', + 'endophyte', + 'endoplasm', + 'endorse', + 'endorsed', + 'endorsee', + 'endorsement', + 'endoscope', + 'endoskeleton', + 'endosmosis', + 'endosperm', + 'endospore', + 'endosteum', + 'endostosis', + 'endothecium', + 'endothelioma', + 'endothelium', + 'endothermic', + 'endotoxin', + 'endow', + 'endowment', + 'endpaper', + 'endplay', + 'endrin', + 'endue', + 'endurable', + 'endurance', + 'endurant', + 'endure', + 'enduring', + 'endways', + 'enema', + 'enemy', + 'energetic', + 'energetics', + 'energid', + 'energize', + 'energumen', + 'energy', + 'enervate', + 'enervated', + 'enface', + 'enfeeble', + 'enfeoff', + 'enfilade', + 'enfleurage', + 'enfold', + 'enforce', + 'enforcement', + 'enfranchise', + 'eng', + 'engage', + 'engaged', + 'engagement', + 'engaging', + 'engender', + 'engine', + 'engineer', + 'engineering', + 'engineman', + 'enginery', + 'engird', + 'englacial', + 'englut', + 'engobe', + 'engorge', + 'engraft', + 'engrail', + 'engrain', + 'engram', + 'engrave', + 'engraving', + 'engross', + 'engrossing', + 'engrossment', + 'engulf', + 'enhance', + 'enhanced', + 'enharmonic', + 'enigma', + 'enigmatic', + 'enisle', + 'enjambement', + 'enjambment', + 'enjoin', + 'enjoy', + 'enjoyable', + 'enjoyment', + 'enkindle', + 'enlace', + 'enlarge', + 'enlargement', + 'enlarger', + 'enlighten', + 'enlightenment', + 'enlist', + 'enlistee', + 'enlistment', + 'enliven', + 'enmesh', + 'enmity', + 'ennead', + 'enneagon', + 'enneahedron', + 'enneastyle', + 'ennoble', + 'ennui', + 'enol', + 'enormity', + 'enormous', + 'enosis', + 'enough', + 'enounce', + 'enow', + 'enphytotic', + 'enplane', + 'enquire', + 'enrage', + 'enrapture', + 'enravish', + 'enrich', + 'enrichment', + 'enrobe', + 'enrol', + 'enroll', + 'enrollee', + 'enrollment', + 'enroot', + 'ens', + 'ensample', + 'ensanguine', + 'ensconce', + 'enscroll', + 'ensemble', + 'ensepulcher', + 'ensheathe', + 'enshrine', + 'enshroud', + 'ensiform', + 'ensign', + 'ensilage', + 'ensile', + 'enslave', + 'ensnare', + 'ensoul', + 'ensphere', + 'enstatite', + 'ensue', + 'ensure', + 'enswathe', + 'entablature', + 'entablement', + 'entail', + 'entangle', + 'entanglement', + 'entasis', + 'entelechy', + 'entellus', + 'entente', + 'enter', + 'enterectomy', + 'enteric', + 'enteritis', + 'enterogastrone', + 'enteron', + 'enterostomy', + 'enterotomy', + 'enterovirus', + 'enterprise', + 'enterpriser', + 'enterprising', + 'entertain', + 'entertainer', + 'entertaining', + 'entertainment', + 'enthalpy', + 'enthetic', + 'enthral', + 'enthrall', + 'enthrone', + 'enthronement', + 'enthuse', + 'enthusiasm', + 'enthusiast', + 'enthusiastic', + 'enthymeme', + 'entice', + 'enticement', + 'entire', + 'entirely', + 'entirety', + 'entitle', + 'entity', + 'entoblast', + 'entoderm', + 'entoil', + 'entomb', + 'entomologize', + 'entomology', + 'entomophagous', + 'entomophilous', + 'entomostracan', + 'entophyte', + 'entopic', + 'entourage', + 'entozoic', + 'entozoon', + 'entrails', + 'entrain', + 'entrammel', + 'entrance', + 'entranceway', + 'entrant', + 'entrap', + 'entreat', + 'entreaty', + 'entrechat', + 'entree', + 'entremets', + 'entrench', + 'entrenchment', + 'entrepreneur', + 'entresol', + 'entropy', + 'entrust', + 'entry', + 'entryway', + 'entwine', + 'enucleate', + 'enumerate', + 'enumeration', + 'enunciate', + 'enunciation', + 'enure', + 'enuresis', + 'envelop', + 'envelope', + 'envelopment', + 'envenom', + 'enviable', + 'envious', + 'environ', + 'environment', + 'environmentalist', + 'environs', + 'envisage', + 'envision', + 'envoi', + 'envoy', + 'envy', + 'enwind', + 'enwomb', + 'enwrap', + 'enwreathe', + 'enzootic', + 'enzyme', + 'enzymology', + 'enzymolysis', + 'eohippus', + 'eolith', + 'eolithic', + 'eon', + 'eonian', + 'eonism', + 'eosin', + 'eosinophil', + 'epact', + 'epagoge', + 'epanaphora', + 'epanodos', + 'epanorthosis', + 'eparch', + 'eparchy', + 'epaulet', + 'epeirogeny', + 'epencephalon', + 'epenthesis', + 'epergne', + 'epexegesis', + 'ephah', + 'ephebe', + 'ephedrine', + 'ephemera', + 'ephemeral', + 'ephemerality', + 'ephemerid', + 'ephemeris', + 'ephemeron', + 'ephod', + 'ephor', + 'epiblast', + 'epiboly', + 'epic', + 'epicalyx', + 'epicanthus', + 'epicardium', + 'epicarp', + 'epicedium', + 'epicene', + 'epicenter', + 'epiclesis', + 'epicontinental', + 'epicotyl', + 'epicrisis', + 'epicritic', + 'epicure', + 'epicurean', + 'epicycle', + 'epicycloid', + 'epideictic', + 'epidemic', + 'epidemiology', + 'epidermis', + 'epidiascope', + 'epididymis', + 'epidote', + 'epifocal', + 'epigastrium', + 'epigeal', + 'epigene', + 'epigenesis', + 'epigenous', + 'epigeous', + 'epiglottis', + 'epigone', + 'epigram', + 'epigrammatist', + 'epigrammatize', + 'epigraph', + 'epigraphic', + 'epigraphy', + 'epigynous', + 'epilate', + 'epilepsy', + 'epileptic', + 'epileptoid', + 'epilimnion', + 'epilogue', + 'epimorphosis', + 'epinasty', + 'epinephrine', + 'epineurium', + 'epiphany', + 'epiphenomenalism', + 'epiphenomenon', + 'epiphora', + 'epiphragm', + 'epiphysis', + 'epiphyte', + 'epiphytotic', + 'epirogeny', + 'episcopacy', + 'episcopal', + 'episcopalian', + 'episcopalism', + 'episcopate', + 'episiotomy', + 'episode', + 'episodic', + 'epispastic', + 'epistasis', + 'epistaxis', + 'epistemic', + 'epistemology', + 'episternum', + 'epistle', + 'epistrophe', + 'epistyle', + 'epitaph', + 'epitasis', + 'epithalamium', + 'epithelioma', + 'epithelium', + 'epithet', + 'epitome', + 'epitomize', + 'epizoic', + 'epizoon', + 'epizootic', + 'epoch', + 'epochal', + 'epode', + 'eponym', + 'eponymous', + 'eponymy', + 'epos', + 'epoxy', + 'epsilon', + 'epsomite', + 'equable', + 'equal', + 'equalitarian', + 'equality', + 'equalize', + 'equalizer', + 'equally', + 'equanimity', + 'equanimous', + 'equate', + 'equation', + 'equator', + 'equatorial', + 'equerry', + 'equestrian', + 'equestrienne', + 'equiangular', + 'equidistance', + 'equidistant', + 'equilateral', + 'equilibrant', + 'equilibrate', + 'equilibrist', + 'equilibrium', + 'equimolecular', + 'equine', + 'equinoctial', + 'equinox', + 'equip', + 'equipage', + 'equipment', + 'equipoise', + 'equipollent', + 'equiponderance', + 'equiponderate', + 'equipotential', + 'equiprobable', + 'equisetum', + 'equitable', + 'equitant', + 'equitation', + 'equites', + 'equities', + 'equity', + 'equivalence', + 'equivalency', + 'equivalent', + 'equivocal', + 'equivocate', + 'equivocation', + 'equivoque', + 'er', + 'era', + 'eradiate', + 'eradicate', + 'erase', + 'erased', + 'eraser', + 'erasion', + 'erasure', + 'erbium', + 'ere', + 'erect', + 'erectile', + 'erection', + 'erective', + 'erector', + 'erelong', + 'eremite', + 'erenow', + 'erepsin', + 'erethism', + 'erewhile', + 'erg', + 'ergo', + 'ergocalciferol', + 'ergograph', + 'ergonomics', + 'ergosterol', + 'ergot', + 'ergotism', + 'ericaceous', + 'erigeron', + 'erinaceous', + 'eringo', + 'eristic', + 'erk', + 'erlking', + 'ermine', + 'ermines', + 'erminois', + 'erne', + 'erode', + 'erogenous', + 'erose', + 'erosion', + 'erosive', + 'erotic', + 'erotica', + 'eroticism', + 'erotogenic', + 'erotomania', + 'err', + 'errancy', + 'errand', + 'errant', + 'errantry', + 'errata', + 'erratic', + 'erratum', + 'errhine', + 'erring', + 'erroneous', + 'error', + 'ersatz', + 'erst', + 'erstwhile', + 'erubescence', + 'erubescent', + 'eruct', + 'eructate', + 'erudite', + 'erudition', + 'erumpent', + 'erupt', + 'eruption', + 'eruptive', + 'eryngo', + 'erysipelas', + 'erysipeloid', + 'erythema', + 'erythrism', + 'erythrite', + 'erythritol', + 'erythroblast', + 'erythroblastosis', + 'erythrocyte', + 'erythrocytometer', + 'erythromycin', + 'erythropoiesis', + 'escadrille', + 'escalade', + 'escalate', + 'escalator', + 'escallop', + 'escapade', + 'escape', + 'escapee', + 'escapement', + 'escapism', + 'escargot', + 'escarole', + 'escarp', + 'escarpment', + 'eschalot', + 'eschar', + 'escharotic', + 'eschatology', + 'escheat', + 'eschew', + 'escolar', + 'escort', + 'escribe', + 'escritoire', + 'escrow', + 'escuage', + 'escudo', + 'esculent', + 'escutcheon', + 'esemplastic', + 'eserine', + 'esker', + 'esophagitis', + 'esophagus', + 'esoteric', + 'esoterica', + 'esotropia', + 'espadrille', + 'espagnole', + 'espalier', + 'esparto', + 'especial', + 'especially', + 'esperance', + 'espial', + 'espionage', + 'esplanade', + 'espousal', + 'espouse', + 'espresso', + 'esprit', + 'espy', + 'esquire', + 'essay', + 'essayist', + 'essayistic', + 'esse', + 'essence', + 'essential', + 'essentialism', + 'essentiality', + 'essive', + 'essonite', + 'establish', + 'establishment', + 'establishmentarian', + 'estafette', + 'estaminet', + 'estancia', + 'estate', + 'esteem', + 'ester', + 'esterase', + 'esterify', + 'esthesia', + 'esthete', + 'estimable', + 'estimate', + 'estimation', + 'estimative', + 'estipulate', + 'estival', + 'estivate', + 'estivation', + 'estop', + 'estoppel', + 'estovers', + 'estrade', + 'estradiol', + 'estragon', + 'estrange', + 'estranged', + 'estray', + 'estreat', + 'estrin', + 'estriol', + 'estrogen', + 'estrone', + 'estrous', + 'estrus', + 'estuarine', + 'estuary', + 'esurient', + 'eta', + 'etalon', + 'etamine', + 'etch', + 'etching', + 'eternal', + 'eternalize', + 'eterne', + 'eternity', + 'eternize', + 'etesian', + 'ethane', + 'ethanol', + 'ethene', + 'ether', + 'ethereal', + 'etherealize', + 'etherify', + 'etherize', + 'ethic', + 'ethical', + 'ethicize', + 'ethics', + 'ethmoid', + 'ethnarch', + 'ethnic', + 'ethnocentrism', + 'ethnogeny', + 'ethnography', + 'ethnology', + 'ethnomusicology', + 'ethology', + 'ethos', + 'ethyl', + 'ethylate', + 'ethylene', + 'ethyne', + 'etiolate', + 'etiology', + 'etiquette', + 'etna', + 'etude', + 'etui', + 'etymologize', + 'etymology', + 'etymon', + 'eucaine', + 'eucalyptol', + 'eucalyptus', + 'eucharis', + 'euchologion', + 'euchology', + 'euchre', + 'euchromatin', + 'euchromosome', + 'eudemon', + 'eudemonia', + 'eudemonics', + 'eudemonism', + 'eudiometer', + 'eugenics', + 'eugenol', + 'euglena', + 'euhemerism', + 'euhemerize', + 'eulachon', + 'eulogia', + 'eulogist', + 'eulogistic', + 'eulogium', + 'eulogize', + 'eulogy', + 'eunuch', + 'eunuchize', + 'eunuchoidism', + 'euonymus', + 'eupatorium', + 'eupatrid', + 'eupepsia', + 'euphemism', + 'euphemize', + 'euphonic', + 'euphonious', + 'euphonium', + 'euphonize', + 'euphony', + 'euphorbia', + 'euphorbiaceous', + 'euphoria', + 'euphrasy', + 'euphroe', + 'euphuism', + 'euplastic', + 'eureka', + 'eurhythmic', + 'eurhythmics', + 'eurhythmy', + 'euripus', + 'europium', + 'eurypterid', + 'eurythermal', + 'eurythmic', + 'eurythmics', + 'eusporangiate', + 'eutectic', + 'eutectoid', + 'euthanasia', + 'euthenics', + 'eutherian', + 'eutrophic', + 'euxenite', + 'evacuant', + 'evacuate', + 'evacuation', + 'evacuee', + 'evade', + 'evaginate', + 'evaluate', + 'evanesce', + 'evanescent', + 'evangel', + 'evangelical', + 'evangelicalism', + 'evangelism', + 'evangelist', + 'evangelistic', + 'evangelize', + 'evanish', + 'evaporate', + 'evaporation', + 'evaporimeter', + 'evaporite', + 'evapotranspiration', + 'evasion', + 'evasive', + 'eve', + 'evection', + 'even', + 'evenfall', + 'evenhanded', + 'evening', + 'evenings', + 'evensong', + 'event', + 'eventful', + 'eventide', + 'eventual', + 'eventuality', + 'eventually', + 'eventuate', + 'ever', + 'everglade', + 'evergreen', + 'everlasting', + 'evermore', + 'eversion', + 'evert', + 'evertor', + 'every', + 'everybody', + 'everyday', + 'everyone', + 'everyplace', + 'everything', + 'everyway', + 'everywhere', + 'evict', + 'evictee', + 'evidence', + 'evident', + 'evidential', + 'evidentiary', + 'evidently', + 'evil', + 'evildoer', + 'evince', + 'evincive', + 'eviscerate', + 'evitable', + 'evite', + 'evocation', + 'evocative', + 'evocator', + 'evoke', + 'evolute', + 'evolution', + 'evolutionary', + 'evolutionist', + 'evolve', + 'evonymus', + 'evulsion', + 'evzone', + 'ewe', + 'ewer', + 'ex', + 'exacerbate', + 'exact', + 'exacting', + 'exaction', + 'exactitude', + 'exactly', + 'exaggerate', + 'exaggerated', + 'exaggeration', + 'exaggerative', + 'exalt', + 'exaltation', + 'exalted', + 'exam', + 'examen', + 'examinant', + 'examination', + 'examine', + 'examinee', + 'example', + 'exanimate', + 'exanthema', + 'exarate', + 'exarch', + 'exarchate', + 'exasperate', + 'exasperation', + 'excaudate', + 'excavate', + 'excavation', + 'excavator', + 'exceed', + 'exceeding', + 'exceedingly', + 'excel', + 'excellence', + 'excellency', + 'excellent', + 'excelsior', + 'except', + 'excepting', + 'exception', + 'exceptionable', + 'exceptional', + 'exceptive', + 'excerpt', + 'excerpta', + 'excess', + 'excessive', + 'exchange', + 'exchangeable', + 'exchequer', + 'excide', + 'excipient', + 'excisable', + 'excise', + 'exciseman', + 'excision', + 'excitability', + 'excitable', + 'excitant', + 'excitation', + 'excite', + 'excited', + 'excitement', + 'exciter', + 'exciting', + 'excitor', + 'exclaim', + 'exclamation', + 'exclamatory', + 'exclave', + 'exclosure', + 'exclude', + 'exclusion', + 'exclusive', + 'excogitate', + 'excommunicate', + 'excommunication', + 'excommunicative', + 'excommunicatory', + 'excoriate', + 'excoriation', + 'excrement', + 'excrescence', + 'excrescency', + 'excrescent', + 'excreta', + 'excrete', + 'excretion', + 'excretory', + 'excruciate', + 'excruciating', + 'excruciation', + 'exculpate', + 'excurrent', + 'excursion', + 'excursionist', + 'excursive', + 'excursus', + 'excurvate', + 'excurvature', + 'excurved', + 'excusatory', + 'excuse', + 'exeat', + 'execrable', + 'execrate', + 'execration', + 'execrative', + 'execratory', + 'executant', + 'execute', + 'execution', + 'executioner', + 'executive', + 'executor', + 'executory', + 'executrix', + 'exedra', + 'exegesis', + 'exegete', + 'exegetic', + 'exegetics', + 'exemplar', + 'exemplary', + 'exemplification', + 'exemplificative', + 'exemplify', + 'exemplum', + 'exempt', + 'exemption', + 'exenterate', + 'exequatur', + 'exequies', + 'exercise', + 'exerciser', + 'exercitation', + 'exergue', + 'exert', + 'exertion', + 'exeunt', + 'exfoliate', + 'exfoliation', + 'exhalant', + 'exhalation', + 'exhale', + 'exhaust', + 'exhaustion', + 'exhaustive', + 'exhaustless', + 'exhibit', + 'exhibition', + 'exhibitioner', + 'exhibitionism', + 'exhibitionist', + 'exhibitive', + 'exhibitor', + 'exhilarant', + 'exhilarate', + 'exhilaration', + 'exhilarative', + 'exhort', + 'exhortation', + 'exhortative', + 'exhume', + 'exigency', + 'exigent', + 'exigible', + 'exiguous', + 'exile', + 'eximious', + 'exine', + 'exist', + 'existence', + 'existent', + 'existential', + 'existentialism', + 'exit', + 'exobiology', + 'exocarp', + 'exocentric', + 'exocrine', + 'exodontics', + 'exodontist', + 'exodus', + 'exoenzyme', + 'exoergic', + 'exogamy', + 'exogenous', + 'exon', + 'exonerate', + 'exophthalmos', + 'exorable', + 'exorbitance', + 'exorbitant', + 'exorcise', + 'exorcism', + 'exorcist', + 'exordium', + 'exoskeleton', + 'exosmosis', + 'exosphere', + 'exospore', + 'exostosis', + 'exoteric', + 'exothermic', + 'exotic', + 'exotoxin', + 'expand', + 'expanded', + 'expander', + 'expanse', + 'expansible', + 'expansile', + 'expansion', + 'expansionism', + 'expansive', + 'expatiate', + 'expatriate', + 'expect', + 'expectancy', + 'expectant', + 'expectation', + 'expecting', + 'expectorant', + 'expectorate', + 'expectoration', + 'expediency', + 'expedient', + 'expediential', + 'expedite', + 'expedition', + 'expeditionary', + 'expeditious', + 'expel', + 'expellant', + 'expellee', + 'expeller', + 'expend', + 'expendable', + 'expenditure', + 'expense', + 'expensive', + 'experience', + 'experienced', + 'experiential', + 'experientialism', + 'experiment', + 'experimental', + 'experimentalism', + 'experimentalize', + 'experimentation', + 'expert', + 'expertise', + 'expertism', + 'expertize', + 'expiable', + 'expiate', + 'expiation', + 'expiatory', + 'expiration', + 'expiratory', + 'expire', + 'expiry', + 'explain', + 'explanation', + 'explanatory', + 'explant', + 'expletive', + 'explicable', + 'explicate', + 'explication', + 'explicative', + 'explicit', + 'explode', + 'exploit', + 'exploitation', + 'exploiter', + 'exploration', + 'exploratory', + 'explore', + 'explorer', + 'explosion', + 'explosive', + 'exponent', + 'exponential', + 'exponible', + 'export', + 'exportation', + 'expose', + 'exposed', + 'exposition', + 'expositor', + 'expository', + 'expostulate', + 'expostulation', + 'expostulatory', + 'exposure', + 'expound', + 'express', + 'expressage', + 'expression', + 'expressionism', + 'expressive', + 'expressivity', + 'expressly', + 'expressman', + 'expressway', + 'expropriate', + 'expugnable', + 'expulsion', + 'expulsive', + 'expunction', + 'expunge', + 'expurgate', + 'expurgatory', + 'exquisite', + 'exsanguinate', + 'exsanguine', + 'exscind', + 'exsect', + 'exsert', + 'exsiccate', + 'exstipulate', + 'extant', + 'extemporaneous', + 'extemporary', + 'extempore', + 'extemporize', + 'extend', + 'extended', + 'extender', + 'extensible', + 'extensile', + 'extension', + 'extensity', + 'extensive', + 'extensometer', + 'extensor', + 'extent', + 'extenuate', + 'extenuation', + 'extenuatory', + 'exterior', + 'exteriorize', + 'exterminate', + 'exterminatory', + 'extern', + 'external', + 'externalism', + 'externality', + 'externalization', + 'externalize', + 'exteroceptor', + 'exterritorial', + 'extinct', + 'extinction', + 'extinctive', + 'extine', + 'extinguish', + 'extinguisher', + 'extirpate', + 'extol', + 'extort', + 'extortion', + 'extortionary', + 'extortionate', + 'extortioner', + 'extra', + 'extrabold', + 'extracanonical', + 'extracellular', + 'extract', + 'extraction', + 'extractive', + 'extractor', + 'extracurricular', + 'extraditable', + 'extradite', + 'extradition', + 'extrados', + 'extragalactic', + 'extrajudicial', + 'extramarital', + 'extramundane', + 'extramural', + 'extraneous', + 'extranuclear', + 'extraordinary', + 'extrapolate', + 'extrasensory', + 'extrasystole', + 'extraterrestrial', + 'extraterritorial', + 'extraterritoriality', + 'extrauterine', + 'extravagance', + 'extravagancy', + 'extravagant', + 'extravaganza', + 'extravagate', + 'extravasate', + 'extravasation', + 'extravascular', + 'extravehicular', + 'extraversion', + 'extravert', + 'extreme', + 'extremely', + 'extremism', + 'extremist', + 'extremity', + 'extricate', + 'extrinsic', + 'extrorse', + 'extroversion', + 'extrovert', + 'extrude', + 'extrusion', + 'extrusive', + 'exuberance', + 'exuberant', + 'exuberate', + 'exudate', + 'exudation', + 'exude', + 'exult', + 'exultant', + 'exultation', + 'exurb', + 'exurbanite', + 'exurbia', + 'exuviae', + 'exuviate', + 'eyas', + 'eye', + 'eyeball', + 'eyebolt', + 'eyebright', + 'eyebrow', + 'eyecup', + 'eyed', + 'eyeful', + 'eyeglass', + 'eyeglasses', + 'eyehole', + 'eyelash', + 'eyeless', + 'eyelet', + 'eyeleteer', + 'eyelid', + 'eyepiece', + 'eyeshade', + 'eyeshot', + 'eyesight', + 'eyesore', + 'eyespot', + 'eyestalk', + 'eyestrain', + 'eyetooth', + 'eyewash', + 'eyewitness', + 'eyot', + 'eyra', + 'eyre', + 'eyrie', + 'eyrir', + 'f', + 'fa', + 'fab', + 'fabaceous', + 'fable', + 'fabled', + 'fabliau', + 'fabric', + 'fabricant', + 'fabricate', + 'fabrication', + 'fabulist', + 'fabulous', + 'face', + 'faceless', + 'faceplate', + 'facer', + 'facet', + 'facetiae', + 'facetious', + 'facia', + 'facial', + 'facies', + 'facile', + 'facilitate', + 'facilitation', + 'facility', + 'facing', + 'facsimile', + 'fact', + 'faction', + 'factional', + 'factious', + 'factitious', + 'factitive', + 'factor', + 'factorage', + 'factorial', + 'factoring', + 'factorize', + 'factory', + 'factotum', + 'factual', + 'facture', + 'facula', + 'facultative', + 'faculty', + 'fad', + 'faddish', + 'faddist', + 'fade', + 'fadeless', + 'fader', + 'fadge', + 'fading', + 'fado', + 'faeces', + 'faena', + 'faerie', + 'faery', + 'fag', + 'fagaceous', + 'faggot', + 'faggoting', + 'fagot', + 'fagoting', + 'fahlband', + 'faience', + 'fail', + 'failing', + 'faille', + 'failure', + 'fain', + 'faint', + 'faintheart', + 'fainthearted', + 'faints', + 'fair', + 'fairground', + 'fairing', + 'fairish', + 'fairlead', + 'fairly', + 'fairway', + 'fairy', + 'fairyland', + 'faith', + 'faithful', + 'faithless', + 'faitour', + 'fake', + 'faker', + 'fakery', + 'fakir', + 'falbala', + 'falcate', + 'falchion', + 'falciform', + 'falcon', + 'falconer', + 'falconet', + 'falconiform', + 'falconry', + 'faldstool', + 'fall', + 'fallacious', + 'fallacy', + 'fallal', + 'fallen', + 'faller', + 'fallfish', + 'fallible', + 'fallout', + 'fallow', + 'false', + 'falsehood', + 'falsetto', + 'falsework', + 'falsify', + 'falsity', + 'faltboat', + 'falter', + 'fame', + 'famed', + 'familial', + 'familiar', + 'familiarity', + 'familiarize', + 'family', + 'famine', + 'famish', + 'famished', + 'famous', + 'famulus', + 'fan', + 'fanatic', + 'fanatical', + 'fanaticism', + 'fanaticize', + 'fancied', + 'fancier', + 'fanciful', + 'fancy', + 'fancywork', + 'fandango', + 'fane', + 'fanfare', + 'fanfaron', + 'fanfaronade', + 'fang', + 'fango', + 'fanion', + 'fanjet', + 'fanlight', + 'fanny', + 'fanon', + 'fantail', + 'fantasia', + 'fantasist', + 'fantasize', + 'fantasm', + 'fantast', + 'fantastic', + 'fantastically', + 'fantasy', + 'fantoccini', + 'fantom', + 'faqir', + 'far', + 'farad', + 'faradic', + 'faradism', + 'faradize', + 'faradmeter', + 'farandole', + 'faraway', + 'farce', + 'farceur', + 'farceuse', + 'farci', + 'farcical', + 'farcy', + 'fard', + 'fardel', + 'fare', + 'farewell', + 'farfetched', + 'farina', + 'farinaceous', + 'farinose', + 'farl', + 'farm', + 'farmer', + 'farmhand', + 'farmhouse', + 'farming', + 'farmland', + 'farmstead', + 'farmyard', + 'farnesol', + 'faro', + 'farouche', + 'farrago', + 'farrier', + 'farriery', + 'farrow', + 'farseeing', + 'farsighted', + 'fart', + 'farther', + 'farthermost', + 'farthest', + 'farthing', + 'farthingale', + 'fasces', + 'fascia', + 'fasciate', + 'fasciation', + 'fascicle', + 'fascicule', + 'fasciculus', + 'fascinate', + 'fascinating', + 'fascination', + 'fascinator', + 'fascine', + 'fascism', + 'fascist', + 'fash', + 'fashion', + 'fashionable', + 'fast', + 'fastback', + 'fasten', + 'fastening', + 'fastidious', + 'fastigiate', + 'fastigium', + 'fastness', + 'fat', + 'fatal', + 'fatalism', + 'fatality', + 'fatally', + 'fatback', + 'fate', + 'fated', + 'fateful', + 'fathead', + 'father', + 'fatherhood', + 'fatherland', + 'fatherless', + 'fatherly', + 'fathom', + 'fathomless', + 'fatidic', + 'fatigue', + 'fatigued', + 'fatling', + 'fatness', + 'fatso', + 'fatten', + 'fattish', + 'fatty', + 'fatuitous', + 'fatuity', + 'fatuous', + 'faubourg', + 'faucal', + 'fauces', + 'faucet', + 'faugh', + 'fault', + 'faultfinder', + 'faultfinding', + 'faultless', + 'faulty', + 'faun', + 'fauna', + 'fauteuil', + 'faveolate', + 'favonian', + 'favor', + 'favorable', + 'favored', + 'favorite', + 'favoritism', + 'favour', + 'favourable', + 'favourite', + 'favouritism', + 'favus', + 'fawn', + 'fay', + 'fayalite', + 'faze', + 'feal', + 'fealty', + 'fear', + 'fearful', + 'fearfully', + 'fearless', + 'fearnought', + 'fearsome', + 'feasible', + 'feast', + 'feat', + 'feather', + 'featherbedding', + 'featherbrain', + 'feathercut', + 'feathered', + 'featheredge', + 'featherhead', + 'feathering', + 'feathers', + 'featherstitch', + 'featherweight', + 'feathery', + 'featly', + 'feature', + 'featured', + 'featureless', + 'feaze', + 'febricity', + 'febrifacient', + 'febrific', + 'febrifugal', + 'febrifuge', + 'febrile', + 'fecal', + 'feces', + 'fecit', + 'feck', + 'feckless', + 'fecula', + 'feculent', + 'fecund', + 'fecundate', + 'fecundity', + 'fed', + 'federal', + 'federalese', + 'federalism', + 'federalist', + 'federalize', + 'federate', + 'federation', + 'federative', + 'fedora', + 'fee', + 'feeble', + 'feebleminded', + 'feed', + 'feedback', + 'feeder', + 'feeding', + 'feel', + 'feeler', + 'feeling', + 'feet', + 'feeze', + 'feign', + 'feigned', + 'feint', + 'feints', + 'feisty', + 'felafel', + 'feldspar', + 'felicific', + 'felicitate', + 'felicitation', + 'felicitous', + 'felicity', + 'felid', + 'feline', + 'fell', + 'fellah', + 'fellatio', + 'feller', + 'fellmonger', + 'felloe', + 'fellow', + 'fellowman', + 'fellowship', + 'felly', + 'felon', + 'felonious', + 'felonry', + 'felony', + 'felsite', + 'felspar', + 'felt', + 'felting', + 'felucca', + 'female', + 'feme', + 'feminacy', + 'femineity', + 'feminine', + 'femininity', + 'feminism', + 'feminize', + 'femme', + 'femoral', + 'femur', + 'fen', + 'fence', + 'fencer', + 'fencible', + 'fencing', + 'fend', + 'fender', + 'fenestella', + 'fenestra', + 'fenestrated', + 'fenestration', + 'fenland', + 'fennec', + 'fennel', + 'fennelflower', + 'fenny', + 'fenugreek', + 'feoff', + 'feoffee', + 'feral', + 'ferbam', + 'fere', + 'feretory', + 'feria', + 'ferial', + 'ferine', + 'ferity', + 'fermata', + 'ferment', + 'fermentation', + 'fermentative', + 'fermi', + 'fermion', + 'fermium', + 'fern', + 'fernery', + 'ferocious', + 'ferocity', + 'ferrate', + 'ferreous', + 'ferret', + 'ferriage', + 'ferric', + 'ferricyanide', + 'ferriferous', + 'ferrite', + 'ferritin', + 'ferrocene', + 'ferrochromium', + 'ferroconcrete', + 'ferrocyanide', + 'ferroelectric', + 'ferromagnesian', + 'ferromagnetic', + 'ferromagnetism', + 'ferromanganese', + 'ferrosilicon', + 'ferrotype', + 'ferrous', + 'ferruginous', + 'ferrule', + 'ferry', + 'ferryboat', + 'ferryman', + 'fertile', + 'fertility', + 'fertilization', + 'fertilize', + 'fertilizer', + 'ferula', + 'ferule', + 'fervency', + 'fervent', + 'fervid', + 'fervor', + 'fescue', + 'fess', + 'festal', + 'fester', + 'festinate', + 'festination', + 'festival', + 'festive', + 'festivity', + 'festoon', + 'festoonery', + 'fetal', + 'fetation', + 'fetch', + 'fetching', + 'fete', + 'fetial', + 'fetich', + 'feticide', + 'fetid', + 'fetiparous', + 'fetish', + 'fetishism', + 'fetishist', + 'fetlock', + 'fetor', + 'fetter', + 'fetterlock', + 'fettle', + 'fettling', + 'fetus', + 'feu', + 'feuar', + 'feud', + 'feudal', + 'feudalism', + 'feudality', + 'feudalize', + 'feudatory', + 'feudist', + 'feuilleton', + 'fever', + 'feverfew', + 'feverish', + 'feverous', + 'feverroot', + 'feverwort', + 'few', + 'fewer', + 'fewness', + 'fey', + 'fez', + 'fiacre', + 'fiance', + 'fiasco', + 'fiat', + 'fib', + 'fiber', + 'fiberboard', + 'fibered', + 'fiberglass', + 'fibre', + 'fibriform', + 'fibril', + 'fibrilla', + 'fibrillation', + 'fibrilliform', + 'fibrin', + 'fibrinogen', + 'fibrinolysin', + 'fibrinolysis', + 'fibrinous', + 'fibroblast', + 'fibroid', + 'fibroin', + 'fibroma', + 'fibrosis', + 'fibrous', + 'fibrovascular', + 'fibster', + 'fibula', + 'fiche', + 'fichu', + 'fickle', + 'fico', + 'fictile', + 'fiction', + 'fictional', + 'fictionalize', + 'fictionist', + 'fictitious', + 'fictive', + 'fid', + 'fiddle', + 'fiddlehead', + 'fiddler', + 'fiddlestick', + 'fiddlewood', + 'fiddling', + 'fideicommissary', + 'fideicommissum', + 'fideism', + 'fidelity', + 'fidge', + 'fidget', + 'fidgety', + 'fiducial', + 'fiduciary', + 'fie', + 'fief', + 'field', + 'fielder', + 'fieldfare', + 'fieldpiece', + 'fieldsman', + 'fieldstone', + 'fieldwork', + 'fiend', + 'fiendish', + 'fierce', + 'fiery', + 'fiesta', + 'fife', + 'fifteen', + 'fifteenth', + 'fifth', + 'fiftieth', + 'fifty', + 'fig', + 'fight', + 'fighter', + 'figment', + 'figural', + 'figurant', + 'figurate', + 'figuration', + 'figurative', + 'figure', + 'figured', + 'figurehead', + 'figurine', + 'figwort', + 'filagree', + 'filament', + 'filamentary', + 'filamentous', + 'filar', + 'filaria', + 'filariasis', + 'filature', + 'filbert', + 'filch', + 'file', + 'filefish', + 'filet', + 'filial', + 'filiate', + 'filiation', + 'filibeg', + 'filibuster', + 'filicide', + 'filiform', + 'filigree', + 'filigreed', + 'filing', + 'filings', + 'fill', + 'fillagree', + 'filler', + 'fillet', + 'filling', + 'fillip', + 'fillister', + 'filly', + 'film', + 'filmdom', + 'filmy', + 'filoplume', + 'filose', + 'fils', + 'filter', + 'filterable', + 'filth', + 'filthy', + 'filtrate', + 'filtration', + 'filum', + 'fimble', + 'fimbria', + 'fimbriate', + 'fimbriation', + 'fin', + 'finable', + 'finagle', + 'final', + 'finale', + 'finalism', + 'finalist', + 'finality', + 'finalize', + 'finally', + 'finance', + 'financial', + 'financier', + 'finback', + 'finch', + 'find', + 'finder', + 'finding', + 'fine', + 'fineable', + 'finely', + 'fineness', + 'finer', + 'finery', + 'finespun', + 'finesse', + 'finfoot', + 'finger', + 'fingerboard', + 'fingerbreadth', + 'fingered', + 'fingering', + 'fingerling', + 'fingernail', + 'fingerprint', + 'fingerstall', + 'fingertip', + 'finial', + 'finical', + 'finicking', + 'finicky', + 'fining', + 'finis', + 'finish', + 'finished', + 'finite', + 'finitude', + 'fink', + 'finned', + 'finny', + 'fino', + 'finochio', + 'fiord', + 'fiorin', + 'fioritura', + 'fipple', + 'fir', + 'fire', + 'firearm', + 'fireback', + 'fireball', + 'firebird', + 'fireboard', + 'fireboat', + 'firebox', + 'firebrand', + 'firebrat', + 'firebreak', + 'firebrick', + 'firebug', + 'firecracker', + 'firecrest', + 'firedamp', + 'firedog', + 'firedrake', + 'firefly', + 'fireguard', + 'firehouse', + 'firelock', + 'fireman', + 'fireplace', + 'fireplug', + 'firepower', + 'fireproof', + 'fireproofing', + 'firer', + 'fireside', + 'firestone', + 'firetrap', + 'firewarden', + 'firewater', + 'fireweed', + 'firewood', + 'firework', + 'fireworks', + 'fireworm', + 'firing', + 'firkin', + 'firm', + 'firmament', + 'firn', + 'firry', + 'first', + 'firsthand', + 'firstling', + 'firstly', + 'firth', + 'fisc', + 'fiscal', + 'fish', + 'fishbolt', + 'fishbowl', + 'fisher', + 'fisherman', + 'fishery', + 'fishgig', + 'fishhook', + 'fishing', + 'fishmonger', + 'fishnet', + 'fishplate', + 'fishtail', + 'fishwife', + 'fishworm', + 'fishy', + 'fissile', + 'fission', + 'fissionable', + 'fissiparous', + 'fissirostral', + 'fissure', + 'fist', + 'fistic', + 'fisticuffs', + 'fistula', + 'fistulous', + 'fit', + 'fitch', + 'fitful', + 'fitly', + 'fitment', + 'fitted', + 'fitter', + 'fitting', + 'five', + 'fivefold', + 'fivepenny', + 'fiver', + 'fives', + 'fix', + 'fixate', + 'fixation', + 'fixative', + 'fixed', + 'fixer', + 'fixing', + 'fixity', + 'fixture', + 'fizgig', + 'fizz', + 'fizzle', + 'fizzy', + 'fjeld', + 'fjord', + 'flabbergast', + 'flabby', + 'flabellate', + 'flabellum', + 'flaccid', + 'flack', + 'flacon', + 'flag', + 'flagella', + 'flagellant', + 'flagellate', + 'flagelliform', + 'flagellum', + 'flageolet', + 'flagging', + 'flaggy', + 'flagitious', + 'flagman', + 'flagon', + 'flagpole', + 'flagrant', + 'flagship', + 'flagstaff', + 'flagstone', + 'flail', + 'flair', + 'flak', + 'flake', + 'flaky', + 'flam', + 'flambeau', + 'flamboyant', + 'flame', + 'flamen', + 'flamenco', + 'flameproof', + 'flamethrower', + 'flaming', + 'flamingo', + 'flammable', + 'flan', + 'flanch', + 'flange', + 'flank', + 'flanker', + 'flannel', + 'flannelette', + 'flap', + 'flapdoodle', + 'flapjack', + 'flapper', + 'flare', + 'flaring', + 'flash', + 'flashback', + 'flashboard', + 'flashbulb', + 'flashcube', + 'flasher', + 'flashgun', + 'flashing', + 'flashlight', + 'flashover', + 'flashy', + 'flask', + 'flasket', + 'flat', + 'flatboat', + 'flatcar', + 'flatfish', + 'flatfoot', + 'flatfooted', + 'flathead', + 'flatiron', + 'flatling', + 'flats', + 'flatten', + 'flatter', + 'flattery', + 'flattie', + 'flatting', + 'flattish', + 'flattop', + 'flatulent', + 'flatus', + 'flatware', + 'flatways', + 'flatwise', + 'flatworm', + 'flaunch', + 'flaunt', + 'flaunty', + 'flautist', + 'flavescent', + 'flavin', + 'flavine', + 'flavone', + 'flavoprotein', + 'flavopurpurin', + 'flavor', + 'flavorful', + 'flavoring', + 'flavorous', + 'flavorsome', + 'flavory', + 'flavour', + 'flavourful', + 'flavouring', + 'flaw', + 'flawed', + 'flawy', + 'flax', + 'flaxen', + 'flaxseed', + 'flay', + 'flea', + 'fleabag', + 'fleabane', + 'fleabite', + 'fleam', + 'fleawort', + 'fleck', + 'flection', + 'fled', + 'fledge', + 'fledgling', + 'fledgy', + 'flee', + 'fleece', + 'fleecy', + 'fleer', + 'fleet', + 'fleeting', + 'flense', + 'flesh', + 'flesher', + 'fleshings', + 'fleshly', + 'fleshpots', + 'fleshy', + 'fletch', + 'fletcher', + 'fleurette', + 'fleuron', + 'flew', + 'flews', + 'flex', + 'flexed', + 'flexible', + 'flexile', + 'flexion', + 'flexor', + 'flexuosity', + 'flexuous', + 'flexure', + 'fley', + 'flibbertigibbet', + 'flick', + 'flicker', + 'flickertail', + 'flied', + 'flier', + 'flight', + 'flightless', + 'flighty', + 'flimflam', + 'flimsy', + 'flinch', + 'flinders', + 'fling', + 'flinger', + 'flint', + 'flintlock', + 'flinty', + 'flip', + 'flippant', + 'flipper', + 'flirt', + 'flirtation', + 'flirtatious', + 'flit', + 'flitch', + 'flite', + 'flitter', + 'flittermouse', + 'flitting', + 'flivver', + 'float', + 'floatable', + 'floatage', + 'floatation', + 'floater', + 'floating', + 'floatplane', + 'floats', + 'floatstone', + 'floaty', + 'floc', + 'floccose', + 'flocculant', + 'flocculate', + 'floccule', + 'flocculent', + 'flocculus', + 'floccus', + 'flock', + 'flocky', + 'floe', + 'flog', + 'flogging', + 'flong', + 'flood', + 'flooded', + 'floodgate', + 'floodlight', + 'floor', + 'floorage', + 'floorboard', + 'floorer', + 'flooring', + 'floorman', + 'floorwalker', + 'floozy', + 'flop', + 'flophouse', + 'floppy', + 'flora', + 'floral', + 'floreated', + 'florescence', + 'floret', + 'floriated', + 'floribunda', + 'floriculture', + 'florid', + 'florilegium', + 'florin', + 'florist', + 'floristic', + 'floruit', + 'flory', + 'floss', + 'flossy', + 'flotage', + 'flotation', + 'flotilla', + 'flotsam', + 'flounce', + 'flouncing', + 'flounder', + 'flour', + 'flourish', + 'flourishing', + 'floury', + 'flout', + 'flow', + 'flowage', + 'flower', + 'flowerage', + 'flowered', + 'flowerer', + 'floweret', + 'flowering', + 'flowerless', + 'flowerlike', + 'flowerpot', + 'flowery', + 'flowing', + 'flown', + 'flu', + 'flub', + 'fluctuant', + 'fluctuate', + 'fluctuation', + 'flue', + 'fluency', + 'fluent', + 'fluff', + 'fluffy', + 'flugelhorn', + 'fluid', + 'fluidextract', + 'fluidics', + 'fluidize', + 'fluke', + 'fluky', + 'flume', + 'flummery', + 'flummox', + 'flump', + 'flung', + 'flunk', + 'flunkey', + 'flunky', + 'fluor', + 'fluorene', + 'fluoresce', + 'fluorescein', + 'fluorescence', + 'fluorescent', + 'fluoric', + 'fluoridate', + 'fluoridation', + 'fluoride', + 'fluorinate', + 'fluorine', + 'fluorite', + 'fluorocarbon', + 'fluorometer', + 'fluoroscope', + 'fluoroscopy', + 'fluorosis', + 'fluorspar', + 'flurried', + 'flurry', + 'flush', + 'fluster', + 'flute', + 'fluted', + 'fluter', + 'fluting', + 'flutist', + 'flutter', + 'flutterboard', + 'fluttery', + 'fluvial', + 'fluviatile', + 'fluviomarine', + 'flux', + 'fluxion', + 'fluxmeter', + 'fly', + 'flyaway', + 'flyback', + 'flyblow', + 'flyblown', + 'flyboat', + 'flycatcher', + 'flyer', + 'flying', + 'flyleaf', + 'flyman', + 'flyover', + 'flypaper', + 'flyspeck', + 'flyte', + 'flytrap', + 'flyweight', + 'flywheel', + 'foal', + 'foam', + 'foamflower', + 'foamy', + 'fob', + 'focal', + 'focalize', + 'focus', + 'fodder', + 'foe', + 'foehn', + 'foeman', + 'foetation', + 'foeticide', + 'foetid', + 'foetor', + 'foetus', + 'fog', + 'fogbound', + 'fogbow', + 'fogdog', + 'fogged', + 'foggy', + 'foghorn', + 'fogy', + 'foible', + 'foil', + 'foiled', + 'foilsman', + 'foin', + 'foison', + 'foist', + 'folacin', + 'fold', + 'foldaway', + 'foldboat', + 'folder', + 'folderol', + 'folia', + 'foliaceous', + 'foliage', + 'foliar', + 'foliate', + 'foliated', + 'foliation', + 'folie', + 'folio', + 'foliolate', + 'foliole', + 'foliose', + 'folium', + 'folk', + 'folklore', + 'folkmoot', + 'folksy', + 'folkway', + 'folkways', + 'follicle', + 'folliculin', + 'follow', + 'follower', + 'following', + 'folly', + 'foment', + 'fomentation', + 'fond', + 'fondant', + 'fondle', + 'fondly', + 'fondness', + 'fondue', + 'font', + 'fontanel', + 'food', + 'foodstuff', + 'foofaraw', + 'fool', + 'foolery', + 'foolhardy', + 'foolish', + 'foolproof', + 'foolscap', + 'foot', + 'footage', + 'football', + 'footboard', + 'footboy', + 'footbridge', + 'footcloth', + 'footed', + 'footer', + 'footfall', + 'footgear', + 'foothill', + 'foothold', + 'footie', + 'footing', + 'footle', + 'footless', + 'footlight', + 'footlights', + 'footling', + 'footlocker', + 'footloose', + 'footman', + 'footmark', + 'footnote', + 'footpace', + 'footpad', + 'footpath', + 'footplate', + 'footprint', + 'footrace', + 'footrest', + 'footrope', + 'footsie', + 'footslog', + 'footsore', + 'footstalk', + 'footstall', + 'footstep', + 'footstone', + 'footstool', + 'footwall', + 'footway', + 'footwear', + 'footwork', + 'footworn', + 'footy', + 'foozle', + 'fop', + 'foppery', + 'foppish', + 'for', + 'forage', + 'foramen', + 'foraminifer', + 'foray', + 'forayer', + 'forb', + 'forbade', + 'forbear', + 'forbearance', + 'forbid', + 'forbiddance', + 'forbidden', + 'forbidding', + 'forbore', + 'forborne', + 'forby', + 'force', + 'forced', + 'forceful', + 'forcemeat', + 'forceps', + 'forcer', + 'forcible', + 'ford', + 'fordo', + 'fordone', + 'fore', + 'forearm', + 'forebear', + 'forebode', + 'foreboding', + 'forebrain', + 'forecast', + 'forecastle', + 'foreclose', + 'foreclosure', + 'foreconscious', + 'forecourse', + 'forecourt', + 'foredate', + 'foredeck', + 'foredo', + 'foredoom', + 'forefather', + 'forefend', + 'forefinger', + 'forefoot', + 'forefront', + 'foregather', + 'foreglimpse', + 'forego', + 'foregoing', + 'foregone', + 'foreground', + 'foregut', + 'forehand', + 'forehanded', + 'forehead', + 'foreign', + 'foreigner', + 'foreignism', + 'forejudge', + 'foreknow', + 'foreknowledge', + 'forelady', + 'foreland', + 'foreleg', + 'forelimb', + 'forelock', + 'foreman', + 'foremast', + 'foremost', + 'forename', + 'forenamed', + 'forenoon', + 'forensic', + 'forensics', + 'foreordain', + 'foreordination', + 'forepart', + 'forepaw', + 'forepeak', + 'foreplay', + 'forepleasure', + 'forequarter', + 'forereach', + 'forerun', + 'forerunner', + 'foresaid', + 'foresail', + 'foresee', + 'foreshadow', + 'foreshank', + 'foresheet', + 'foreshore', + 'foreshorten', + 'foreshow', + 'foreside', + 'foresight', + 'foreskin', + 'forespeak', + 'forespent', + 'forest', + 'forestage', + 'forestall', + 'forestation', + 'forestay', + 'forestaysail', + 'forester', + 'forestry', + 'foretaste', + 'foretell', + 'forethought', + 'forethoughtful', + 'foretime', + 'foretoken', + 'foretooth', + 'foretop', + 'forever', + 'forevermore', + 'forewarn', + 'forewent', + 'forewing', + 'forewoman', + 'foreword', + 'foreworn', + 'foreyard', + 'forfeit', + 'forfeiture', + 'forfend', + 'forficate', + 'forgat', + 'forgather', + 'forgave', + 'forge', + 'forgery', + 'forget', + 'forgetful', + 'forging', + 'forgive', + 'forgiven', + 'forgiveness', + 'forgiving', + 'forgo', + 'forgot', + 'forgotten', + 'forint', + 'forjudge', + 'fork', + 'forked', + 'forklift', + 'forlorn', + 'form', + 'formal', + 'formaldehyde', + 'formalin', + 'formalism', + 'formality', + 'formalize', + 'formally', + 'formant', + 'format', + 'formate', + 'formation', + 'formative', + 'forme', + 'former', + 'formerly', + 'formfitting', + 'formic', + 'formicary', + 'formication', + 'formidable', + 'formless', + 'formula', + 'formulaic', + 'formularize', + 'formulary', + 'formulate', + 'formulism', + 'formwork', + 'formyl', + 'fornicate', + 'fornication', + 'fornix', + 'forsake', + 'forsaken', + 'forsook', + 'forsooth', + 'forspent', + 'forsterite', + 'forswear', + 'forsworn', + 'forsythia', + 'fort', + 'fortalice', + 'forte', + 'forth', + 'forthcoming', + 'forthright', + 'forthwith', + 'fortieth', + 'fortification', + 'fortify', + 'fortis', + 'fortissimo', + 'fortitude', + 'fortnight', + 'fortnightly', + 'fortress', + 'fortuitism', + 'fortuitous', + 'fortuity', + 'fortunate', + 'fortune', + 'fortuneteller', + 'fortunetelling', + 'forty', + 'fortyish', + 'forum', + 'forward', + 'forwarder', + 'forwarding', + 'forwardness', + 'forwards', + 'forwent', + 'forwhy', + 'forworn', + 'forzando', + 'fossa', + 'fosse', + 'fossette', + 'fossick', + 'fossil', + 'fossiliferous', + 'fossilize', + 'fossorial', + 'foster', + 'fosterage', + 'fosterling', + 'fou', + 'foudroyant', + 'fought', + 'foul', + 'foulard', + 'foulmouthed', + 'foulness', + 'foumart', + 'found', + 'foundation', + 'founder', + 'foundling', + 'foundry', + 'fount', + 'fountain', + 'fountainhead', + 'four', + 'fourchette', + 'fourflusher', + 'fourfold', + 'fourgon', + 'fourpence', + 'fourpenny', + 'fourscore', + 'foursome', + 'foursquare', + 'fourteen', + 'fourteenth', + 'fourth', + 'fourthly', + 'fovea', + 'foveola', + 'fowl', + 'fowling', + 'fox', + 'foxed', + 'foxglove', + 'foxhole', + 'foxhound', + 'foxing', + 'foxtail', + 'foxy', + 'foyer', + 'fp', + 'fracas', + 'fraction', + 'fractional', + 'fractionate', + 'fractionize', + 'fractious', + 'fractocumulus', + 'fractostratus', + 'fracture', + 'frae', + 'fraenum', + 'frag', + 'fragile', + 'fragment', + 'fragmental', + 'fragmentary', + 'fragmentation', + 'fragrance', + 'fragrant', + 'frail', + 'frailty', + 'fraise', + 'frambesia', + 'framboise', + 'frame', + 'framework', + 'framing', + 'franc', + 'franchise', + 'francium', + 'francolin', + 'frangible', + 'frangipane', + 'frangipani', + 'frank', + 'frankalmoign', + 'frankforter', + 'frankfurter', + 'frankincense', + 'franklin', + 'franklinite', + 'frankly', + 'frankness', + 'frankpledge', + 'frantic', + 'frap', + 'frater', + 'fraternal', + 'fraternity', + 'fraternize', + 'fratricide', + 'fraud', + 'fraudulent', + 'fraught', + 'fraxinella', + 'fray', + 'frazil', + 'frazzle', + 'frazzled', + 'freak', + 'freakish', + 'freaky', + 'freckle', + 'freckly', + 'free', + 'freeboard', + 'freeboot', + 'freebooter', + 'freeborn', + 'freedman', + 'freedom', + 'freedwoman', + 'freehand', + 'freehold', + 'freeholder', + 'freelance', + 'freeload', + 'freeloader', + 'freely', + 'freeman', + 'freemartin', + 'freemasonry', + 'freeness', + 'freer', + 'freesia', + 'freestanding', + 'freestone', + 'freestyle', + 'freethinker', + 'freeway', + 'freewheel', + 'freewheeling', + 'freewill', + 'freeze', + 'freezer', + 'freezing', + 'freight', + 'freightage', + 'freighter', + 'fremd', + 'fremitus', + 'frenetic', + 'frenulum', + 'frenum', + 'frenzied', + 'frenzy', + 'frequency', + 'frequent', + 'frequentation', + 'frequentative', + 'frequently', + 'fresco', + 'fresh', + 'freshen', + 'fresher', + 'freshet', + 'freshman', + 'freshwater', + 'fresnel', + 'fret', + 'fretful', + 'fretted', + 'fretwork', + 'friable', + 'friar', + 'friarbird', + 'friary', + 'fribble', + 'fricandeau', + 'fricassee', + 'frication', + 'fricative', + 'friction', + 'frictional', + 'fridge', + 'fried', + 'friedcake', + 'friend', + 'friendly', + 'friendship', + 'frier', + 'frieze', + 'frig', + 'frigate', + 'frigging', + 'fright', + 'frighten', + 'frightened', + 'frightful', + 'frightfully', + 'frigid', + 'frigidarium', + 'frigorific', + 'frijol', + 'frill', + 'frilling', + 'fringe', + 'frippery', + 'frisette', + 'friseur', + 'frisk', + 'frisket', + 'frisky', + 'frit', + 'frith', + 'fritillary', + 'fritter', + 'frivol', + 'frivolity', + 'frivolous', + 'frizette', + 'frizz', + 'frizzle', + 'frizzly', + 'frizzy', + 'fro', + 'frock', + 'froe', + 'frog', + 'frogfish', + 'froggy', + 'froghopper', + 'frogman', + 'frogmouth', + 'frolic', + 'frolicsome', + 'from', + 'fromenty', + 'frond', + 'frondescence', + 'frons', + 'front', + 'frontage', + 'frontal', + 'frontality', + 'frontier', + 'frontiersman', + 'frontispiece', + 'frontlet', + 'frontogenesis', + 'frontolysis', + 'fronton', + 'frontward', + 'frontwards', + 'frore', + 'frost', + 'frostbite', + 'frostbitten', + 'frosted', + 'frosting', + 'frostwork', + 'frosty', + 'froth', + 'frothy', + 'frottage', + 'froufrou', + 'frow', + 'froward', + 'frown', + 'frowst', + 'frowsty', + 'frowsy', + 'frowzy', + 'froze', + 'frozen', + 'fructiferous', + 'fructification', + 'fructificative', + 'fructify', + 'fructose', + 'fructuous', + 'frug', + 'frugal', + 'frugivorous', + 'fruit', + 'fruitage', + 'fruitarian', + 'fruitcake', + 'fruiter', + 'fruiterer', + 'fruitful', + 'fruition', + 'fruitless', + 'fruity', + 'frumentaceous', + 'frumenty', + 'frump', + 'frumpish', + 'frumpy', + 'frustrate', + 'frustrated', + 'frustration', + 'frustule', + 'frustum', + 'frutescent', + 'fry', + 'fryer', + 'fubsy', + 'fuchsia', + 'fuchsin', + 'fucoid', + 'fucus', + 'fuddle', + 'fudge', + 'fuel', + 'fug', + 'fugacious', + 'fugacity', + 'fugal', + 'fugato', + 'fugitive', + 'fugleman', + 'fugue', + 'fulcrum', + 'fulfil', + 'fulfill', + 'fulfillment', + 'fulgent', + 'fulgor', + 'fulgurant', + 'fulgurate', + 'fulgurating', + 'fulguration', + 'fulgurite', + 'fulgurous', + 'fuliginous', + 'full', + 'fullback', + 'fuller', + 'fully', + 'fulmar', + 'fulminant', + 'fulminate', + 'fulmination', + 'fulminous', + 'fulsome', + 'fulvous', + 'fumarole', + 'fumatorium', + 'fumble', + 'fume', + 'fumed', + 'fumigant', + 'fumigate', + 'fumigator', + 'fumitory', + 'fumy', + 'fun', + 'funambulist', + 'function', + 'functional', + 'functionalism', + 'functionary', + 'fund', + 'fundament', + 'fundamental', + 'fundamentalism', + 'funds', + 'fundus', + 'funeral', + 'funerary', + 'funereal', + 'funest', + 'fungal', + 'fungi', + 'fungible', + 'fungicide', + 'fungiform', + 'fungistat', + 'fungoid', + 'fungosity', + 'fungous', + 'fungus', + 'funicle', + 'funicular', + 'funiculate', + 'funiculus', + 'funk', + 'funky', + 'funnel', + 'funnelform', + 'funny', + 'funnyman', + 'fur', + 'furan', + 'furbelow', + 'furbish', + 'furcate', + 'furcula', + 'furculum', + 'furfur', + 'furfuraceous', + 'furfural', + 'furfuran', + 'furious', + 'furl', + 'furlana', + 'furlong', + 'furlough', + 'furmenty', + 'furnace', + 'furnish', + 'furnishing', + 'furnishings', + 'furniture', + 'furor', + 'furore', + 'furred', + 'furrier', + 'furriery', + 'furring', + 'furrow', + 'furry', + 'further', + 'furtherance', + 'furthermore', + 'furthermost', + 'furthest', + 'furtive', + 'furuncle', + 'furunculosis', + 'fury', + 'furze', + 'fusain', + 'fuscous', + 'fuse', + 'fusee', + 'fuselage', + 'fusibility', + 'fusible', + 'fusiform', + 'fusil', + 'fusilier', + 'fusillade', + 'fusion', + 'fusionism', + 'fuss', + 'fussbudget', + 'fusspot', + 'fussy', + 'fustanella', + 'fustian', + 'fustic', + 'fustigate', + 'fusty', + 'futhark', + 'futile', + 'futilitarian', + 'futility', + 'futtock', + 'future', + 'futures', + 'futurism', + 'futuristic', + 'futurity', + 'fuze', + 'fuzee', + 'fuzz', + 'fuzzy', + 'fyke', + 'fylfot', + 'fyrd', + 'g', + 'gab', + 'gabardine', + 'gabble', + 'gabbro', + 'gabby', + 'gabelle', + 'gaberdine', + 'gaberlunzie', + 'gabfest', + 'gabion', + 'gabionade', + 'gable', + 'gablet', + 'gaby', + 'gad', + 'gadabout', + 'gadfly', + 'gadget', + 'gadgeteer', + 'gadgetry', + 'gadid', + 'gadoid', + 'gadolinite', + 'gadolinium', + 'gadroon', + 'gadwall', + 'gaff', + 'gaffe', + 'gaffer', + 'gag', + 'gaga', + 'gage', + 'gagger', + 'gaggle', + 'gagman', + 'gahnite', + 'gaiety', + 'gaillardia', + 'gaily', + 'gain', + 'gainer', + 'gainful', + 'gainless', + 'gainly', + 'gains', + 'gainsay', + 'gait', + 'gaiter', + 'gal', + 'gala', + 'galactagogue', + 'galactic', + 'galactometer', + 'galactopoietic', + 'galactose', + 'galah', + 'galangal', + 'galantine', + 'galatea', + 'galaxy', + 'galbanum', + 'gale', + 'galea', + 'galeiform', + 'galena', + 'galenical', + 'galilee', + 'galimatias', + 'galingale', + 'galiot', + 'galipot', + 'gall', + 'gallant', + 'gallantry', + 'gallbladder', + 'galleass', + 'galleon', + 'gallery', + 'galley', + 'gallfly', + 'galliard', + 'gallic', + 'galligaskins', + 'gallimaufry', + 'gallinacean', + 'gallinaceous', + 'galling', + 'gallinule', + 'galliot', + 'gallipot', + 'gallium', + 'gallivant', + 'galliwasp', + 'gallnut', + 'galloglass', + 'gallon', + 'gallonage', + 'galloon', + 'galloot', + 'gallop', + 'gallopade', + 'galloping', + 'gallous', + 'gallows', + 'gallstone', + 'galluses', + 'galoot', + 'galop', + 'galore', + 'galosh', + 'galoshes', + 'galumph', + 'galvanic', + 'galvanism', + 'galvanize', + 'galvanometer', + 'galvanoscope', + 'galvanotropism', + 'galyak', + 'gam', + 'gamb', + 'gamba', + 'gambado', + 'gambeson', + 'gambier', + 'gambit', + 'gamble', + 'gamboge', + 'gambol', + 'gambrel', + 'game', + 'gamecock', + 'gamekeeper', + 'gamelan', + 'gamely', + 'gameness', + 'gamesmanship', + 'gamesome', + 'gamester', + 'gametangium', + 'gamete', + 'gametocyte', + 'gametogenesis', + 'gametophore', + 'gametophyte', + 'gamic', + 'gamin', + 'gamine', + 'gaming', + 'gamma', + 'gammadion', + 'gammer', + 'gammon', + 'gammy', + 'gamogenesis', + 'gamone', + 'gamopetalous', + 'gamophyllous', + 'gamosepalous', + 'gamp', + 'gamut', + 'gamy', + 'gan', + 'gander', + 'ganef', + 'gang', + 'gangboard', + 'ganger', + 'gangland', + 'gangling', + 'ganglion', + 'gangplank', + 'gangrel', + 'gangrene', + 'gangster', + 'gangue', + 'gangway', + 'ganister', + 'ganja', + 'gannet', + 'ganof', + 'ganoid', + 'gantlet', + 'gantline', + 'gantry', + 'gaol', + 'gap', + 'gape', + 'gapes', + 'gapeworm', + 'gar', + 'garage', + 'garb', + 'garbage', + 'garbanzo', + 'garble', + 'garboard', + 'garboil', + 'garcon', + 'gardant', + 'garden', + 'gardener', + 'gardenia', + 'gardening', + 'garderobe', + 'garfish', + 'garganey', + 'gargantuan', + 'garget', + 'gargle', + 'gargoyle', + 'garibaldi', + 'garish', + 'garland', + 'garlic', + 'garlicky', + 'garment', + 'garner', + 'garnet', + 'garnierite', + 'garnish', + 'garnishee', + 'garnishment', + 'garniture', + 'garotte', + 'garpike', + 'garret', + 'garrison', + 'garrote', + 'garrotte', + 'garrulity', + 'garrulous', + 'garter', + 'garth', + 'garvey', + 'gas', + 'gasbag', + 'gasconade', + 'gaselier', + 'gaseous', + 'gash', + 'gasholder', + 'gasiform', + 'gasify', + 'gasket', + 'gaskin', + 'gaslight', + 'gaslit', + 'gasman', + 'gasolier', + 'gasoline', + 'gasometer', + 'gasometry', + 'gasp', + 'gasper', + 'gasser', + 'gassing', + 'gassy', + 'gasteropod', + 'gastight', + 'gastralgia', + 'gastrectomy', + 'gastric', + 'gastrin', + 'gastritis', + 'gastrocnemius', + 'gastroenteritis', + 'gastroenterology', + 'gastroenterostomy', + 'gastrointestinal', + 'gastrolith', + 'gastrology', + 'gastronome', + 'gastronomy', + 'gastropod', + 'gastroscope', + 'gastrostomy', + 'gastrotomy', + 'gastrotrich', + 'gastrovascular', + 'gastrula', + 'gastrulation', + 'gasworks', + 'gat', + 'gate', + 'gatefold', + 'gatehouse', + 'gatekeeper', + 'gatepost', + 'gateway', + 'gather', + 'gathering', + 'gauche', + 'gaucherie', + 'gaucho', + 'gaud', + 'gaudery', + 'gaudy', + 'gauffer', + 'gauge', + 'gauger', + 'gaultheria', + 'gaunt', + 'gauntlet', + 'gauntry', + 'gaur', + 'gauss', + 'gaussmeter', + 'gauze', + 'gauzy', + 'gavage', + 'gave', + 'gavel', + 'gavelkind', + 'gavial', + 'gavotte', + 'gawk', + 'gawky', + 'gay', + 'gaze', + 'gazebo', + 'gazehound', + 'gazelle', + 'gazette', + 'gazetteer', + 'gazpacho', + 'gean', + 'geanticlinal', + 'geanticline', + 'gear', + 'gearbox', + 'gearing', + 'gearshift', + 'gearwheel', + 'gecko', + 'gee', + 'geek', + 'geese', + 'geest', + 'geezer', + 'gegenschein', + 'gehlenite', + 'geisha', + 'gel', + 'gelatin', + 'gelatinate', + 'gelatinize', + 'gelatinoid', + 'gelatinous', + 'gelation', + 'geld', + 'gelding', + 'gelid', + 'gelignite', + 'gelsemium', + 'gelt', + 'gem', + 'gemeinschaft', + 'geminate', + 'gemination', + 'gemma', + 'gemmate', + 'gemmation', + 'gemmiparous', + 'gemmulation', + 'gemmule', + 'gemology', + 'gemot', + 'gemsbok', + 'gemstone', + 'gen', + 'genappe', + 'gendarme', + 'gendarmerie', + 'gender', + 'gene', + 'genealogy', + 'genera', + 'generable', + 'general', + 'generalissimo', + 'generalist', + 'generality', + 'generalization', + 'generalize', + 'generally', + 'generalship', + 'generate', + 'generation', + 'generative', + 'generator', + 'generatrix', + 'generic', + 'generosity', + 'generous', + 'genesis', + 'genet', + 'genethlialogy', + 'genetic', + 'geneticist', + 'genetics', + 'geneva', + 'genial', + 'geniality', + 'genic', + 'geniculate', + 'genie', + 'genii', + 'genip', + 'genipap', + 'genista', + 'genital', + 'genitalia', + 'genitals', + 'genitive', + 'genitor', + 'genitourinary', + 'genius', + 'genoa', + 'genocide', + 'genome', + 'genotype', + 'genre', + 'genro', + 'gens', + 'gent', + 'genteel', + 'genteelism', + 'gentian', + 'gentianaceous', + 'gentianella', + 'gentile', + 'gentilesse', + 'gentilism', + 'gentility', + 'gentle', + 'gentlefolk', + 'gentleman', + 'gentlemanly', + 'gentleness', + 'gentlewoman', + 'gentry', + 'genu', + 'genuflect', + 'genuflection', + 'genuine', + 'genus', + 'geocentric', + 'geochemistry', + 'geochronology', + 'geode', + 'geodesic', + 'geodesy', + 'geodetic', + 'geodynamics', + 'geognosy', + 'geographer', + 'geographical', + 'geography', + 'geoid', + 'geologize', + 'geology', + 'geomancer', + 'geomancy', + 'geometer', + 'geometric', + 'geometrician', + 'geometrid', + 'geometrize', + 'geometry', + 'geomorphic', + 'geomorphology', + 'geophagy', + 'geophilous', + 'geophysics', + 'geophyte', + 'geopolitics', + 'geoponic', + 'geoponics', + 'georama', + 'georgic', + 'geosphere', + 'geostatic', + 'geostatics', + 'geostrophic', + 'geosynclinal', + 'geosyncline', + 'geotaxis', + 'geotectonic', + 'geothermal', + 'geotropism', + 'gerah', + 'geraniaceous', + 'geranial', + 'geranium', + 'geratology', + 'gerbil', + 'gerent', + 'gerenuk', + 'gerfalcon', + 'geriatric', + 'geriatrician', + 'geriatrics', + 'germ', + 'german', + 'germander', + 'germane', + 'germanic', + 'germanium', + 'germanous', + 'germen', + 'germicide', + 'germinal', + 'germinant', + 'germinate', + 'germinative', + 'gerontocracy', + 'gerontology', + 'gerrymander', + 'gerund', + 'gerundive', + 'gesellschaft', + 'gesso', + 'gest', + 'gestalt', + 'gestate', + 'gestation', + 'gesticulate', + 'gesticulation', + 'gesticulative', + 'gesticulatory', + 'gesture', + 'gesundheit', + 'get', + 'getaway', + 'getter', + 'getup', + 'geum', + 'gewgaw', + 'gey', + 'geyser', + 'geyserite', + 'gharry', + 'ghastly', + 'ghat', + 'ghazi', + 'ghee', + 'gherkin', + 'ghetto', + 'ghost', + 'ghostly', + 'ghostwrite', + 'ghoul', + 'ghyll', + 'giant', + 'giantess', + 'giantism', + 'giaour', + 'gib', + 'gibber', + 'gibberish', + 'gibbet', + 'gibbon', + 'gibbosity', + 'gibbous', + 'gibbsite', + 'gibe', + 'giblet', + 'giblets', + 'gid', + 'giddy', + 'gie', + 'gift', + 'gifted', + 'gig', + 'gigahertz', + 'gigantean', + 'gigantic', + 'gigantism', + 'giggle', + 'gigolo', + 'gigot', + 'gigue', + 'gilbert', + 'gild', + 'gilded', + 'gilder', + 'gilding', + 'gilgai', + 'gill', + 'gillie', + 'gills', + 'gillyflower', + 'gilt', + 'gilthead', + 'gimbals', + 'gimcrack', + 'gimcrackery', + 'gimel', + 'gimlet', + 'gimmal', + 'gimmick', + 'gimp', + 'gin', + 'ginger', + 'gingerbread', + 'gingerly', + 'gingersnap', + 'gingery', + 'gingham', + 'gingili', + 'gingivitis', + 'ginglymus', + 'gink', + 'ginkgo', + 'ginseng', + 'gip', + 'gipon', + 'giraffe', + 'girandole', + 'girasol', + 'gird', + 'girder', + 'girdle', + 'girdler', + 'girl', + 'girlfriend', + 'girlhood', + 'girlie', + 'girlish', + 'giro', + 'girosol', + 'girt', + 'girth', + 'gisarme', + 'gismo', + 'gist', + 'git', + 'gittern', + 'give', + 'giveaway', + 'given', + 'gizmo', + 'gizzard', + 'glabella', + 'glabrate', + 'glabrescent', + 'glabrous', + 'glace', + 'glacial', + 'glacialist', + 'glaciate', + 'glacier', + 'glaciology', + 'glacis', + 'glad', + 'gladden', + 'glade', + 'gladiate', + 'gladiator', + 'gladiatorial', + 'gladiolus', + 'gladsome', + 'glaikit', + 'glair', + 'glairy', + 'glaive', + 'glamorize', + 'glamorous', + 'glamour', + 'glance', + 'gland', + 'glanders', + 'glandular', + 'glandule', + 'glandulous', + 'glans', + 'glare', + 'glaring', + 'glary', + 'glass', + 'glassblowing', + 'glasses', + 'glassful', + 'glasshouse', + 'glassine', + 'glassman', + 'glassware', + 'glasswork', + 'glassworker', + 'glassworks', + 'glasswort', + 'glassy', + 'glaucescent', + 'glaucoma', + 'glauconite', + 'glaucous', + 'glaze', + 'glazed', + 'glazer', + 'glazier', + 'glazing', + 'gleam', + 'glean', + 'gleaning', + 'gleanings', + 'glebe', + 'glede', + 'glee', + 'gleeful', + 'gleeman', + 'gleesome', + 'gleet', + 'glen', + 'glengarry', + 'glenoid', + 'gley', + 'glia', + 'gliadin', + 'glib', + 'glide', + 'glider', + 'glim', + 'glimmer', + 'glimmering', + 'glimpse', + 'glint', + 'glioma', + 'glissade', + 'glissando', + 'glisten', + 'glister', + 'glitter', + 'glittery', + 'gloam', + 'gloaming', + 'gloat', + 'glob', + 'global', + 'globate', + 'globe', + 'globefish', + 'globeflower', + 'globetrotter', + 'globigerina', + 'globin', + 'globoid', + 'globose', + 'globular', + 'globule', + 'globuliferous', + 'globulin', + 'glochidiate', + 'glochidium', + 'glockenspiel', + 'glomerate', + 'glomeration', + 'glomerule', + 'glomerulonephritis', + 'glomerulus', + 'gloom', + 'glooming', + 'gloomy', + 'glop', + 'glorification', + 'glorify', + 'gloriole', + 'glorious', + 'glory', + 'gloss', + 'glossa', + 'glossal', + 'glossary', + 'glossator', + 'glossectomy', + 'glossematics', + 'glosseme', + 'glossitis', + 'glossographer', + 'glossography', + 'glossolalia', + 'glossology', + 'glossotomy', + 'glossy', + 'glottal', + 'glottalized', + 'glottic', + 'glottis', + 'glottochronology', + 'glottology', + 'glove', + 'glover', + 'glow', + 'glower', + 'glowing', + 'glowworm', + 'gloxinia', + 'gloze', + 'glucinum', + 'gluconeogenesis', + 'glucoprotein', + 'glucose', + 'glucoside', + 'glucosuria', + 'glue', + 'gluey', + 'glum', + 'glume', + 'glut', + 'glutamate', + 'glutamine', + 'glutathione', + 'gluteal', + 'glutelin', + 'gluten', + 'glutenous', + 'gluteus', + 'glutinous', + 'glutton', + 'gluttonize', + 'gluttonous', + 'gluttony', + 'glyceric', + 'glyceride', + 'glycerin', + 'glycerinate', + 'glycerite', + 'glycerol', + 'glyceryl', + 'glycine', + 'glycogen', + 'glycogenesis', + 'glycol', + 'glycolysis', + 'glyconeogenesis', + 'glycoprotein', + 'glycoside', + 'glycosuria', + 'glyoxaline', + 'glyph', + 'glyphography', + 'glyptic', + 'glyptics', + 'glyptodont', + 'glyptograph', + 'glyptography', + 'gnarl', + 'gnarled', + 'gnarly', + 'gnash', + 'gnat', + 'gnatcatcher', + 'gnathic', + 'gnathion', + 'gnathonic', + 'gnaw', + 'gnawing', + 'gneiss', + 'gnome', + 'gnomic', + 'gnomon', + 'gnosis', + 'gnostic', + 'gnotobiotics', + 'gnu', + 'go', + 'goa', + 'goad', + 'goal', + 'goalie', + 'goalkeeper', + 'goaltender', + 'goat', + 'goatee', + 'goatfish', + 'goatherd', + 'goatish', + 'goatsbeard', + 'goatskin', + 'goatsucker', + 'gob', + 'gobang', + 'gobbet', + 'gobble', + 'gobbledegook', + 'gobbledygook', + 'gobbler', + 'gobioid', + 'goblet', + 'goblin', + 'gobo', + 'goby', + 'god', + 'godchild', + 'goddamn', + 'goddamned', + 'goddaughter', + 'goddess', + 'godfather', + 'godforsaken', + 'godhead', + 'godhood', + 'godless', + 'godlike', + 'godly', + 'godmother', + 'godown', + 'godparent', + 'godroon', + 'godsend', + 'godship', + 'godson', + 'godwit', + 'goer', + 'goethite', + 'goffer', + 'goggle', + 'goggler', + 'goggles', + 'goglet', + 'going', + 'goiter', + 'gold', + 'goldarn', + 'goldarned', + 'goldbrick', + 'goldcrest', + 'golden', + 'goldeneye', + 'goldenrod', + 'goldenseal', + 'goldeye', + 'goldfinch', + 'goldfish', + 'goldilocks', + 'goldsmith', + 'goldstone', + 'goldthread', + 'golem', + 'golf', + 'golfer', + 'goliard', + 'golly', + 'gombroon', + 'gomphosis', + 'gomuti', + 'gonad', + 'gonadotropin', + 'gondola', + 'gondolier', + 'gone', + 'goneness', + 'goner', + 'gonfalon', + 'gonfalonier', + 'gonfanon', + 'gong', + 'gonidium', + 'goniometer', + 'gonion', + 'gonna', + 'gonococcus', + 'gonocyte', + 'gonophore', + 'gonorrhea', + 'goo', + 'goober', + 'good', + 'goodbye', + 'goodish', + 'goodly', + 'goodman', + 'goodness', + 'goods', + 'goodwife', + 'goodwill', + 'goody', + 'gooey', + 'goof', + 'goofball', + 'goofy', + 'googly', + 'googol', + 'googolplex', + 'gook', + 'goon', + 'goop', + 'goosander', + 'goose', + 'gooseberry', + 'goosefish', + 'gooseflesh', + 'goosefoot', + 'goosegog', + 'gooseherd', + 'gooseneck', + 'goosy', + 'gopak', + 'gopher', + 'gopherwood', + 'goral', + 'gorblimey', + 'gorcock', + 'gore', + 'gorge', + 'gorged', + 'gorgeous', + 'gorgerin', + 'gorget', + 'gorgoneion', + 'gorilla', + 'goring', + 'gormand', + 'gormandize', + 'gormless', + 'gorse', + 'gory', + 'gosh', + 'goshawk', + 'gosling', + 'gospel', + 'gospodin', + 'gosport', + 'gossamer', + 'gossip', + 'gossipmonger', + 'gossipry', + 'gossipy', + 'gossoon', + 'got', + 'gotten', + 'gouache', + 'gouge', + 'goulash', + 'gourami', + 'gourd', + 'gourde', + 'gourmand', + 'gourmandise', + 'gourmet', + 'gout', + 'goutweed', + 'gouty', + 'govern', + 'governance', + 'governess', + 'government', + 'governor', + 'governorship', + 'gowan', + 'gowk', + 'gown', + 'gownsman', + 'goy', + 'grab', + 'grabble', + 'graben', + 'grace', + 'graceful', + 'graceless', + 'gracile', + 'gracioso', + 'gracious', + 'grackle', + 'grad', + 'gradate', + 'gradatim', + 'gradation', + 'grade', + 'gradely', + 'grader', + 'gradient', + 'gradin', + 'gradual', + 'gradualism', + 'graduate', + 'graduated', + 'graduation', + 'gradus', + 'graffito', + 'graft', + 'grafting', + 'graham', + 'grain', + 'grained', + 'grainfield', + 'grainy', + 'grallatorial', + 'gram', + 'gramarye', + 'gramercy', + 'gramicidin', + 'gramineous', + 'graminivorous', + 'grammalogue', + 'grammar', + 'grammarian', + 'grammatical', + 'gramme', + 'gramps', + 'grampus', + 'granadilla', + 'granary', + 'grand', + 'grandam', + 'grandaunt', + 'grandchild', + 'granddad', + 'granddaddy', + 'granddaughter', + 'grandee', + 'grandeur', + 'grandfather', + 'grandfatherly', + 'grandiloquence', + 'grandiloquent', + 'grandiose', + 'grandioso', + 'grandma', + 'grandmamma', + 'grandmother', + 'grandmotherly', + 'grandnephew', + 'grandniece', + 'grandpa', + 'grandpapa', + 'grandparent', + 'grandsire', + 'grandson', + 'grandstand', + 'granduncle', + 'grange', + 'granger', + 'grangerize', + 'granite', + 'graniteware', + 'granitite', + 'granivorous', + 'granny', + 'granophyre', + 'grant', + 'grantee', + 'grantor', + 'granular', + 'granulate', + 'granulation', + 'granule', + 'granulite', + 'granulocyte', + 'granuloma', + 'granulose', + 'grape', + 'grapefruit', + 'grapery', + 'grapeshot', + 'grapevine', + 'graph', + 'grapheme', + 'graphemics', + 'graphic', + 'graphics', + 'graphite', + 'graphitize', + 'graphology', + 'graphomotor', + 'grapnel', + 'grappa', + 'grapple', + 'grappling', + 'graptolite', + 'grasp', + 'grasping', + 'grass', + 'grasshopper', + 'grassland', + 'grassplot', + 'grassquit', + 'grassy', + 'grate', + 'grateful', + 'grater', + 'graticule', + 'gratification', + 'gratify', + 'gratifying', + 'gratin', + 'grating', + 'gratis', + 'gratitude', + 'gratuitous', + 'gratuity', + 'gratulant', + 'gratulate', + 'gratulation', + 'graupel', + 'gravamen', + 'grave', + 'graveclothes', + 'gravedigger', + 'gravel', + 'gravelly', + 'graven', + 'graver', + 'gravestone', + 'graveyard', + 'gravid', + 'gravimeter', + 'gravimetric', + 'gravitate', + 'gravitation', + 'gravitative', + 'graviton', + 'gravity', + 'gravure', + 'gravy', + 'gray', + 'grayback', + 'graybeard', + 'grayish', + 'grayling', + 'graze', + 'grazier', + 'grazing', + 'grease', + 'greaseball', + 'greasepaint', + 'greaser', + 'greasewood', + 'greasy', + 'great', + 'greatcoat', + 'greaten', + 'greatest', + 'greathearted', + 'greatly', + 'greave', + 'greaves', + 'grebe', + 'gree', + 'greed', + 'greedy', + 'greegree', + 'green', + 'greenback', + 'greenbelt', + 'greenbrier', + 'greenery', + 'greenfinch', + 'greengage', + 'greengrocer', + 'greengrocery', + 'greenhead', + 'greenheart', + 'greenhorn', + 'greenhouse', + 'greening', + 'greenish', + 'greenlet', + 'greenling', + 'greenness', + 'greenockite', + 'greenroom', + 'greensand', + 'greenshank', + 'greensickness', + 'greenstone', + 'greensward', + 'greenwood', + 'greet', + 'greeting', + 'gregale', + 'gregarine', + 'gregarious', + 'greige', + 'greisen', + 'gremial', + 'gremlin', + 'grenade', + 'grenadier', + 'grenadine', + 'gressorial', + 'grew', + 'grey', + 'greyback', + 'greybeard', + 'greyhen', + 'greyhound', + 'greylag', + 'greywacke', + 'gribble', + 'grid', + 'griddle', + 'griddlecake', + 'gride', + 'gridiron', + 'grief', + 'grievance', + 'grieve', + 'grievous', + 'griffe', + 'griffin', + 'griffon', + 'grig', + 'grigri', + 'grill', + 'grillage', + 'grille', + 'grilled', + 'grillroom', + 'grillwork', + 'grilse', + 'grim', + 'grimace', + 'grimalkin', + 'grime', + 'grimy', + 'grin', + 'grind', + 'grindelia', + 'grinder', + 'grindery', + 'grindstone', + 'gringo', + 'grip', + 'gripe', + 'grippe', + 'gripper', + 'gripping', + 'gripsack', + 'grisaille', + 'griseofulvin', + 'griseous', + 'grisette', + 'griskin', + 'grisly', + 'grison', + 'grist', + 'gristle', + 'gristly', + 'gristmill', + 'grit', + 'grith', + 'grits', + 'gritty', + 'grivation', + 'grivet', + 'grizzle', + 'grizzled', + 'grizzly', + 'groan', + 'groat', + 'groats', + 'grocer', + 'groceries', + 'grocery', + 'groceryman', + 'grog', + 'groggery', + 'groggy', + 'grogram', + 'grogshop', + 'groin', + 'grommet', + 'gromwell', + 'groom', + 'groomsman', + 'groove', + 'grooved', + 'groovy', + 'grope', + 'groping', + 'grosbeak', + 'groschen', + 'grosgrain', + 'gross', + 'grossularite', + 'grosz', + 'grot', + 'grotesque', + 'grotesquery', + 'grotto', + 'grouch', + 'grouchy', + 'ground', + 'groundage', + 'grounder', + 'groundhog', + 'groundless', + 'groundling', + 'groundmass', + 'groundnut', + 'groundsel', + 'groundsheet', + 'groundsill', + 'groundspeed', + 'groundwork', + 'group', + 'grouper', + 'groupie', + 'grouping', + 'grouse', + 'grout', + 'grouty', + 'grove', + 'grovel', + 'grow', + 'grower', + 'growing', + 'growl', + 'growler', + 'grown', + 'grownup', + 'growth', + 'groyne', + 'grub', + 'grubby', + 'grubstake', + 'grudge', + 'grudging', + 'gruel', + 'grueling', + 'gruelling', + 'gruesome', + 'gruff', + 'grugru', + 'grum', + 'grumble', + 'grume', + 'grummet', + 'grumous', + 'grumpy', + 'grunion', + 'grunt', + 'grunter', + 'gryphon', + 'guacharo', + 'guacin', + 'guaco', + 'guaiacol', + 'guaiacum', + 'guan', + 'guanabana', + 'guanaco', + 'guanase', + 'guanidine', + 'guanine', + 'guano', + 'guarani', + 'guarantee', + 'guarantor', + 'guaranty', + 'guard', + 'guardant', + 'guarded', + 'guardhouse', + 'guardian', + 'guardianship', + 'guardrail', + 'guardroom', + 'guardsman', + 'guava', + 'guayule', + 'gubernatorial', + 'guberniya', + 'guck', + 'guddle', + 'gudgeon', + 'guenon', + 'guerdon', + 'guereza', + 'guerrilla', + 'guess', + 'guesstimate', + 'guesswork', + 'guest', + 'guesthouse', + 'guff', + 'guffaw', + 'guggle', + 'guib', + 'guidance', + 'guide', + 'guideboard', + 'guidebook', + 'guideline', + 'guidepost', + 'guidon', + 'guild', + 'guilder', + 'guildhall', + 'guildsman', + 'guile', + 'guileful', + 'guileless', + 'guillemot', + 'guilloche', + 'guillotine', + 'guilt', + 'guiltless', + 'guilty', + 'guimpe', + 'guinea', + 'guipure', + 'guise', + 'guitar', + 'guitarfish', + 'guitarist', + 'gula', + 'gulch', + 'gulden', + 'gules', + 'gulf', + 'gulfweed', + 'gull', + 'gullet', + 'gullible', + 'gully', + 'gulosity', + 'gulp', + 'gum', + 'gumbo', + 'gumboil', + 'gumbotil', + 'gumdrop', + 'gumma', + 'gummite', + 'gummosis', + 'gummous', + 'gummy', + 'gumption', + 'gumshoe', + 'gumwood', + 'gun', + 'gunboat', + 'guncotton', + 'gunfight', + 'gunfire', + 'gunflint', + 'gunk', + 'gunlock', + 'gunmaker', + 'gunman', + 'gunnel', + 'gunner', + 'gunnery', + 'gunning', + 'gunny', + 'gunnysack', + 'gunpaper', + 'gunplay', + 'gunpoint', + 'gunpowder', + 'gunrunning', + 'gunsel', + 'gunshot', + 'gunslinger', + 'gunsmith', + 'gunstock', + 'gunter', + 'gunwale', + 'gunyah', + 'guppy', + 'gurdwara', + 'gurge', + 'gurgitation', + 'gurgle', + 'gurglet', + 'gurnard', + 'guru', + 'gush', + 'gusher', + 'gushy', + 'gusset', + 'gust', + 'gustation', + 'gustative', + 'gustatory', + 'gusto', + 'gusty', + 'gut', + 'gutbucket', + 'gutsy', + 'gutta', + 'guttate', + 'gutter', + 'guttering', + 'guttersnipe', + 'guttle', + 'guttural', + 'gutturalize', + 'gutty', + 'guv', + 'guy', + 'guyot', + 'guzzle', + 'gybe', + 'gym', + 'gymkhana', + 'gymnasiarch', + 'gymnasiast', + 'gymnasium', + 'gymnast', + 'gymnastic', + 'gymnastics', + 'gymnosophist', + 'gymnosperm', + 'gynaeceum', + 'gynaecocracy', + 'gynaecology', + 'gynaecomastia', + 'gynandromorph', + 'gynandrous', + 'gynandry', + 'gynarchy', + 'gynecic', + 'gynecium', + 'gynecocracy', + 'gynecoid', + 'gynecologist', + 'gynecology', + 'gyniatrics', + 'gynoecium', + 'gynophore', + 'gyp', + 'gypsophila', + 'gypsum', + 'gyral', + 'gyrate', + 'gyration', + 'gyratory', + 'gyre', + 'gyrfalcon', + 'gyro', + 'gyrocompass', + 'gyromagnetic', + 'gyron', + 'gyronny', + 'gyroplane', + 'gyroscope', + 'gyrose', + 'gyrostabilizer', + 'gyrostat', + 'gyrostatic', + 'gyrostatics', + 'gyrus', + 'gyve', + 'h', + 'ha', + 'haaf', + 'haar', + 'habanera', + 'haberdasher', + 'haberdashery', + 'habergeon', + 'habile', + 'habiliment', + 'habilitate', + 'habit', + 'habitable', + 'habitancy', + 'habitant', + 'habitat', + 'habitation', + 'habited', + 'habitual', + 'habituate', + 'habitude', + 'habitue', + 'hachure', + 'hacienda', + 'hack', + 'hackamore', + 'hackberry', + 'hackbut', + 'hackery', + 'hacking', + 'hackle', + 'hackman', + 'hackney', + 'hackneyed', + 'hacksaw', + 'had', + 'haddock', + 'hade', + 'hadj', + 'hadji', + 'hadron', + 'hadst', + 'hae', + 'haecceity', + 'haemachrome', + 'haemagglutinate', + 'haemal', + 'haematic', + 'haematin', + 'haematinic', + 'haematite', + 'haematoblast', + 'haematocele', + 'haematocryal', + 'haematogenesis', + 'haematogenous', + 'haematoid', + 'haematoma', + 'haematopoiesis', + 'haematosis', + 'haematothermal', + 'haematoxylin', + 'haematoxylon', + 'haematozoon', + 'haemic', + 'haemin', + 'haemocyte', + 'haemoglobin', + 'haemoid', + 'haemolysin', + 'haemolysis', + 'haemophilia', + 'haemophiliac', + 'haemophilic', + 'haemorrhage', + 'haemostasis', + 'haemostat', + 'haemostatic', + 'haeres', + 'hafiz', + 'hafnium', + 'haft', + 'hag', + 'hagberry', + 'hagbut', + 'hagfish', + 'haggadist', + 'haggard', + 'haggis', + 'haggle', + 'hagiarchy', + 'hagiocracy', + 'hagiographer', + 'hagiography', + 'hagiolatry', + 'hagiology', + 'hagioscope', + 'hagride', + 'hah', + 'haik', + 'haiku', + 'hail', + 'hailstone', + 'hailstorm', + 'hair', + 'hairball', + 'hairbreadth', + 'hairbrush', + 'haircloth', + 'haircut', + 'hairdo', + 'hairdresser', + 'hairless', + 'hairline', + 'hairpiece', + 'hairpin', + 'hairsplitter', + 'hairsplitting', + 'hairspring', + 'hairstreak', + 'hairstyle', + 'hairtail', + 'hairworm', + 'hairy', + 'hajj', + 'hajji', + 'hake', + 'hakim', + 'halation', + 'halberd', + 'halcyon', + 'hale', + 'haler', + 'half', + 'halfback', + 'halfbeak', + 'halfhearted', + 'halfpenny', + 'halftone', + 'halfway', + 'halibut', + 'halide', + 'halidom', + 'halite', + 'halitosis', + 'hall', + 'hallah', + 'hallelujah', + 'halliard', + 'hallmark', + 'hallo', + 'halloo', + 'hallow', + 'hallowed', + 'hallucinate', + 'hallucination', + 'hallucinatory', + 'hallucinogen', + 'hallucinosis', + 'hallux', + 'hallway', + 'halm', + 'halo', + 'halogen', + 'halogenate', + 'haloid', + 'halophyte', + 'halothane', + 'halt', + 'halter', + 'halting', + 'halutz', + 'halvah', + 'halve', + 'halves', + 'halyard', + 'ham', + 'hamadryad', + 'hamal', + 'hamamelidaceous', + 'hamartia', + 'hamate', + 'hamburger', + 'hame', + 'hamlet', + 'hammer', + 'hammered', + 'hammerhead', + 'hammering', + 'hammerless', + 'hammerlock', + 'hammertoe', + 'hammock', + 'hammy', + 'hamper', + 'hamster', + 'hamstring', + 'hamulus', + 'hamza', + 'hanaper', + 'hance', + 'hand', + 'handbag', + 'handball', + 'handbarrow', + 'handbill', + 'handbook', + 'handbreadth', + 'handcar', + 'handcart', + 'handclap', + 'handclasp', + 'handcraft', + 'handcrafted', + 'handcuff', + 'handed', + 'handedness', + 'handfast', + 'handfasting', + 'handful', + 'handgrip', + 'handgun', + 'handhold', + 'handicap', + 'handicapped', + 'handicapper', + 'handicraft', + 'handicraftsman', + 'handily', + 'handiness', + 'handiwork', + 'handkerchief', + 'handle', + 'handlebar', + 'handler', + 'handling', + 'handmade', + 'handmaid', + 'handmaiden', + 'handout', + 'handpick', + 'handrail', + 'hands', + 'handsaw', + 'handsel', + 'handset', + 'handshake', + 'handshaker', + 'handsome', + 'handsomely', + 'handspike', + 'handspring', + 'handstand', + 'handwork', + 'handwoven', + 'handwriting', + 'handy', + 'handyman', + 'hang', + 'hangar', + 'hangbird', + 'hangdog', + 'hanger', + 'hanging', + 'hangman', + 'hangnail', + 'hangout', + 'hangover', + 'hank', + 'hanker', + 'hankering', + 'hanky', + 'hansel', + 'hansom', + 'hanuman', + 'hap', + 'haphazard', + 'haphazardly', + 'hapless', + 'haplite', + 'haplography', + 'haploid', + 'haplology', + 'haplosis', + 'haply', + 'happen', + 'happening', + 'happenstance', + 'happily', + 'happiness', + 'happy', + 'hapten', + 'harangue', + 'harass', + 'harassed', + 'harbinger', + 'harbor', + 'harborage', + 'harbour', + 'harbourage', + 'hard', + 'hardback', + 'hardball', + 'hardboard', + 'harden', + 'hardened', + 'hardener', + 'hardening', + 'hardhack', + 'hardheaded', + 'hardhearted', + 'hardihood', + 'hardily', + 'hardiness', + 'hardly', + 'hardness', + 'hardpan', + 'hards', + 'hardship', + 'hardtack', + 'hardtop', + 'hardware', + 'hardwood', + 'hardworking', + 'hardy', + 'hare', + 'harebell', + 'harebrained', + 'harelip', + 'harem', + 'haricot', + 'hark', + 'harken', + 'harl', + 'harlequin', + 'harlequinade', + 'harlot', + 'harlotry', + 'harm', + 'harmattan', + 'harmful', + 'harmless', + 'harmonic', + 'harmonica', + 'harmonicon', + 'harmonics', + 'harmonious', + 'harmonist', + 'harmonium', + 'harmonize', + 'harmony', + 'harmotome', + 'harness', + 'harp', + 'harper', + 'harping', + 'harpist', + 'harpoon', + 'harpsichord', + 'harpy', + 'harquebus', + 'harquebusier', + 'harridan', + 'harrier', + 'harrow', + 'harrumph', + 'harry', + 'harsh', + 'harslet', + 'hart', + 'hartal', + 'hartebeest', + 'hartshorn', + 'haruspex', + 'haruspicy', + 'harvest', + 'harvester', + 'harvestman', + 'has', + 'hash', + 'hashish', + 'haslet', + 'hasp', + 'hassle', + 'hassock', + 'hast', + 'hastate', + 'haste', + 'hasten', + 'hasty', + 'hat', + 'hatband', + 'hatbox', + 'hatch', + 'hatchel', + 'hatchery', + 'hatchet', + 'hatching', + 'hatchment', + 'hatchway', + 'hate', + 'hateful', + 'hath', + 'hatpin', + 'hatred', + 'hatter', + 'haubergeon', + 'hauberk', + 'haugh', + 'haughty', + 'haul', + 'haulage', + 'hauler', + 'haulm', + 'haunch', + 'haunt', + 'haunted', + 'haunting', + 'hausfrau', + 'haustellum', + 'haustorium', + 'hautbois', + 'hautboy', + 'hauteur', + 'have', + 'havelock', + 'haven', + 'haver', + 'haversack', + 'haversine', + 'havildar', + 'havoc', + 'haw', + 'hawfinch', + 'hawk', + 'hawkbill', + 'hawker', + 'hawking', + 'hawkshaw', + 'hawkweed', + 'hawse', + 'hawsehole', + 'hawsepiece', + 'hawsepipe', + 'hawser', + 'hawthorn', + 'hay', + 'haycock', + 'hayfield', + 'hayfork', + 'hayloft', + 'haymaker', + 'haymow', + 'hayrack', + 'hayrick', + 'hayseed', + 'haystack', + 'hayward', + 'haywire', + 'hazan', + 'hazard', + 'hazardous', + 'haze', + 'hazel', + 'hazelnut', + 'hazing', + 'hazy', + 'he', + 'head', + 'headache', + 'headachy', + 'headband', + 'headboard', + 'headcheese', + 'headcloth', + 'headdress', + 'headed', + 'header', + 'headfirst', + 'headforemost', + 'headgear', + 'heading', + 'headland', + 'headless', + 'headlight', + 'headline', + 'headliner', + 'headlock', + 'headlong', + 'headman', + 'headmaster', + 'headmistress', + 'headmost', + 'headphone', + 'headpiece', + 'headpin', + 'headquarters', + 'headrace', + 'headrail', + 'headreach', + 'headrest', + 'headroom', + 'heads', + 'headsail', + 'headset', + 'headship', + 'headsman', + 'headspring', + 'headstall', + 'headstand', + 'headstock', + 'headstone', + 'headstream', + 'headstrong', + 'headwaiter', + 'headward', + 'headwards', + 'headwater', + 'headwaters', + 'headway', + 'headwind', + 'headword', + 'headwork', + 'heady', + 'heal', + 'healing', + 'health', + 'healthful', + 'healthy', + 'heap', + 'hear', + 'hearing', + 'hearken', + 'hearsay', + 'hearse', + 'heart', + 'heartache', + 'heartbeat', + 'heartbreak', + 'heartbreaker', + 'heartbreaking', + 'heartbroken', + 'heartburn', + 'heartburning', + 'hearten', + 'heartfelt', + 'hearth', + 'hearthside', + 'hearthstone', + 'heartily', + 'heartland', + 'heartless', + 'heartrending', + 'hearts', + 'heartsease', + 'heartsick', + 'heartsome', + 'heartstrings', + 'heartthrob', + 'heartwood', + 'heartworm', + 'hearty', + 'heat', + 'heated', + 'heater', + 'heath', + 'heathberry', + 'heathen', + 'heathendom', + 'heathenish', + 'heathenism', + 'heathenize', + 'heathenry', + 'heather', + 'heatstroke', + 'heaume', + 'heave', + 'heaven', + 'heavenly', + 'heavenward', + 'heaver', + 'heaves', + 'heavily', + 'heaviness', + 'heavy', + 'heavyhearted', + 'heavyset', + 'heavyweight', + 'hebdomad', + 'hebdomadal', + 'hebdomadary', + 'hebephrenia', + 'hebetate', + 'hebetic', + 'hebetude', + 'hecatomb', + 'heck', + 'heckelphone', + 'heckle', + 'hectare', + 'hectic', + 'hectocotylus', + 'hectogram', + 'hectograph', + 'hectoliter', + 'hectometer', + 'hector', + 'heddle', + 'heder', + 'hedge', + 'hedgehog', + 'hedgehop', + 'hedger', + 'hedgerow', + 'hedonic', + 'hedonics', + 'hedonism', + 'heed', + 'heedful', + 'heedless', + 'heehaw', + 'heel', + 'heeled', + 'heeler', + 'heeling', + 'heelpiece', + 'heelpost', + 'heeltap', + 'heft', + 'hefty', + 'hegemony', + 'hegira', + 'hegumen', + 'heifer', + 'height', + 'heighten', + 'heinous', + 'heir', + 'heirdom', + 'heiress', + 'heirloom', + 'heirship', + 'heist', + 'held', + 'heliacal', + 'helianthus', + 'helical', + 'helices', + 'helicline', + 'helicograph', + 'helicoid', + 'helicon', + 'helicopter', + 'heliocentric', + 'heliograph', + 'heliogravure', + 'heliolatry', + 'heliometer', + 'heliostat', + 'heliotaxis', + 'heliotherapy', + 'heliotrope', + 'heliotropin', + 'heliotropism', + 'heliotype', + 'heliozoan', + 'heliport', + 'helium', + 'helix', + 'hell', + 'hellbender', + 'hellbent', + 'hellbox', + 'hellcat', + 'helldiver', + 'hellebore', + 'heller', + 'hellfire', + 'hellgrammite', + 'hellhole', + 'hellhound', + 'hellion', + 'hellish', + 'hellkite', + 'hello', + 'helluva', + 'helm', + 'helmet', + 'helminth', + 'helminthiasis', + 'helminthic', + 'helminthology', + 'helmsman', + 'helot', + 'helotism', + 'helotry', + 'help', + 'helper', + 'helpful', + 'helping', + 'helpless', + 'helpmate', + 'helpmeet', + 'helve', + 'hem', + 'hemangioma', + 'hematite', + 'hematology', + 'hematuria', + 'hemelytron', + 'hemeralopia', + 'hemialgia', + 'hemianopsia', + 'hemicellulose', + 'hemichordate', + 'hemicrania', + 'hemicycle', + 'hemidemisemiquaver', + 'hemielytron', + 'hemihedral', + 'hemihydrate', + 'hemimorphic', + 'hemimorphite', + 'hemiplegia', + 'hemipode', + 'hemipterous', + 'hemisphere', + 'hemispheroid', + 'hemistich', + 'hemiterpene', + 'hemitrope', + 'hemline', + 'hemlock', + 'hemmer', + 'hemocyte', + 'hemoglobin', + 'hemolysis', + 'hemophilia', + 'hemorrhage', + 'hemorrhoid', + 'hemorrhoidectomy', + 'hemostat', + 'hemotherapy', + 'hemp', + 'hemstitch', + 'hen', + 'henbane', + 'henbit', + 'hence', + 'henceforth', + 'henceforward', + 'henchman', + 'hendecagon', + 'hendecahedron', + 'hendecasyllable', + 'hendiadys', + 'henequen', + 'henhouse', + 'henna', + 'hennery', + 'henotheism', + 'henpeck', + 'henry', + 'hent', + 'hep', + 'heparin', + 'hepatic', + 'hepatica', + 'hepatitis', + 'hepcat', + 'heptachord', + 'heptad', + 'heptagon', + 'heptagonal', + 'heptahedron', + 'heptamerous', + 'heptameter', + 'heptane', + 'heptangular', + 'heptarchy', + 'heptastich', + 'heptavalent', + 'heptode', + 'her', + 'herald', + 'heraldic', + 'heraldry', + 'herb', + 'herbaceous', + 'herbage', + 'herbal', + 'herbalist', + 'herbarium', + 'herbicide', + 'herbivore', + 'herbivorous', + 'herby', + 'herculean', + 'herd', + 'herder', + 'herdic', + 'herdsman', + 'here', + 'hereabout', + 'hereabouts', + 'hereafter', + 'hereat', + 'hereby', + 'heredes', + 'hereditable', + 'hereditament', + 'hereditary', + 'heredity', + 'herein', + 'hereinafter', + 'hereinbefore', + 'hereinto', + 'hereof', + 'hereon', + 'heres', + 'heresiarch', + 'heresy', + 'heretic', + 'heretical', + 'hereto', + 'heretofore', + 'hereunder', + 'hereunto', + 'hereupon', + 'herewith', + 'heriot', + 'heritable', + 'heritage', + 'heritor', + 'herl', + 'herm', + 'hermaphrodite', + 'hermaphroditism', + 'hermeneutic', + 'hermeneutics', + 'hermetic', + 'hermit', + 'hermitage', + 'hern', + 'hernia', + 'herniorrhaphy', + 'herniotomy', + 'hero', + 'heroic', + 'heroics', + 'heroin', + 'heroine', + 'heroism', + 'heron', + 'heronry', + 'herpes', + 'herpetology', + 'herring', + 'herringbone', + 'hers', + 'herself', + 'hertz', + 'hesitancy', + 'hesitant', + 'hesitate', + 'hesitation', + 'hesperidin', + 'hesperidium', + 'hessian', + 'hessite', + 'hest', + 'hetaera', + 'hetaerism', + 'heterocercal', + 'heterochromatic', + 'heterochromatin', + 'heterochromosome', + 'heterochromous', + 'heteroclite', + 'heterocyclic', + 'heterodox', + 'heterodoxy', + 'heterodyne', + 'heteroecious', + 'heterogamete', + 'heterogamy', + 'heterogeneity', + 'heterogeneous', + 'heterogenesis', + 'heterogenetic', + 'heterogenous', + 'heterogony', + 'heterograft', + 'heterography', + 'heterogynous', + 'heterolecithal', + 'heterologous', + 'heterolysis', + 'heteromerous', + 'heteromorphic', + 'heteronomous', + 'heteronomy', + 'heteronym', + 'heterophony', + 'heterophyllous', + 'heterophyte', + 'heteroplasty', + 'heteropolar', + 'heteropterous', + 'heterosexual', + 'heterosexuality', + 'heterosis', + 'heterosporous', + 'heterotaxis', + 'heterothallic', + 'heterotopia', + 'heterotrophic', + 'heterotypic', + 'heterozygote', + 'heterozygous', + 'heth', + 'hetman', + 'heulandite', + 'heuristic', + 'hew', + 'hex', + 'hexachlorophene', + 'hexachord', + 'hexad', + 'hexaemeron', + 'hexagon', + 'hexagonal', + 'hexagram', + 'hexahedron', + 'hexahydrate', + 'hexamerous', + 'hexameter', + 'hexamethylenetetramine', + 'hexane', + 'hexangular', + 'hexapartite', + 'hexapla', + 'hexapod', + 'hexapody', + 'hexarchy', + 'hexastich', + 'hexastyle', + 'hexavalent', + 'hexone', + 'hexosan', + 'hexose', + 'hexyl', + 'hexylresorcinol', + 'hey', + 'heyday', + 'hg', + 'hhd', + 'hi', + 'hiatus', + 'hibachi', + 'hibernaculum', + 'hibernal', + 'hibernate', + 'hibiscus', + 'hic', + 'hiccup', + 'hick', + 'hickey', + 'hickory', + 'hid', + 'hidalgo', + 'hidden', + 'hiddenite', + 'hide', + 'hideaway', + 'hidebound', + 'hideous', + 'hideout', + 'hiding', + 'hidrosis', + 'hie', + 'hiemal', + 'hieracosphinx', + 'hierarch', + 'hierarchize', + 'hierarchy', + 'hieratic', + 'hierocracy', + 'hierodule', + 'hieroglyphic', + 'hierogram', + 'hierolatry', + 'hierology', + 'hierophant', + 'hifalutin', + 'higgle', + 'higgler', + 'high', + 'highball', + 'highbinder', + 'highborn', + 'highboy', + 'highbred', + 'highbrow', + 'highchair', + 'highfalutin', + 'highflier', + 'highjack', + 'highland', + 'highlight', + 'highline', + 'highly', + 'highness', + 'highroad', + 'hight', + 'hightail', + 'highway', + 'highwayman', + 'hijack', + 'hijacker', + 'hike', + 'hilarious', + 'hilarity', + 'hill', + 'hillbilly', + 'hillock', + 'hillside', + 'hilltop', + 'hilly', + 'hilt', + 'hilum', + 'him', + 'himation', + 'himself', + 'hin', + 'hind', + 'hindbrain', + 'hinder', + 'hindermost', + 'hindgut', + 'hindmost', + 'hindquarter', + 'hindrance', + 'hindsight', + 'hindward', + 'hinge', + 'hinny', + 'hint', + 'hinterland', + 'hip', + 'hipbone', + 'hipparch', + 'hipped', + 'hippie', + 'hippo', + 'hippocampus', + 'hippocras', + 'hippodrome', + 'hippogriff', + 'hippopotamus', + 'hippy', + 'hipster', + 'hiragana', + 'hircine', + 'hire', + 'hireling', + 'hirsute', + 'hirsutism', + 'hirudin', + 'hirundine', + 'his', + 'hispid', + 'hispidulous', + 'hiss', + 'hissing', + 'hist', + 'histaminase', + 'histamine', + 'histidine', + 'histiocyte', + 'histochemistry', + 'histogen', + 'histogenesis', + 'histogram', + 'histoid', + 'histology', + 'histolysis', + 'histone', + 'histopathology', + 'histoplasmosis', + 'historian', + 'historiated', + 'historic', + 'historical', + 'historicism', + 'historicity', + 'historied', + 'historiographer', + 'historiography', + 'history', + 'histrionic', + 'histrionics', + 'histrionism', + 'hit', + 'hitch', + 'hitchhike', + 'hither', + 'hithermost', + 'hitherto', + 'hitherward', + 'hive', + 'hives', + 'hl', + 'hm', + 'ho', + 'hoactzin', + 'hoagy', + 'hoar', + 'hoard', + 'hoarding', + 'hoarfrost', + 'hoarhound', + 'hoarse', + 'hoarsen', + 'hoary', + 'hoatzin', + 'hoax', + 'hob', + 'hobble', + 'hobbledehoy', + 'hobby', + 'hobbyhorse', + 'hobgoblin', + 'hobnail', + 'hobnailed', + 'hobnob', + 'hobo', + 'hock', + 'hockey', + 'hocus', + 'hod', + 'hodden', + 'hodgepodge', + 'hodman', + 'hodometer', + 'hoe', + 'hoecake', + 'hoedown', + 'hog', + 'hogan', + 'hogback', + 'hogfish', + 'hoggish', + 'hognut', + 'hogshead', + 'hogtie', + 'hogwash', + 'hogweed', + 'hoick', + 'hoicks', + 'hoiden', + 'hoist', + 'hokku', + 'hokum', + 'hold', + 'holdall', + 'holdback', + 'holden', + 'holder', + 'holdfast', + 'holding', + 'holdover', + 'holdup', + 'hole', + 'holeproof', + 'holiday', + 'holily', + 'holiness', + 'holism', + 'holler', + 'hollo', + 'hollow', + 'holly', + 'hollyhock', + 'holm', + 'holmic', + 'holmium', + 'holoblastic', + 'holocaust', + 'holocrine', + 'holoenzyme', + 'holograph', + 'holography', + 'holohedral', + 'holomorphic', + 'holophrastic', + 'holophytic', + 'holothurian', + 'holotype', + 'holozoic', + 'holp', + 'holpen', + 'hols', + 'holster', + 'holt', + 'holy', + 'holystone', + 'holytide', + 'homage', + 'homager', + 'hombre', + 'homburg', + 'home', + 'homebody', + 'homebred', + 'homecoming', + 'homegrown', + 'homeland', + 'homeless', + 'homelike', + 'homely', + 'homemade', + 'homemaker', + 'homemaking', + 'homeomorphism', + 'homeopathic', + 'homeopathist', + 'homeopathy', + 'homeostasis', + 'homer', + 'homeroom', + 'homesick', + 'homespun', + 'homestead', + 'homesteader', + 'homestretch', + 'homeward', + 'homework', + 'homey', + 'homicidal', + 'homicide', + 'homiletic', + 'homiletics', + 'homily', + 'homing', + 'hominid', + 'hominoid', + 'hominy', + 'homo', + 'homocentric', + 'homocercal', + 'homochromatic', + 'homochromous', + 'homocyclic', + 'homoeroticism', + 'homogamy', + 'homogeneity', + 'homogeneous', + 'homogenesis', + 'homogenetic', + 'homogenize', + 'homogenous', + 'homogeny', + 'homogony', + 'homograft', + 'homograph', + 'homologate', + 'homologize', + 'homologous', + 'homolographic', + 'homologue', + 'homology', + 'homomorphism', + 'homonym', + 'homophile', + 'homophone', + 'homophonic', + 'homophonous', + 'homophony', + 'homopolar', + 'homopterous', + 'homorganic', + 'homosexual', + 'homosexuality', + 'homosporous', + 'homotaxis', + 'homothallic', + 'homothermal', + 'homozygote', + 'homozygous', + 'homunculus', + 'homy', + 'hon', + 'hone', + 'honest', + 'honestly', + 'honesty', + 'honewort', + 'honey', + 'honeybee', + 'honeybunch', + 'honeycomb', + 'honeydew', + 'honeyed', + 'honeymoon', + 'honeysucker', + 'honeysuckle', + 'hong', + 'honied', + 'honk', + 'honky', + 'honor', + 'honorable', + 'honorarium', + 'honorary', + 'honorific', + 'honour', + 'honourable', + 'hoo', + 'hooch', + 'hood', + 'hooded', + 'hoodlum', + 'hoodoo', + 'hoodwink', + 'hooey', + 'hoof', + 'hoofbeat', + 'hoofbound', + 'hoofed', + 'hoofer', + 'hook', + 'hookah', + 'hooked', + 'hooker', + 'hooknose', + 'hookup', + 'hookworm', + 'hooky', + 'hooligan', + 'hoop', + 'hooper', + 'hoopla', + 'hoopoe', + 'hooray', + 'hoosegow', + 'hoot', + 'hootenanny', + 'hooves', + 'hop', + 'hope', + 'hopeful', + 'hopefully', + 'hopeless', + 'hophead', + 'hoplite', + 'hopper', + 'hopping', + 'hopple', + 'hopscotch', + 'hoptoad', + 'hora', + 'horal', + 'horary', + 'horde', + 'hordein', + 'horehound', + 'horizon', + 'horizontal', + 'horme', + 'hormonal', + 'hormone', + 'horn', + 'hornbeam', + 'hornbill', + 'hornblende', + 'hornbook', + 'horned', + 'hornet', + 'hornpipe', + 'hornstone', + 'hornswoggle', + 'horntail', + 'hornwort', + 'horny', + 'horologe', + 'horologist', + 'horologium', + 'horology', + 'horoscope', + 'horoscopy', + 'horotelic', + 'horrendous', + 'horrible', + 'horribly', + 'horrid', + 'horrific', + 'horrified', + 'horrify', + 'horripilate', + 'horripilation', + 'horror', + 'horse', + 'horseback', + 'horsecar', + 'horseflesh', + 'horsefly', + 'horsehair', + 'horsehide', + 'horselaugh', + 'horseleech', + 'horseman', + 'horsemanship', + 'horsemint', + 'horseplay', + 'horsepower', + 'horseradish', + 'horseshit', + 'horseshoe', + 'horseshoes', + 'horsetail', + 'horseweed', + 'horsewhip', + 'horsewoman', + 'horsey', + 'horst', + 'horsy', + 'hortative', + 'hortatory', + 'horticulture', + 'hosanna', + 'hose', + 'hosier', + 'hosiery', + 'hospice', + 'hospitable', + 'hospital', + 'hospitality', + 'hospitalization', + 'hospitalize', + 'hospitium', + 'hospodar', + 'host', + 'hostage', + 'hostel', + 'hostelry', + 'hostess', + 'hostile', + 'hostility', + 'hostler', + 'hot', + 'hotbed', + 'hotbox', + 'hotchpot', + 'hotchpotch', + 'hotel', + 'hotfoot', + 'hothead', + 'hotheaded', + 'hothouse', + 'hotshot', + 'hotspur', + 'hough', + 'hound', + 'hounding', + 'houppelande', + 'hour', + 'hourglass', + 'houri', + 'hourly', + 'house', + 'houseboat', + 'housebound', + 'houseboy', + 'housebreak', + 'housebreaker', + 'housebreaking', + 'housebroken', + 'housecarl', + 'houseclean', + 'housecoat', + 'housefather', + 'housefly', + 'household', + 'householder', + 'housekeeper', + 'housekeeping', + 'housel', + 'houseleek', + 'houseless', + 'houselights', + 'houseline', + 'housemaid', + 'houseman', + 'housemaster', + 'housemother', + 'houseroom', + 'housetop', + 'housewares', + 'housewarming', + 'housewife', + 'housewifely', + 'housewifery', + 'housework', + 'housing', + 'houstonia', + 'hove', + 'hovel', + 'hover', + 'hovercraft', + 'how', + 'howbeit', + 'howdah', + 'howdy', + 'however', + 'howitzer', + 'howl', + 'howler', + 'howlet', + 'howling', + 'howsoever', + 'hoy', + 'hoyden', + 'huarache', + 'hub', + 'hubbub', + 'hubby', + 'hubris', + 'huckaback', + 'huckleberry', + 'huckster', + 'huddle', + 'hue', + 'hued', + 'huff', + 'huffish', + 'huffy', + 'hug', + 'huge', + 'hugely', + 'huh', + 'hula', + 'hulk', + 'hulking', + 'hulky', + 'hull', + 'hullabaloo', + 'hullo', + 'hum', + 'human', + 'humane', + 'humanism', + 'humanist', + 'humanitarian', + 'humanitarianism', + 'humanity', + 'humanize', + 'humankind', + 'humanly', + 'humanoid', + 'humble', + 'humblebee', + 'humbug', + 'humbuggery', + 'humdinger', + 'humdrum', + 'humectant', + 'humeral', + 'humerus', + 'humic', + 'humid', + 'humidifier', + 'humidify', + 'humidistat', + 'humidity', + 'humidor', + 'humiliate', + 'humiliating', + 'humiliation', + 'humility', + 'humming', + 'hummingbird', + 'hummock', + 'hummocky', + 'humor', + 'humoral', + 'humoresque', + 'humorist', + 'humorous', + 'humour', + 'hump', + 'humpback', + 'humpbacked', + 'humph', + 'humpy', + 'humus', + 'hunch', + 'hunchback', + 'hunchbacked', + 'hundred', + 'hundredfold', + 'hundredth', + 'hundredweight', + 'hung', + 'hunger', + 'hungry', + 'hunk', + 'hunker', + 'hunkers', + 'hunks', + 'hunt', + 'hunter', + 'hunting', + 'huntress', + 'huntsman', + 'huppah', + 'hurdle', + 'hurds', + 'hurl', + 'hurley', + 'hurling', + 'hurrah', + 'hurricane', + 'hurried', + 'hurry', + 'hurst', + 'hurt', + 'hurter', + 'hurtful', + 'hurtle', + 'hurtless', + 'husband', + 'husbandman', + 'husbandry', + 'hush', + 'hushaby', + 'husk', + 'husking', + 'husky', + 'hussar', + 'hussy', + 'hustings', + 'hustle', + 'hustler', + 'hut', + 'hutch', + 'hutment', + 'huzzah', + 'hwan', + 'hyacinth', + 'hyaena', + 'hyaline', + 'hyalite', + 'hyaloid', + 'hyaloplasm', + 'hyaluronidase', + 'hybrid', + 'hybridism', + 'hybridize', + 'hybris', + 'hydantoin', + 'hydatid', + 'hydnocarpate', + 'hydra', + 'hydracid', + 'hydrangea', + 'hydrant', + 'hydranth', + 'hydrargyrum', + 'hydrastine', + 'hydrastinine', + 'hydrastis', + 'hydrate', + 'hydrated', + 'hydraulic', + 'hydraulics', + 'hydrazine', + 'hydria', + 'hydric', + 'hydride', + 'hydro', + 'hydrobomb', + 'hydrocarbon', + 'hydrocele', + 'hydrocellulose', + 'hydrocephalus', + 'hydrochloride', + 'hydrocortisone', + 'hydrodynamic', + 'hydrodynamics', + 'hydroelectric', + 'hydrofoil', + 'hydrogen', + 'hydrogenate', + 'hydrogenize', + 'hydrogenolysis', + 'hydrogenous', + 'hydrogeology', + 'hydrograph', + 'hydrography', + 'hydroid', + 'hydrokinetic', + 'hydrokinetics', + 'hydrology', + 'hydrolysate', + 'hydrolyse', + 'hydrolysis', + 'hydrolyte', + 'hydrolytic', + 'hydrolyze', + 'hydromagnetics', + 'hydromancy', + 'hydromechanics', + 'hydromedusa', + 'hydromel', + 'hydrometallurgy', + 'hydrometeor', + 'hydrometer', + 'hydropathy', + 'hydrophane', + 'hydrophilic', + 'hydrophilous', + 'hydrophobia', + 'hydrophobic', + 'hydrophone', + 'hydrophyte', + 'hydropic', + 'hydroplane', + 'hydroponics', + 'hydrops', + 'hydroquinone', + 'hydroscope', + 'hydrosol', + 'hydrosome', + 'hydrosphere', + 'hydrostat', + 'hydrostatic', + 'hydrostatics', + 'hydrotaxis', + 'hydrotherapeutics', + 'hydrotherapy', + 'hydrothermal', + 'hydrothorax', + 'hydrotropism', + 'hydrous', + 'hydroxide', + 'hydroxy', + 'hydroxyl', + 'hydroxylamine', + 'hydrozoan', + 'hyena', + 'hyetal', + 'hyetograph', + 'hyetography', + 'hyetology', + 'hygiene', + 'hygienic', + 'hygienics', + 'hygienist', + 'hygrograph', + 'hygrometer', + 'hygrometric', + 'hygrometry', + 'hygrophilous', + 'hygroscope', + 'hygroscopic', + 'hygrostat', + 'hygrothermograph', + 'hying', + 'hyla', + 'hylomorphism', + 'hylophagous', + 'hylotheism', + 'hylozoism', + 'hymen', + 'hymeneal', + 'hymenium', + 'hymenopteran', + 'hymenopterous', + 'hymn', + 'hymnal', + 'hymnist', + 'hymnody', + 'hymnology', + 'hyoid', + 'hyoscine', + 'hyoscyamine', + 'hyoscyamus', + 'hypabyssal', + 'hypaesthesia', + 'hypaethral', + 'hypallage', + 'hypanthium', + 'hype', + 'hyperacidity', + 'hyperactive', + 'hyperaemia', + 'hyperaesthesia', + 'hyperbaric', + 'hyperbaton', + 'hyperbola', + 'hyperbole', + 'hyperbolic', + 'hyperbolism', + 'hyperbolize', + 'hyperboloid', + 'hyperborean', + 'hypercatalectic', + 'hypercorrect', + 'hypercorrection', + 'hypercritical', + 'hypercriticism', + 'hyperdulia', + 'hyperemia', + 'hyperesthesia', + 'hyperextension', + 'hyperform', + 'hypergolic', + 'hyperkeratosis', + 'hyperkinesia', + 'hypermeter', + 'hypermetropia', + 'hyperon', + 'hyperopia', + 'hyperostosis', + 'hyperparathyroidism', + 'hyperphagia', + 'hyperphysical', + 'hyperpituitarism', + 'hyperplane', + 'hyperplasia', + 'hyperploid', + 'hyperpyrexia', + 'hypersensitive', + 'hypersensitize', + 'hypersonic', + 'hyperspace', + 'hypersthene', + 'hypertension', + 'hypertensive', + 'hyperthermia', + 'hyperthyroidism', + 'hypertonic', + 'hypertrophy', + 'hyperventilation', + 'hypervitaminosis', + 'hypesthesia', + 'hypethral', + 'hypha', + 'hyphen', + 'hyphenate', + 'hyphenated', + 'hypnoanalysis', + 'hypnogenesis', + 'hypnology', + 'hypnosis', + 'hypnotherapy', + 'hypnotic', + 'hypnotism', + 'hypnotist', + 'hypnotize', + 'hypo', + 'hypoacidity', + 'hypoblast', + 'hypocaust', + 'hypochlorite', + 'hypochondria', + 'hypochondriac', + 'hypochondriasis', + 'hypochondrium', + 'hypochromia', + 'hypocorism', + 'hypocoristic', + 'hypocotyl', + 'hypocrisy', + 'hypocrite', + 'hypocycloid', + 'hypoderm', + 'hypoderma', + 'hypodermic', + 'hypodermis', + 'hypogastrium', + 'hypogeal', + 'hypogene', + 'hypogenous', + 'hypogeous', + 'hypogeum', + 'hypoglossal', + 'hypoglycemia', + 'hypognathous', + 'hypogynous', + 'hypolimnion', + 'hypomania', + 'hyponasty', + 'hyponitrite', + 'hypophosphate', + 'hypophosphite', + 'hypophyge', + 'hypophysis', + 'hypopituitarism', + 'hypoplasia', + 'hypoploid', + 'hyposensitize', + 'hypostasis', + 'hypostasize', + 'hypostatize', + 'hyposthenia', + 'hypostyle', + 'hypotaxis', + 'hypotension', + 'hypotenuse', + 'hypothalamus', + 'hypothec', + 'hypothecate', + 'hypothermal', + 'hypothermia', + 'hypothesis', + 'hypothesize', + 'hypothetical', + 'hypothyroidism', + 'hypotonic', + 'hypotrachelium', + 'hypoxanthine', + 'hypoxia', + 'hypozeugma', + 'hypozeuxis', + 'hypsography', + 'hypsometer', + 'hypsometry', + 'hyracoid', + 'hyrax', + 'hyson', + 'hyssop', + 'hysterectomize', + 'hysterectomy', + 'hysteresis', + 'hysteria', + 'hysteric', + 'hysterical', + 'hysterics', + 'hysterogenic', + 'hysteroid', + 'hysterotomy', + 'i', + 'iamb', + 'iambic', + 'iambus', + 'iatric', + 'iatrochemistry', + 'iatrogenic', + 'ibex', + 'ibidem', + 'ibis', + 'ice', + 'iceberg', + 'iceblink', + 'iceboat', + 'icebound', + 'icebox', + 'icebreaker', + 'icecap', + 'iced', + 'icefall', + 'icehouse', + 'iceman', + 'ichneumon', + 'ichnite', + 'ichnography', + 'ichnology', + 'ichor', + 'ichthyic', + 'ichthyoid', + 'ichthyolite', + 'ichthyology', + 'ichthyornis', + 'ichthyosaur', + 'ichthyosis', + 'icicle', + 'icily', + 'iciness', + 'icing', + 'icky', + 'icon', + 'iconic', + 'iconoclasm', + 'iconoclast', + 'iconoduly', + 'iconography', + 'iconolatry', + 'iconology', + 'iconoscope', + 'iconostasis', + 'icosahedron', + 'icterus', + 'ictus', + 'icy', + 'id', + 'idea', + 'ideal', + 'idealism', + 'idealist', + 'idealistic', + 'ideality', + 'idealize', + 'ideally', + 'ideate', + 'ideation', + 'ideational', + 'ideatum', + 'idem', + 'idempotent', + 'identic', + 'identical', + 'identification', + 'identify', + 'identity', + 'ideogram', + 'ideograph', + 'ideography', + 'ideologist', + 'ideology', + 'ideomotor', + 'ides', + 'idioblast', + 'idiocrasy', + 'idiocy', + 'idioglossia', + 'idiographic', + 'idiolect', + 'idiom', + 'idiomatic', + 'idiomorphic', + 'idiopathy', + 'idiophone', + 'idioplasm', + 'idiosyncrasy', + 'idiot', + 'idiotic', + 'idiotism', + 'idle', + 'idler', + 'idocrase', + 'idol', + 'idolater', + 'idolatrize', + 'idolatrous', + 'idolatry', + 'idolism', + 'idolist', + 'idolize', + 'idolum', + 'idyll', + 'idyllic', + 'idyllist', + 'if', + 'iffy', + 'igloo', + 'igneous', + 'ignescent', + 'ignite', + 'igniter', + 'ignition', + 'ignitron', + 'ignoble', + 'ignominious', + 'ignominy', + 'ignoramus', + 'ignorance', + 'ignorant', + 'ignore', + 'iguana', + 'iguanodon', + 'ihram', + 'ikebana', + 'ikon', + 'ileac', + 'ileitis', + 'ileostomy', + 'ileum', + 'ileus', + 'ilex', + 'iliac', + 'ilium', + 'ilk', + 'ill', + 'illation', + 'illative', + 'illaudable', + 'illegal', + 'illegality', + 'illegalize', + 'illegible', + 'illegitimacy', + 'illegitimate', + 'illiberal', + 'illicit', + 'illimitable', + 'illinium', + 'illiquid', + 'illiteracy', + 'illiterate', + 'illness', + 'illogic', + 'illogical', + 'illogicality', + 'illume', + 'illuminance', + 'illuminant', + 'illuminate', + 'illuminati', + 'illuminating', + 'illumination', + 'illuminative', + 'illuminator', + 'illumine', + 'illuminism', + 'illuminometer', + 'illusion', + 'illusionary', + 'illusionism', + 'illusionist', + 'illusive', + 'illusory', + 'illustrate', + 'illustration', + 'illustrational', + 'illustrative', + 'illustrator', + 'illustrious', + 'illuviation', + 'ilmenite', + 'image', + 'imagery', + 'imaginable', + 'imaginal', + 'imaginary', + 'imagination', + 'imaginative', + 'imagine', + 'imagism', + 'imago', + 'imam', + 'imamate', + 'imaret', + 'imbalance', + 'imbecile', + 'imbecilic', + 'imbecility', + 'imbed', + 'imbibe', + 'imbibition', + 'imbricate', + 'imbrication', + 'imbroglio', + 'imbrue', + 'imbue', + 'imidazole', + 'imide', + 'imine', + 'iminourea', + 'imitable', + 'imitate', + 'imitation', + 'imitative', + 'immaculate', + 'immanent', + 'immaterial', + 'immaterialism', + 'immateriality', + 'immaterialize', + 'immature', + 'immeasurable', + 'immediacy', + 'immediate', + 'immediately', + 'immedicable', + 'immemorial', + 'immense', + 'immensity', + 'immensurable', + 'immerge', + 'immerse', + 'immersed', + 'immersion', + 'immersionism', + 'immesh', + 'immethodical', + 'immigrant', + 'immigrate', + 'immigration', + 'imminence', + 'imminent', + 'immingle', + 'immiscible', + 'immitigable', + 'immix', + 'immixture', + 'immobile', + 'immobility', + 'immobilize', + 'immoderacy', + 'immoderate', + 'immoderation', + 'immodest', + 'immolate', + 'immolation', + 'immoral', + 'immoralist', + 'immorality', + 'immortal', + 'immortality', + 'immortalize', + 'immortelle', + 'immotile', + 'immovable', + 'immune', + 'immunity', + 'immunize', + 'immunochemistry', + 'immunogenetics', + 'immunogenic', + 'immunology', + 'immunoreaction', + 'immunotherapy', + 'immure', + 'immutable', + 'imp', + 'impact', + 'impacted', + 'impaction', + 'impair', + 'impala', + 'impale', + 'impalpable', + 'impanation', + 'impanel', + 'imparadise', + 'imparipinnate', + 'imparisyllabic', + 'imparity', + 'impart', + 'impartial', + 'impartible', + 'impassable', + 'impasse', + 'impassible', + 'impassion', + 'impassioned', + 'impassive', + 'impaste', + 'impasto', + 'impatience', + 'impatiens', + 'impatient', + 'impeach', + 'impeachable', + 'impeachment', + 'impearl', + 'impeccable', + 'impeccant', + 'impecunious', + 'impedance', + 'impede', + 'impediment', + 'impedimenta', + 'impeditive', + 'impel', + 'impellent', + 'impeller', + 'impend', + 'impendent', + 'impending', + 'impenetrability', + 'impenetrable', + 'impenitent', + 'imperative', + 'imperator', + 'imperceptible', + 'imperception', + 'imperceptive', + 'impercipient', + 'imperfect', + 'imperfection', + 'imperfective', + 'imperforate', + 'imperial', + 'imperialism', + 'imperil', + 'imperious', + 'imperishable', + 'imperium', + 'impermanent', + 'impermeable', + 'impermissible', + 'impersonal', + 'impersonality', + 'impersonalize', + 'impersonate', + 'impertinence', + 'impertinent', + 'imperturbable', + 'imperturbation', + 'impervious', + 'impetigo', + 'impetrate', + 'impetuosity', + 'impetuous', + 'impetus', + 'impi', + 'impiety', + 'impignorate', + 'impinge', + 'impious', + 'impish', + 'implacable', + 'implacental', + 'implant', + 'implantation', + 'implausibility', + 'implausible', + 'implead', + 'implement', + 'impletion', + 'implicate', + 'implication', + 'implicative', + 'implicatory', + 'implicit', + 'implied', + 'implode', + 'implore', + 'implosion', + 'implosive', + 'imply', + 'impolicy', + 'impolite', + 'impolitic', + 'imponderabilia', + 'imponderable', + 'import', + 'importance', + 'important', + 'importation', + 'importunacy', + 'importunate', + 'importune', + 'importunity', + 'impose', + 'imposing', + 'imposition', + 'impossibility', + 'impossible', + 'impossibly', + 'impost', + 'impostor', + 'impostume', + 'imposture', + 'impotence', + 'impotent', + 'impound', + 'impoverish', + 'impoverished', + 'impower', + 'impracticable', + 'impractical', + 'imprecate', + 'imprecation', + 'imprecise', + 'imprecision', + 'impregnable', + 'impregnate', + 'impresa', + 'impresario', + 'imprescriptible', + 'impress', + 'impressible', + 'impression', + 'impressionable', + 'impressionism', + 'impressionist', + 'impressive', + 'impressment', + 'impressure', + 'imprest', + 'imprimatur', + 'imprimis', + 'imprint', + 'imprinting', + 'imprison', + 'imprisonment', + 'improbability', + 'improbable', + 'improbity', + 'impromptu', + 'improper', + 'impropriate', + 'impropriety', + 'improve', + 'improvement', + 'improvident', + 'improvisation', + 'improvisator', + 'improvisatory', + 'improvise', + 'improvised', + 'improvvisatore', + 'imprudent', + 'impudence', + 'impudent', + 'impudicity', + 'impugn', + 'impuissant', + 'impulse', + 'impulsion', + 'impulsive', + 'impunity', + 'impure', + 'impurity', + 'imputable', + 'imputation', + 'impute', + 'in', + 'inability', + 'inaccessible', + 'inaccuracy', + 'inaccurate', + 'inaction', + 'inactivate', + 'inactive', + 'inadequate', + 'inadmissible', + 'inadvertence', + 'inadvertency', + 'inadvertent', + 'inadvisable', + 'inalienable', + 'inalterable', + 'inamorata', + 'inamorato', + 'inane', + 'inanimate', + 'inanition', + 'inanity', + 'inappetence', + 'inapplicable', + 'inapposite', + 'inappreciable', + 'inappreciative', + 'inapprehensible', + 'inapprehensive', + 'inapproachable', + 'inappropriate', + 'inapt', + 'inaptitude', + 'inarch', + 'inarticulate', + 'inartificial', + 'inartistic', + 'inattention', + 'inattentive', + 'inaudible', + 'inaugural', + 'inaugurate', + 'inauspicious', + 'inbeing', + 'inboard', + 'inborn', + 'inbound', + 'inbreathe', + 'inbred', + 'inbreed', + 'inbreeding', + 'incalculable', + 'incalescent', + 'incandesce', + 'incandescence', + 'incandescent', + 'incantation', + 'incantatory', + 'incapable', + 'incapacious', + 'incapacitate', + 'incapacity', + 'incarcerate', + 'incardinate', + 'incardination', + 'incarnadine', + 'incarnate', + 'incarnation', + 'incase', + 'incautious', + 'incendiarism', + 'incendiary', + 'incense', + 'incensory', + 'incentive', + 'incept', + 'inception', + 'inceptive', + 'incertitude', + 'incessant', + 'incest', + 'incestuous', + 'inch', + 'inchmeal', + 'inchoate', + 'inchoation', + 'inchoative', + 'inchworm', + 'incidence', + 'incident', + 'incidental', + 'incidentally', + 'incinerate', + 'incinerator', + 'incipient', + 'incipit', + 'incise', + 'incised', + 'incision', + 'incisive', + 'incisor', + 'incisure', + 'incite', + 'incitement', + 'incivility', + 'inclement', + 'inclinable', + 'inclination', + 'inclinatory', + 'incline', + 'inclined', + 'inclining', + 'inclinometer', + 'inclose', + 'include', + 'included', + 'inclusion', + 'inclusive', + 'incoercible', + 'incogitable', + 'incogitant', + 'incognito', + 'incognizant', + 'incoherence', + 'incoherent', + 'incombustible', + 'income', + 'incomer', + 'incoming', + 'incommensurable', + 'incommensurate', + 'incommode', + 'incommodious', + 'incommodity', + 'incommunicable', + 'incommunicado', + 'incommunicative', + 'incommutable', + 'incomparable', + 'incompatible', + 'incompetence', + 'incompetent', + 'incomplete', + 'incompletion', + 'incompliant', + 'incomprehensible', + 'incomprehension', + 'incomprehensive', + 'incompressible', + 'incomputable', + 'inconceivable', + 'inconclusive', + 'incondensable', + 'incondite', + 'inconformity', + 'incongruent', + 'incongruity', + 'incongruous', + 'inconsecutive', + 'inconsequent', + 'inconsequential', + 'inconsiderable', + 'inconsiderate', + 'inconsistency', + 'inconsistent', + 'inconsolable', + 'inconsonant', + 'inconspicuous', + 'inconstant', + 'inconsumable', + 'incontestable', + 'incontinent', + 'incontrollable', + 'incontrovertible', + 'inconvenience', + 'inconveniency', + 'inconvenient', + 'inconvertible', + 'inconvincible', + 'incoordinate', + 'incoordination', + 'incorporable', + 'incorporate', + 'incorporated', + 'incorporating', + 'incorporation', + 'incorporator', + 'incorporeal', + 'incorporeity', + 'incorrect', + 'incorrigible', + 'incorrupt', + 'incorruptible', + 'incorruption', + 'incrassate', + 'increase', + 'increasing', + 'increate', + 'incredible', + 'incredulity', + 'incredulous', + 'increment', + 'increscent', + 'incretion', + 'incriminate', + 'incrust', + 'incrustation', + 'incubate', + 'incubation', + 'incubator', + 'incubus', + 'incudes', + 'inculcate', + 'inculpable', + 'inculpate', + 'incult', + 'incumbency', + 'incumbent', + 'incumber', + 'incunabula', + 'incunabulum', + 'incur', + 'incurable', + 'incurious', + 'incurrence', + 'incurrent', + 'incursion', + 'incursive', + 'incurvate', + 'incurve', + 'incus', + 'incuse', + 'indaba', + 'indamine', + 'indebted', + 'indebtedness', + 'indecency', + 'indecent', + 'indeciduous', + 'indecipherable', + 'indecision', + 'indecisive', + 'indeclinable', + 'indecorous', + 'indecorum', + 'indeed', + 'indefatigable', + 'indefeasible', + 'indefectible', + 'indefensible', + 'indefinable', + 'indefinite', + 'indehiscent', + 'indeliberate', + 'indelible', + 'indelicacy', + 'indelicate', + 'indemnification', + 'indemnify', + 'indemnity', + 'indemonstrable', + 'indene', + 'indent', + 'indentation', + 'indented', + 'indention', + 'indenture', + 'independence', + 'independency', + 'independent', + 'indescribable', + 'indestructible', + 'indeterminable', + 'indeterminacy', + 'indeterminate', + 'indetermination', + 'indeterminism', + 'indevout', + 'index', + 'indican', + 'indicant', + 'indicate', + 'indication', + 'indicative', + 'indicator', + 'indicatory', + 'indices', + 'indicia', + 'indict', + 'indictable', + 'indiction', + 'indictment', + 'indifference', + 'indifferent', + 'indifferentism', + 'indigence', + 'indigene', + 'indigenous', + 'indigent', + 'indigested', + 'indigestible', + 'indigestion', + 'indigestive', + 'indign', + 'indignant', + 'indignation', + 'indignity', + 'indigo', + 'indigoid', + 'indigotin', + 'indirect', + 'indirection', + 'indiscernible', + 'indiscerptible', + 'indiscipline', + 'indiscreet', + 'indiscrete', + 'indiscretion', + 'indiscriminate', + 'indiscrimination', + 'indispensable', + 'indispose', + 'indisposed', + 'indisposition', + 'indisputable', + 'indissoluble', + 'indistinct', + 'indistinctive', + 'indistinguishable', + 'indite', + 'indium', + 'indivertible', + 'individual', + 'individualism', + 'individualist', + 'individuality', + 'individualize', + 'individually', + 'individuate', + 'individuation', + 'indivisible', + 'indocile', + 'indoctrinate', + 'indole', + 'indolence', + 'indolent', + 'indomitability', + 'indomitable', + 'indoor', + 'indoors', + 'indophenol', + 'indorse', + 'indoxyl', + 'indraft', + 'indrawn', + 'indubitability', + 'indubitable', + 'induce', + 'inducement', + 'induct', + 'inductance', + 'inductee', + 'inductile', + 'induction', + 'inductive', + 'inductor', + 'indue', + 'indulge', + 'indulgence', + 'indulgent', + 'induline', + 'indult', + 'induna', + 'induplicate', + 'indurate', + 'induration', + 'indusium', + 'industrial', + 'industrialism', + 'industrialist', + 'industrialize', + 'industrials', + 'industrious', + 'industry', + 'indwell', + 'inearth', + 'inebriant', + 'inebriate', + 'inebriety', + 'inedible', + 'inedited', + 'ineducable', + 'ineducation', + 'ineffable', + 'ineffaceable', + 'ineffective', + 'ineffectual', + 'inefficacious', + 'inefficacy', + 'inefficiency', + 'inefficient', + 'inelastic', + 'inelegance', + 'inelegancy', + 'inelegant', + 'ineligible', + 'ineloquent', + 'ineluctable', + 'ineludible', + 'inenarrable', + 'inept', + 'ineptitude', + 'inequality', + 'inequitable', + 'inequity', + 'ineradicable', + 'inerasable', + 'inerrable', + 'inerrant', + 'inert', + 'inertia', + 'inescapable', + 'inescutcheon', + 'inessential', + 'inessive', + 'inestimable', + 'inevasible', + 'inevitable', + 'inexact', + 'inexactitude', + 'inexcusable', + 'inexecution', + 'inexertion', + 'inexhaustible', + 'inexistent', + 'inexorable', + 'inexpedient', + 'inexpensive', + 'inexperience', + 'inexperienced', + 'inexpert', + 'inexpiable', + 'inexplicable', + 'inexplicit', + 'inexpressible', + 'inexpressive', + 'inexpugnable', + 'inexpungible', + 'inextensible', + 'inextinguishable', + 'inextirpable', + 'inextricable', + 'infallibilism', + 'infallible', + 'infamous', + 'infamy', + 'infancy', + 'infant', + 'infanta', + 'infante', + 'infanticide', + 'infantile', + 'infantilism', + 'infantine', + 'infantry', + 'infantryman', + 'infarct', + 'infarction', + 'infare', + 'infatuate', + 'infatuated', + 'infatuation', + 'infeasible', + 'infect', + 'infection', + 'infectious', + 'infective', + 'infecund', + 'infelicitous', + 'infelicity', + 'infer', + 'inference', + 'inferential', + 'inferior', + 'infernal', + 'inferno', + 'infertile', + 'infest', + 'infestation', + 'infeudation', + 'infidel', + 'infidelity', + 'infield', + 'infielder', + 'infighting', + 'infiltrate', + 'infiltration', + 'infinite', + 'infinitesimal', + 'infinitive', + 'infinitude', + 'infinity', + 'infirm', + 'infirmary', + 'infirmity', + 'infix', + 'inflame', + 'inflammable', + 'inflammation', + 'inflammatory', + 'inflatable', + 'inflate', + 'inflated', + 'inflation', + 'inflationary', + 'inflationism', + 'inflect', + 'inflection', + 'inflectional', + 'inflexed', + 'inflexible', + 'inflexion', + 'inflict', + 'infliction', + 'inflorescence', + 'inflow', + 'influence', + 'influent', + 'influential', + 'influenza', + 'influx', + 'infold', + 'inform', + 'informal', + 'informality', + 'informant', + 'information', + 'informative', + 'informed', + 'informer', + 'infra', + 'infracostal', + 'infract', + 'infraction', + 'infralapsarian', + 'infrangible', + 'infrared', + 'infrasonic', + 'infrastructure', + 'infrequency', + 'infrequent', + 'infringe', + 'infringement', + 'infundibuliform', + 'infundibulum', + 'infuriate', + 'infuscate', + 'infuse', + 'infusible', + 'infusion', + 'infusionism', + 'infusive', + 'infusorian', + 'ingate', + 'ingather', + 'ingathering', + 'ingeminate', + 'ingenerate', + 'ingenious', + 'ingenue', + 'ingenuity', + 'ingenuous', + 'ingest', + 'ingesta', + 'ingle', + 'inglenook', + 'ingleside', + 'inglorious', + 'ingoing', + 'ingot', + 'ingraft', + 'ingrain', + 'ingrained', + 'ingrate', + 'ingratiate', + 'ingratiating', + 'ingratitude', + 'ingravescent', + 'ingredient', + 'ingress', + 'ingressive', + 'ingroup', + 'ingrowing', + 'ingrown', + 'ingrowth', + 'inguinal', + 'ingulf', + 'ingurgitate', + 'inhabit', + 'inhabitancy', + 'inhabitant', + 'inhabited', + 'inhabiter', + 'inhalant', + 'inhalation', + 'inhalator', + 'inhale', + 'inhaler', + 'inharmonic', + 'inharmonious', + 'inhaul', + 'inhere', + 'inherence', + 'inherent', + 'inherit', + 'inheritable', + 'inheritance', + 'inherited', + 'inheritor', + 'inheritrix', + 'inhesion', + 'inhibit', + 'inhibition', + 'inhibitor', + 'inhibitory', + 'inhospitable', + 'inhospitality', + 'inhuman', + 'inhumane', + 'inhumanity', + 'inhumation', + 'inhume', + 'inimical', + 'inimitable', + 'inion', + 'iniquitous', + 'iniquity', + 'initial', + 'initiate', + 'initiation', + 'initiative', + 'initiatory', + 'inject', + 'injection', + 'injector', + 'injudicious', + 'injunction', + 'injure', + 'injured', + 'injurious', + 'injury', + 'injustice', + 'ink', + 'inkberry', + 'inkblot', + 'inkhorn', + 'inkle', + 'inkling', + 'inkstand', + 'inkwell', + 'inky', + 'inlaid', + 'inland', + 'inlay', + 'inlet', + 'inlier', + 'inly', + 'inmate', + 'inmesh', + 'inmost', + 'inn', + 'innards', + 'innate', + 'inner', + 'innermost', + 'innervate', + 'innerve', + 'inning', + 'innings', + 'innkeeper', + 'innocence', + 'innocency', + 'innocent', + 'innocuous', + 'innominate', + 'innovate', + 'innovation', + 'innoxious', + 'innuendo', + 'innumerable', + 'innutrition', + 'inobservance', + 'inoculable', + 'inoculate', + 'inoculation', + 'inoculum', + 'inodorous', + 'inoffensive', + 'inofficious', + 'inoperable', + 'inoperative', + 'inopportune', + 'inordinate', + 'inorganic', + 'inosculate', + 'inositol', + 'inotropic', + 'inpatient', + 'inpour', + 'input', + 'inquest', + 'inquietude', + 'inquiline', + 'inquire', + 'inquiring', + 'inquiry', + 'inquisition', + 'inquisitionist', + 'inquisitive', + 'inquisitor', + 'inquisitorial', + 'inroad', + 'inrush', + 'insalivate', + 'insalubrious', + 'insane', + 'insanitary', + 'insanity', + 'insatiable', + 'insatiate', + 'inscribe', + 'inscription', + 'inscrutable', + 'insect', + 'insectarium', + 'insecticide', + 'insectile', + 'insectivore', + 'insectivorous', + 'insecure', + 'insecurity', + 'inseminate', + 'insensate', + 'insensibility', + 'insensible', + 'insensitive', + 'insentient', + 'inseparable', + 'insert', + 'inserted', + 'insertion', + 'insessorial', + 'inset', + 'inseverable', + 'inshore', + 'inshrine', + 'inside', + 'insider', + 'insidious', + 'insight', + 'insightful', + 'insignia', + 'insignificance', + 'insignificancy', + 'insignificant', + 'insincere', + 'insincerity', + 'insinuate', + 'insinuating', + 'insinuation', + 'insipid', + 'insipience', + 'insist', + 'insistence', + 'insistency', + 'insistent', + 'insnare', + 'insobriety', + 'insociable', + 'insolate', + 'insolation', + 'insole', + 'insolence', + 'insolent', + 'insoluble', + 'insolvable', + 'insolvency', + 'insolvent', + 'insomnia', + 'insomniac', + 'insomnolence', + 'insomuch', + 'insouciance', + 'insouciant', + 'inspan', + 'inspect', + 'inspection', + 'inspector', + 'inspectorate', + 'insphere', + 'inspiration', + 'inspirational', + 'inspiratory', + 'inspire', + 'inspired', + 'inspirit', + 'inspissate', + 'instability', + 'instable', + 'instal', + 'install', + 'installation', + 'installment', + 'instalment', + 'instance', + 'instancy', + 'instant', + 'instantaneity', + 'instantaneous', + 'instanter', + 'instantly', + 'instar', + 'instate', + 'instauration', + 'instead', + 'instep', + 'instigate', + 'instigation', + 'instil', + 'instill', + 'instillation', + 'instinct', + 'instinctive', + 'institute', + 'institution', + 'institutional', + 'institutionalism', + 'institutionalize', + 'institutive', + 'institutor', + 'instruct', + 'instruction', + 'instructions', + 'instructive', + 'instructor', + 'instrument', + 'instrumental', + 'instrumentalism', + 'instrumentalist', + 'instrumentality', + 'instrumentation', + 'insubordinate', + 'insubstantial', + 'insufferable', + 'insufficiency', + 'insufficient', + 'insufflate', + 'insula', + 'insular', + 'insulate', + 'insulation', + 'insulator', + 'insulin', + 'insult', + 'insulting', + 'insuperable', + 'insupportable', + 'insuppressible', + 'insurable', + 'insurance', + 'insure', + 'insured', + 'insurer', + 'insurgence', + 'insurgency', + 'insurgent', + 'insurmountable', + 'insurrection', + 'insurrectionary', + 'insusceptible', + 'intact', + 'intaglio', + 'intake', + 'intangible', + 'intarsia', + 'integer', + 'integral', + 'integrand', + 'integrant', + 'integrate', + 'integrated', + 'integration', + 'integrator', + 'integrity', + 'integument', + 'integumentary', + 'intellect', + 'intellection', + 'intellectual', + 'intellectualism', + 'intellectuality', + 'intellectualize', + 'intelligence', + 'intelligencer', + 'intelligent', + 'intelligentsia', + 'intelligibility', + 'intelligible', + 'intemerate', + 'intemperance', + 'intemperate', + 'intend', + 'intendance', + 'intendancy', + 'intendant', + 'intended', + 'intendment', + 'intenerate', + 'intense', + 'intensifier', + 'intensify', + 'intension', + 'intensity', + 'intensive', + 'intent', + 'intention', + 'intentional', + 'inter', + 'interact', + 'interaction', + 'interactive', + 'interatomic', + 'interbedded', + 'interblend', + 'interbrain', + 'interbreed', + 'intercalary', + 'intercalate', + 'intercalation', + 'intercede', + 'intercellular', + 'intercept', + 'interception', + 'interceptor', + 'intercession', + 'intercessor', + 'intercessory', + 'interchange', + 'interchangeable', + 'interclavicle', + 'intercollegiate', + 'intercolumniation', + 'intercom', + 'intercommunicate', + 'intercommunion', + 'interconnect', + 'intercontinental', + 'intercostal', + 'intercourse', + 'intercrop', + 'intercross', + 'intercurrent', + 'intercut', + 'interdenominational', + 'interdental', + 'interdepartmental', + 'interdependent', + 'interdict', + 'interdiction', + 'interdictory', + 'interdigitate', + 'interdisciplinary', + 'interest', + 'interested', + 'interesting', + 'interface', + 'interfaith', + 'interfere', + 'interference', + 'interferometer', + 'interferon', + 'interfertile', + 'interfile', + 'interflow', + 'interfluent', + 'interfluve', + 'interfuse', + 'interglacial', + 'intergrade', + 'interim', + 'interinsurance', + 'interior', + 'interjacent', + 'interject', + 'interjection', + 'interjoin', + 'interknit', + 'interlace', + 'interlaminate', + 'interlanguage', + 'interlard', + 'interlay', + 'interleaf', + 'interleave', + 'interline', + 'interlinear', + 'interlineate', + 'interlining', + 'interlink', + 'interlock', + 'interlocution', + 'interlocutor', + 'interlocutory', + 'interlocutress', + 'interlocutrix', + 'interlope', + 'interloper', + 'interlude', + 'interlunar', + 'interlunation', + 'intermarriage', + 'intermarry', + 'intermeddle', + 'intermediacy', + 'intermediary', + 'intermediate', + 'interment', + 'intermezzo', + 'intermigration', + 'interminable', + 'intermingle', + 'intermission', + 'intermit', + 'intermittent', + 'intermix', + 'intermixture', + 'intermolecular', + 'intern', + 'internal', + 'internalize', + 'international', + 'internationalism', + 'internationalist', + 'internationalize', + 'interne', + 'internecine', + 'internee', + 'internist', + 'internment', + 'internode', + 'internship', + 'internuncial', + 'internuncio', + 'interoceptor', + 'interoffice', + 'interosculate', + 'interpellant', + 'interpellate', + 'interpellation', + 'interpenetrate', + 'interphase', + 'interphone', + 'interplanetary', + 'interplay', + 'interplead', + 'interpleader', + 'interpolate', + 'interpolation', + 'interpose', + 'interposition', + 'interpret', + 'interpretation', + 'interpretative', + 'interpreter', + 'interpretive', + 'interracial', + 'interradial', + 'interregnum', + 'interrelate', + 'interrelated', + 'interrelation', + 'interrex', + 'interrogate', + 'interrogation', + 'interrogative', + 'interrogator', + 'interrogatory', + 'interrupt', + 'interrupted', + 'interrupter', + 'interruption', + 'interscholastic', + 'intersect', + 'intersection', + 'intersex', + 'intersexual', + 'intersidereal', + 'interspace', + 'intersperse', + 'interstadial', + 'interstate', + 'interstellar', + 'interstice', + 'interstitial', + 'interstratify', + 'intertexture', + 'intertidal', + 'intertwine', + 'intertwist', + 'interurban', + 'interval', + 'intervale', + 'intervalometer', + 'intervene', + 'intervenient', + 'intervention', + 'interventionist', + 'interview', + 'interviewee', + 'interviewer', + 'intervocalic', + 'interweave', + 'interwork', + 'intestate', + 'intestinal', + 'intestine', + 'intima', + 'intimacy', + 'intimate', + 'intimidate', + 'intimist', + 'intinction', + 'intine', + 'intitule', + 'into', + 'intolerable', + 'intolerance', + 'intolerant', + 'intonate', + 'intonation', + 'intone', + 'intorsion', + 'intort', + 'intoxicant', + 'intoxicate', + 'intoxicated', + 'intoxicating', + 'intoxication', + 'intoxicative', + 'intracardiac', + 'intracellular', + 'intracranial', + 'intractable', + 'intracutaneous', + 'intradermal', + 'intrados', + 'intramolecular', + 'intramundane', + 'intramural', + 'intramuscular', + 'intransigeance', + 'intransigence', + 'intransigent', + 'intransitive', + 'intranuclear', + 'intrastate', + 'intratelluric', + 'intrauterine', + 'intravasation', + 'intravenous', + 'intreat', + 'intrench', + 'intrepid', + 'intricacy', + 'intricate', + 'intrigant', + 'intrigante', + 'intrigue', + 'intrinsic', + 'intro', + 'introduce', + 'introduction', + 'introductory', + 'introgression', + 'introit', + 'introject', + 'introjection', + 'intromission', + 'intromit', + 'introrse', + 'introspect', + 'introspection', + 'introversion', + 'introvert', + 'intrude', + 'intrusion', + 'intrusive', + 'intrust', + 'intubate', + 'intuit', + 'intuition', + 'intuitional', + 'intuitionism', + 'intuitive', + 'intuitivism', + 'intumesce', + 'intumescence', + 'intussuscept', + 'intussusception', + 'intwine', + 'inulin', + 'inunction', + 'inundate', + 'inurbane', + 'inure', + 'inurn', + 'inutile', + 'inutility', + 'invade', + 'invaginate', + 'invagination', + 'invalid', + 'invalidate', + 'invalidism', + 'invalidity', + 'invaluable', + 'invariable', + 'invariant', + 'invasion', + 'invasive', + 'invective', + 'inveigh', + 'inveigle', + 'invent', + 'invention', + 'inventive', + 'inventor', + 'inventory', + 'inveracity', + 'inverse', + 'inversely', + 'inversion', + 'invert', + 'invertase', + 'invertebrate', + 'inverter', + 'invest', + 'investigate', + 'investigation', + 'investigator', + 'investiture', + 'investment', + 'inveteracy', + 'inveterate', + 'invidious', + 'invigilate', + 'invigorate', + 'invincible', + 'inviolable', + 'inviolate', + 'invisible', + 'invitation', + 'invitatory', + 'invite', + 'inviting', + 'invocate', + 'invocation', + 'invoice', + 'invoke', + 'involucel', + 'involucre', + 'involucrum', + 'involuntary', + 'involute', + 'involuted', + 'involution', + 'involutional', + 'involve', + 'involved', + 'invulnerable', + 'inward', + 'inwardly', + 'inwardness', + 'inwards', + 'inweave', + 'inwrap', + 'inwrought', + 'iodate', + 'iodic', + 'iodide', + 'iodine', + 'iodism', + 'iodize', + 'iodoform', + 'iodometry', + 'iodous', + 'iolite', + 'ion', + 'ionic', + 'ionium', + 'ionization', + 'ionize', + 'ionogen', + 'ionone', + 'ionopause', + 'ionosphere', + 'iota', + 'iotacism', + 'ipecac', + 'ipomoea', + 'iracund', + 'irade', + 'irascible', + 'irate', + 'ire', + 'ireful', + 'irenic', + 'irenics', + 'iridaceous', + 'iridectomy', + 'iridescence', + 'iridescent', + 'iridic', + 'iridium', + 'iridize', + 'iridosmine', + 'iridotomy', + 'iris', + 'irisation', + 'iritis', + 'irk', + 'irksome', + 'iron', + 'ironbark', + 'ironbound', + 'ironclad', + 'ironhanded', + 'ironic', + 'ironing', + 'ironist', + 'ironlike', + 'ironmaster', + 'ironmonger', + 'irons', + 'ironsides', + 'ironsmith', + 'ironstone', + 'ironware', + 'ironwood', + 'ironwork', + 'ironworker', + 'ironworks', + 'irony', + 'irradiance', + 'irradiant', + 'irradiate', + 'irradiation', + 'irrational', + 'irrationality', + 'irreclaimable', + 'irreconcilable', + 'irrecoverable', + 'irrecusable', + 'irredeemable', + 'irredentist', + 'irreducible', + 'irreformable', + 'irrefragable', + 'irrefrangible', + 'irrefutable', + 'irregular', + 'irregularity', + 'irrelative', + 'irrelevance', + 'irrelevancy', + 'irrelevant', + 'irrelievable', + 'irreligion', + 'irreligious', + 'irremeable', + 'irremediable', + 'irremissible', + 'irremovable', + 'irreparable', + 'irrepealable', + 'irreplaceable', + 'irrepressible', + 'irreproachable', + 'irresistible', + 'irresoluble', + 'irresolute', + 'irresolution', + 'irresolvable', + 'irrespective', + 'irrespirable', + 'irresponsible', + 'irresponsive', + 'irretentive', + 'irretrievable', + 'irreverence', + 'irreverent', + 'irreversible', + 'irrevocable', + 'irrigate', + 'irrigation', + 'irriguous', + 'irritability', + 'irritable', + 'irritant', + 'irritate', + 'irritated', + 'irritating', + 'irritation', + 'irritative', + 'irrupt', + 'irruption', + 'irruptive', + 'is', + 'isagoge', + 'isagogics', + 'isallobar', + 'isatin', + 'ischium', + 'isentropic', + 'isinglass', + 'island', + 'islander', + 'isle', + 'islet', + 'ism', + 'isoagglutination', + 'isoagglutinin', + 'isobar', + 'isobaric', + 'isobath', + 'isocheim', + 'isochor', + 'isochromatic', + 'isochronal', + 'isochronism', + 'isochronize', + 'isochronous', + 'isochroous', + 'isoclinal', + 'isocline', + 'isocracy', + 'isocyanide', + 'isodiametric', + 'isodimorphism', + 'isodynamic', + 'isoelectronic', + 'isogamete', + 'isogamy', + 'isogloss', + 'isogonic', + 'isolate', + 'isolated', + 'isolating', + 'isolation', + 'isolationism', + 'isolationist', + 'isolative', + 'isolecithal', + 'isoleucine', + 'isoline', + 'isologous', + 'isomagnetic', + 'isomer', + 'isomeric', + 'isomerism', + 'isomerize', + 'isomerous', + 'isometric', + 'isometrics', + 'isometropia', + 'isometry', + 'isomorph', + 'isomorphism', + 'isoniazid', + 'isonomy', + 'isooctane', + 'isopiestic', + 'isopleth', + 'isopod', + 'isoprene', + 'isopropanol', + 'isopropyl', + 'isosceles', + 'isostasy', + 'isosteric', + 'isothere', + 'isotherm', + 'isothermal', + 'isotone', + 'isotonic', + 'isotope', + 'isotron', + 'isotropic', + 'issuable', + 'issuance', + 'issuant', + 'issue', + 'isthmian', + 'isthmus', + 'istle', + 'it', + 'itacolumite', + 'italic', + 'italicize', + 'itch', + 'itching', + 'itchy', + 'item', + 'itemize', + 'itemized', + 'iterate', + 'iterative', + 'ithyphallic', + 'itinerancy', + 'itinerant', + 'itinerary', + 'itinerate', + 'its', + 'itself', + 'ivied', + 'ivories', + 'ivory', + 'ivy', + 'iwis', + 'ixia', + 'ixtle', + 'izard', + 'izzard', + 'j', + 'ja', + 'jab', + 'jabber', + 'jabberwocky', + 'jabiru', + 'jaborandi', + 'jabot', + 'jacal', + 'jacamar', + 'jacaranda', + 'jacinth', + 'jack', + 'jackal', + 'jackanapes', + 'jackass', + 'jackboot', + 'jackdaw', + 'jackeroo', + 'jacket', + 'jackfish', + 'jackfruit', + 'jackhammer', + 'jackknife', + 'jackleg', + 'jacklight', + 'jacklighter', + 'jackpot', + 'jackrabbit', + 'jacks', + 'jackscrew', + 'jackshaft', + 'jacksmelt', + 'jacksnipe', + 'jackstay', + 'jackstraw', + 'jackstraws', + 'jacobus', + 'jaconet', + 'jacquard', + 'jactation', + 'jactitation', + 'jade', + 'jaded', + 'jadeite', + 'jaeger', + 'jag', + 'jagged', + 'jaggery', + 'jaggy', + 'jaguar', + 'jaguarundi', + 'jail', + 'jailbird', + 'jailbreak', + 'jailer', + 'jailhouse', + 'jakes', + 'jalap', + 'jalopy', + 'jalousie', + 'jam', + 'jamb', + 'jambalaya', + 'jambeau', + 'jamboree', + 'jampan', + 'jane', + 'jangle', + 'janitor', + 'janitress', + 'japan', + 'jape', + 'japonica', + 'jar', + 'jardiniere', + 'jargon', + 'jargonize', + 'jarl', + 'jarosite', + 'jarvey', + 'jasmine', + 'jasper', + 'jato', + 'jaundice', + 'jaundiced', + 'jaunt', + 'jaunty', + 'javelin', + 'jaw', + 'jawbone', + 'jawbreaker', + 'jaws', + 'jay', + 'jaywalk', + 'jazz', + 'jazzman', + 'jazzy', + 'jealous', + 'jealousy', + 'jean', + 'jeans', + 'jebel', + 'jeep', + 'jeepers', + 'jeer', + 'jefe', + 'jehad', + 'jejune', + 'jejunum', + 'jell', + 'jellaba', + 'jellied', + 'jellify', + 'jelly', + 'jellybean', + 'jellyfish', + 'jemadar', + 'jemmy', + 'jennet', + 'jenny', + 'jeopardize', + 'jeopardous', + 'jeopardy', + 'jequirity', + 'jerboa', + 'jeremiad', + 'jerid', + 'jerk', + 'jerkin', + 'jerkwater', + 'jerky', + 'jeroboam', + 'jerreed', + 'jerry', + 'jersey', + 'jess', + 'jessamine', + 'jest', + 'jester', + 'jesting', + 'jet', + 'jetliner', + 'jetport', + 'jetsam', + 'jettison', + 'jetton', + 'jetty', + 'jewel', + 'jeweler', + 'jewelfish', + 'jeweller', + 'jewelry', + 'jewfish', + 'jib', + 'jibber', + 'jibe', + 'jiffy', + 'jig', + 'jigaboo', + 'jigger', + 'jiggered', + 'jiggermast', + 'jigging', + 'jiggle', + 'jigsaw', + 'jihad', + 'jill', + 'jillion', + 'jilt', + 'jimjams', + 'jimmy', + 'jimsonweed', + 'jingle', + 'jingo', + 'jingoism', + 'jink', + 'jinn', + 'jinni', + 'jinrikisha', + 'jinx', + 'jipijapa', + 'jitney', + 'jitter', + 'jitterbug', + 'jitters', + 'jittery', + 'jiujitsu', + 'jiva', + 'jive', + 'jo', + 'joannes', + 'job', + 'jobber', + 'jobbery', + 'jobholder', + 'jobless', + 'jock', + 'jockey', + 'jocko', + 'jockstrap', + 'jocose', + 'jocosity', + 'jocular', + 'jocularity', + 'jocund', + 'jocundity', + 'jodhpur', + 'jodhpurs', + 'joey', + 'jog', + 'joggle', + 'johannes', + 'john', + 'johnny', + 'johnnycake', + 'join', + 'joinder', + 'joiner', + 'joinery', + 'joint', + 'jointed', + 'jointer', + 'jointless', + 'jointly', + 'jointress', + 'jointure', + 'jointworm', + 'joist', + 'joke', + 'joker', + 'jokester', + 'jollification', + 'jollify', + 'jollity', + 'jolly', + 'jolt', + 'jolty', + 'jongleur', + 'jonquil', + 'jook', + 'jornada', + 'jorum', + 'josh', + 'joss', + 'jostle', + 'jot', + 'jota', + 'jotter', + 'jotting', + 'joule', + 'jounce', + 'journal', + 'journalese', + 'journalism', + 'journalist', + 'journalistic', + 'journalize', + 'journey', + 'journeyman', + 'journeywork', + 'joust', + 'jovial', + 'joviality', + 'jowl', + 'joy', + 'joyance', + 'joyful', + 'joyless', + 'joyous', + 'juba', + 'jubbah', + 'jube', + 'jubilant', + 'jubilate', + 'jubilation', + 'jubilee', + 'judge', + 'judgeship', + 'judgment', + 'judicable', + 'judicative', + 'judicator', + 'judicatory', + 'judicature', + 'judicial', + 'judiciary', + 'judicious', + 'judo', + 'judoka', + 'jug', + 'jugal', + 'jugate', + 'juggernaut', + 'juggins', + 'juggle', + 'juggler', + 'jugglery', + 'jughead', + 'juglandaceous', + 'jugular', + 'jugulate', + 'jugum', + 'juice', + 'juicy', + 'jujitsu', + 'juju', + 'jujube', + 'jujutsu', + 'jukebox', + 'julep', + 'julienne', + 'jumble', + 'jumbled', + 'jumbo', + 'jumbuck', + 'jump', + 'jumper', + 'jumpy', + 'juncaceous', + 'junco', + 'junction', + 'juncture', + 'jungle', + 'jungly', + 'junior', + 'juniority', + 'juniper', + 'junk', + 'junket', + 'junkie', + 'junkman', + 'junkyard', + 'junta', + 'junto', + 'jupon', + 'jura', + 'jural', + 'jurat', + 'juratory', + 'jurel', + 'juridical', + 'jurisconsult', + 'jurisdiction', + 'jurisprudence', + 'jurisprudent', + 'jurist', + 'juristic', + 'juror', + 'jury', + 'juryman', + 'jurywoman', + 'jus', + 'jussive', + 'just', + 'justice', + 'justiceship', + 'justiciable', + 'justiciar', + 'justiciary', + 'justifiable', + 'justification', + 'justificatory', + 'justifier', + 'justify', + 'justle', + 'justly', + 'justness', + 'jut', + 'jute', + 'jutty', + 'juvenal', + 'juvenescence', + 'juvenescent', + 'juvenile', + 'juvenilia', + 'juvenility', + 'juxtapose', + 'juxtaposition', + 'k', + 'ka', + 'kab', + 'kabob', + 'kabuki', + 'kachina', + 'kadi', + 'kaffiyeh', + 'kaftan', + 'kagu', + 'kaiak', + 'kaif', + 'kail', + 'kailyard', + 'kain', + 'kainite', + 'kaiser', + 'kaiserdom', + 'kaiserism', + 'kaisership', + 'kaka', + 'kakapo', + 'kakemono', + 'kaki', + 'kale', + 'kaleidoscope', + 'kaleidoscopic', + 'kalends', + 'kaleyard', + 'kali', + 'kalian', + 'kalif', + 'kalmia', + 'kalong', + 'kalpa', + 'kalpak', + 'kalsomine', + 'kamacite', + 'kamala', + 'kame', + 'kami', + 'kamikaze', + 'kampong', + 'kamseen', + 'kana', + 'kangaroo', + 'kanji', + 'kantar', + 'kanzu', + 'kaoliang', + 'kaolin', + 'kaolinite', + 'kaon', + 'kaph', + 'kapok', + 'kappa', + 'kaput', + 'karakul', + 'karat', + 'karate', + 'karma', + 'karmadharaya', + 'kaross', + 'karst', + 'karyogamy', + 'karyokinesis', + 'karyolymph', + 'karyolysis', + 'karyoplasm', + 'karyosome', + 'karyotin', + 'karyotype', + 'kasha', + 'kasher', + 'kashmir', + 'kat', + 'katabasis', + 'katabatic', + 'katabolism', + 'katakana', + 'katharsis', + 'katydid', + 'katzenjammer', + 'kauri', + 'kava', + 'kayak', + 'kayo', + 'kazachok', + 'kazoo', + 'kb', + 'kc', + 'kcal', + 'kea', + 'kebab', + 'keck', + 'ked', + 'keddah', + 'kedge', + 'kedgeree', + 'keef', + 'keek', + 'keel', + 'keelboat', + 'keelhaul', + 'keelson', + 'keen', + 'keening', + 'keep', + 'keeper', + 'keeping', + 'keepsake', + 'keeshond', + 'kef', + 'keffiyeh', + 'keg', + 'kegler', + 'keister', + 'keitloa', + 'kelly', + 'keloid', + 'kelp', + 'kelpie', + 'kelson', + 'kelt', + 'kelter', + 'ken', + 'kenaf', + 'kendo', + 'kennel', + 'kenning', + 'keno', + 'kenogenesis', + 'kenosis', + 'kenspeckle', + 'kentledge', + 'kep', + 'kepi', + 'kept', + 'keramic', + 'keramics', + 'keratin', + 'keratinize', + 'keratitis', + 'keratogenous', + 'keratoid', + 'keratoplasty', + 'keratose', + 'keratosis', + 'kerb', + 'kerbing', + 'kerbstone', + 'kerchief', + 'kerf', + 'kermes', + 'kermis', + 'kern', + 'kernel', + 'kernite', + 'kero', + 'kerosene', + 'kerplunk', + 'kersey', + 'kerseymere', + 'kestrel', + 'ketch', + 'ketchup', + 'ketene', + 'ketone', + 'ketonuria', + 'ketose', + 'ketosis', + 'kettle', + 'kettledrum', + 'kettledrummer', + 'kevel', + 'kex', + 'key', + 'keyboard', + 'keyhole', + 'keynote', + 'keystone', + 'keystroke', + 'keyway', + 'kg', + 'khaddar', + 'khaki', + 'khalif', + 'khamsin', + 'khan', + 'khanate', + 'kharif', + 'khat', + 'kheda', + 'khedive', + 'kiang', + 'kibble', + 'kibbutz', + 'kibbutznik', + 'kibe', + 'kibitka', + 'kibitz', + 'kibitzer', + 'kiblah', + 'kibosh', + 'kick', + 'kickback', + 'kicker', + 'kickoff', + 'kickshaw', + 'kicksorter', + 'kickstand', + 'kid', + 'kidding', + 'kiddy', + 'kidnap', + 'kidney', + 'kidskin', + 'kief', + 'kier', + 'kieselguhr', + 'kieserite', + 'kif', + 'kike', + 'kilderkin', + 'kill', + 'killdeer', + 'killer', + 'killick', + 'killifish', + 'killing', + 'killjoy', + 'kiln', + 'kilo', + 'kilocalorie', + 'kilocycle', + 'kilogram', + 'kilohertz', + 'kiloliter', + 'kilometer', + 'kiloton', + 'kilovolt', + 'kilowatt', + 'kilt', + 'kilter', + 'kimberlite', + 'kimono', + 'kin', + 'kinaesthesia', + 'kinase', + 'kind', + 'kindergarten', + 'kindergartner', + 'kindhearted', + 'kindle', + 'kindless', + 'kindliness', + 'kindling', + 'kindly', + 'kindness', + 'kindred', + 'kine', + 'kinematics', + 'kinematograph', + 'kinescope', + 'kinesics', + 'kinesiology', + 'kinesthesia', + 'kinetic', + 'kinetics', + 'kinfolk', + 'king', + 'kingbird', + 'kingbolt', + 'kingcraft', + 'kingcup', + 'kingdom', + 'kingfish', + 'kingfisher', + 'kinghood', + 'kinglet', + 'kingly', + 'kingmaker', + 'kingpin', + 'kingship', + 'kingwood', + 'kinin', + 'kink', + 'kinkajou', + 'kinky', + 'kinnikinnick', + 'kino', + 'kinsfolk', + 'kinship', + 'kinsman', + 'kinswoman', + 'kiosk', + 'kip', + 'kipper', + 'kirk', + 'kirkman', + 'kirmess', + 'kirtle', + 'kish', + 'kishke', + 'kismet', + 'kiss', + 'kissable', + 'kisser', + 'kist', + 'kit', + 'kitchen', + 'kitchener', + 'kitchenette', + 'kitchenmaid', + 'kitchenware', + 'kite', + 'kith', + 'kithara', + 'kitsch', + 'kitten', + 'kittenish', + 'kittiwake', + 'kittle', + 'kitty', + 'kiva', + 'kiwi', + 'klaxon', + 'klepht', + 'kleptomania', + 'klipspringer', + 'klong', + 'kloof', + 'klutz', + 'klystron', + 'km', + 'knack', + 'knacker', + 'knackwurst', + 'knap', + 'knapsack', + 'knapweed', + 'knar', + 'knave', + 'knavery', + 'knavish', + 'knawel', + 'knead', + 'knee', + 'kneecap', + 'kneehole', + 'kneel', + 'kneepad', + 'kneepan', + 'knell', + 'knelt', + 'knew', + 'knickerbockers', + 'knickers', + 'knickknack', + 'knife', + 'knight', + 'knighthead', + 'knighthood', + 'knightly', + 'knish', + 'knit', + 'knitted', + 'knitting', + 'knitwear', + 'knives', + 'knob', + 'knobby', + 'knobkerrie', + 'knock', + 'knockabout', + 'knocker', + 'knockout', + 'knockwurst', + 'knoll', + 'knop', + 'knot', + 'knotgrass', + 'knothole', + 'knotted', + 'knotting', + 'knotty', + 'knotweed', + 'knout', + 'know', + 'knowable', + 'knowing', + 'knowledge', + 'knowledgeable', + 'known', + 'knuckle', + 'knucklebone', + 'knucklehead', + 'knur', + 'knurl', + 'knurled', + 'knurly', + 'koa', + 'koala', + 'koan', + 'kob', + 'kobold', + 'koel', + 'kohl', + 'kohlrabi', + 'koine', + 'kokanee', + 'kola', + 'kolinsky', + 'kolkhoz', + 'kolo', + 'komatik', + 'koniology', + 'koodoo', + 'kook', + 'kookaburra', + 'kooky', + 'kop', + 'kopeck', + 'koph', + 'kopje', + 'kor', + 'koruna', + 'kos', + 'kosher', + 'koto', + 'koumis', + 'kowtow', + 'kraal', + 'kraft', + 'krait', + 'kraken', + 'kreplach', + 'kreutzer', + 'kriegspiel', + 'krill', + 'krimmer', + 'kris', + 'krona', + 'krone', + 'kroon', + 'kruller', + 'krummhorn', + 'krypton', + 'kuchen', + 'kudos', + 'kudu', + 'kukri', + 'kulak', + 'kumiss', + 'kummerbund', + 'kumquat', + 'kunzite', + 'kurbash', + 'kurrajong', + 'kurtosis', + 'kurus', + 'kuvasz', + 'kvass', + 'kwashiorkor', + 'kyanite', + 'kyanize', + 'kyat', + 'kyle', + 'kylix', + 'kymograph', + 'kyphosis', + 'l', + 'la', + 'laager', + 'lab', + 'labarum', + 'labdanum', + 'labefaction', + 'label', + 'labellum', + 'labia', + 'labial', + 'labialize', + 'labialized', + 'labiate', + 'labile', + 'labiodental', + 'labionasal', + 'labiovelar', + 'labium', + 'lablab', + 'labor', + 'laboratory', + 'labored', + 'laborer', + 'laborious', + 'labour', + 'laboured', + 'labourer', + 'labradorite', + 'labret', + 'labroid', + 'labrum', + 'laburnum', + 'labyrinth', + 'labyrinthine', + 'labyrinthodont', + 'lac', + 'laccolith', + 'lace', + 'lacerate', + 'lacerated', + 'laceration', + 'lacewing', + 'lacework', + 'laches', + 'lachrymal', + 'lachrymator', + 'lachrymatory', + 'lachrymose', + 'lacing', + 'laciniate', + 'lack', + 'lackadaisical', + 'lackaday', + 'lacker', + 'lackey', + 'lacking', + 'lackluster', + 'laconic', + 'laconism', + 'lacquer', + 'lacrimal', + 'lacrimator', + 'lacrimatory', + 'lacrosse', + 'lactalbumin', + 'lactam', + 'lactary', + 'lactase', + 'lactate', + 'lactation', + 'lacteal', + 'lacteous', + 'lactescent', + 'lactic', + 'lactiferous', + 'lactobacillus', + 'lactoflavin', + 'lactometer', + 'lactone', + 'lactoprotein', + 'lactoscope', + 'lactose', + 'lacuna', + 'lacunar', + 'lacustrine', + 'lacy', + 'lad', + 'ladanum', + 'ladder', + 'laddie', + 'lade', + 'laden', + 'lading', + 'ladino', + 'ladle', + 'lady', + 'ladybird', + 'ladybug', + 'ladyfinger', + 'ladylike', + 'ladylove', + 'ladyship', + 'laevogyrate', + 'laevorotation', + 'laevorotatory', + 'lag', + 'lagan', + 'lagena', + 'lager', + 'laggard', + 'lagging', + 'lagniappe', + 'lagomorph', + 'lagoon', + 'laic', + 'laicize', + 'laid', + 'lain', + 'lair', + 'laird', + 'laity', + 'lake', + 'laker', + 'lakh', + 'laky', + 'lalapalooza', + 'lallation', + 'lallygag', + 'lam', + 'lama', + 'lamasery', + 'lamb', + 'lambaste', + 'lambda', + 'lambdacism', + 'lambdoid', + 'lambency', + 'lambent', + 'lambert', + 'lambkin', + 'lamblike', + 'lambrequin', + 'lambskin', + 'lame', + 'lamebrain', + 'lamed', + 'lamella', + 'lamellar', + 'lamellate', + 'lamellibranch', + 'lamellicorn', + 'lamelliform', + 'lamellirostral', + 'lament', + 'lamentable', + 'lamentation', + 'lamented', + 'lamia', + 'lamina', + 'laminar', + 'laminate', + 'laminated', + 'lamination', + 'laminitis', + 'laminous', + 'lammergeier', + 'lamp', + 'lampas', + 'lampblack', + 'lampion', + 'lamplighter', + 'lampoon', + 'lamppost', + 'lamprey', + 'lamprophyre', + 'lampyrid', + 'lanai', + 'lanate', + 'lance', + 'lancelet', + 'lanceolate', + 'lancer', + 'lancers', + 'lancet', + 'lanceted', + 'lancewood', + 'lanciform', + 'lancinate', + 'land', + 'landau', + 'landaulet', + 'landed', + 'landfall', + 'landgrave', + 'landgraviate', + 'landgravine', + 'landholder', + 'landing', + 'landlady', + 'landlocked', + 'landloper', + 'landlord', + 'landlordism', + 'landlubber', + 'landman', + 'landmark', + 'landmass', + 'landowner', + 'lands', + 'landscape', + 'landscapist', + 'landside', + 'landsknecht', + 'landslide', + 'landsman', + 'landwaiter', + 'landward', + 'lane', + 'lang', + 'langlauf', + 'langouste', + 'langrage', + 'langsyne', + 'language', + 'langue', + 'languet', + 'languid', + 'languish', + 'languishing', + 'languishment', + 'languor', + 'languorous', + 'langur', + 'laniard', + 'laniary', + 'laniferous', + 'lank', + 'lanky', + 'lanner', + 'lanneret', + 'lanolin', + 'lanose', + 'lansquenet', + 'lantana', + 'lantern', + 'lanthanide', + 'lanthanum', + 'lanthorn', + 'lanugo', + 'lanyard', + 'lap', + 'laparotomy', + 'lapboard', + 'lapel', + 'lapful', + 'lapidary', + 'lapidate', + 'lapidify', + 'lapillus', + 'lapin', + 'lappet', + 'lapse', + 'lapstrake', + 'lapsus', + 'lapwing', + 'lar', + 'larboard', + 'larcener', + 'larcenous', + 'larceny', + 'larch', + 'lard', + 'lardaceous', + 'larder', + 'lardon', + 'lardy', + 'large', + 'largely', + 'largess', + 'larghetto', + 'largish', + 'largo', + 'lariat', + 'larine', + 'lark', + 'larkspur', + 'larrigan', + 'larrikin', + 'larrup', + 'larum', + 'larva', + 'larval', + 'larvicide', + 'laryngeal', + 'laryngitis', + 'laryngology', + 'laryngoscope', + 'laryngotomy', + 'larynx', + 'lasagne', + 'lascar', + 'lascivious', + 'lase', + 'laser', + 'lash', + 'lashing', + 'lass', + 'lassie', + 'lassitude', + 'lasso', + 'last', + 'lasting', + 'lastly', + 'lat', + 'latch', + 'latchet', + 'latchkey', + 'latchstring', + 'late', + 'latecomer', + 'lated', + 'lateen', + 'lately', + 'latency', + 'latent', + 'later', + 'lateral', + 'laterality', + 'laterite', + 'lateritious', + 'latest', + 'latex', + 'lath', + 'lathe', + 'lather', + 'lathery', + 'lathi', + 'lathing', + 'lathy', + 'latices', + 'laticiferous', + 'latifundium', + 'latish', + 'latitude', + 'latitudinarian', + 'latria', + 'latrine', + 'latten', + 'latter', + 'latterly', + 'lattermost', + 'lattice', + 'latticed', + 'latticework', + 'laud', + 'laudable', + 'laudanum', + 'laudation', + 'laudatory', + 'lauds', + 'laugh', + 'laughable', + 'laughing', + 'laughingstock', + 'laughter', + 'launce', + 'launch', + 'launcher', + 'launder', + 'launderette', + 'laundress', + 'laundry', + 'laundryman', + 'laundrywoman', + 'lauraceous', + 'laureate', + 'laurel', + 'laurustinus', + 'lava', + 'lavabo', + 'lavage', + 'lavaliere', + 'lavation', + 'lavatory', + 'lave', + 'lavender', + 'laver', + 'laverock', + 'lavish', + 'lavolta', + 'law', + 'lawbreaker', + 'lawful', + 'lawgiver', + 'lawless', + 'lawmaker', + 'lawman', + 'lawn', + 'lawrencium', + 'lawsuit', + 'lawyer', + 'lax', + 'laxation', + 'laxative', + 'laxity', + 'lay', + 'layer', + 'layette', + 'layman', + 'layoff', + 'layout', + 'laywoman', + 'lazar', + 'lazaretto', + 'laze', + 'lazuli', + 'lazulite', + 'lazurite', + 'lazy', + 'lazybones', + 'lea', + 'leach', + 'lead', + 'leaden', + 'leader', + 'leadership', + 'leading', + 'leadsman', + 'leadwort', + 'leaf', + 'leafage', + 'leaflet', + 'leafstalk', + 'leafy', + 'league', + 'leaguer', + 'leak', + 'leakage', + 'leaky', + 'leal', + 'lean', + 'leaning', + 'leant', + 'leap', + 'leaper', + 'leapfrog', + 'leapt', + 'learn', + 'learned', + 'learning', + 'learnt', + 'lease', + 'leaseback', + 'leasehold', + 'leaseholder', + 'leash', + 'least', + 'leastways', + 'leastwise', + 'leather', + 'leatherback', + 'leatherjacket', + 'leatherleaf', + 'leathern', + 'leatherneck', + 'leatherwood', + 'leatherworker', + 'leathery', + 'leave', + 'leaved', + 'leaven', + 'leavening', + 'leaves', + 'leaving', + 'leavings', + 'lebkuchen', + 'lecher', + 'lecherous', + 'lechery', + 'lecithin', + 'lecithinase', + 'lectern', + 'lection', + 'lectionary', + 'lector', + 'lecture', + 'lecturer', + 'lectureship', + 'lecythus', + 'led', + 'lederhosen', + 'ledge', + 'ledger', + 'lee', + 'leeboard', + 'leech', + 'leek', + 'leer', + 'leery', + 'lees', + 'leet', + 'leeward', + 'leeway', + 'left', + 'leftist', + 'leftover', + 'leftward', + 'leftwards', + 'lefty', + 'leg', + 'legacy', + 'legal', + 'legalese', + 'legalism', + 'legality', + 'legalize', + 'legate', + 'legatee', + 'legation', + 'legato', + 'legator', + 'legend', + 'legendary', + 'legerdemain', + 'leges', + 'legged', + 'legging', + 'leggy', + 'leghorn', + 'legibility', + 'legible', + 'legion', + 'legionary', + 'legionnaire', + 'legislate', + 'legislation', + 'legislative', + 'legislator', + 'legislatorial', + 'legislature', + 'legist', + 'legit', + 'legitimacy', + 'legitimate', + 'legitimatize', + 'legitimist', + 'legitimize', + 'legman', + 'legroom', + 'legume', + 'legumin', + 'leguminous', + 'legwork', + 'lehr', + 'lei', + 'leishmania', + 'leishmaniasis', + 'leister', + 'leisure', + 'leisured', + 'leisurely', + 'leitmotif', + 'leitmotiv', + 'lek', + 'leman', + 'lemma', + 'lemming', + 'lemniscate', + 'lemniscus', + 'lemon', + 'lemonade', + 'lempira', + 'lemur', + 'lemures', + 'lemuroid', + 'lend', + 'length', + 'lengthen', + 'lengthways', + 'lengthwise', + 'lengthy', + 'leniency', + 'lenient', + 'lenis', + 'lenitive', + 'lenity', + 'leno', + 'lens', + 'lent', + 'lentamente', + 'lentic', + 'lenticel', + 'lenticular', + 'lenticularis', + 'lentiginous', + 'lentigo', + 'lentil', + 'lentissimo', + 'lento', + 'leonine', + 'leopard', + 'leotard', + 'leper', + 'lepidolite', + 'lepidopteran', + 'lepidopterous', + 'lepidosiren', + 'lepidote', + 'leporid', + 'leporide', + 'leporine', + 'leprechaun', + 'leprosarium', + 'leprose', + 'leprosy', + 'leprous', + 'lepton', + 'leptophyllous', + 'leptorrhine', + 'leptosome', + 'leptospirosis', + 'lesbian', + 'lesbianism', + 'lesion', + 'less', + 'lessee', + 'lessen', + 'lesser', + 'lesson', + 'lessor', + 'lest', + 'let', + 'letch', + 'letdown', + 'lethal', + 'lethargic', + 'lethargy', + 'letter', + 'lettered', + 'letterhead', + 'lettering', + 'letterpress', + 'letters', + 'lettuce', + 'letup', + 'leu', + 'leucine', + 'leucite', + 'leucocratic', + 'leucocyte', + 'leucocytosis', + 'leucoderma', + 'leucoma', + 'leucomaine', + 'leucopenia', + 'leucoplast', + 'leucopoiesis', + 'leucotomy', + 'leukemia', + 'leukocyte', + 'leukoderma', + 'leukorrhea', + 'lev', + 'levant', + 'levanter', + 'levator', + 'levee', + 'level', + 'levelheaded', + 'leveller', + 'lever', + 'leverage', + 'leveret', + 'leviable', + 'leviathan', + 'levigate', + 'levin', + 'levirate', + 'levitate', + 'levitation', + 'levity', + 'levorotation', + 'levorotatory', + 'levulose', + 'levy', + 'lewd', + 'lewis', + 'lewisite', + 'lex', + 'lexeme', + 'lexical', + 'lexicographer', + 'lexicography', + 'lexicologist', + 'lexicology', + 'lexicon', + 'lexicostatistics', + 'lexigraphy', + 'lexis', + 'ley', + 'li', + 'liabilities', + 'liability', + 'liable', + 'liaison', + 'liana', + 'liar', + 'liard', + 'lib', + 'libation', + 'libeccio', + 'libel', + 'libelant', + 'libelee', + 'libeler', + 'libelous', + 'liber', + 'liberal', + 'liberalism', + 'liberality', + 'liberalize', + 'liberate', + 'libertarian', + 'liberticide', + 'libertinage', + 'libertine', + 'libertinism', + 'liberty', + 'libidinous', + 'libido', + 'libra', + 'librarian', + 'librarianship', + 'library', + 'librate', + 'libration', + 'libratory', + 'librettist', + 'libretto', + 'libriform', + 'lice', + 'licence', + 'license', + 'licensee', + 'licentiate', + 'licentious', + 'lichee', + 'lichen', + 'lichenin', + 'lichenology', + 'lichi', + 'licit', + 'lick', + 'lickerish', + 'licking', + 'lickspittle', + 'licorice', + 'lictor', + 'lid', + 'lidless', + 'lido', + 'lie', + 'lied', + 'lief', + 'liege', + 'liegeman', + 'lien', + 'lientery', + 'lierne', + 'lieu', + 'lieutenancy', + 'lieutenant', + 'life', + 'lifeblood', + 'lifeboat', + 'lifeguard', + 'lifeless', + 'lifelike', + 'lifeline', + 'lifelong', + 'lifer', + 'lifesaver', + 'lifesaving', + 'lifetime', + 'lifework', + 'lift', + 'ligament', + 'ligamentous', + 'ligan', + 'ligate', + 'ligation', + 'ligature', + 'liger', + 'light', + 'lighten', + 'lightening', + 'lighter', + 'lighterage', + 'lighterman', + 'lightface', + 'lighthearted', + 'lighthouse', + 'lighting', + 'lightish', + 'lightless', + 'lightly', + 'lightness', + 'lightning', + 'lightproof', + 'lights', + 'lightship', + 'lightsome', + 'lightweight', + 'lignaloes', + 'ligneous', + 'ligniform', + 'lignify', + 'lignin', + 'lignite', + 'lignocellulose', + 'ligroin', + 'ligula', + 'ligulate', + 'ligule', + 'ligure', + 'likable', + 'like', + 'likelihood', + 'likely', + 'liken', + 'likeness', + 'likewise', + 'liking', + 'likker', + 'lilac', + 'liliaceous', + 'lilt', + 'lily', + 'limacine', + 'limb', + 'limbate', + 'limber', + 'limbic', + 'limbo', + 'limbus', + 'lime', + 'limeade', + 'limekiln', + 'limelight', + 'limen', + 'limerick', + 'limes', + 'limestone', + 'limewater', + 'limey', + 'limicoline', + 'limicolous', + 'liminal', + 'limit', + 'limitary', + 'limitation', + 'limitative', + 'limited', + 'limiter', + 'limiting', + 'limitless', + 'limn', + 'limner', + 'limnetic', + 'limnology', + 'limonene', + 'limonite', + 'limousine', + 'limp', + 'limpet', + 'limpid', + 'limpkin', + 'limulus', + 'limy', + 'linage', + 'linalool', + 'linchpin', + 'linctus', + 'lindane', + 'linden', + 'lindy', + 'line', + 'lineage', + 'lineal', + 'lineament', + 'linear', + 'linearity', + 'lineate', + 'lineation', + 'linebacker', + 'linebreeding', + 'lineman', + 'linen', + 'lineolate', + 'liner', + 'lines', + 'linesman', + 'lineup', + 'ling', + 'lingam', + 'lingcod', + 'linger', + 'lingerie', + 'lingo', + 'lingonberry', + 'lingua', + 'lingual', + 'linguiform', + 'linguini', + 'linguist', + 'linguistic', + 'linguistician', + 'linguistics', + 'lingulate', + 'liniment', + 'linin', + 'lining', + 'link', + 'linkage', + 'linkboy', + 'linked', + 'linkman', + 'links', + 'linkwork', + 'linn', + 'linnet', + 'linocut', + 'linoleum', + 'linsang', + 'linseed', + 'linstock', + 'lint', + 'lintel', + 'linter', + 'lintwhite', + 'lion', + 'lioness', + 'lionfish', + 'lionhearted', + 'lionize', + 'lip', + 'lipase', + 'lipid', + 'lipocaic', + 'lipography', + 'lipoid', + 'lipolysis', + 'lipoma', + 'lipophilic', + 'lipoprotein', + 'lipstick', + 'liquate', + 'liquefacient', + 'liquefy', + 'liquesce', + 'liquescent', + 'liqueur', + 'liquid', + 'liquidambar', + 'liquidate', + 'liquidation', + 'liquidator', + 'liquidity', + 'liquidize', + 'liquor', + 'liquorice', + 'liquorish', + 'lira', + 'liriodendron', + 'liripipe', + 'lisle', + 'lisp', + 'lissome', + 'lissotrichous', + 'list', + 'listed', + 'listel', + 'listen', + 'lister', + 'listing', + 'listless', + 'listlessness', + 'lists', + 'lit', + 'litany', + 'litchi', + 'liter', + 'literacy', + 'literal', + 'literalism', + 'literality', + 'literally', + 'literary', + 'literate', + 'literati', + 'literatim', + 'literator', + 'literature', + 'litharge', + 'lithe', + 'lithesome', + 'lithia', + 'lithiasis', + 'lithic', + 'lithium', + 'litho', + 'lithograph', + 'lithographer', + 'lithography', + 'lithoid', + 'lithology', + 'lithomarge', + 'lithometeor', + 'lithophyte', + 'lithopone', + 'lithosphere', + 'lithotomy', + 'lithotrity', + 'litigable', + 'litigant', + 'litigate', + 'litigation', + 'litigious', + 'litmus', + 'litotes', + 'litre', + 'litter', + 'litterbug', + 'little', + 'littlest', + 'littoral', + 'liturgical', + 'liturgics', + 'liturgist', + 'liturgy', + 'lituus', + 'livable', + 'live', + 'livelihood', + 'livelong', + 'lively', + 'liven', + 'liver', + 'liveried', + 'liverish', + 'liverwort', + 'liverwurst', + 'livery', + 'liveryman', + 'lives', + 'livestock', + 'livid', + 'living', + 'livraison', + 'livre', + 'lixiviate', + 'lixivium', + 'lizard', + 'llama', + 'llano', + 'lm', + 'ln', + 'lo', + 'loach', + 'load', + 'loaded', + 'loader', + 'loading', + 'loads', + 'loadstar', + 'loadstone', + 'loaf', + 'loafer', + 'loaiasis', + 'loam', + 'loan', + 'loaning', + 'loath', + 'loathe', + 'loathing', + 'loathly', + 'loathsome', + 'loaves', + 'lob', + 'lobar', + 'lobate', + 'lobation', + 'lobby', + 'lobbyism', + 'lobbyist', + 'lobe', + 'lobectomy', + 'lobelia', + 'lobeline', + 'loblolly', + 'lobo', + 'lobotomy', + 'lobscouse', + 'lobster', + 'lobule', + 'lobworm', + 'local', + 'locale', + 'localism', + 'locality', + 'localize', + 'locally', + 'locate', + 'location', + 'locative', + 'loch', + 'lochia', + 'loci', + 'lock', + 'lockage', + 'locker', + 'locket', + 'lockjaw', + 'lockout', + 'locksmith', + 'lockup', + 'loco', + 'locoism', + 'locomobile', + 'locomotion', + 'locomotive', + 'locomotor', + 'locoweed', + 'locular', + 'locule', + 'loculus', + 'locus', + 'locust', + 'locution', + 'lode', + 'loden', + 'lodestar', + 'lodestone', + 'lodge', + 'lodged', + 'lodger', + 'lodging', + 'lodgings', + 'lodgment', + 'lodicule', + 'loess', + 'loft', + 'lofty', + 'log', + 'logan', + 'loganberry', + 'loganiaceous', + 'logarithm', + 'logarithmic', + 'logbook', + 'loge', + 'logger', + 'loggerhead', + 'loggia', + 'logging', + 'logia', + 'logic', + 'logical', + 'logician', + 'logicize', + 'logion', + 'logistic', + 'logistician', + 'logistics', + 'logjam', + 'logo', + 'logogram', + 'logographic', + 'logography', + 'logogriph', + 'logomachy', + 'logorrhea', + 'logos', + 'logotype', + 'logroll', + 'logrolling', + 'logway', + 'logwood', + 'logy', + 'loin', + 'loincloth', + 'loiter', + 'loll', + 'lollapalooza', + 'lollipop', + 'lollop', + 'lolly', + 'lollygag', + 'loment', + 'lone', + 'lonely', + 'loner', + 'lonesome', + 'long', + 'longan', + 'longanimity', + 'longboat', + 'longbow', + 'longcloth', + 'longe', + 'longeron', + 'longevity', + 'longevous', + 'longhair', + 'longhand', + 'longicorn', + 'longing', + 'longish', + 'longitude', + 'longitudinal', + 'longs', + 'longship', + 'longshore', + 'longshoreman', + 'longsome', + 'longspur', + 'longueur', + 'longways', + 'longwise', + 'loo', + 'looby', + 'look', + 'looker', + 'lookout', + 'loom', + 'looming', + 'loon', + 'looney', + 'loony', + 'loop', + 'looper', + 'loophole', + 'loopy', + 'loose', + 'loosen', + 'loosestrife', + 'loosing', + 'loot', + 'lop', + 'lope', + 'lophobranch', + 'lophophore', + 'loppy', + 'lopsided', + 'loquacious', + 'loquacity', + 'loquat', + 'loquitur', + 'loran', + 'lord', + 'lording', + 'lordling', + 'lordly', + 'lordosis', + 'lordship', + 'lore', + 'lorgnette', + 'lorgnon', + 'lorica', + 'loricate', + 'lorikeet', + 'lorimer', + 'loris', + 'lorn', + 'lorry', + 'lory', + 'lose', + 'losel', + 'loser', + 'losing', + 'loss', + 'lost', + 'lot', + 'lota', + 'loth', + 'lotic', + 'lotion', + 'lots', + 'lottery', + 'lotto', + 'lotus', + 'loud', + 'louden', + 'loudish', + 'loudmouth', + 'loudmouthed', + 'loudspeaker', + 'lough', + 'louis', + 'lounge', + 'lounging', + 'loup', + 'loupe', + 'lour', + 'louse', + 'lousewort', + 'lousy', + 'lout', + 'loutish', + 'louvar', + 'louver', + 'louvre', + 'lovable', + 'lovage', + 'love', + 'lovebird', + 'lovegrass', + 'loveless', + 'lovelock', + 'lovelorn', + 'lovely', + 'lovemaking', + 'lover', + 'loverly', + 'lovesick', + 'lovesome', + 'loving', + 'low', + 'lowborn', + 'lowboy', + 'lowbred', + 'lowbrow', + 'lower', + 'lowerclassman', + 'lowering', + 'lowermost', + 'lowland', + 'lowlife', + 'lowly', + 'lox', + 'loxodrome', + 'loxodromic', + 'loxodromics', + 'loyal', + 'loyalist', + 'loyalty', + 'lozenge', + 'lozengy', + 'luau', + 'lubber', + 'lubberly', + 'lubra', + 'lubric', + 'lubricant', + 'lubricate', + 'lubricator', + 'lubricious', + 'lubricity', + 'lubricous', + 'lucarne', + 'luce', + 'lucent', + 'lucerne', + 'lucid', + 'lucifer', + 'luciferase', + 'luciferin', + 'luciferous', + 'luck', + 'luckily', + 'luckless', + 'lucky', + 'lucrative', + 'lucre', + 'lucubrate', + 'lucubration', + 'luculent', + 'ludicrous', + 'lues', + 'luetic', + 'luff', + 'luffa', + 'lug', + 'luge', + 'luggage', + 'lugger', + 'lugsail', + 'lugubrious', + 'lugworm', + 'lukewarm', + 'lull', + 'lullaby', + 'lulu', + 'lumbago', + 'lumbar', + 'lumber', + 'lumbering', + 'lumberjack', + 'lumberman', + 'lumberyard', + 'lumbricalis', + 'lumbricoid', + 'lumen', + 'luminance', + 'luminary', + 'luminesce', + 'luminescence', + 'luminescent', + 'luminiferous', + 'luminosity', + 'luminous', + 'lumisterol', + 'lummox', + 'lump', + 'lumpen', + 'lumper', + 'lumpfish', + 'lumpish', + 'lumpy', + 'lunacy', + 'lunar', + 'lunarian', + 'lunate', + 'lunatic', + 'lunation', + 'lunch', + 'luncheon', + 'luncheonette', + 'lunchroom', + 'lune', + 'lunette', + 'lung', + 'lungan', + 'lunge', + 'lungfish', + 'lungi', + 'lungworm', + 'lungwort', + 'lunisolar', + 'lunitidal', + 'lunkhead', + 'lunula', + 'lunular', + 'lunulate', + 'lupine', + 'lupulin', + 'lupus', + 'lur', + 'lurch', + 'lurcher', + 'lurdan', + 'lure', + 'lurid', + 'lurk', + 'luscious', + 'lush', + 'lushy', + 'lust', + 'luster', + 'lusterware', + 'lustful', + 'lustihood', + 'lustral', + 'lustrate', + 'lustre', + 'lustreware', + 'lustring', + 'lustrous', + 'lustrum', + 'lusty', + 'lutanist', + 'lute', + 'luteal', + 'lutenist', + 'luteolin', + 'luteous', + 'lutestring', + 'lutetium', + 'luthern', + 'luting', + 'lutist', + 'lux', + 'luxate', + 'luxe', + 'luxuriance', + 'luxuriant', + 'luxuriate', + 'luxurious', + 'luxury', + 'lx', + 'lycanthrope', + 'lycanthropy', + 'lyceum', + 'lychnis', + 'lycopodium', + 'lyddite', + 'lye', + 'lying', + 'lymph', + 'lymphadenitis', + 'lymphangial', + 'lymphangitis', + 'lymphatic', + 'lymphoblast', + 'lymphocyte', + 'lymphocytosis', + 'lymphoid', + 'lymphoma', + 'lymphosarcoma', + 'lyncean', + 'lynch', + 'lynching', + 'lynx', + 'lyonnaise', + 'lyophilic', + 'lyophilize', + 'lyophobic', + 'lyrate', + 'lyre', + 'lyrebird', + 'lyric', + 'lyricism', + 'lyricist', + 'lyrism', + 'lyrist', + 'lyse', + 'lysimeter', + 'lysin', + 'lysine', + 'lysis', + 'lysozyme', + 'lyssa', + 'lythraceous', + 'lytic', + 'lytta', + 'm', + 'ma', + 'mac', + 'macabre', + 'macaco', + 'macadam', + 'macadamia', + 'macaque', + 'macaroni', + 'macaronic', + 'macaroon', + 'macaw', + 'maccaboy', + 'mace', + 'macedoine', + 'macerate', + 'machete', + 'machicolate', + 'machicolation', + 'machinate', + 'machination', + 'machine', + 'machinery', + 'machinist', + 'machismo', + 'machree', + 'machzor', + 'macintosh', + 'mackerel', + 'mackinaw', + 'mackintosh', + 'mackle', + 'macle', + 'macrobiotic', + 'macrobiotics', + 'macroclimate', + 'macrocosm', + 'macrogamete', + 'macrography', + 'macromolecule', + 'macron', + 'macronucleus', + 'macrophage', + 'macrophysics', + 'macropterous', + 'macroscopic', + 'macrospore', + 'macruran', + 'macula', + 'maculate', + 'maculation', + 'macule', + 'mad', + 'madam', + 'madame', + 'madcap', + 'madden', + 'maddening', + 'madder', + 'madding', + 'made', + 'mademoiselle', + 'madhouse', + 'madly', + 'madman', + 'madness', + 'madras', + 'madrepore', + 'madrigal', + 'madrigalist', + 'maduro', + 'madwort', + 'maelstrom', + 'maenad', + 'maestoso', + 'maestro', + 'maffick', + 'mag', + 'magazine', + 'magdalen', + 'mage', + 'magenta', + 'maggot', + 'maggoty', + 'magi', + 'magic', + 'magical', + 'magically', + 'magician', + 'magisterial', + 'magistery', + 'magistracy', + 'magistral', + 'magistrate', + 'magma', + 'magnanimity', + 'magnanimous', + 'magnate', + 'magnesia', + 'magnesite', + 'magnesium', + 'magnet', + 'magnetic', + 'magnetics', + 'magnetism', + 'magnetite', + 'magnetize', + 'magneto', + 'magnetochemistry', + 'magnetoelectricity', + 'magnetograph', + 'magnetohydrodynamics', + 'magnetometer', + 'magnetomotive', + 'magneton', + 'magnetostriction', + 'magnetron', + 'magnific', + 'magnification', + 'magnificence', + 'magnificent', + 'magnifico', + 'magnify', + 'magniloquent', + 'magnitude', + 'magnolia', + 'magnoliaceous', + 'magnum', + 'magpie', + 'magus', + 'maharaja', + 'maharajah', + 'maharanee', + 'maharani', + 'mahatma', + 'mahlstick', + 'mahogany', + 'mahout', + 'maid', + 'maidan', + 'maiden', + 'maidenhair', + 'maidenhead', + 'maidenhood', + 'maidenly', + 'maidservant', + 'maieutic', + 'maigre', + 'maihem', + 'mail', + 'mailable', + 'mailbag', + 'mailbox', + 'mailed', + 'mailer', + 'maillot', + 'mailman', + 'maim', + 'main', + 'mainland', + 'mainly', + 'mainmast', + 'mainsail', + 'mainsheet', + 'mainspring', + 'mainstay', + 'mainstream', + 'maintain', + 'maintenance', + 'maintop', + 'maiolica', + 'maisonette', + 'maize', + 'majestic', + 'majesty', + 'majolica', + 'major', + 'majordomo', + 'majorette', + 'majority', + 'majuscule', + 'make', + 'makefast', + 'maker', + 'makeshift', + 'makeup', + 'makeweight', + 'making', + 'makings', + 'mako', + 'malachite', + 'malacology', + 'malacostracan', + 'maladapted', + 'maladjusted', + 'maladjustment', + 'maladminister', + 'maladroit', + 'malady', + 'malaguena', + 'malaise', + 'malamute', + 'malapert', + 'malapropism', + 'malapropos', + 'malar', + 'malaria', + 'malarkey', + 'malcontent', + 'male', + 'maleate', + 'maledict', + 'malediction', + 'malefaction', + 'malefactor', + 'malefic', + 'maleficence', + 'maleficent', + 'malemute', + 'malevolent', + 'malfeasance', + 'malformation', + 'malfunction', + 'malice', + 'malicious', + 'malign', + 'malignancy', + 'malignant', + 'malignity', + 'malines', + 'malinger', + 'malison', + 'mall', + 'mallard', + 'malleable', + 'mallee', + 'mallemuck', + 'malleolus', + 'mallet', + 'malleus', + 'mallow', + 'malm', + 'malmsey', + 'malnourished', + 'malnutrition', + 'malocclusion', + 'malodorous', + 'malonylurea', + 'malpighiaceous', + 'malposition', + 'malpractice', + 'malt', + 'maltase', + 'maltha', + 'maltose', + 'maltreat', + 'malvaceous', + 'malvasia', + 'malversation', + 'malvoisie', + 'mam', + 'mama', + 'mamba', + 'mambo', + 'mamelon', + 'mamey', + 'mamma', + 'mammal', + 'mammalian', + 'mammalogy', + 'mammary', + 'mammet', + 'mammiferous', + 'mammilla', + 'mammillary', + 'mammillate', + 'mammon', + 'mammoth', + 'mammy', + 'man', + 'mana', + 'manacle', + 'manage', + 'manageable', + 'management', + 'manager', + 'managerial', + 'managing', + 'manakin', + 'manana', + 'manas', + 'manatee', + 'manchineel', + 'manciple', + 'mandamus', + 'mandarin', + 'mandate', + 'mandatory', + 'mandible', + 'mandibular', + 'mandola', + 'mandolin', + 'mandorla', + 'mandragora', + 'mandrake', + 'mandrel', + 'mandrill', + 'manducate', + 'mane', + 'manes', + 'maneuver', + 'manful', + 'manganate', + 'manganese', + 'manganite', + 'manganous', + 'mange', + 'manger', + 'mangle', + 'mango', + 'mangonel', + 'mangosteen', + 'mangrove', + 'manhandle', + 'manhole', + 'manhood', + 'manhunt', + 'mania', + 'maniac', + 'maniacal', + 'manic', + 'manicotti', + 'manicure', + 'manicurist', + 'manifest', + 'manifestation', + 'manifestative', + 'manifesto', + 'manifold', + 'manikin', + 'manilla', + 'manille', + 'maniple', + 'manipular', + 'manipulate', + 'manipulator', + 'mankind', + 'manlike', + 'manly', + 'manna', + 'manned', + 'mannequin', + 'manner', + 'mannered', + 'mannerism', + 'mannerless', + 'mannerly', + 'manners', + 'mannikin', + 'mannish', + 'mannose', + 'manoeuvre', + 'manometer', + 'manor', + 'manpower', + 'manque', + 'manrope', + 'mansard', + 'manse', + 'manservant', + 'mansion', + 'manslaughter', + 'manslayer', + 'manstopper', + 'mansuetude', + 'manta', + 'manteau', + 'mantel', + 'mantelet', + 'mantelletta', + 'mantellone', + 'mantelpiece', + 'manteltree', + 'mantic', + 'mantilla', + 'mantis', + 'mantissa', + 'mantle', + 'mantling', + 'mantra', + 'mantua', + 'manual', + 'manubrium', + 'manufactory', + 'manufacture', + 'manufacturer', + 'manumission', + 'manumit', + 'manure', + 'manuscript', + 'many', + 'manyplies', + 'manzanilla', + 'map', + 'maple', + 'mapping', + 'maquette', + 'maquis', + 'mar', + 'mara', + 'marabou', + 'marabout', + 'maraca', + 'marasca', + 'maraschino', + 'marasmus', + 'marathon', + 'maraud', + 'marauding', + 'maravedi', + 'marble', + 'marbleize', + 'marbles', + 'marbling', + 'marc', + 'marcasite', + 'marcel', + 'marcescent', + 'march', + 'marcher', + 'marchesa', + 'marchese', + 'marchioness', + 'marchland', + 'marchpane', + 'marconigraph', + 'mare', + 'maremma', + 'margarine', + 'margarite', + 'margay', + 'marge', + 'margent', + 'margin', + 'marginal', + 'marginalia', + 'marginate', + 'margrave', + 'margravine', + 'marguerite', + 'marigold', + 'marigraph', + 'marijuana', + 'marimba', + 'marina', + 'marinade', + 'marinara', + 'marinate', + 'marine', + 'mariner', + 'marionette', + 'marish', + 'marital', + 'maritime', + 'marjoram', + 'mark', + 'markdown', + 'marked', + 'marker', + 'market', + 'marketable', + 'marketing', + 'marketplace', + 'markhor', + 'marking', + 'markka', + 'marksman', + 'markswoman', + 'markup', + 'marl', + 'marlin', + 'marline', + 'marlinespike', + 'marlite', + 'marmalade', + 'marmite', + 'marmoreal', + 'marmoset', + 'marmot', + 'marocain', + 'maroon', + 'marplot', + 'marque', + 'marquee', + 'marquess', + 'marquetry', + 'marquis', + 'marquisate', + 'marquise', + 'marquisette', + 'marriage', + 'marriageable', + 'married', + 'marron', + 'marrow', + 'marrowbone', + 'marrowfat', + 'marry', + 'marseilles', + 'marsh', + 'marshal', + 'marshland', + 'marshmallow', + 'marshy', + 'marsipobranch', + 'marsupial', + 'marsupium', + 'mart', + 'martellato', + 'marten', + 'martensite', + 'martial', + 'martin', + 'martinet', + 'martingale', + 'martini', + 'martlet', + 'martyr', + 'martyrdom', + 'martyrize', + 'martyrology', + 'martyry', + 'marvel', + 'marvellous', + 'marvelous', + 'marzipan', + 'mascara', + 'mascle', + 'mascon', + 'mascot', + 'masculine', + 'maser', + 'mash', + 'mashie', + 'masjid', + 'mask', + 'maskanonge', + 'masked', + 'masker', + 'masochism', + 'mason', + 'masonic', + 'masonry', + 'masque', + 'masquer', + 'masquerade', + 'mass', + 'massacre', + 'massage', + 'massasauga', + 'masseter', + 'masseur', + 'masseuse', + 'massicot', + 'massif', + 'massive', + 'massotherapy', + 'massy', + 'mast', + 'mastaba', + 'mastectomy', + 'master', + 'masterful', + 'masterly', + 'mastermind', + 'masterpiece', + 'mastership', + 'mastersinger', + 'masterstroke', + 'masterwork', + 'mastery', + 'masthead', + 'mastic', + 'masticate', + 'masticatory', + 'mastiff', + 'mastigophoran', + 'mastitis', + 'mastodon', + 'mastoid', + 'mastoidectomy', + 'mastoiditis', + 'masturbate', + 'masturbation', + 'masurium', + 'mat', + 'matador', + 'match', + 'matchboard', + 'matchbook', + 'matchbox', + 'matchless', + 'matchlock', + 'matchmaker', + 'matchmark', + 'matchwood', + 'mate', + 'matelot', + 'matelote', + 'materfamilias', + 'material', + 'materialism', + 'materialist', + 'materiality', + 'materialize', + 'materially', + 'materials', + 'materiel', + 'maternal', + 'maternity', + 'matey', + 'math', + 'mathematical', + 'mathematician', + 'mathematics', + 'matin', + 'matinee', + 'mating', + 'matins', + 'matrass', + 'matriarch', + 'matriarchate', + 'matriarchy', + 'matrices', + 'matriculate', + 'matriculation', + 'matrilateral', + 'matrilineage', + 'matrilineal', + 'matrilocal', + 'matrimonial', + 'matrimony', + 'matrix', + 'matroclinous', + 'matron', + 'matronage', + 'matronize', + 'matronly', + 'matronymic', + 'matt', + 'matte', + 'matted', + 'matter', + 'matting', + 'mattins', + 'mattock', + 'mattoid', + 'mattress', + 'maturate', + 'maturation', + 'mature', + 'maturity', + 'matutinal', + 'matzo', + 'maudlin', + 'maugre', + 'maul', + 'maulstick', + 'maun', + 'maund', + 'maunder', + 'maundy', + 'mausoleum', + 'mauve', + 'maverick', + 'mavis', + 'maw', + 'mawkin', + 'mawkish', + 'maxi', + 'maxilla', + 'maxillary', + 'maxilliped', + 'maxim', + 'maximal', + 'maximin', + 'maximize', + 'maximum', + 'maxiskirt', + 'maxwell', + 'may', + 'maya', + 'mayapple', + 'maybe', + 'mayest', + 'mayflower', + 'mayfly', + 'mayhap', + 'mayhem', + 'mayonnaise', + 'mayor', + 'mayoralty', + 'maypole', + 'mayst', + 'mayweed', + 'mazard', + 'maze', + 'mazer', + 'mazuma', + 'mazurka', + 'mazy', + 'mazzard', + 'mb', + 'me', + 'mead', + 'meadow', + 'meadowlark', + 'meadowsweet', + 'meager', + 'meagre', + 'meal', + 'mealie', + 'mealtime', + 'mealworm', + 'mealy', + 'mealymouthed', + 'mean', + 'meander', + 'meandrous', + 'meanie', + 'meaning', + 'meaningful', + 'meaningless', + 'meanly', + 'means', + 'meant', + 'meantime', + 'meanwhile', + 'meany', + 'measles', + 'measly', + 'measurable', + 'measure', + 'measured', + 'measureless', + 'measurement', + 'measures', + 'meat', + 'meatball', + 'meathead', + 'meatiness', + 'meatman', + 'meatus', + 'meaty', + 'mechanic', + 'mechanical', + 'mechanician', + 'mechanics', + 'mechanism', + 'mechanist', + 'mechanistic', + 'mechanize', + 'mechanotherapy', + 'medal', + 'medalist', + 'medallion', + 'medallist', + 'meddle', + 'meddlesome', + 'media', + 'mediacy', + 'mediaeval', + 'medial', + 'median', + 'mediant', + 'mediate', + 'mediation', + 'mediative', + 'mediatize', + 'mediator', + 'mediatorial', + 'mediatory', + 'medic', + 'medicable', + 'medical', + 'medicament', + 'medicate', + 'medication', + 'medicinal', + 'medicine', + 'medick', + 'medico', + 'medieval', + 'medievalism', + 'medievalist', + 'mediocre', + 'mediocrity', + 'meditate', + 'meditation', + 'medium', + 'medius', + 'medlar', + 'medley', + 'medulla', + 'medullary', + 'medullated', + 'medusa', + 'meed', + 'meek', + 'meerkat', + 'meerschaum', + 'meet', + 'meeting', + 'meetinghouse', + 'meetly', + 'megacycle', + 'megadeath', + 'megagamete', + 'megalith', + 'megalocardia', + 'megalomania', + 'megalopolis', + 'megaphone', + 'megaron', + 'megasporangium', + 'megaspore', + 'megasporophyll', + 'megass', + 'megathere', + 'megaton', + 'megavolt', + 'megawatt', + 'megillah', + 'megilp', + 'megohm', + 'megrim', + 'megrims', + 'meiny', + 'meiosis', + 'mel', + 'melamed', + 'melamine', + 'melancholia', + 'melancholic', + 'melancholy', + 'melanic', + 'melanin', + 'melanism', + 'melanite', + 'melanochroi', + 'melanoid', + 'melanoma', + 'melanosis', + 'melanous', + 'melaphyre', + 'melatonin', + 'meld', + 'melee', + 'melic', + 'melilot', + 'melinite', + 'meliorate', + 'melioration', + 'meliorism', + 'melisma', + 'melliferous', + 'mellifluent', + 'mellifluous', + 'mellophone', + 'mellow', + 'melodeon', + 'melodia', + 'melodic', + 'melodics', + 'melodion', + 'melodious', + 'melodist', + 'melodize', + 'melodrama', + 'melodramatic', + 'melodramatize', + 'melody', + 'meloid', + 'melon', + 'melt', + 'meltage', + 'melton', + 'meltwater', + 'mem', + 'member', + 'membership', + 'membrane', + 'membranophone', + 'membranous', + 'memento', + 'memo', + 'memoir', + 'memoirs', + 'memorabilia', + 'memorable', + 'memorandum', + 'memorial', + 'memorialist', + 'memorialize', + 'memoried', + 'memorize', + 'memory', + 'men', + 'menace', + 'menadione', + 'menagerie', + 'menarche', + 'mend', + 'mendacious', + 'mendacity', + 'mendelevium', + 'mender', + 'mendicant', + 'mendicity', + 'mending', + 'mene', + 'menfolk', + 'menhaden', + 'menhir', + 'menial', + 'meninges', + 'meningitis', + 'meniscus', + 'menispermaceous', + 'menology', + 'menopause', + 'menorah', + 'menorrhagia', + 'mensal', + 'menses', + 'menstrual', + 'menstruate', + 'menstruation', + 'menstruum', + 'mensurable', + 'mensural', + 'mensuration', + 'menswear', + 'mental', + 'mentalism', + 'mentalist', + 'mentality', + 'mentally', + 'menthol', + 'mentholated', + 'menticide', + 'mention', + 'mentor', + 'menu', + 'meow', + 'meperidine', + 'mephitic', + 'mephitis', + 'meprobamate', + 'merbromin', + 'mercantile', + 'mercantilism', + 'mercaptide', + 'mercaptopurine', + 'mercenary', + 'mercer', + 'mercerize', + 'merchandise', + 'merchandising', + 'merchant', + 'merchantable', + 'merchantman', + 'merciful', + 'merciless', + 'mercurate', + 'mercurial', + 'mercurialism', + 'mercurialize', + 'mercuric', + 'mercurous', + 'mercury', + 'mercy', + 'mere', + 'merely', + 'merengue', + 'meretricious', + 'merganser', + 'merge', + 'merger', + 'meridian', + 'meridional', + 'meringue', + 'merino', + 'meristem', + 'meristic', + 'merit', + 'merited', + 'meritocracy', + 'meritorious', + 'merits', + 'merle', + 'merlin', + 'merlon', + 'mermaid', + 'merman', + 'meroblastic', + 'merocrine', + 'merozoite', + 'merriment', + 'merry', + 'merrymaker', + 'merrymaking', + 'merrythought', + 'mesa', + 'mesarch', + 'mescal', + 'mescaline', + 'mesdames', + 'mesdemoiselles', + 'meseems', + 'mesencephalon', + 'mesenchyme', + 'mesentery', + 'mesh', + 'meshuga', + 'meshwork', + 'mesial', + 'mesic', + 'mesitylene', + 'mesmerism', + 'mesmerize', + 'mesnalty', + 'mesne', + 'mesoblast', + 'mesocarp', + 'mesocratic', + 'mesoderm', + 'mesoglea', + 'mesognathous', + 'mesomorph', + 'mesomorphic', + 'meson', + 'mesonephros', + 'mesopause', + 'mesosphere', + 'mesothelium', + 'mesothorax', + 'mesothorium', + 'mesotron', + 'mesquite', + 'mess', + 'message', + 'messaline', + 'messenger', + 'messieurs', + 'messily', + 'messmate', + 'messroom', + 'messuage', + 'messy', + 'mestee', + 'mestizo', + 'met', + 'metabolic', + 'metabolism', + 'metabolite', + 'metabolize', + 'metacarpal', + 'metacarpus', + 'metacenter', + 'metachromatism', + 'metagalaxy', + 'metage', + 'metagenesis', + 'metagnathous', + 'metal', + 'metalanguage', + 'metalepsis', + 'metalinguistic', + 'metalinguistics', + 'metallic', + 'metalliferous', + 'metalline', + 'metallist', + 'metallize', + 'metallography', + 'metalloid', + 'metallophone', + 'metallurgy', + 'metalware', + 'metalwork', + 'metalworking', + 'metamathematics', + 'metamer', + 'metameric', + 'metamerism', + 'metamorphic', + 'metamorphism', + 'metamorphose', + 'metamorphosis', + 'metanephros', + 'metaphase', + 'metaphor', + 'metaphosphate', + 'metaphrase', + 'metaphrast', + 'metaphysic', + 'metaphysical', + 'metaphysics', + 'metaplasia', + 'metaplasm', + 'metaprotein', + 'metapsychology', + 'metasomatism', + 'metastasis', + 'metastasize', + 'metatarsal', + 'metatarsus', + 'metatherian', + 'metathesis', + 'metathesize', + 'metaxylem', + 'mete', + 'metempirics', + 'metempsychosis', + 'metencephalon', + 'meteor', + 'meteoric', + 'meteorite', + 'meteoritics', + 'meteorograph', + 'meteoroid', + 'meteorology', + 'meter', + 'methacrylate', + 'methadone', + 'methaemoglobin', + 'methane', + 'methanol', + 'metheglin', + 'methenamine', + 'methinks', + 'methionine', + 'method', + 'methodical', + 'methodize', + 'methodology', + 'methoxychlor', + 'methyl', + 'methylal', + 'methylamine', + 'methylene', + 'methylnaphthalene', + 'metic', + 'meticulous', + 'metonym', + 'metonymy', + 'metope', + 'metopic', + 'metralgia', + 'metre', + 'metric', + 'metrical', + 'metrics', + 'metrify', + 'metrist', + 'metritis', + 'metro', + 'metrology', + 'metronome', + 'metronymic', + 'metropolis', + 'metropolitan', + 'metrorrhagia', + 'mettle', + 'mettlesome', + 'mew', + 'mewl', + 'mews', + 'mezcaline', + 'mezereon', + 'mezereum', + 'mezuzah', + 'mezzanine', + 'mezzo', + 'mezzotint', + 'mf', + 'mg', + 'mho', + 'mi', + 'miaow', + 'miasma', + 'mica', + 'mice', + 'micelle', + 'micra', + 'microampere', + 'microanalysis', + 'microbalance', + 'microbarograph', + 'microbe', + 'microbicide', + 'microbiology', + 'microchemistry', + 'microcircuit', + 'microclimate', + 'microclimatology', + 'microcline', + 'micrococcus', + 'microcopy', + 'microcosm', + 'microcrystalline', + 'microcurie', + 'microcyte', + 'microdont', + 'microdot', + 'microeconomics', + 'microelectronics', + 'microelement', + 'microfarad', + 'microfiche', + 'microfilm', + 'microgamete', + 'microgram', + 'micrography', + 'microgroove', + 'microhenry', + 'microlith', + 'micrometeorite', + 'micrometeorology', + 'micrometer', + 'micrometry', + 'micromho', + 'micromillimeter', + 'microminiaturization', + 'micron', + 'micronucleus', + 'micronutrient', + 'microorganism', + 'micropaleontology', + 'microparasite', + 'micropathology', + 'microphone', + 'microphotograph', + 'microphysics', + 'microphyte', + 'microprint', + 'micropyle', + 'microreader', + 'microscope', + 'microscopic', + 'microscopy', + 'microsecond', + 'microseism', + 'microsome', + 'microsporangium', + 'microspore', + 'microsporophyll', + 'microstructure', + 'microsurgery', + 'microtome', + 'microtone', + 'microvolt', + 'microwatt', + 'microwave', + 'micturition', + 'mid', + 'midbrain', + 'midcourse', + 'midday', + 'midden', + 'middle', + 'middlebreaker', + 'middlebrow', + 'middlebuster', + 'middleman', + 'middlemost', + 'middleweight', + 'middling', + 'middlings', + 'middy', + 'midge', + 'midget', + 'midgut', + 'midi', + 'midinette', + 'midiron', + 'midland', + 'midmost', + 'midnight', + 'midpoint', + 'midrash', + 'midrib', + 'midriff', + 'midsection', + 'midship', + 'midshipman', + 'midshipmite', + 'midships', + 'midst', + 'midstream', + 'midsummer', + 'midterm', + 'midtown', + 'midway', + 'midweek', + 'midwife', + 'midwifery', + 'midwinter', + 'midyear', + 'mien', + 'miff', + 'miffy', + 'mig', + 'might', + 'mightily', + 'mighty', + 'mignon', + 'mignonette', + 'migraine', + 'migrant', + 'migrate', + 'migration', + 'migratory', + 'mihrab', + 'mikado', + 'mike', + 'mikvah', + 'mil', + 'milady', + 'milch', + 'mild', + 'milden', + 'mildew', + 'mile', + 'mileage', + 'milepost', + 'miler', + 'milestone', + 'miliaria', + 'miliary', + 'milieu', + 'militant', + 'militarism', + 'militarist', + 'militarize', + 'military', + 'militate', + 'militia', + 'militiaman', + 'milium', + 'milk', + 'milker', + 'milkfish', + 'milkmaid', + 'milkman', + 'milksop', + 'milkweed', + 'milkwort', + 'milky', + 'mill', + 'millboard', + 'milldam', + 'milled', + 'millenarian', + 'millenarianism', + 'millenary', + 'millennial', + 'millennium', + 'millepede', + 'millepore', + 'miller', + 'millerite', + 'millesimal', + 'millet', + 'milliard', + 'milliary', + 'millibar', + 'millieme', + 'milligram', + 'millihenry', + 'milliliter', + 'millimeter', + 'millimicron', + 'milline', + 'milliner', + 'millinery', + 'milling', + 'million', + 'millionaire', + 'millipede', + 'millisecond', + 'millpond', + 'millrace', + 'millrun', + 'millstone', + 'millstream', + 'millwork', + 'millwright', + 'milo', + 'milord', + 'milquetoast', + 'milreis', + 'milt', + 'milter', + 'mim', + 'mime', + 'mimeograph', + 'mimesis', + 'mimetic', + 'mimic', + 'mimicry', + 'mimosa', + 'mimosaceous', + 'min', + 'mina', + 'minacious', + 'minaret', + 'minatory', + 'mince', + 'mincemeat', + 'mincing', + 'mind', + 'minded', + 'mindful', + 'mindless', + 'mine', + 'minefield', + 'minelayer', + 'miner', + 'mineral', + 'mineralize', + 'mineralogist', + 'mineralogy', + 'mineraloid', + 'minestrone', + 'minesweeper', + 'mingle', + 'mingy', + 'mini', + 'miniature', + 'miniaturist', + 'miniaturize', + 'minicam', + 'minify', + 'minim', + 'minima', + 'minimal', + 'minimize', + 'minimum', + 'minimus', + 'mining', + 'minion', + 'miniskirt', + 'minister', + 'ministerial', + 'ministrant', + 'ministration', + 'ministry', + 'minium', + 'miniver', + 'minivet', + 'mink', + 'minnesinger', + 'minnow', + 'minor', + 'minority', + 'minster', + 'minstrel', + 'minstrelsy', + 'mint', + 'mintage', + 'minuend', + 'minuet', + 'minus', + 'minuscule', + 'minute', + 'minutely', + 'minutes', + 'minutia', + 'minutiae', + 'minx', + 'minyan', + 'miosis', + 'mir', + 'miracidium', + 'miracle', + 'miraculous', + 'mirador', + 'mirage', + 'mire', + 'mirepoix', + 'mirk', + 'mirror', + 'mirth', + 'mirthful', + 'mirthless', + 'miry', + 'mirza', + 'misadventure', + 'misadvise', + 'misalliance', + 'misanthrope', + 'misanthropy', + 'misapply', + 'misapprehend', + 'misapprehension', + 'misappropriate', + 'misbecome', + 'misbegotten', + 'misbehave', + 'misbehavior', + 'misbelief', + 'misbeliever', + 'miscalculate', + 'miscall', + 'miscarriage', + 'miscarry', + 'miscegenation', + 'miscellanea', + 'miscellaneous', + 'miscellany', + 'mischance', + 'mischief', + 'mischievous', + 'miscible', + 'misconceive', + 'misconception', + 'misconduct', + 'misconstruction', + 'misconstrue', + 'miscount', + 'miscreance', + 'miscreant', + 'miscreated', + 'miscue', + 'misdate', + 'misdeal', + 'misdeed', + 'misdeem', + 'misdemean', + 'misdemeanant', + 'misdemeanor', + 'misdirect', + 'misdirection', + 'misdo', + 'misdoing', + 'misdoubt', + 'mise', + 'miser', + 'miserable', + 'misericord', + 'miserly', + 'misery', + 'misesteem', + 'misestimate', + 'misfeasance', + 'misfeasor', + 'misfile', + 'misfire', + 'misfit', + 'misfortune', + 'misgive', + 'misgiving', + 'misgovern', + 'misguidance', + 'misguide', + 'misguided', + 'mishandle', + 'mishap', + 'mishear', + 'mishmash', + 'misinform', + 'misinterpret', + 'misjoinder', + 'misjudge', + 'mislay', + 'mislead', + 'misleading', + 'mislike', + 'mismanage', + 'mismatch', + 'mismate', + 'misname', + 'misnomer', + 'misogamy', + 'misogynist', + 'misogyny', + 'misology', + 'mispickel', + 'misplace', + 'misplay', + 'mispleading', + 'misprint', + 'misprision', + 'misprize', + 'mispronounce', + 'misquotation', + 'misquote', + 'misread', + 'misreckon', + 'misreport', + 'misrepresent', + 'misrule', + 'miss', + 'missal', + 'missend', + 'misshape', + 'misshapen', + 'missile', + 'missilery', + 'missing', + 'mission', + 'missionary', + 'missioner', + 'missis', + 'missive', + 'misspeak', + 'misspell', + 'misspend', + 'misstate', + 'misstep', + 'missus', + 'missy', + 'mist', + 'mistakable', + 'mistake', + 'mistaken', + 'misteach', + 'mister', + 'mistime', + 'mistletoe', + 'mistook', + 'mistral', + 'mistranslate', + 'mistreat', + 'mistress', + 'mistrial', + 'mistrust', + 'mistrustful', + 'misty', + 'misunderstand', + 'misunderstanding', + 'misunderstood', + 'misusage', + 'misuse', + 'misvalue', + 'mite', + 'miter', + 'miterwort', + 'mither', + 'mithridate', + 'mithridatism', + 'miticide', + 'mitigate', + 'mitis', + 'mitochondrion', + 'mitosis', + 'mitrailleuse', + 'mitre', + 'mitrewort', + 'mitt', + 'mitten', + 'mittimus', + 'mitzvah', + 'mix', + 'mixed', + 'mixer', + 'mixologist', + 'mixture', + 'mizzen', + 'mizzenmast', + 'mizzle', + 'ml', + 'mm', + 'mneme', + 'mnemonic', + 'mnemonics', + 'mo', + 'moa', + 'moan', + 'moat', + 'mob', + 'mobcap', + 'mobile', + 'mobility', + 'mobilize', + 'mobocracy', + 'mobster', + 'moccasin', + 'mocha', + 'mock', + 'mockery', + 'mockingbird', + 'mod', + 'modal', + 'modality', + 'mode', + 'model', + 'modeling', + 'moderate', + 'moderation', + 'moderato', + 'moderator', + 'modern', + 'modernism', + 'modernistic', + 'modernity', + 'modernize', + 'modest', + 'modesty', + 'modicum', + 'modification', + 'modifier', + 'modify', + 'modillion', + 'modiolus', + 'modish', + 'modiste', + 'modular', + 'modulate', + 'modulation', + 'modulator', + 'module', + 'modulus', + 'mofette', + 'mog', + 'mogul', + 'mohair', + 'mohur', + 'moidore', + 'moiety', + 'moil', + 'moire', + 'moist', + 'moisten', + 'moisture', + 'moke', + 'mol', + 'mola', + 'molal', + 'molality', + 'molar', + 'molarity', + 'molasses', + 'mold', + 'moldboard', + 'molder', + 'molding', + 'moldy', + 'mole', + 'molecular', + 'molecule', + 'molehill', + 'moleskin', + 'moleskins', + 'molest', + 'moline', + 'moll', + 'mollescent', + 'mollify', + 'mollusc', + 'molluscoid', + 'molly', + 'mollycoddle', + 'molt', + 'molten', + 'moly', + 'molybdate', + 'molybdenite', + 'molybdenous', + 'molybdenum', + 'molybdic', + 'molybdous', + 'mom', + 'moment', + 'momentarily', + 'momentary', + 'momently', + 'momentous', + 'momentum', + 'momism', + 'monachal', + 'monachism', + 'monacid', + 'monad', + 'monadelphous', + 'monadism', + 'monadnock', + 'monandrous', + 'monandry', + 'monanthous', + 'monarch', + 'monarchal', + 'monarchism', + 'monarchist', + 'monarchy', + 'monarda', + 'monas', + 'monastery', + 'monastic', + 'monasticism', + 'monatomic', + 'monaural', + 'monaxial', + 'monazite', + 'monde', + 'monecious', + 'monetary', + 'money', + 'moneybag', + 'moneybags', + 'moneychanger', + 'moneyed', + 'moneyer', + 'moneylender', + 'moneymaker', + 'moneymaking', + 'moneywort', + 'mong', + 'monger', + 'mongo', + 'mongolism', + 'mongoloid', + 'mongoose', + 'mongrel', + 'mongrelize', + 'monied', + 'monies', + 'moniker', + 'moniliform', + 'monism', + 'monition', + 'monitor', + 'monitorial', + 'monitory', + 'monk', + 'monkery', + 'monkey', + 'monkeypot', + 'monkfish', + 'monkhood', + 'monkish', + 'monkshood', + 'mono', + 'monoacid', + 'monoatomic', + 'monobasic', + 'monocarpic', + 'monochasium', + 'monochloride', + 'monochord', + 'monochromat', + 'monochromatic', + 'monochromatism', + 'monochrome', + 'monocle', + 'monoclinous', + 'monocoque', + 'monocot', + 'monocotyledon', + 'monocular', + 'monoculture', + 'monocycle', + 'monocyclic', + 'monocyte', + 'monodic', + 'monodrama', + 'monody', + 'monofilament', + 'monogamist', + 'monogamous', + 'monogamy', + 'monogenesis', + 'monogenetic', + 'monogenic', + 'monogram', + 'monograph', + 'monogyny', + 'monohydric', + 'monohydroxy', + 'monoicous', + 'monolatry', + 'monolayer', + 'monolingual', + 'monolith', + 'monolithic', + 'monologue', + 'monomania', + 'monomer', + 'monomerous', + 'monometallic', + 'monometallism', + 'monomial', + 'monomolecular', + 'monomorphic', + 'mononuclear', + 'mononucleosis', + 'monopetalous', + 'monophagous', + 'monophonic', + 'monophony', + 'monophthong', + 'monophyletic', + 'monoplane', + 'monoplegia', + 'monoploid', + 'monopode', + 'monopolist', + 'monopolize', + 'monopoly', + 'monopteros', + 'monorail', + 'monosaccharide', + 'monosepalous', + 'monosome', + 'monospermous', + 'monostich', + 'monostome', + 'monostrophe', + 'monostylous', + 'monosyllabic', + 'monosyllable', + 'monosymmetric', + 'monotheism', + 'monotint', + 'monotone', + 'monotonous', + 'monotony', + 'monotype', + 'monovalent', + 'monoxide', + 'monsieur', + 'monsignor', + 'monsoon', + 'monster', + 'monstrance', + 'monstrosity', + 'monstrous', + 'montage', + 'montane', + 'monte', + 'monteith', + 'montero', + 'montgolfier', + 'month', + 'monthly', + 'monticule', + 'monument', + 'monumental', + 'monumentalize', + 'monzonite', + 'moo', + 'mooch', + 'mood', + 'moody', + 'moolah', + 'moon', + 'moonbeam', + 'mooncalf', + 'mooned', + 'mooneye', + 'moonfish', + 'moonlight', + 'moonlighting', + 'moonlit', + 'moonraker', + 'moonrise', + 'moonscape', + 'moonseed', + 'moonset', + 'moonshine', + 'moonshiner', + 'moonshot', + 'moonstone', + 'moonstruck', + 'moonwort', + 'moony', + 'moor', + 'moorfowl', + 'mooring', + 'moorings', + 'moorland', + 'moorwort', + 'moose', + 'moot', + 'mop', + 'mopboard', + 'mope', + 'mopes', + 'mopey', + 'moppet', + 'moquette', + 'mora', + 'moraceous', + 'moraine', + 'moral', + 'morale', + 'moralist', + 'morality', + 'moralize', + 'morass', + 'moratorium', + 'moray', + 'morbid', + 'morbidezza', + 'morbidity', + 'morbific', + 'morbilli', + 'morceau', + 'mordacious', + 'mordancy', + 'mordant', + 'mordent', + 'more', + 'moreen', + 'morel', + 'morello', + 'moreover', + 'mores', + 'morganatic', + 'morganite', + 'morgen', + 'morgue', + 'moribund', + 'morion', + 'morn', + 'morning', + 'mornings', + 'morocco', + 'moron', + 'morose', + 'morph', + 'morpheme', + 'morphia', + 'morphine', + 'morphinism', + 'morphogenesis', + 'morphology', + 'morphophoneme', + 'morphophonemics', + 'morphosis', + 'morris', + 'morrow', + 'morse', + 'morsel', + 'mort', + 'mortal', + 'mortality', + 'mortar', + 'mortarboard', + 'mortgage', + 'mortgagee', + 'mortgagor', + 'mortician', + 'mortification', + 'mortify', + 'mortise', + 'mortmain', + 'mortuary', + 'morula', + 'mosaic', + 'mosasaur', + 'moschatel', + 'mosey', + 'mosque', + 'mosquito', + 'moss', + 'mossback', + 'mossbunker', + 'mosstrooper', + 'mossy', + 'most', + 'mostly', + 'mot', + 'mote', + 'motel', + 'motet', + 'moth', + 'mothball', + 'mother', + 'motherhood', + 'mothering', + 'motherland', + 'motherless', + 'motherly', + 'motherwort', + 'mothy', + 'motif', + 'motile', + 'motion', + 'motionless', + 'motivate', + 'motivation', + 'motive', + 'motivity', + 'motley', + 'motmot', + 'motoneuron', + 'motor', + 'motorbike', + 'motorboat', + 'motorboating', + 'motorbus', + 'motorcade', + 'motorcar', + 'motorcycle', + 'motoring', + 'motorist', + 'motorize', + 'motorman', + 'motorway', + 'motte', + 'mottle', + 'mottled', + 'motto', + 'moue', + 'mouflon', + 'moujik', + 'mould', + 'moulder', + 'moulding', + 'mouldy', + 'moulin', + 'moult', + 'mound', + 'mount', + 'mountain', + 'mountaineer', + 'mountaineering', + 'mountainous', + 'mountainside', + 'mountaintop', + 'mountebank', + 'mounting', + 'mourn', + 'mourner', + 'mournful', + 'mourning', + 'mouse', + 'mousebird', + 'mouser', + 'mousetail', + 'mousetrap', + 'mousey', + 'moussaka', + 'mousse', + 'mousseline', + 'moustache', + 'mousy', + 'mouth', + 'mouthful', + 'mouthpart', + 'mouthpiece', + 'mouthwash', + 'mouthy', + 'mouton', + 'movable', + 'move', + 'movement', + 'mover', + 'movie', + 'moving', + 'mow', + 'mown', + 'moxa', + 'moxie', + 'mozzarella', + 'mozzetta', + 'mu', + 'much', + 'muchness', + 'mucilage', + 'mucilaginous', + 'mucin', + 'muck', + 'mucker', + 'muckrake', + 'muckraker', + 'muckworm', + 'mucky', + 'mucoid', + 'mucoprotein', + 'mucor', + 'mucosa', + 'mucous', + 'mucoviscidosis', + 'mucro', + 'mucronate', + 'mucus', + 'mud', + 'mudcat', + 'muddle', + 'muddlehead', + 'muddleheaded', + 'muddler', + 'muddy', + 'mudfish', + 'mudguard', + 'mudlark', + 'mudpack', + 'mudra', + 'mudskipper', + 'mudslinger', + 'mudslinging', + 'mudstone', + 'muenster', + 'muezzin', + 'muff', + 'muffin', + 'muffle', + 'muffler', + 'mufti', + 'mug', + 'mugger', + 'muggins', + 'muggy', + 'mugwump', + 'mujik', + 'mukluk', + 'mulatto', + 'mulberry', + 'mulch', + 'mulct', + 'mule', + 'muleteer', + 'muley', + 'muliebrity', + 'mulish', + 'mull', + 'mullah', + 'mullein', + 'muller', + 'mullet', + 'mulley', + 'mulligan', + 'mulligatawny', + 'mulligrubs', + 'mullion', + 'mullite', + 'mullock', + 'multiangular', + 'multicellular', + 'multicolor', + 'multicolored', + 'multidisciplinary', + 'multifaceted', + 'multifarious', + 'multifid', + 'multiflorous', + 'multifoil', + 'multifold', + 'multifoliate', + 'multiform', + 'multilateral', + 'multilingual', + 'multimillionaire', + 'multinational', + 'multinuclear', + 'multipara', + 'multiparous', + 'multipartite', + 'multiped', + 'multiphase', + 'multiple', + 'multiplex', + 'multiplicand', + 'multiplicate', + 'multiplication', + 'multiplicity', + 'multiplier', + 'multiply', + 'multipurpose', + 'multiracial', + 'multistage', + 'multitude', + 'multitudinous', + 'multivalent', + 'multiversity', + 'multivibrator', + 'multivocal', + 'multure', + 'mum', + 'mumble', + 'mummer', + 'mummery', + 'mummify', + 'mummy', + 'mump', + 'mumps', + 'munch', + 'mundane', + 'municipal', + 'municipality', + 'municipalize', + 'munificent', + 'muniment', + 'muniments', + 'munition', + 'munitions', + 'muntin', + 'muntjac', + 'muon', + 'murage', + 'mural', + 'murder', + 'murderous', + 'mure', + 'murex', + 'muriate', + 'muricate', + 'murine', + 'murk', + 'murky', + 'murmur', + 'murmuration', + 'murmurous', + 'murphy', + 'murrain', + 'murre', + 'murrelet', + 'murrey', + 'murrhine', + 'murther', + 'musaceous', + 'muscadel', + 'muscadine', + 'muscarine', + 'muscat', + 'muscatel', + 'muscid', + 'muscle', + 'muscovado', + 'muscular', + 'musculature', + 'muse', + 'museology', + 'musette', + 'museum', + 'mush', + 'mushroom', + 'mushy', + 'music', + 'musical', + 'musicale', + 'musician', + 'musicianship', + 'musicology', + 'musing', + 'musjid', + 'musk', + 'muskeg', + 'muskellunge', + 'musket', + 'musketeer', + 'musketry', + 'muskmelon', + 'muskrat', + 'musky', + 'muslin', + 'musquash', + 'muss', + 'mussel', + 'must', + 'mustache', + 'mustachio', + 'mustang', + 'mustard', + 'mustee', + 'musteline', + 'muster', + 'musty', + 'mut', + 'mutable', + 'mutant', + 'mutate', + 'mutation', + 'mute', + 'muticous', + 'mutilate', + 'mutineer', + 'mutinous', + 'mutiny', + 'mutism', + 'mutt', + 'mutter', + 'mutton', + 'muttonchops', + 'muttonhead', + 'mutual', + 'mutualism', + 'mutualize', + 'mutule', + 'muumuu', + 'muzhik', + 'muzz', + 'muzzle', + 'muzzy', + 'my', + 'myalgia', + 'myall', + 'myasthenia', + 'mycetozoan', + 'mycobacterium', + 'mycology', + 'mycorrhiza', + 'mycosis', + 'mydriasis', + 'mydriatic', + 'myelencephalon', + 'myelitis', + 'myeloid', + 'myiasis', + 'mylohyoid', + 'mylonite', + 'myna', + 'myocardiograph', + 'myocarditis', + 'myocardium', + 'myogenic', + 'myoglobin', + 'myology', + 'myopia', + 'myopic', + 'myosin', + 'myosotis', + 'myotome', + 'myotonia', + 'myriad', + 'myriagram', + 'myriameter', + 'myriapod', + 'myrica', + 'myrmecology', + 'myrmecophagous', + 'myrmidon', + 'myrobalan', + 'myrrh', + 'myrtaceous', + 'myrtle', + 'myself', + 'mystagogue', + 'mysterious', + 'mystery', + 'mystic', + 'mystical', + 'mysticism', + 'mystify', + 'mystique', + 'myth', + 'mythical', + 'mythicize', + 'mythify', + 'mythological', + 'mythologize', + 'mythology', + 'mythomania', + 'mythopoeia', + 'mythopoeic', + 'mythos', + 'myxedema', + 'myxoma', + 'myxomatosis', + 'myxomycete', + 'n', + 'nab', + 'nabob', + 'nacelle', + 'nacre', + 'nacred', + 'nacreous', + 'nadir', + 'nae', + 'naevus', + 'nag', + 'nagana', + 'nagging', + 'nagual', + 'naiad', + 'naif', + 'nail', + 'nailbrush', + 'nailhead', + 'nainsook', + 'naissant', + 'naive', + 'naivete', + 'naked', + 'naker', + 'name', + 'nameless', + 'namely', + 'nameplate', + 'namesake', + 'nance', + 'nancy', + 'nankeen', + 'nanny', + 'nanoid', + 'nanosecond', + 'naos', + 'nap', + 'napalm', + 'nape', + 'napery', + 'naphtha', + 'naphthalene', + 'naphthol', + 'naphthyl', + 'napiform', + 'napkin', + 'napoleon', + 'nappe', + 'napper', + 'nappy', + 'narceine', + 'narcissism', + 'narcissus', + 'narcoanalysis', + 'narcolepsy', + 'narcoma', + 'narcose', + 'narcosis', + 'narcosynthesis', + 'narcotic', + 'narcotism', + 'narcotize', + 'nard', + 'nardoo', + 'nares', + 'narghile', + 'narial', + 'narrate', + 'narration', + 'narrative', + 'narrow', + 'narrows', + 'narthex', + 'narwhal', + 'nasal', + 'nasalize', + 'nascent', + 'naseberry', + 'nasion', + 'nasopharynx', + 'nasturtium', + 'nasty', + 'natal', + 'natality', + 'natant', + 'natation', + 'natator', + 'natatorial', + 'natatorium', + 'natatory', + 'natch', + 'nates', + 'natheless', + 'nation', + 'national', + 'nationalism', + 'nationalist', + 'nationality', + 'nationalize', + 'nationwide', + 'native', + 'nativism', + 'nativity', + 'natron', + 'natter', + 'natterjack', + 'natty', + 'natural', + 'naturalism', + 'naturalist', + 'naturalistic', + 'naturalize', + 'naturally', + 'nature', + 'naturism', + 'naturopathy', + 'naught', + 'naughty', + 'naumachia', + 'nauplius', + 'nausea', + 'nauseate', + 'nauseating', + 'nauseous', + 'nautch', + 'nautical', + 'nautilus', + 'naval', + 'navar', + 'nave', + 'navel', + 'navelwort', + 'navicert', + 'navicular', + 'navigable', + 'navigate', + 'navigation', + 'navigator', + 'navvy', + 'navy', + 'nawab', + 'nay', + 'neap', + 'near', + 'nearby', + 'nearly', + 'nearsighted', + 'neat', + 'neaten', + 'neath', + 'neb', + 'nebula', + 'nebulize', + 'nebulose', + 'nebulosity', + 'nebulous', + 'necessarian', + 'necessaries', + 'necessarily', + 'necessary', + 'necessitarianism', + 'necessitate', + 'necessitous', + 'necessity', + 'neck', + 'neckband', + 'neckcloth', + 'neckerchief', + 'necking', + 'necklace', + 'neckline', + 'neckpiece', + 'necktie', + 'neckwear', + 'necrolatry', + 'necrology', + 'necromancy', + 'necrophilia', + 'necrophilism', + 'necrophobia', + 'necropolis', + 'necropsy', + 'necroscopy', + 'necrose', + 'necrosis', + 'necrotomy', + 'nectar', + 'nectareous', + 'nectarine', + 'nectarous', + 'nee', + 'need', + 'needful', + 'neediness', + 'needle', + 'needlecraft', + 'needlefish', + 'needleful', + 'needlepoint', + 'needless', + 'needlewoman', + 'needlework', + 'needs', + 'needy', + 'nefarious', + 'negate', + 'negation', + 'negative', + 'negativism', + 'negatron', + 'neglect', + 'neglectful', + 'negligee', + 'negligence', + 'negligent', + 'negligible', + 'negotiable', + 'negotiant', + 'negotiate', + 'negotiation', + 'negus', + 'neigh', + 'neighbor', + 'neighborhood', + 'neighboring', + 'neighborly', + 'neither', + 'nekton', + 'nelly', + 'nelson', + 'nemathelminth', + 'nematic', + 'nematode', + 'nemertean', + 'nemesis', + 'neoarsphenamine', + 'neoclassic', + 'neoclassicism', + 'neocolonialism', + 'neodymium', + 'neoimpressionism', + 'neolith', + 'neologism', + 'neologize', + 'neology', + 'neomycin', + 'neon', + 'neonatal', + 'neonate', + 'neophyte', + 'neoplasm', + 'neoplasticism', + 'neoplasty', + 'neoprene', + 'neoteny', + 'neoteric', + 'neoterism', + 'neoterize', + 'neotype', + 'nepenthe', + 'neper', + 'nepheline', + 'nephelinite', + 'nephelometer', + 'nephew', + 'nephogram', + 'nephograph', + 'nephology', + 'nephoscope', + 'nephralgia', + 'nephrectomy', + 'nephridium', + 'nephritic', + 'nephritis', + 'nephrolith', + 'nephron', + 'nephrosis', + 'nephrotomy', + 'nepotism', + 'neptunium', + 'neral', + 'neritic', + 'nerval', + 'nerve', + 'nerveless', + 'nerves', + 'nervine', + 'nervous', + 'nervy', + 'nescience', + 'ness', + 'nest', + 'nestle', + 'nestling', + 'net', + 'nether', + 'nethermost', + 'netsuke', + 'netting', + 'nettle', + 'nettlesome', + 'netty', + 'network', + 'neume', + 'neural', + 'neuralgia', + 'neurasthenia', + 'neurasthenic', + 'neurilemma', + 'neuritis', + 'neuroblast', + 'neurocoele', + 'neurogenic', + 'neuroglia', + 'neurogram', + 'neurologist', + 'neurology', + 'neuroma', + 'neuromuscular', + 'neuron', + 'neuropath', + 'neuropathy', + 'neurophysiology', + 'neuropsychiatry', + 'neurosis', + 'neurosurgery', + 'neurotic', + 'neuroticism', + 'neurotomy', + 'neurovascular', + 'neuter', + 'neutral', + 'neutralism', + 'neutrality', + 'neutralization', + 'neutralize', + 'neutretto', + 'neutrino', + 'neutron', + 'neutrophil', + 'never', + 'nevermore', + 'nevertheless', + 'nevus', + 'new', + 'newborn', + 'newcomer', + 'newel', + 'newfangled', + 'newfashioned', + 'newish', + 'newly', + 'newlywed', + 'newness', + 'news', + 'newsboy', + 'newscast', + 'newsdealer', + 'newsletter', + 'newsmagazine', + 'newsman', + 'newsmonger', + 'newspaper', + 'newspaperman', + 'newspaperwoman', + 'newsprint', + 'newsreel', + 'newsstand', + 'newsworthy', + 'newsy', + 'newt', + 'newton', + 'next', + 'nexus', + 'niacin', + 'nib', + 'nibble', + 'niblick', + 'niccolite', + 'nice', + 'nicety', + 'niche', + 'nick', + 'nickel', + 'nickelic', + 'nickeliferous', + 'nickelodeon', + 'nickelous', + 'nicker', + 'nicknack', + 'nickname', + 'nicotiana', + 'nicotine', + 'nicotinism', + 'nictitate', + 'niddering', + 'nide', + 'nidicolous', + 'nidifugous', + 'nidify', + 'nidus', + 'niece', + 'niello', + 'nifty', + 'niggard', + 'niggardly', + 'nigger', + 'niggerhead', + 'niggle', + 'niggling', + 'nigh', + 'night', + 'nightcap', + 'nightclub', + 'nightdress', + 'nightfall', + 'nightgown', + 'nighthawk', + 'nightie', + 'nightingale', + 'nightjar', + 'nightlong', + 'nightly', + 'nightmare', + 'nightrider', + 'nightshade', + 'nightshirt', + 'nightspot', + 'nightstick', + 'nighttime', + 'nightwalker', + 'nightwear', + 'nigrescent', + 'nigrify', + 'nigritude', + 'nigrosine', + 'nihil', + 'nihilism', + 'nihility', + 'nikethamide', + 'nil', + 'nilgai', + 'nim', + 'nimble', + 'nimbostratus', + 'nimbus', + 'nimiety', + 'nincompoop', + 'nine', + 'ninebark', + 'ninefold', + 'ninepins', + 'nineteen', + 'nineteenth', + 'ninetieth', + 'ninety', + 'ninny', + 'ninnyhammer', + 'ninon', + 'ninth', + 'niobic', + 'niobium', + 'niobous', + 'nip', + 'nipa', + 'niphablepsia', + 'nipper', + 'nippers', + 'nipping', + 'nipple', + 'nippy', + 'nirvana', + 'nisi', + 'nisus', + 'nit', + 'niter', + 'nitid', + 'nitramine', + 'nitrate', + 'nitre', + 'nitride', + 'nitriding', + 'nitrification', + 'nitrile', + 'nitrite', + 'nitrobacteria', + 'nitrobenzene', + 'nitrochloroform', + 'nitrogen', + 'nitrogenize', + 'nitrogenous', + 'nitroglycerin', + 'nitrometer', + 'nitroparaffin', + 'nitrosamine', + 'nitroso', + 'nitrosyl', + 'nitrous', + 'nitty', + 'nitwit', + 'nival', + 'niveous', + 'nix', + 'no', + 'nob', + 'nobby', + 'nobelium', + 'nobility', + 'noble', + 'nobleman', + 'noblesse', + 'noblewoman', + 'nobody', + 'nock', + 'noctambulism', + 'noctambulous', + 'noctiluca', + 'noctilucent', + 'noctule', + 'nocturn', + 'nocturnal', + 'nocturne', + 'nocuous', + 'nod', + 'nodal', + 'noddle', + 'noddy', + 'node', + 'nodical', + 'nodose', + 'nodular', + 'nodule', + 'nodus', + 'noesis', + 'noetic', + 'nog', + 'noggin', + 'nogging', + 'noil', + 'noise', + 'noiseless', + 'noisemaker', + 'noisette', + 'noisome', + 'noisy', + 'noma', + 'nomad', + 'nomadic', + 'nomadize', + 'nomarch', + 'nomarchy', + 'nombles', + 'nombril', + 'nomen', + 'nomenclator', + 'nomenclature', + 'nominal', + 'nominalism', + 'nominate', + 'nomination', + 'nominative', + 'nominee', + 'nomism', + 'nomography', + 'nomology', + 'nomothetic', + 'nonage', + 'nonagenarian', + 'nonaggression', + 'nonagon', + 'nonalcoholic', + 'nonaligned', + 'nonalignment', + 'nonappearance', + 'nonary', + 'nonattendance', + 'nonbeliever', + 'nonbelligerent', + 'nonce', + 'nonchalance', + 'nonchalant', + 'noncombatant', + 'noncommittal', + 'noncompliance', + 'nonconcurrence', + 'nonconductor', + 'nonconformance', + 'nonconformist', + 'nonconformity', + 'noncontributory', + 'noncooperation', + 'nondescript', + 'nondisjunction', + 'none', + 'noneffective', + 'nonego', + 'nonentity', + 'nones', + 'nonessential', + 'nonesuch', + 'nonet', + 'nonetheless', + 'nonexistence', + 'nonfeasance', + 'nonferrous', + 'nonfiction', + 'nonflammable', + 'nonfulfillment', + 'nonillion', + 'noninterference', + 'nonintervention', + 'nonjoinder', + 'nonjuror', + 'nonlegal', + 'nonlinearity', + 'nonmaterial', + 'nonmetal', + 'nonmetallic', + 'nonmoral', + 'nonobedience', + 'nonobjective', + 'nonobservance', + 'nonoccurrence', + 'nonpareil', + 'nonparous', + 'nonparticipating', + 'nonparticipation', + 'nonpartisan', + 'nonpayment', + 'nonperformance', + 'nonperishable', + 'nonplus', + 'nonproductive', + 'nonprofessional', + 'nonprofit', + 'nonrecognition', + 'nonrepresentational', + 'nonresident', + 'nonresistance', + 'nonresistant', + 'nonrestrictive', + 'nonreturnable', + 'nonrigid', + 'nonscheduled', + 'nonsectarian', + 'nonsense', + 'nonsmoker', + 'nonstandard', + 'nonstop', + 'nonstriated', + 'nonsuch', + 'nonsuit', + 'nonunion', + 'nonunionism', + 'nonviolence', + 'noodle', + 'noodlehead', + 'nook', + 'noon', + 'noonday', + 'noontide', + 'noontime', + 'noose', + 'nope', + 'nor', + 'noria', + 'norite', + 'norland', + 'norm', + 'normal', + 'normalcy', + 'normalize', + 'normally', + 'normative', + 'north', + 'northbound', + 'northeast', + 'northeaster', + 'northeasterly', + 'northeastward', + 'northeastwards', + 'norther', + 'northerly', + 'northern', + 'northernmost', + 'northing', + 'northward', + 'northwards', + 'northwest', + 'northwester', + 'northwesterly', + 'northwestward', + 'northwestwards', + 'nose', + 'noseband', + 'nosebleed', + 'nosegay', + 'nosepiece', + 'nosewheel', + 'nosey', + 'nosh', + 'nosing', + 'nosography', + 'nosology', + 'nostalgia', + 'nostoc', + 'nostology', + 'nostomania', + 'nostril', + 'nostrum', + 'nosy', + 'not', + 'notability', + 'notable', + 'notarial', + 'notarize', + 'notary', + 'notate', + 'notation', + 'notch', + 'note', + 'notebook', + 'notecase', + 'noted', + 'notepaper', + 'noteworthy', + 'nothing', + 'nothingness', + 'notice', + 'noticeable', + 'notification', + 'notify', + 'notion', + 'notional', + 'notions', + 'notochord', + 'notorious', + 'notornis', + 'notum', + 'notwithstanding', + 'nougat', + 'nought', + 'noumenon', + 'noun', + 'nourish', + 'nourishing', + 'nourishment', + 'nous', + 'nova', + 'novaculite', + 'novation', + 'novel', + 'novelette', + 'novelist', + 'novelistic', + 'novelize', + 'novella', + 'novelty', + 'novena', + 'novercal', + 'novice', + 'novitiate', + 'novobiocin', + 'now', + 'nowadays', + 'noway', + 'nowhere', + 'nowhither', + 'nowise', + 'nowt', + 'noxious', + 'noyade', + 'nozzle', + 'nth', + 'nu', + 'nuance', + 'nub', + 'nubbin', + 'nubble', + 'nubbly', + 'nubile', + 'nubilous', + 'nucellus', + 'nuclear', + 'nuclease', + 'nucleate', + 'nuclei', + 'nucleolar', + 'nucleolated', + 'nucleolus', + 'nucleon', + 'nucleonics', + 'nucleoplasm', + 'nucleoprotein', + 'nucleoside', + 'nucleotidase', + 'nucleotide', + 'nucleus', + 'nuclide', + 'nude', + 'nudge', + 'nudibranch', + 'nudicaul', + 'nudism', + 'nudity', + 'nudnik', + 'nugatory', + 'nuggar', + 'nugget', + 'nuisance', + 'nuke', + 'null', + 'nullification', + 'nullifidian', + 'nullify', + 'nullipore', + 'nullity', + 'numb', + 'numbat', + 'number', + 'numberless', + 'numbfish', + 'numbing', + 'numbles', + 'numbskull', + 'numen', + 'numerable', + 'numeral', + 'numerary', + 'numerate', + 'numeration', + 'numerator', + 'numerical', + 'numerology', + 'numerous', + 'numinous', + 'numismatics', + 'numismatist', + 'numismatology', + 'nummary', + 'nummular', + 'nummulite', + 'numskull', + 'nun', + 'nunatak', + 'nunciature', + 'nuncio', + 'nuncle', + 'nuncupative', + 'nunhood', + 'nunnery', + 'nuptial', + 'nuptials', + 'nurse', + 'nursemaid', + 'nursery', + 'nurserymaid', + 'nurseryman', + 'nursling', + 'nurture', + 'nut', + 'nutation', + 'nutbrown', + 'nutcracker', + 'nutgall', + 'nuthatch', + 'nuthouse', + 'nutlet', + 'nutmeg', + 'nutpick', + 'nutria', + 'nutrient', + 'nutrilite', + 'nutriment', + 'nutrition', + 'nutritionist', + 'nutritious', + 'nutritive', + 'nuts', + 'nutshell', + 'nutting', + 'nutty', + 'nutwood', + 'nuzzle', + 'nyala', + 'nyctaginaceous', + 'nyctalopia', + 'nyctophobia', + 'nylghau', + 'nylon', + 'nylons', + 'nymph', + 'nympha', + 'nymphalid', + 'nymphet', + 'nympho', + 'nympholepsy', + 'nymphomania', + 'nystagmus', + 'nystatin', + 'o', + 'oaf', + 'oak', + 'oaken', + 'oakum', + 'oar', + 'oared', + 'oarfish', + 'oarlock', + 'oarsman', + 'oasis', + 'oast', + 'oat', + 'oatcake', + 'oaten', + 'oath', + 'oatmeal', + 'obbligato', + 'obcordate', + 'obduce', + 'obdurate', + 'obeah', + 'obedience', + 'obedient', + 'obeisance', + 'obelisk', + 'obelize', + 'obese', + 'obey', + 'obfuscate', + 'obi', + 'obit', + 'obituary', + 'object', + 'objectify', + 'objection', + 'objectionable', + 'objective', + 'objectivism', + 'objectivity', + 'objurgate', + 'oblast', + 'oblate', + 'oblation', + 'obligate', + 'obligation', + 'obligato', + 'obligatory', + 'oblige', + 'obligee', + 'obliging', + 'obligor', + 'oblique', + 'obliquely', + 'obliquity', + 'obliterate', + 'obliteration', + 'oblivion', + 'oblivious', + 'oblong', + 'obloquy', + 'obmutescence', + 'obnoxious', + 'obnubilate', + 'oboe', + 'obolus', + 'obovate', + 'obovoid', + 'obreption', + 'obscene', + 'obscenity', + 'obscurant', + 'obscurantism', + 'obscuration', + 'obscure', + 'obscurity', + 'obsecrate', + 'obsequent', + 'obsequies', + 'obsequious', + 'observable', + 'observance', + 'observant', + 'observation', + 'observatory', + 'observe', + 'observer', + 'obsess', + 'obsession', + 'obsessive', + 'obsidian', + 'obsolesce', + 'obsolescent', + 'obsolete', + 'obstacle', + 'obstetric', + 'obstetrician', + 'obstetrics', + 'obstinacy', + 'obstinate', + 'obstipation', + 'obstreperous', + 'obstruct', + 'obstruction', + 'obstructionist', + 'obstruent', + 'obtain', + 'obtect', + 'obtest', + 'obtrude', + 'obtrusive', + 'obtund', + 'obturate', + 'obtuse', + 'obumbrate', + 'obverse', + 'obvert', + 'obviate', + 'obvious', + 'obvolute', + 'ocarina', + 'occasion', + 'occasional', + 'occasionalism', + 'occasionally', + 'occident', + 'occidental', + 'occipital', + 'occiput', + 'occlude', + 'occlusion', + 'occlusive', + 'occult', + 'occultation', + 'occultism', + 'occupancy', + 'occupant', + 'occupation', + 'occupational', + 'occupier', + 'occupy', + 'occur', + 'occurrence', + 'ocean', + 'oceanic', + 'oceanography', + 'ocelot', + 'och', + 'ocher', + 'ochlocracy', + 'ochlophobia', + 'ochone', + 'ochre', + 'ochrea', + 'ocotillo', + 'ocrea', + 'ocreate', + 'octachord', + 'octad', + 'octagon', + 'octagonal', + 'octahedral', + 'octahedrite', + 'octahedron', + 'octal', + 'octamerous', + 'octameter', + 'octan', + 'octane', + 'octangle', + 'octangular', + 'octant', + 'octarchy', + 'octastyle', + 'octavalent', + 'octave', + 'octavo', + 'octennial', + 'octet', + 'octillion', + 'octodecillion', + 'octodecimo', + 'octofoil', + 'octogenarian', + 'octonary', + 'octopod', + 'octopus', + 'octoroon', + 'octosyllabic', + 'octosyllable', + 'octroi', + 'octuple', + 'ocular', + 'oculist', + 'oculomotor', + 'oculus', + 'od', + 'odalisque', + 'odd', + 'oddball', + 'oddity', + 'oddment', + 'odds', + 'ode', + 'odeum', + 'odious', + 'odium', + 'odometer', + 'odontalgia', + 'odontoblast', + 'odontograph', + 'odontoid', + 'odontology', + 'odor', + 'odoriferous', + 'odorous', + 'odyl', + 'oecology', + 'oedema', + 'oeillade', + 'oenomel', + 'oersted', + 'oesophagus', + 'oestradiol', + 'oestrin', + 'oestriol', + 'oestrogen', + 'oestrone', + 'oeuvre', + 'of', + 'ofay', + 'off', + 'offal', + 'offbeat', + 'offence', + 'offend', + 'offense', + 'offenseless', + 'offensive', + 'offer', + 'offering', + 'offertory', + 'offhand', + 'office', + 'officeholder', + 'officer', + 'official', + 'officialdom', + 'officialese', + 'officialism', + 'officiant', + 'officiary', + 'officiate', + 'officinal', + 'officious', + 'offing', + 'offish', + 'offprint', + 'offset', + 'offshoot', + 'offshore', + 'offside', + 'offspring', + 'offstage', + 'oft', + 'often', + 'oftentimes', + 'ogdoad', + 'ogee', + 'ogham', + 'ogive', + 'ogle', + 'ogre', + 'oh', + 'ohm', + 'ohmage', + 'ohmmeter', + 'oho', + 'oidium', + 'oil', + 'oilbird', + 'oilcan', + 'oilcloth', + 'oilcup', + 'oiler', + 'oilskin', + 'oilstone', + 'oily', + 'oink', + 'ointment', + 'oka', + 'okapi', + 'okay', + 'oke', + 'okra', + 'old', + 'olden', + 'older', + 'oldest', + 'oldfangled', + 'oldie', + 'oldster', + 'oldwife', + 'oleaceous', + 'oleaginous', + 'oleander', + 'oleaster', + 'oleate', + 'olecranon', + 'oleic', + 'olein', + 'oleo', + 'oleograph', + 'oleomargarine', + 'oleoresin', + 'olericulture', + 'oleum', + 'olfaction', + 'olfactory', + 'olibanum', + 'olid', + 'oligarch', + 'oligarchy', + 'oligochaete', + 'oligoclase', + 'oligopoly', + 'oligopsony', + 'oligosaccharide', + 'oliguria', + 'olio', + 'olivaceous', + 'olive', + 'olivenite', + 'olivine', + 'olla', + 'ology', + 'oloroso', + 'omasum', + 'ombre', + 'ombudsman', + 'omega', + 'omelet', + 'omen', + 'omentum', + 'omer', + 'ominous', + 'omission', + 'omit', + 'ommatidium', + 'ommatophore', + 'omnibus', + 'omnidirectional', + 'omnifarious', + 'omnipotence', + 'omnipotent', + 'omnipresent', + 'omnirange', + 'omniscience', + 'omniscient', + 'omnivore', + 'omnivorous', + 'omophagia', + 'omphalos', + 'on', + 'onager', + 'onagraceous', + 'onanism', + 'once', + 'oncoming', + 'ondometer', + 'one', + 'oneiric', + 'oneirocritic', + 'oneiromancy', + 'oneness', + 'onerous', + 'oneself', + 'onetime', + 'ongoing', + 'onion', + 'onionskin', + 'onlooker', + 'only', + 'onomasiology', + 'onomastic', + 'onomastics', + 'onomatology', + 'onomatopoeia', + 'onrush', + 'onset', + 'onshore', + 'onslaught', + 'onstage', + 'onto', + 'ontogeny', + 'ontologism', + 'ontology', + 'onus', + 'onward', + 'onwards', + 'onyx', + 'oocyte', + 'oodles', + 'oof', + 'oogenesis', + 'oogonium', + 'oolite', + 'oology', + 'oomph', + 'oophorectomy', + 'oops', + 'oosperm', + 'oosphere', + 'oospore', + 'ootid', + 'ooze', + 'oozy', + 'opacity', + 'opah', + 'opal', + 'opalesce', + 'opalescent', + 'opaline', + 'opaque', + 'ope', + 'open', + 'opener', + 'openhanded', + 'opening', + 'openwork', + 'opera', + 'operable', + 'operand', + 'operant', + 'operate', + 'operatic', + 'operation', + 'operational', + 'operative', + 'operator', + 'operculum', + 'operetta', + 'operon', + 'operose', + 'ophicleide', + 'ophidian', + 'ophiolatry', + 'ophiology', + 'ophite', + 'ophthalmia', + 'ophthalmic', + 'ophthalmitis', + 'ophthalmologist', + 'ophthalmology', + 'ophthalmoscope', + 'ophthalmoscopy', + 'opiate', + 'opine', + 'opinicus', + 'opinion', + 'opinionated', + 'opinionative', + 'opisthognathous', + 'opium', + 'opiumism', + 'opossum', + 'oppidan', + 'oppilate', + 'opponent', + 'opportune', + 'opportunism', + 'opportunist', + 'opportunity', + 'opposable', + 'oppose', + 'opposite', + 'opposition', + 'oppress', + 'oppression', + 'oppressive', + 'opprobrious', + 'opprobrium', + 'oppugn', + 'oppugnant', + 'opsonin', + 'opsonize', + 'opt', + 'optative', + 'optic', + 'optical', + 'optician', + 'optics', + 'optimal', + 'optime', + 'optimism', + 'optimist', + 'optimistic', + 'optimize', + 'optimum', + 'option', + 'optional', + 'optometer', + 'optometrist', + 'optometry', + 'opulence', + 'opulent', + 'opuntia', + 'opus', + 'opuscule', + 'oquassa', + 'or', + 'ora', + 'oracle', + 'oracular', + 'oral', + 'orang', + 'orange', + 'orangeade', + 'orangery', + 'orangewood', + 'orangutan', + 'orangy', + 'orate', + 'oration', + 'orator', + 'oratorical', + 'oratorio', + 'oratory', + 'orb', + 'orbicular', + 'orbiculate', + 'orbit', + 'orbital', + 'orcein', + 'orchard', + 'orchardist', + 'orchardman', + 'orchestra', + 'orchestral', + 'orchestrate', + 'orchestrion', + 'orchid', + 'orchidaceous', + 'orchidectomy', + 'orchitis', + 'orcinol', + 'ordain', + 'ordeal', + 'order', + 'orderly', + 'ordinal', + 'ordinance', + 'ordinand', + 'ordinarily', + 'ordinary', + 'ordinate', + 'ordination', + 'ordnance', + 'ordonnance', + 'ordure', + 'ore', + 'oread', + 'orectic', + 'oregano', + 'organ', + 'organdy', + 'organelle', + 'organic', + 'organicism', + 'organism', + 'organist', + 'organization', + 'organize', + 'organizer', + 'organogenesis', + 'organography', + 'organology', + 'organometallic', + 'organon', + 'organotherapy', + 'organza', + 'organzine', + 'orgasm', + 'orgeat', + 'orgiastic', + 'orgulous', + 'orgy', + 'oriel', + 'orient', + 'oriental', + 'orientate', + 'orientation', + 'orifice', + 'oriflamme', + 'origami', + 'origan', + 'origin', + 'original', + 'originality', + 'originally', + 'originate', + 'originative', + 'orinasal', + 'oriole', + 'orison', + 'orle', + 'orlop', + 'ormolu', + 'ornament', + 'ornamental', + 'ornamentation', + 'ornamented', + 'ornate', + 'ornery', + 'ornis', + 'ornithic', + 'ornithine', + 'ornithischian', + 'ornithology', + 'ornithomancy', + 'ornithopod', + 'ornithopter', + 'ornithorhynchus', + 'ornithosis', + 'orobanchaceous', + 'orogeny', + 'orography', + 'orometer', + 'orotund', + 'orphan', + 'orphanage', + 'orphrey', + 'orpiment', + 'orpine', + 'orrery', + 'orris', + 'orthicon', + 'orthocephalic', + 'orthochromatic', + 'orthoclase', + 'orthodontia', + 'orthodontics', + 'orthodontist', + 'orthodox', + 'orthodoxy', + 'orthoepy', + 'orthogenesis', + 'orthogenetic', + 'orthogenic', + 'orthognathous', + 'orthogonal', + 'orthographize', + 'orthography', + 'orthohydrogen', + 'orthopedic', + 'orthopedics', + 'orthopedist', + 'orthopsychiatry', + 'orthopter', + 'orthopteran', + 'orthopterous', + 'orthoptic', + 'orthorhombic', + 'orthoscope', + 'orthostichy', + 'orthotropic', + 'orthotropous', + 'ortolan', + 'orts', + 'oryx', + 'os', + 'oscillate', + 'oscillation', + 'oscillator', + 'oscillatory', + 'oscillogram', + 'oscillograph', + 'oscilloscope', + 'oscine', + 'oscitancy', + 'oscitant', + 'oscular', + 'osculate', + 'osculation', + 'osculum', + 'osier', + 'osmic', + 'osmious', + 'osmium', + 'osmometer', + 'osmose', + 'osmosis', + 'osmunda', + 'osprey', + 'ossein', + 'osseous', + 'ossicle', + 'ossiferous', + 'ossification', + 'ossified', + 'ossifrage', + 'ossify', + 'ossuary', + 'osteal', + 'osteitis', + 'ostensible', + 'ostensive', + 'ostensorium', + 'ostensory', + 'ostentation', + 'osteoarthritis', + 'osteoblast', + 'osteoclasis', + 'osteoclast', + 'osteogenesis', + 'osteoid', + 'osteology', + 'osteoma', + 'osteomalacia', + 'osteomyelitis', + 'osteopath', + 'osteopathy', + 'osteophyte', + 'osteoplastic', + 'osteoporosis', + 'osteotome', + 'osteotomy', + 'ostiary', + 'ostiole', + 'ostium', + 'ostler', + 'ostmark', + 'ostosis', + 'ostracism', + 'ostracize', + 'ostracod', + 'ostracoderm', + 'ostracon', + 'ostrich', + 'otalgia', + 'other', + 'otherness', + 'otherwhere', + 'otherwise', + 'otherworld', + 'otherworldly', + 'otic', + 'otiose', + 'otitis', + 'otocyst', + 'otolaryngology', + 'otolith', + 'otology', + 'otoplasty', + 'otorhinolaryngology', + 'otoscope', + 'ottar', + 'ottava', + 'otter', + 'otto', + 'ottoman', + 'ouabain', + 'oubliette', + 'ouch', + 'oud', + 'ought', + 'oui', + 'ounce', + 'ouphe', + 'our', + 'ours', + 'ourself', + 'ourselves', + 'ousel', + 'oust', + 'ouster', + 'out', + 'outage', + 'outbalance', + 'outbid', + 'outboard', + 'outbound', + 'outbrave', + 'outbreak', + 'outbreed', + 'outbuilding', + 'outburst', + 'outcast', + 'outcaste', + 'outclass', + 'outcome', + 'outcrop', + 'outcross', + 'outcry', + 'outcurve', + 'outdare', + 'outdate', + 'outdated', + 'outdistance', + 'outdo', + 'outdoor', + 'outdoors', + 'outer', + 'outermost', + 'outface', + 'outfall', + 'outfield', + 'outfielder', + 'outfight', + 'outfit', + 'outfitter', + 'outflank', + 'outflow', + 'outfoot', + 'outfox', + 'outgeneral', + 'outgo', + 'outgoing', + 'outgoings', + 'outgrow', + 'outgrowth', + 'outguard', + 'outguess', + 'outhaul', + 'outhouse', + 'outing', + 'outland', + 'outlander', + 'outlandish', + 'outlast', + 'outlaw', + 'outlawry', + 'outlay', + 'outleap', + 'outlet', + 'outlier', + 'outline', + 'outlive', + 'outlook', + 'outlying', + 'outman', + 'outmaneuver', + 'outmarch', + 'outmoded', + 'outmost', + 'outnumber', + 'outpatient', + 'outplay', + 'outpoint', + 'outport', + 'outpost', + 'outpour', + 'outpouring', + 'output', + 'outrage', + 'outrageous', + 'outrange', + 'outrank', + 'outreach', + 'outride', + 'outrider', + 'outrigger', + 'outright', + 'outroar', + 'outrun', + 'outrush', + 'outsail', + 'outsell', + 'outsert', + 'outset', + 'outshine', + 'outshoot', + 'outshout', + 'outside', + 'outsider', + 'outsize', + 'outskirts', + 'outsmart', + 'outsoar', + 'outsole', + 'outspan', + 'outspeak', + 'outspoken', + 'outspread', + 'outstand', + 'outstanding', + 'outstare', + 'outstation', + 'outstay', + 'outstretch', + 'outstretched', + 'outstrip', + 'outtalk', + 'outthink', + 'outturn', + 'outvote', + 'outward', + 'outwardly', + 'outwards', + 'outwash', + 'outwear', + 'outweigh', + 'outwit', + 'outwork', + 'outworn', + 'ouzel', + 'ouzo', + 'ova', + 'oval', + 'ovarian', + 'ovariectomy', + 'ovariotomy', + 'ovaritis', + 'ovary', + 'ovate', + 'ovation', + 'oven', + 'ovenbird', + 'ovenware', + 'over', + 'overabound', + 'overabundance', + 'overact', + 'overactive', + 'overage', + 'overall', + 'overalls', + 'overanxious', + 'overarch', + 'overarm', + 'overawe', + 'overbalance', + 'overbear', + 'overbearing', + 'overbid', + 'overbite', + 'overblouse', + 'overblown', + 'overboard', + 'overbold', + 'overbuild', + 'overburden', + 'overburdensome', + 'overcapitalize', + 'overcareful', + 'overcast', + 'overcasting', + 'overcautious', + 'overcharge', + 'overcheck', + 'overcloud', + 'overcoat', + 'overcome', + 'overcompensation', + 'overcritical', + 'overcrop', + 'overcurious', + 'overdevelop', + 'overdo', + 'overdone', + 'overdose', + 'overdraft', + 'overdraw', + 'overdress', + 'overdrive', + 'overdue', + 'overdye', + 'overeager', + 'overeat', + 'overelaborate', + 'overestimate', + 'overexcite', + 'overexert', + 'overexpose', + 'overfeed', + 'overfill', + 'overflight', + 'overflow', + 'overfly', + 'overglaze', + 'overgrow', + 'overgrowth', + 'overhand', + 'overhang', + 'overhappy', + 'overhasty', + 'overhaul', + 'overhead', + 'overhear', + 'overheat', + 'overindulge', + 'overissue', + 'overjoy', + 'overkill', + 'overland', + 'overlap', + 'overlarge', + 'overlay', + 'overleap', + 'overliberal', + 'overlie', + 'overline', + 'overlive', + 'overload', + 'overlong', + 'overlook', + 'overlooker', + 'overlord', + 'overly', + 'overlying', + 'overman', + 'overmantel', + 'overmaster', + 'overmatch', + 'overmatter', + 'overmeasure', + 'overmodest', + 'overmuch', + 'overnice', + 'overnight', + 'overpass', + 'overpay', + 'overplay', + 'overplus', + 'overpower', + 'overpowering', + 'overpraise', + 'overprint', + 'overprize', + 'overrate', + 'overreach', + 'overreact', + 'overrefinement', + 'override', + 'overriding', + 'overripe', + 'overrule', + 'overrun', + 'overscore', + 'overscrupulous', + 'overseas', + 'oversee', + 'overseer', + 'oversell', + 'overset', + 'oversew', + 'oversexed', + 'overshadow', + 'overshine', + 'overshoe', + 'overshoot', + 'overside', + 'oversight', + 'oversize', + 'overskirt', + 'overslaugh', + 'oversleep', + 'oversold', + 'oversoul', + 'overspend', + 'overspill', + 'overspread', + 'overstate', + 'overstay', + 'overstep', + 'overstock', + 'overstrain', + 'overstretch', + 'overstride', + 'overstrung', + 'overstudy', + 'overstuff', + 'overstuffed', + 'oversubscribe', + 'oversubtle', + 'oversubtlety', + 'oversupply', + 'oversweet', + 'overt', + 'overtake', + 'overtask', + 'overtax', + 'overthrow', + 'overthrust', + 'overtime', + 'overtire', + 'overtly', + 'overtone', + 'overtop', + 'overtrade', + 'overtrick', + 'overtrump', + 'overture', + 'overturn', + 'overuse', + 'overvalue', + 'overview', + 'overweary', + 'overweening', + 'overweigh', + 'overweight', + 'overwhelm', + 'overwhelming', + 'overwind', + 'overwinter', + 'overword', + 'overwork', + 'overwrite', + 'overwrought', + 'overzealous', + 'oviduct', + 'oviform', + 'ovine', + 'oviparous', + 'oviposit', + 'ovipositor', + 'ovoid', + 'ovolo', + 'ovotestis', + 'ovovitellin', + 'ovoviviparous', + 'ovular', + 'ovule', + 'ovum', + 'ow', + 'owe', + 'owing', + 'owl', + 'owlet', + 'owlish', + 'own', + 'owner', + 'ownership', + 'ox', + 'oxalate', + 'oxalis', + 'oxazine', + 'oxblood', + 'oxbow', + 'oxcart', + 'oxen', + 'oxford', + 'oxheart', + 'oxidase', + 'oxidate', + 'oxidation', + 'oxide', + 'oxidimetry', + 'oxidize', + 'oxime', + 'oxpecker', + 'oxtail', + 'oxyacetylene', + 'oxyacid', + 'oxycephaly', + 'oxygen', + 'oxygenate', + 'oxyhydrogen', + 'oxymoron', + 'oxysalt', + 'oxytetracycline', + 'oxytocic', + 'oxytocin', + 'oyer', + 'oyez', + 'oyster', + 'oystercatcher', + 'oysterman', + 'ozone', + 'ozonide', + 'ozoniferous', + 'ozonize', + 'ozonolysis', + 'ozonosphere', + 'p', + 'pa', + 'pabulum', + 'pace', + 'pacemaker', + 'pacer', + 'pacesetter', + 'pacha', + 'pachalic', + 'pachisi', + 'pachyderm', + 'pachydermatous', + 'pachysandra', + 'pacific', + 'pacifically', + 'pacification', + 'pacificism', + 'pacifier', + 'pacifism', + 'pacifist', + 'pacifistic', + 'pacify', + 'pack', + 'package', + 'packaging', + 'packer', + 'packet', + 'packhorse', + 'packing', + 'packsaddle', + 'packthread', + 'pact', + 'paction', + 'pad', + 'padauk', + 'padding', + 'paddle', + 'paddlefish', + 'paddock', + 'paddy', + 'pademelon', + 'padlock', + 'padnag', + 'padre', + 'padrone', + 'paduasoy', + 'paean', + 'paederast', + 'paediatrician', + 'paediatrics', + 'paedogenesis', + 'paella', + 'paeon', + 'pagan', + 'pagandom', + 'paganism', + 'paganize', + 'page', + 'pageant', + 'pageantry', + 'pageboy', + 'paginal', + 'paginate', + 'pagination', + 'pagoda', + 'pagurian', + 'pah', + 'pahoehoe', + 'paid', + 'pail', + 'paillasse', + 'paillette', + 'pain', + 'pained', + 'painful', + 'painkiller', + 'painless', + 'pains', + 'painstaking', + 'paint', + 'paintbox', + 'paintbrush', + 'painter', + 'painterly', + 'painting', + 'painty', + 'pair', + 'pairs', + 'paisa', + 'paisano', + 'paisley', + 'pajamas', + 'pal', + 'palace', + 'paladin', + 'palaeobotany', + 'palaeography', + 'palaeontography', + 'palaeontology', + 'palaeozoology', + 'palaestra', + 'palais', + 'palanquin', + 'palatable', + 'palatal', + 'palatalized', + 'palate', + 'palatial', + 'palatinate', + 'palatine', + 'palaver', + 'palazzo', + 'pale', + 'paleethnology', + 'paleface', + 'paleobiology', + 'paleobotany', + 'paleoclimatology', + 'paleoecology', + 'paleogeography', + 'paleography', + 'paleolith', + 'paleontography', + 'paleontology', + 'paleopsychology', + 'paleozoology', + 'palestra', + 'paletot', + 'palette', + 'palfrey', + 'palikar', + 'palimpsest', + 'palindrome', + 'paling', + 'palingenesis', + 'palinode', + 'palisade', + 'palish', + 'pall', + 'palladic', + 'palladium', + 'palladous', + 'pallbearer', + 'pallet', + 'pallette', + 'palliasse', + 'palliate', + 'palliative', + 'pallid', + 'pallium', + 'pallor', + 'palm', + 'palmaceous', + 'palmar', + 'palmary', + 'palmate', + 'palmation', + 'palmer', + 'palmette', + 'palmetto', + 'palmistry', + 'palmitate', + 'palmitin', + 'palmy', + 'palomino', + 'palp', + 'palpable', + 'palpate', + 'palpebrate', + 'palpitant', + 'palpitate', + 'palpitation', + 'palsgrave', + 'palstave', + 'palsy', + 'palter', + 'paltry', + 'paludal', + 'paly', + 'pampa', + 'pampas', + 'pamper', + 'pampero', + 'pamphlet', + 'pamphleteer', + 'pan', + 'panacea', + 'panache', + 'panada', + 'panatella', + 'pancake', + 'panchromatic', + 'pancratium', + 'pancreas', + 'pancreatin', + 'pancreatotomy', + 'panda', + 'pandanus', + 'pandect', + 'pandemic', + 'pandemonium', + 'pander', + 'pandiculation', + 'pandit', + 'pandora', + 'pandour', + 'pandowdy', + 'pandurate', + 'pandybat', + 'pane', + 'panegyric', + 'panegyrize', + 'panel', + 'panelboard', + 'paneling', + 'panelist', + 'panettone', + 'panfish', + 'pang', + 'panga', + 'pangenesis', + 'pangolin', + 'panhandle', + 'panic', + 'panicle', + 'paniculate', + 'panier', + 'panjandrum', + 'panlogism', + 'panne', + 'pannier', + 'pannikin', + 'panocha', + 'panoply', + 'panoptic', + 'panorama', + 'panpipe', + 'panpsychist', + 'pansophy', + 'pansy', + 'pant', + 'pantalets', + 'pantaloon', + 'pantaloons', + 'pantechnicon', + 'pantelegraph', + 'pantheism', + 'pantheon', + 'panther', + 'pantie', + 'panties', + 'pantile', + 'pantisocracy', + 'panto', + 'pantograph', + 'pantomime', + 'pantomimist', + 'pantry', + 'pants', + 'pantsuit', + 'pantywaist', + 'panzer', + 'pap', + 'papa', + 'papacy', + 'papain', + 'papal', + 'papaveraceous', + 'papaverine', + 'papaw', + 'papaya', + 'paper', + 'paperback', + 'paperboard', + 'paperboy', + 'paperhanger', + 'paperweight', + 'papery', + 'papeterie', + 'papilionaceous', + 'papilla', + 'papillary', + 'papilloma', + 'papillon', + 'papillose', + 'papillote', + 'papism', + 'papist', + 'papistry', + 'papoose', + 'pappose', + 'pappus', + 'pappy', + 'paprika', + 'papule', + 'papyraceous', + 'papyrology', + 'papyrus', + 'par', + 'para', + 'parabasis', + 'parable', + 'parabola', + 'parabolic', + 'parabolize', + 'paraboloid', + 'paracasein', + 'parachronism', + 'parachute', + 'parade', + 'paradiddle', + 'paradigm', + 'paradise', + 'paradisiacal', + 'parados', + 'paradox', + 'paradrop', + 'paraesthesia', + 'paraffin', + 'paraffinic', + 'paraformaldehyde', + 'paraglider', + 'paragon', + 'paragraph', + 'paragrapher', + 'paragraphia', + 'parahydrogen', + 'parakeet', + 'paraldehyde', + 'paralipomena', + 'parallax', + 'parallel', + 'parallelepiped', + 'parallelism', + 'parallelize', + 'parallelogram', + 'paralogism', + 'paralyse', + 'paralysis', + 'paralytic', + 'paralyze', + 'paramagnet', + 'paramagnetic', + 'paramagnetism', + 'paramatta', + 'paramecium', + 'paramedic', + 'paramedical', + 'parament', + 'parameter', + 'paramilitary', + 'paramnesia', + 'paramo', + 'paramorph', + 'paramorphism', + 'paramount', + 'paramour', + 'parang', + 'paranoia', + 'paranoiac', + 'paranoid', + 'paranymph', + 'parapet', + 'paraph', + 'paraphernalia', + 'paraphrase', + 'paraphrast', + 'paraphrastic', + 'paraplegia', + 'parapodium', + 'paraprofessional', + 'parapsychology', + 'parasang', + 'paraselene', + 'parasite', + 'parasitic', + 'parasiticide', + 'parasitism', + 'parasitize', + 'parasitology', + 'parasol', + 'parasympathetic', + 'parasynapsis', + 'parasynthesis', + 'parathion', + 'parathyroid', + 'paratrooper', + 'paratroops', + 'paratuberculosis', + 'paratyphoid', + 'paravane', + 'parboil', + 'parbuckle', + 'parcel', + 'parceling', + 'parcenary', + 'parch', + 'parchment', + 'parclose', + 'pard', + 'pardon', + 'pardoner', + 'pare', + 'paregmenon', + 'paregoric', + 'pareira', + 'parent', + 'parentage', + 'parental', + 'parenteral', + 'parenthesis', + 'parenthesize', + 'parenthood', + 'paresis', + 'paresthesia', + 'pareu', + 'parfait', + 'parfleche', + 'parget', + 'pargeting', + 'parhelion', + 'pariah', + 'paries', + 'parietal', + 'paring', + 'paripinnate', + 'parish', + 'parishioner', + 'parity', + 'park', + 'parka', + 'parkin', + 'parkland', + 'parkway', + 'parlance', + 'parlando', + 'parlay', + 'parley', + 'parliament', + 'parliamentarian', + 'parliamentarianism', + 'parliamentary', + 'parlor', + 'parlormaid', + 'parlour', + 'parlous', + 'parochial', + 'parochialism', + 'parodic', + 'parodist', + 'parody', + 'paroicous', + 'parol', + 'parole', + 'parolee', + 'paronomasia', + 'paronychia', + 'paronym', + 'paronymous', + 'parotic', + 'parotid', + 'parotitis', + 'paroxysm', + 'parquet', + 'parquetry', + 'parr', + 'parrakeet', + 'parricide', + 'parrot', + 'parrotfish', + 'parry', + 'parse', + 'parsec', + 'parsimonious', + 'parsimony', + 'parsley', + 'parsnip', + 'parson', + 'parsonage', + 'part', + 'partake', + 'partan', + 'parted', + 'parterre', + 'parthenocarpy', + 'parthenogenesis', + 'partial', + 'partiality', + 'partible', + 'participant', + 'participate', + 'participation', + 'participial', + 'participle', + 'particle', + 'particular', + 'particularism', + 'particularity', + 'particularize', + 'particularly', + 'particulate', + 'parting', + 'partisan', + 'partite', + 'partition', + 'partitive', + 'partizan', + 'partlet', + 'partly', + 'partner', + 'partnership', + 'parton', + 'partook', + 'partridge', + 'partridgeberry', + 'parts', + 'parturient', + 'parturifacient', + 'parturition', + 'party', + 'parulis', + 'parve', + 'parvenu', + 'parvis', + 'pas', + 'pase', + 'pash', + 'pasha', + 'pashalik', + 'pashm', + 'pasqueflower', + 'pasquil', + 'pasquinade', + 'pass', + 'passable', + 'passably', + 'passacaglia', + 'passade', + 'passage', + 'passageway', + 'passant', + 'passbook', + 'passe', + 'passed', + 'passel', + 'passementerie', + 'passenger', + 'passer', + 'passerine', + 'passible', + 'passifloraceous', + 'passim', + 'passing', + 'passion', + 'passional', + 'passionate', + 'passionless', + 'passive', + 'passivism', + 'passkey', + 'passport', + 'passus', + 'password', + 'past', + 'pasta', + 'paste', + 'pasteboard', + 'pastel', + 'pastelist', + 'pastern', + 'pasteurism', + 'pasteurization', + 'pasteurize', + 'pasteurizer', + 'pasticcio', + 'pastiche', + 'pastille', + 'pastime', + 'pastiness', + 'pastis', + 'pastor', + 'pastoral', + 'pastorale', + 'pastoralist', + 'pastoralize', + 'pastorate', + 'pastorship', + 'pastose', + 'pastrami', + 'pastry', + 'pasturage', + 'pasture', + 'pasty', + 'pat', + 'patagium', + 'patch', + 'patchouli', + 'patchwork', + 'patchy', + 'pate', + 'patella', + 'patellate', + 'paten', + 'patency', + 'patent', + 'patentee', + 'patently', + 'patentor', + 'pater', + 'paterfamilias', + 'paternal', + 'paternalism', + 'paternity', + 'paternoster', + 'path', + 'pathetic', + 'pathfinder', + 'pathic', + 'pathless', + 'pathogen', + 'pathogenesis', + 'pathogenic', + 'pathognomy', + 'pathological', + 'pathology', + 'pathoneurosis', + 'pathos', + 'pathway', + 'patience', + 'patient', + 'patin', + 'patina', + 'patinated', + 'patinous', + 'patio', + 'patisserie', + 'patois', + 'patriarch', + 'patriarchate', + 'patriarchy', + 'patrician', + 'patriciate', + 'patricide', + 'patrilateral', + 'patrilineage', + 'patrilineal', + 'patriliny', + 'patrilocal', + 'patrimony', + 'patriot', + 'patriotism', + 'patristic', + 'patrol', + 'patrolman', + 'patrology', + 'patron', + 'patronage', + 'patronize', + 'patronizing', + 'patronymic', + 'patroon', + 'patsy', + 'patten', + 'patter', + 'pattern', + 'patty', + 'patulous', + 'paucity', + 'pauldron', + 'paulownia', + 'paunch', + 'paunchy', + 'pauper', + 'pauperism', + 'pauperize', + 'pause', + 'pave', + 'pavement', + 'pavid', + 'pavilion', + 'paving', + 'pavis', + 'pavonine', + 'paw', + 'pawl', + 'pawn', + 'pawnbroker', + 'pawnshop', + 'pawpaw', + 'pax', + 'paxwax', + 'pay', + 'payable', + 'payday', + 'payee', + 'payer', + 'payload', + 'paymaster', + 'payment', + 'paynim', + 'payoff', + 'payola', + 'payroll', + 'pe', + 'pea', + 'peace', + 'peaceable', + 'peaceful', + 'peacemaker', + 'peacetime', + 'peach', + 'peachy', + 'peacoat', + 'peacock', + 'peafowl', + 'peag', + 'peahen', + 'peak', + 'peaked', + 'peal', + 'pean', + 'peanut', + 'peanuts', + 'pear', + 'pearl', + 'pearly', + 'peart', + 'peasant', + 'pease', + 'peasecod', + 'peashooter', + 'peat', + 'peavey', + 'peba', + 'pebble', + 'pebbly', + 'pecan', + 'peccable', + 'peccadillo', + 'peccant', + 'peccary', + 'peccavi', + 'peck', + 'pecker', + 'pectase', + 'pecten', + 'pectin', + 'pectinate', + 'pectize', + 'pectoral', + 'pectoralis', + 'peculate', + 'peculation', + 'peculiar', + 'peculiarity', + 'peculiarize', + 'peculium', + 'pecuniary', + 'pedagogics', + 'pedagogue', + 'pedagogy', + 'pedal', + 'pedalfer', + 'pedant', + 'pedanticism', + 'pedantry', + 'pedate', + 'peddle', + 'peddler', + 'peddling', + 'pederast', + 'pederasty', + 'pedestal', + 'pedestrian', + 'pedestrianism', + 'pedestrianize', + 'pediatrician', + 'pediatrics', + 'pedicab', + 'pedicel', + 'pedicle', + 'pedicular', + 'pediculosis', + 'pedicure', + 'pediform', + 'pedigree', + 'pediment', + 'pedlar', + 'pedology', + 'pedometer', + 'peduncle', + 'pee', + 'peek', + 'peekaboo', + 'peel', + 'peeler', + 'peeling', + 'peen', + 'peep', + 'peeper', + 'peephole', + 'peepul', + 'peer', + 'peerage', + 'peeress', + 'peerless', + 'peeve', + 'peeved', + 'peevish', + 'peewee', + 'peewit', + 'peg', + 'pegboard', + 'pegmatite', + 'peignoir', + 'pejoration', + 'pejorative', + 'pekan', + 'pekoe', + 'pelage', + 'pelagic', + 'pelargonium', + 'pelecypod', + 'pelerine', + 'pelf', + 'pelham', + 'pelican', + 'pelisse', + 'pelite', + 'pellagra', + 'pellet', + 'pellicle', + 'pellitory', + 'pellucid', + 'peloria', + 'pelorus', + 'pelota', + 'pelt', + 'peltast', + 'peltate', + 'pelting', + 'peltry', + 'pelvic', + 'pelvis', + 'pemmican', + 'pemphigus', + 'pen', + 'penal', + 'penalize', + 'penalty', + 'penance', + 'penates', + 'pence', + 'pencel', + 'penchant', + 'pencil', + 'pend', + 'pendant', + 'pendent', + 'pendentive', + 'pending', + 'pendragon', + 'pendulous', + 'pendulum', + 'peneplain', + 'penetralia', + 'penetrance', + 'penetrant', + 'penetrate', + 'penetrating', + 'penetration', + 'peng', + 'penguin', + 'penholder', + 'penicillate', + 'penicillin', + 'penicillium', + 'penile', + 'peninsula', + 'penis', + 'penitence', + 'penitent', + 'penitential', + 'penitentiary', + 'penknife', + 'penman', + 'penmanship', + 'penna', + 'pennant', + 'pennate', + 'penni', + 'penniless', + 'penninite', + 'pennon', + 'pennoncel', + 'penny', + 'pennyroyal', + 'pennyweight', + 'pennyworth', + 'penology', + 'pensile', + 'pension', + 'pensionary', + 'pensioner', + 'pensive', + 'penstemon', + 'penstock', + 'pent', + 'pentachlorophenol', + 'pentacle', + 'pentad', + 'pentadactyl', + 'pentagon', + 'pentagram', + 'pentagrid', + 'pentahedron', + 'pentalpha', + 'pentamerous', + 'pentameter', + 'pentane', + 'pentangular', + 'pentapody', + 'pentaprism', + 'pentarchy', + 'pentastich', + 'pentastyle', + 'pentathlon', + 'pentatomic', + 'pentavalent', + 'penthouse', + 'pentimento', + 'pentlandite', + 'pentobarbital', + 'pentode', + 'pentomic', + 'pentosan', + 'pentose', + 'pentstemon', + 'pentyl', + 'pentylenetetrazol', + 'penuche', + 'penuchle', + 'penult', + 'penultimate', + 'penumbra', + 'penurious', + 'penury', + 'peon', + 'peonage', + 'peony', + 'people', + 'pep', + 'peplos', + 'peplum', + 'pepper', + 'peppercorn', + 'peppergrass', + 'peppermint', + 'peppery', + 'peppy', + 'pepsin', + 'pepsinate', + 'pepsinogen', + 'peptic', + 'peptidase', + 'peptide', + 'peptize', + 'peptone', + 'peptonize', + 'per', + 'peracid', + 'peradventure', + 'perambulate', + 'perambulator', + 'percale', + 'percaline', + 'perceivable', + 'perceive', + 'percent', + 'percentage', + 'percentile', + 'percept', + 'perceptible', + 'perception', + 'perceptive', + 'perceptual', + 'perch', + 'perchance', + 'perchloride', + 'percipient', + 'percolate', + 'percolation', + 'percolator', + 'percuss', + 'percussion', + 'percussionist', + 'percussive', + 'percutaneous', + 'perdition', + 'perdu', + 'perdurable', + 'perdure', + 'peregrinate', + 'peregrination', + 'peregrine', + 'peremptory', + 'perennate', + 'perennial', + 'perfect', + 'perfectible', + 'perfection', + 'perfectionism', + 'perfectionist', + 'perfective', + 'perfectly', + 'perfecto', + 'perfervid', + 'perfidious', + 'perfidy', + 'perfoliate', + 'perforate', + 'perforated', + 'perforation', + 'perforce', + 'perform', + 'performance', + 'performative', + 'performing', + 'perfume', + 'perfumer', + 'perfumery', + 'perfunctory', + 'perfuse', + 'perfusion', + 'pergola', + 'perhaps', + 'peri', + 'perianth', + 'periapt', + 'pericarditis', + 'pericardium', + 'pericarp', + 'perichondrium', + 'pericline', + 'pericope', + 'pericranium', + 'pericycle', + 'pericynthion', + 'periderm', + 'peridium', + 'peridot', + 'peridotite', + 'perigee', + 'perigon', + 'perigynous', + 'perihelion', + 'peril', + 'perilous', + 'perilune', + 'perilymph', + 'perimeter', + 'perimorph', + 'perinephrium', + 'perineum', + 'perineuritis', + 'perineurium', + 'period', + 'periodate', + 'periodic', + 'periodical', + 'periodicity', + 'periodontal', + 'periodontics', + 'perionychium', + 'periosteum', + 'periostitis', + 'periotic', + 'peripatetic', + 'peripeteia', + 'peripheral', + 'periphery', + 'periphrasis', + 'periphrastic', + 'peripteral', + 'perique', + 'perisarc', + 'periscope', + 'perish', + 'perishable', + 'perished', + 'perishing', + 'perissodactyl', + 'peristalsis', + 'peristome', + 'peristyle', + 'perithecium', + 'peritoneum', + 'peritonitis', + 'periwig', + 'periwinkle', + 'perjure', + 'perjured', + 'perjury', + 'perk', + 'perky', + 'perlite', + 'perm', + 'permafrost', + 'permalloy', + 'permanence', + 'permanency', + 'permanent', + 'permanganate', + 'permatron', + 'permeability', + 'permeable', + 'permeance', + 'permeate', + 'permissible', + 'permission', + 'permissive', + 'permit', + 'permittivity', + 'permutation', + 'permute', + 'pernicious', + 'pernickety', + 'peroneus', + 'perorate', + 'peroration', + 'peroxidase', + 'peroxide', + 'peroxidize', + 'perpend', + 'perpendicular', + 'perpetrate', + 'perpetual', + 'perpetuate', + 'perpetuity', + 'perplex', + 'perplexed', + 'perplexity', + 'perquisite', + 'perron', + 'perry', + 'perse', + 'persecute', + 'persecution', + 'perseverance', + 'persevere', + 'persevering', + 'persiflage', + 'persimmon', + 'persist', + 'persistence', + 'persistent', + 'persnickety', + 'person', + 'persona', + 'personable', + 'personage', + 'personal', + 'personalism', + 'personality', + 'personalize', + 'personally', + 'personalty', + 'personate', + 'personification', + 'personify', + 'personnel', + 'perspective', + 'perspicacious', + 'perspicacity', + 'perspicuity', + 'perspicuous', + 'perspiration', + 'perspiratory', + 'perspire', + 'persuade', + 'persuader', + 'persuasion', + 'persuasive', + 'pert', + 'pertain', + 'pertinacious', + 'pertinacity', + 'pertinent', + 'perturb', + 'perturbation', + 'pertussis', + 'peruke', + 'perusal', + 'peruse', + 'pervade', + 'pervasive', + 'perverse', + 'perversion', + 'perversity', + 'pervert', + 'perverted', + 'pervious', + 'pes', + 'pesade', + 'peseta', + 'pesky', + 'peso', + 'pessary', + 'pessimism', + 'pessimist', + 'pest', + 'pester', + 'pesthole', + 'pesthouse', + 'pesticide', + 'pestiferous', + 'pestilence', + 'pestilent', + 'pestilential', + 'pestle', + 'pet', + 'petal', + 'petaliferous', + 'petaloid', + 'petard', + 'petasus', + 'petcock', + 'petechia', + 'peter', + 'petersham', + 'petiolate', + 'petiole', + 'petiolule', + 'petit', + 'petite', + 'petition', + 'petitionary', + 'petitioner', + 'petrel', + 'petrifaction', + 'petrify', + 'petrochemical', + 'petrochemistry', + 'petroglyph', + 'petrography', + 'petrol', + 'petrolatum', + 'petroleum', + 'petrolic', + 'petrology', + 'petronel', + 'petrosal', + 'petrous', + 'petticoat', + 'pettifog', + 'pettifogger', + 'pettifogging', + 'pettish', + 'pettitoes', + 'petty', + 'petulance', + 'petulancy', + 'petulant', + 'petunia', + 'petuntse', + 'pew', + 'pewee', + 'pewit', + 'pewter', + 'peyote', + 'pfennig', + 'phaeton', + 'phage', + 'phagocyte', + 'phagocytosis', + 'phalange', + 'phalangeal', + 'phalanger', + 'phalansterian', + 'phalanstery', + 'phalanx', + 'phalarope', + 'phallic', + 'phallicism', + 'phallus', + 'phanerogam', + 'phanotron', + 'phantasm', + 'phantasmagoria', + 'phantasmal', + 'phantasy', + 'phantom', + 'pharaoh', + 'pharisee', + 'pharmaceutical', + 'pharmaceutics', + 'pharmacist', + 'pharmacognosy', + 'pharmacology', + 'pharmacopoeia', + 'pharmacopsychosis', + 'pharmacy', + 'pharos', + 'pharyngeal', + 'pharyngitis', + 'pharyngology', + 'pharyngoscope', + 'pharynx', + 'phase', + 'phasis', + 'phatic', + 'pheasant', + 'phellem', + 'phelloderm', + 'phenacaine', + 'phenacetin', + 'phenacite', + 'phenanthrene', + 'phenazine', + 'phenetidine', + 'phenetole', + 'phenformin', + 'phenix', + 'phenobarbital', + 'phenobarbitone', + 'phenocryst', + 'phenol', + 'phenolic', + 'phenology', + 'phenolphthalein', + 'phenomena', + 'phenomenal', + 'phenomenalism', + 'phenomenology', + 'phenomenon', + 'phenosafranine', + 'phenothiazine', + 'phenoxide', + 'phenyl', + 'phenylalanine', + 'phenylamine', + 'phenylketonuria', + 'pheon', + 'phew', + 'phi', + 'phial', + 'philander', + 'philanthropic', + 'philanthropist', + 'philanthropy', + 'philately', + 'philharmonic', + 'philhellene', + 'philibeg', + 'philippic', + 'philodendron', + 'philologian', + 'philology', + 'philomel', + 'philoprogenitive', + 'philosopher', + 'philosophical', + 'philosophism', + 'philosophize', + 'philosophy', + 'philter', + 'philtre', + 'phiz', + 'phlebitis', + 'phlebosclerosis', + 'phlebotomize', + 'phlebotomy', + 'phlegm', + 'phlegmatic', + 'phlegmy', + 'phloem', + 'phlogistic', + 'phlogopite', + 'phlox', + 'phlyctena', + 'phobia', + 'phocine', + 'phocomelia', + 'phoebe', + 'phoenix', + 'phonate', + 'phonation', + 'phone', + 'phoneme', + 'phonemic', + 'phonemics', + 'phonetic', + 'phonetician', + 'phonetics', + 'phonetist', + 'phoney', + 'phonic', + 'phonics', + 'phonogram', + 'phonograph', + 'phonography', + 'phonolite', + 'phonologist', + 'phonology', + 'phonometer', + 'phonon', + 'phonoscope', + 'phonotypy', + 'phony', + 'phooey', + 'phosgene', + 'phosgenite', + 'phosphatase', + 'phosphate', + 'phosphatize', + 'phosphaturia', + 'phosphene', + 'phosphide', + 'phosphine', + 'phosphocreatine', + 'phospholipide', + 'phosphoprotein', + 'phosphor', + 'phosphorate', + 'phosphoresce', + 'phosphorescence', + 'phosphorescent', + 'phosphoric', + 'phosphorism', + 'phosphorite', + 'phosphoroscope', + 'phosphorous', + 'phosphorus', + 'phosphorylase', + 'photic', + 'photo', + 'photoactinic', + 'photoactive', + 'photobathic', + 'photocathode', + 'photocell', + 'photochemistry', + 'photochromy', + 'photochronograph', + 'photocompose', + 'photocomposition', + 'photoconduction', + 'photoconductivity', + 'photocopier', + 'photocopy', + 'photocurrent', + 'photodisintegration', + 'photodrama', + 'photodynamics', + 'photoelasticity', + 'photoelectric', + 'photoelectron', + 'photoelectrotype', + 'photoemission', + 'photoengrave', + 'photoengraving', + 'photofinishing', + 'photoflash', + 'photoflood', + 'photofluorography', + 'photogene', + 'photogenic', + 'photogram', + 'photogrammetry', + 'photograph', + 'photographer', + 'photographic', + 'photography', + 'photogravure', + 'photojournalism', + 'photokinesis', + 'photolithography', + 'photoluminescence', + 'photolysis', + 'photomap', + 'photomechanical', + 'photometer', + 'photometry', + 'photomicrograph', + 'photomicroscope', + 'photomontage', + 'photomultiplier', + 'photomural', + 'photon', + 'photoneutron', + 'photoperiod', + 'photophilous', + 'photophobia', + 'photophore', + 'photopia', + 'photoplay', + 'photoreceptor', + 'photoreconnaissance', + 'photosensitive', + 'photosphere', + 'photosynthesis', + 'phototaxis', + 'phototelegraph', + 'phototelegraphy', + 'phototherapy', + 'photothermic', + 'phototonus', + 'phototopography', + 'phototransistor', + 'phototube', + 'phototype', + 'phototypography', + 'phototypy', + 'photovoltaic', + 'photozincography', + 'phrasal', + 'phrase', + 'phraseogram', + 'phraseograph', + 'phraseologist', + 'phraseology', + 'phrasing', + 'phratry', + 'phrenetic', + 'phrenic', + 'phrenology', + 'phrensy', + 'phthalein', + 'phthalocyanine', + 'phthisic', + 'phthisis', + 'phycology', + 'phycomycete', + 'phyla', + 'phylactery', + 'phyle', + 'phyletic', + 'phylloclade', + 'phyllode', + 'phylloid', + 'phyllome', + 'phylloquinone', + 'phyllotaxis', + 'phylloxera', + 'phylogeny', + 'phylum', + 'physic', + 'physical', + 'physicalism', + 'physicality', + 'physician', + 'physicist', + 'physicochemical', + 'physics', + 'physiognomy', + 'physiography', + 'physiological', + 'physiologist', + 'physiology', + 'physiotherapy', + 'physique', + 'physoclistous', + 'physostomous', + 'phytobiology', + 'phytogenesis', + 'phytogeography', + 'phytography', + 'phytohormone', + 'phytology', + 'phytopathology', + 'phytophagous', + 'phytoplankton', + 'phytosociology', + 'pi', + 'piacular', + 'piaffe', + 'pianette', + 'pianism', + 'pianissimo', + 'pianist', + 'piano', + 'pianoforte', + 'piassava', + 'piazza', + 'pibgorn', + 'pibroch', + 'pic', + 'pica', + 'picador', + 'picaresque', + 'picaroon', + 'picayune', + 'piccalilli', + 'piccaninny', + 'piccolo', + 'piccoloist', + 'pice', + 'piceous', + 'pick', + 'pickaback', + 'pickaninny', + 'pickax', + 'pickaxe', + 'picked', + 'picker', + 'pickerel', + 'pickerelweed', + 'picket', + 'pickings', + 'pickle', + 'pickled', + 'picklock', + 'pickpocket', + 'pickup', + 'picky', + 'picnic', + 'picofarad', + 'picoline', + 'picot', + 'picrate', + 'picrite', + 'picrotoxin', + 'pictogram', + 'pictograph', + 'pictorial', + 'picture', + 'picturesque', + 'picturize', + 'picul', + 'piddle', + 'piddling', + 'piddock', + 'pidgin', + 'pie', + 'piebald', + 'piece', + 'piecemeal', + 'piecework', + 'piecrust', + 'pied', + 'pieplant', + 'pier', + 'pierce', + 'piercing', + 'piet', + 'pietism', + 'piety', + 'piezochemistry', + 'piezoelectricity', + 'piffle', + 'pig', + 'pigeon', + 'pigeonhole', + 'pigeonwing', + 'pigfish', + 'piggery', + 'piggin', + 'piggish', + 'piggy', + 'piggyback', + 'pigheaded', + 'piglet', + 'pigling', + 'pigment', + 'pigmentation', + 'pignus', + 'pignut', + 'pigpen', + 'pigskin', + 'pigsty', + 'pigtail', + 'pigweed', + 'pika', + 'pike', + 'pikeman', + 'pikeperch', + 'piker', + 'pikestaff', + 'pilaf', + 'pilaster', + 'pilau', + 'pilch', + 'pilchard', + 'pile', + 'pileate', + 'piled', + 'pileous', + 'piles', + 'pileum', + 'pileup', + 'pileus', + 'pilewort', + 'pilfer', + 'pilferage', + 'pilgarlic', + 'pilgrim', + 'pilgrimage', + 'pili', + 'piliferous', + 'piliform', + 'piling', + 'pill', + 'pillage', + 'pillar', + 'pillbox', + 'pillion', + 'pilliwinks', + 'pillory', + 'pillow', + 'pillowcase', + 'pilocarpine', + 'pilose', + 'pilot', + 'pilotage', + 'pilothouse', + 'piloting', + 'pilpul', + 'pily', + 'pimento', + 'pimiento', + 'pimp', + 'pimpernel', + 'pimple', + 'pimply', + 'pin', + 'pinafore', + 'pinball', + 'pincer', + 'pincers', + 'pinch', + 'pinchbeck', + 'pinchcock', + 'pinchpenny', + 'pincushion', + 'pindling', + 'pine', + 'pineal', + 'pineapple', + 'pinery', + 'pinetum', + 'pinfeather', + 'pinfish', + 'pinfold', + 'ping', + 'pinguid', + 'pinhead', + 'pinhole', + 'pinion', + 'pinite', + 'pink', + 'pinkeye', + 'pinkie', + 'pinkish', + 'pinko', + 'pinky', + 'pinna', + 'pinnace', + 'pinnacle', + 'pinnate', + 'pinnatifid', + 'pinnatipartite', + 'pinnatiped', + 'pinnatisect', + 'pinniped', + 'pinnule', + 'pinochle', + 'pinole', + 'pinpoint', + 'pinprick', + 'pinstripe', + 'pint', + 'pinta', + 'pintail', + 'pintle', + 'pinto', + 'pinup', + 'pinwheel', + 'pinwork', + 'pinworm', + 'pinxit', + 'piny', + 'pion', + 'pioneer', + 'pious', + 'pip', + 'pipage', + 'pipe', + 'pipeline', + 'piper', + 'piperaceous', + 'piperidine', + 'piperine', + 'piperonal', + 'pipestone', + 'pipette', + 'piping', + 'pipistrelle', + 'pipit', + 'pipkin', + 'pippin', + 'pipsissewa', + 'piquant', + 'pique', + 'piquet', + 'piracy', + 'piragua', + 'piranha', + 'pirate', + 'pirn', + 'pirog', + 'pirogue', + 'piroshki', + 'pirouette', + 'piscary', + 'piscator', + 'piscatorial', + 'piscatory', + 'pisciculture', + 'pisciform', + 'piscina', + 'piscine', + 'pish', + 'pishogue', + 'pismire', + 'pisolite', + 'piss', + 'pissed', + 'pistachio', + 'pistareen', + 'piste', + 'pistil', + 'pistol', + 'pistole', + 'pistoleer', + 'piston', + 'pit', + 'pita', + 'pitanga', + 'pitapat', + 'pitch', + 'pitchblende', + 'pitcher', + 'pitchfork', + 'pitching', + 'pitchman', + 'pitchstone', + 'pitchy', + 'piteous', + 'pitfall', + 'pith', + 'pithead', + 'pithecanthropus', + 'pithos', + 'pithy', + 'pitiable', + 'pitiful', + 'pitiless', + 'pitman', + 'piton', + 'pitsaw', + 'pitta', + 'pittance', + 'pituitary', + 'pituri', + 'pity', + 'pivot', + 'pivotal', + 'pivoting', + 'pix', + 'pixie', + 'pixilated', + 'pizza', + 'pizzeria', + 'pizzicato', + 'pl', + 'placable', + 'placard', + 'placate', + 'placative', + 'placatory', + 'place', + 'placebo', + 'placeman', + 'placement', + 'placenta', + 'placentation', + 'placer', + 'placet', + 'placid', + 'placket', + 'placoid', + 'plafond', + 'plagal', + 'plage', + 'plagiarism', + 'plagiarize', + 'plagiary', + 'plagioclase', + 'plague', + 'plaice', + 'plaid', + 'plaided', + 'plain', + 'plainclothesman', + 'plains', + 'plainsman', + 'plainsong', + 'plaint', + 'plaintiff', + 'plaintive', + 'plait', + 'plan', + 'planar', + 'planarian', + 'planchet', + 'planchette', + 'plane', + 'planer', + 'planet', + 'planetarium', + 'planetary', + 'planetesimal', + 'planetoid', + 'plangent', + 'planimeter', + 'planimetry', + 'planish', + 'plank', + 'planking', + 'plankton', + 'planogamete', + 'planography', + 'planometer', + 'planospore', + 'plant', + 'plantain', + 'plantar', + 'plantation', + 'planter', + 'planula', + 'plaque', + 'plash', + 'plashy', + 'plasm', + 'plasma', + 'plasmagel', + 'plasmasol', + 'plasmodium', + 'plasmolysis', + 'plasmosome', + 'plaster', + 'plasterboard', + 'plastered', + 'plasterwork', + 'plastic', + 'plasticity', + 'plasticize', + 'plasticizer', + 'plastid', + 'plastometer', + 'plat', + 'plate', + 'plateau', + 'plated', + 'platelayer', + 'platelet', + 'platen', + 'plater', + 'platform', + 'platina', + 'plating', + 'platinic', + 'platinize', + 'platinocyanide', + 'platinotype', + 'platinous', + 'platinum', + 'platitude', + 'platitudinize', + 'platitudinous', + 'platoon', + 'platter', + 'platy', + 'platyhelminth', + 'platypus', + 'platysma', + 'plaudit', + 'plausible', + 'plausive', + 'play', + 'playa', + 'playacting', + 'playback', + 'playbill', + 'playbook', + 'playboy', + 'player', + 'playful', + 'playgoer', + 'playground', + 'playhouse', + 'playlet', + 'playmate', + 'playpen', + 'playreader', + 'playroom', + 'playsuit', + 'plaything', + 'playtime', + 'playwright', + 'playwriting', + 'plaza', + 'plea', + 'pleach', + 'plead', + 'pleader', + 'pleading', + 'pleadings', + 'pleasance', + 'pleasant', + 'pleasantry', + 'please', + 'pleasing', + 'pleasurable', + 'pleasure', + 'pleat', + 'plebe', + 'plebeian', + 'plebiscite', + 'plebs', + 'plectognath', + 'plectron', + 'plectrum', + 'pled', + 'pledge', + 'pledgee', + 'pledget', + 'pleiad', + 'plenary', + 'plenipotent', + 'plenipotentiary', + 'plenish', + 'plenitude', + 'plenteous', + 'plentiful', + 'plenty', + 'plenum', + 'pleochroism', + 'pleomorphism', + 'pleonasm', + 'pleopod', + 'plesiosaur', + 'plessor', + 'plethora', + 'plethoric', + 'pleura', + 'pleurisy', + 'pleurodynia', + 'pleuron', + 'pleuropneumonia', + 'plexiform', + 'plexor', + 'plexus', + 'pliable', + 'pliant', + 'plica', + 'plicate', + 'plication', + 'plier', + 'pliers', + 'plight', + 'plimsoll', + 'plinth', + 'ploce', + 'plod', + 'plonk', + 'plop', + 'plosion', + 'plosive', + 'plot', + 'plotter', + 'plough', + 'ploughboy', + 'ploughman', + 'ploughshare', + 'plover', + 'plow', + 'plowboy', + 'plowman', + 'plowshare', + 'ploy', + 'pluck', + 'pluckless', + 'plucky', + 'plug', + 'plugboard', + 'plum', + 'plumage', + 'plumate', + 'plumb', + 'plumbaginaceous', + 'plumbago', + 'plumber', + 'plumbery', + 'plumbic', + 'plumbiferous', + 'plumbing', + 'plumbism', + 'plumbum', + 'plumcot', + 'plume', + 'plummet', + 'plummy', + 'plumose', + 'plump', + 'plumper', + 'plumule', + 'plumy', + 'plunder', + 'plunge', + 'plunger', + 'plunk', + 'pluperfect', + 'plural', + 'pluralism', + 'plurality', + 'pluralize', + 'plus', + 'plush', + 'plutocracy', + 'plutocrat', + 'pluton', + 'plutonic', + 'plutonium', + 'pluvial', + 'pluviometer', + 'pluvious', + 'ply', + 'plywood', + 'pneuma', + 'pneumatic', + 'pneumatics', + 'pneumatograph', + 'pneumatology', + 'pneumatometer', + 'pneumatophore', + 'pneumectomy', + 'pneumococcus', + 'pneumoconiosis', + 'pneumodynamics', + 'pneumoencephalogram', + 'pneumogastric', + 'pneumograph', + 'pneumonectomy', + 'pneumonia', + 'pneumonic', + 'pneumonoultramicroscopicsilicovolcanoconiosis', + 'pneumothorax', + 'poaceous', + 'poach', + 'poacher', + 'poachy', + 'pochard', + 'pock', + 'pocked', + 'pocket', + 'pocketbook', + 'pocketful', + 'pocketknife', + 'pockmark', + 'pocky', + 'poco', + 'pocosin', + 'pod', + 'podagra', + 'poddy', + 'podesta', + 'podgy', + 'podiatry', + 'podite', + 'podium', + 'podophyllin', + 'poem', + 'poesy', + 'poet', + 'poetaster', + 'poetess', + 'poeticize', + 'poetics', + 'poetize', + 'poetry', + 'pogey', + 'pogge', + 'pogonia', + 'pogrom', + 'pogy', + 'poi', + 'poignant', + 'poikilothermic', + 'poilu', + 'poinciana', + 'poinsettia', + 'point', + 'pointed', + 'pointer', + 'pointillism', + 'pointing', + 'pointless', + 'pointsman', + 'poise', + 'poised', + 'poison', + 'poisoning', + 'poisonous', + 'poke', + 'pokeberry', + 'pokelogan', + 'poker', + 'pokeweed', + 'pokey', + 'poky', + 'polacca', + 'polacre', + 'polar', + 'polarimeter', + 'polariscope', + 'polarity', + 'polarization', + 'polarize', + 'polder', + 'pole', + 'poleax', + 'poleaxe', + 'polecat', + 'polemic', + 'polemics', + 'polemist', + 'polemoniaceous', + 'polenta', + 'polestar', + 'poleyn', + 'police', + 'policeman', + 'policewoman', + 'policlinic', + 'policy', + 'policyholder', + 'polio', + 'poliomyelitis', + 'polis', + 'polish', + 'polished', + 'polite', + 'politesse', + 'politic', + 'political', + 'politician', + 'politicize', + 'politick', + 'politicking', + 'politico', + 'politics', + 'polity', + 'polka', + 'poll', + 'pollack', + 'pollard', + 'polled', + 'pollen', + 'pollinate', + 'pollination', + 'pollinize', + 'pollinosis', + 'polliwog', + 'pollock', + 'pollster', + 'pollute', + 'polluted', + 'pollywog', + 'polo', + 'polonaise', + 'polonium', + 'poltergeist', + 'poltroon', + 'poltroonery', + 'polyadelphous', + 'polyamide', + 'polyandrist', + 'polyandrous', + 'polyandry', + 'polyanthus', + 'polybasite', + 'polychaete', + 'polychasium', + 'polychromatic', + 'polychrome', + 'polychromy', + 'polyclinic', + 'polycotyledon', + 'polycythemia', + 'polydactyl', + 'polydipsia', + 'polyester', + 'polyethylene', + 'polygamist', + 'polygamous', + 'polygamy', + 'polygenesis', + 'polyglot', + 'polygon', + 'polygraph', + 'polygynist', + 'polygynous', + 'polygyny', + 'polyhedron', + 'polyhistor', + 'polyhydric', + 'polyhydroxy', + 'polymath', + 'polymer', + 'polymeric', + 'polymerism', + 'polymerization', + 'polymerize', + 'polymerous', + 'polymorphism', + 'polymorphonuclear', + 'polymorphous', + 'polymyxin', + 'polyneuritis', + 'polynomial', + 'polynuclear', + 'polyp', + 'polypary', + 'polypeptide', + 'polypetalous', + 'polyphagia', + 'polyphone', + 'polyphonic', + 'polyphony', + 'polyphyletic', + 'polyploid', + 'polypody', + 'polypoid', + 'polypropylene', + 'polyptych', + 'polypus', + 'polysaccharide', + 'polysemy', + 'polysepalous', + 'polystyrene', + 'polysyllabic', + 'polysyllable', + 'polysyndeton', + 'polysynthetic', + 'polytechnic', + 'polytheism', + 'polythene', + 'polytonality', + 'polytrophic', + 'polytypic', + 'polyunsaturated', + 'polyurethane', + 'polyvalent', + 'polyvinyl', + 'polyzoan', + 'polyzoarium', + 'polyzoic', + 'pomace', + 'pomade', + 'pomander', + 'pomatum', + 'pome', + 'pomegranate', + 'pomelo', + 'pomfret', + 'pomiculture', + 'pomiferous', + 'pommel', + 'pomology', + 'pomp', + 'pompadour', + 'pompano', + 'pompon', + 'pomposity', + 'pompous', + 'ponce', + 'ponceau', + 'poncho', + 'pond', + 'ponder', + 'ponderable', + 'ponderous', + 'pondweed', + 'pone', + 'pongee', + 'pongid', + 'poniard', + 'pons', + 'pontifex', + 'pontiff', + 'pontifical', + 'pontificals', + 'pontificate', + 'pontine', + 'pontonier', + 'pontoon', + 'pony', + 'ponytail', + 'pooch', + 'pood', + 'poodle', + 'pooh', + 'pooka', + 'pool', + 'poolroom', + 'poon', + 'poop', + 'poor', + 'poorhouse', + 'poorly', + 'pop', + 'popcorn', + 'pope', + 'popedom', + 'popery', + 'popeyed', + 'popgun', + 'popinjay', + 'popish', + 'poplar', + 'poplin', + 'popliteal', + 'popover', + 'poppied', + 'popple', + 'poppy', + 'poppycock', + 'poppyhead', + 'pops', + 'populace', + 'popular', + 'popularity', + 'popularize', + 'popularly', + 'populate', + 'population', + 'populous', + 'porbeagle', + 'porcelain', + 'porch', + 'porcine', + 'porcupine', + 'pore', + 'porgy', + 'poriferous', + 'porism', + 'pork', + 'porker', + 'porkpie', + 'porky', + 'pornocracy', + 'pornography', + 'porosity', + 'porous', + 'porphyria', + 'porphyrin', + 'porphyritic', + 'porphyroid', + 'porphyry', + 'porpoise', + 'porridge', + 'porringer', + 'port', + 'portable', + 'portage', + 'portal', + 'portamento', + 'portative', + 'portcullis', + 'portend', + 'portent', + 'portentous', + 'porter', + 'porterage', + 'porterhouse', + 'portfire', + 'portfolio', + 'porthole', + 'portico', + 'portiere', + 'portion', + 'portly', + 'portmanteau', + 'portrait', + 'portraitist', + 'portraiture', + 'portray', + 'portulaca', + 'posada', + 'pose', + 'poser', + 'poseur', + 'posh', + 'posit', + 'position', + 'positive', + 'positively', + 'positivism', + 'positron', + 'positronium', + 'posology', + 'posse', + 'possess', + 'possessed', + 'possession', + 'possessive', + 'possessory', + 'posset', + 'possibility', + 'possible', + 'possibly', + 'possie', + 'possum', + 'post', + 'postage', + 'postal', + 'postaxial', + 'postbox', + 'postboy', + 'postcard', + 'postconsonantal', + 'postdate', + 'postdiluvian', + 'postdoctoral', + 'poster', + 'posterior', + 'posterity', + 'postern', + 'postexilian', + 'postfix', + 'postglacial', + 'postgraduate', + 'posthaste', + 'posthumous', + 'postiche', + 'posticous', + 'postilion', + 'postimpressionism', + 'posting', + 'postliminy', + 'postlude', + 'postman', + 'postmark', + 'postmaster', + 'postmeridian', + 'postmillennialism', + 'postmistress', + 'postmortem', + 'postnasal', + 'postnatal', + 'postoperative', + 'postorbital', + 'postpaid', + 'postpone', + 'postpositive', + 'postprandial', + 'postremogeniture', + 'postrider', + 'postscript', + 'postulant', + 'postulate', + 'posture', + 'posturize', + 'postwar', + 'posy', + 'pot', + 'potable', + 'potage', + 'potamic', + 'potash', + 'potassium', + 'potation', + 'potato', + 'potbellied', + 'potbelly', + 'potboiler', + 'potboy', + 'poteen', + 'potence', + 'potency', + 'potent', + 'potentate', + 'potential', + 'potentiality', + 'potentiate', + 'potentilla', + 'potentiometer', + 'potful', + 'pothead', + 'potheen', + 'pother', + 'potherb', + 'pothole', + 'pothook', + 'pothouse', + 'pothunter', + 'potiche', + 'potion', + 'potluck', + 'potman', + 'potoroo', + 'potpie', + 'potpourri', + 'potsherd', + 'potshot', + 'pottage', + 'potted', + 'potter', + 'pottery', + 'pottle', + 'potto', + 'potty', + 'pouch', + 'pouched', + 'pouf', + 'poulard', + 'poult', + 'poulterer', + 'poultice', + 'poultry', + 'poultryman', + 'pounce', + 'pound', + 'poundage', + 'poundal', + 'pour', + 'pourboire', + 'pourparler', + 'pourpoint', + 'poussette', + 'pout', + 'pouter', + 'poverty', + 'pow', + 'powder', + 'powdery', + 'power', + 'powerboat', + 'powered', + 'powerful', + 'powerhouse', + 'powerless', + 'powwow', + 'pox', + 'ppm', + 'practicable', + 'practical', + 'practically', + 'practice', + 'practiced', + 'practise', + 'practitioner', + 'praedial', + 'praefect', + 'praemunire', + 'praenomen', + 'praetor', + 'praetorian', + 'pragmatic', + 'pragmaticism', + 'pragmatics', + 'pragmatism', + 'pragmatist', + 'prairie', + 'praise', + 'praiseworthy', + 'prajna', + 'praline', + 'pralltriller', + 'pram', + 'prana', + 'prance', + 'prandial', + 'prang', + 'prank', + 'prankster', + 'prase', + 'praseodymium', + 'prat', + 'prate', + 'pratfall', + 'pratincole', + 'pratique', + 'prattle', + 'prau', + 'prawn', + 'praxis', + 'pray', + 'prayer', + 'prayerful', + 'preach', + 'preacher', + 'preachment', + 'preachy', + 'preadamite', + 'preamble', + 'preamplifier', + 'prearrange', + 'prebend', + 'prebendary', + 'precancel', + 'precarious', + 'precast', + 'precatory', + 'precaution', + 'precautionary', + 'precautious', + 'precede', + 'precedence', + 'precedency', + 'precedent', + 'precedential', + 'preceding', + 'precentor', + 'precept', + 'preceptive', + 'preceptor', + 'preceptory', + 'precess', + 'precession', + 'precessional', + 'precinct', + 'precincts', + 'preciosity', + 'precious', + 'precipice', + 'precipitancy', + 'precipitant', + 'precipitate', + 'precipitation', + 'precipitin', + 'precipitous', + 'precis', + 'precise', + 'precisian', + 'precision', + 'preclinical', + 'preclude', + 'precocious', + 'precocity', + 'precognition', + 'preconceive', + 'preconception', + 'preconcert', + 'preconcerted', + 'precondemn', + 'precondition', + 'preconize', + 'preconscious', + 'precontract', + 'precritical', + 'precursor', + 'precursory', + 'predacious', + 'predate', + 'predation', + 'predator', + 'predatory', + 'predecease', + 'predecessor', + 'predella', + 'predesignate', + 'predestinarian', + 'predestinate', + 'predestination', + 'predestine', + 'predetermine', + 'predial', + 'predicable', + 'predicament', + 'predicant', + 'predicate', + 'predicative', + 'predict', + 'prediction', + 'predictor', + 'predictory', + 'predigest', + 'predigestion', + 'predikant', + 'predilection', + 'predispose', + 'predisposition', + 'predominance', + 'predominant', + 'predominate', + 'preemie', + 'preeminence', + 'preeminent', + 'preempt', + 'preemption', + 'preen', + 'preengage', + 'preestablish', + 'preexist', + 'prefab', + 'prefabricate', + 'preface', + 'prefatory', + 'prefect', + 'prefecture', + 'prefer', + 'preferable', + 'preference', + 'preferential', + 'preferment', + 'prefiguration', + 'prefigure', + 'prefix', + 'preform', + 'prefrontal', + 'preglacial', + 'pregnable', + 'pregnancy', + 'pregnant', + 'preheat', + 'prehensible', + 'prehensile', + 'prehension', + 'prehistoric', + 'prehistory', + 'prehuman', + 'preindicate', + 'preinstruct', + 'prejudge', + 'prejudice', + 'prejudicial', + 'prelacy', + 'prelate', + 'prelatism', + 'prelature', + 'prelect', + 'preliminaries', + 'preliminary', + 'prelude', + 'prelusive', + 'premarital', + 'premature', + 'premaxilla', + 'premed', + 'premedical', + 'premeditate', + 'premeditation', + 'premier', + 'premiere', + 'premiership', + 'premillenarian', + 'premillennial', + 'premillennialism', + 'premise', + 'premises', + 'premium', + 'premolar', + 'premonish', + 'premonition', + 'premonitory', + 'premundane', + 'prenatal', + 'prenomen', + 'prenotion', + 'prentice', + 'preoccupancy', + 'preoccupation', + 'preoccupied', + 'preoccupy', + 'preordain', + 'preparation', + 'preparative', + 'preparator', + 'preparatory', + 'prepare', + 'prepared', + 'preparedness', + 'prepay', + 'prepense', + 'preponderance', + 'preponderant', + 'preponderate', + 'preposition', + 'prepositive', + 'prepositor', + 'prepossess', + 'prepossessing', + 'prepossession', + 'preposterous', + 'prepotency', + 'prepotent', + 'preprandial', + 'prepuce', + 'prerecord', + 'prerequisite', + 'prerogative', + 'presa', + 'presage', + 'presbyopia', + 'presbyter', + 'presbyterate', + 'presbyterial', + 'presbyterian', + 'presbytery', + 'preschool', + 'prescience', + 'prescind', + 'prescribe', + 'prescript', + 'prescriptible', + 'prescription', + 'prescriptive', + 'preselector', + 'presence', + 'present', + 'presentable', + 'presentation', + 'presentational', + 'presentationism', + 'presentative', + 'presentiment', + 'presently', + 'presentment', + 'preservative', + 'preserve', + 'preset', + 'preshrunk', + 'preside', + 'presidency', + 'president', + 'presidentship', + 'presidio', + 'presidium', + 'presignify', + 'press', + 'presser', + 'pressing', + 'pressman', + 'pressmark', + 'pressor', + 'pressroom', + 'pressure', + 'pressurize', + 'presswork', + 'prestidigitation', + 'prestige', + 'prestigious', + 'prestissimo', + 'presto', + 'prestress', + 'presumable', + 'presumably', + 'presume', + 'presumption', + 'presumptive', + 'presumptuous', + 'presuppose', + 'presurmise', + 'pretence', + 'pretend', + 'pretended', + 'pretender', + 'pretense', + 'pretension', + 'pretentious', + 'preterhuman', + 'preterit', + 'preterite', + 'preterition', + 'preteritive', + 'pretermit', + 'preternatural', + 'pretext', + 'pretonic', + 'pretor', + 'prettify', + 'pretty', + 'pretypify', + 'pretzel', + 'prevail', + 'prevailing', + 'prevalent', + 'prevaricate', + 'prevaricator', + 'prevenient', + 'prevent', + 'preventer', + 'prevention', + 'preventive', + 'preview', + 'previous', + 'previse', + 'prevision', + 'prevocalic', + 'prewar', + 'prey', + 'priapic', + 'priapism', + 'priapitis', + 'price', + 'priceless', + 'prick', + 'pricket', + 'pricking', + 'prickle', + 'prickly', + 'pride', + 'prier', + 'priest', + 'priestcraft', + 'priestess', + 'priesthood', + 'priestly', + 'prig', + 'priggery', + 'priggish', + 'prim', + 'primacy', + 'primal', + 'primarily', + 'primary', + 'primate', + 'primateship', + 'primatology', + 'primavera', + 'prime', + 'primer', + 'primero', + 'primeval', + 'primine', + 'priming', + 'primipara', + 'primitive', + 'primitivism', + 'primo', + 'primogenial', + 'primogenitor', + 'primogeniture', + 'primordial', + 'primordium', + 'primp', + 'primrose', + 'primula', + 'primulaceous', + 'primus', + 'prince', + 'princedom', + 'princeling', + 'princely', + 'princess', + 'principal', + 'principalities', + 'principality', + 'principally', + 'principate', + 'principium', + 'principle', + 'principled', + 'prink', + 'print', + 'printable', + 'printer', + 'printery', + 'printing', + 'printmaker', + 'printmaking', + 'prior', + 'priorate', + 'prioress', + 'priority', + 'priory', + 'prisage', + 'prise', + 'prism', + 'prismatic', + 'prismatoid', + 'prismoid', + 'prison', + 'prisoner', + 'prissy', + 'pristine', + 'prithee', + 'privacy', + 'private', + 'privateer', + 'privation', + 'privative', + 'privet', + 'privilege', + 'privileged', + 'privily', + 'privity', + 'privy', + 'prize', + 'prizefight', + 'prizewinner', + 'pro', + 'proa', + 'probabilism', + 'probability', + 'probable', + 'probably', + 'probate', + 'probation', + 'probationer', + 'probative', + 'probe', + 'probity', + 'problem', + 'problematic', + 'proboscidean', + 'proboscis', + 'procaine', + 'procambium', + 'procarp', + 'procathedral', + 'procedure', + 'proceed', + 'proceeding', + 'proceeds', + 'proceleusmatic', + 'procephalic', + 'process', + 'procession', + 'processional', + 'prochronism', + 'proclaim', + 'proclamation', + 'proclitic', + 'proclivity', + 'proconsul', + 'proconsulate', + 'procrastinate', + 'procreant', + 'procreate', + 'procryptic', + 'proctology', + 'proctor', + 'proctoscope', + 'procumbent', + 'procurable', + 'procurance', + 'procuration', + 'procurator', + 'procure', + 'procurer', + 'prod', + 'prodigal', + 'prodigious', + 'prodigy', + 'prodrome', + 'produce', + 'producer', + 'product', + 'production', + 'productive', + 'proem', + 'profanatory', + 'profane', + 'profanity', + 'profess', + 'professed', + 'profession', + 'professional', + 'professionalism', + 'professionalize', + 'professor', + 'professorate', + 'professoriate', + 'professorship', + 'proffer', + 'proficiency', + 'proficient', + 'profile', + 'profit', + 'profitable', + 'profiteer', + 'profiterole', + 'profligate', + 'profluent', + 'profound', + 'profundity', + 'profuse', + 'profusion', + 'profusive', + 'prog', + 'progenitive', + 'progenitor', + 'progeny', + 'progestational', + 'progesterone', + 'progestin', + 'proglottis', + 'prognathous', + 'prognosis', + 'prognostic', + 'prognosticate', + 'prognostication', + 'program', + 'programme', + 'programmer', + 'progress', + 'progression', + 'progressionist', + 'progressist', + 'progressive', + 'prohibit', + 'prohibition', + 'prohibitionist', + 'prohibitive', + 'prohibitory', + 'project', + 'projectile', + 'projection', + 'projectionist', + 'projective', + 'projector', + 'prolactin', + 'prolamine', + 'prolate', + 'prole', + 'proleg', + 'prolegomenon', + 'prolepsis', + 'proletarian', + 'proletariat', + 'proliferate', + 'proliferation', + 'proliferous', + 'prolific', + 'proline', + 'prolix', + 'prolocutor', + 'prologize', + 'prologue', + 'prolong', + 'prolongate', + 'prolongation', + 'prolonge', + 'prolusion', + 'prom', + 'promenade', + 'promethium', + 'prominence', + 'prominent', + 'promiscuity', + 'promiscuous', + 'promise', + 'promisee', + 'promising', + 'promissory', + 'promontory', + 'promote', + 'promoter', + 'promotion', + 'promotive', + 'prompt', + 'promptbook', + 'prompter', + 'promptitude', + 'promulgate', + 'promycelium', + 'pronate', + 'pronation', + 'pronator', + 'prone', + 'prong', + 'pronghorn', + 'pronominal', + 'pronoun', + 'pronounce', + 'pronounced', + 'pronouncement', + 'pronto', + 'pronucleus', + 'pronunciamento', + 'pronunciation', + 'proof', + 'proofread', + 'prop', + 'propaedeutic', + 'propagable', + 'propaganda', + 'propagandism', + 'propagandist', + 'propagandize', + 'propagate', + 'propagation', + 'propane', + 'proparoxytone', + 'propel', + 'propellant', + 'propeller', + 'propend', + 'propene', + 'propensity', + 'proper', + 'properly', + 'propertied', + 'property', + 'prophase', + 'prophecy', + 'prophesy', + 'prophet', + 'prophetic', + 'prophylactic', + 'prophylaxis', + 'propinquity', + 'propitiate', + 'propitiatory', + 'propitious', + 'propjet', + 'propman', + 'propolis', + 'proponent', + 'proportion', + 'proportionable', + 'proportional', + 'proportionate', + 'proportioned', + 'proposal', + 'propose', + 'proposition', + 'propositus', + 'propound', + 'propraetor', + 'proprietary', + 'proprietor', + 'proprietress', + 'propriety', + 'proprioceptor', + 'proptosis', + 'propulsion', + 'propylaeum', + 'propylene', + 'propylite', + 'prorate', + 'prorogue', + 'prosaic', + 'prosaism', + 'proscenium', + 'prosciutto', + 'proscribe', + 'proscription', + 'prose', + 'prosector', + 'prosecute', + 'prosecution', + 'prosecutor', + 'proselyte', + 'proselytism', + 'proselytize', + 'prosenchyma', + 'proser', + 'prosimian', + 'prosit', + 'prosody', + 'prosopopoeia', + 'prospect', + 'prospective', + 'prospector', + 'prospectus', + 'prosper', + 'prosperity', + 'prosperous', + 'prostate', + 'prostatectomy', + 'prostatitis', + 'prosthesis', + 'prosthetics', + 'prosthodontics', + 'prosthodontist', + 'prostitute', + 'prostitution', + 'prostomium', + 'prostrate', + 'prostration', + 'prostyle', + 'prosy', + 'protactinium', + 'protagonist', + 'protamine', + 'protanopia', + 'protasis', + 'protean', + 'protease', + 'protect', + 'protecting', + 'protection', + 'protectionism', + 'protectionist', + 'protective', + 'protector', + 'protectorate', + 'protege', + 'proteiform', + 'protein', + 'proteinase', + 'proteolysis', + 'proteose', + 'protest', + 'protestation', + 'prothalamion', + 'prothalamium', + 'prothallus', + 'prothesis', + 'prothonotary', + 'prothorax', + 'prothrombin', + 'protist', + 'protium', + 'protoactinium', + 'protochordate', + 'protocol', + 'protohistory', + 'protohuman', + 'protolanguage', + 'protolithic', + 'protomartyr', + 'protomorphic', + 'proton', + 'protonema', + 'protoplasm', + 'protoplast', + 'protostele', + 'prototherian', + 'prototrophic', + 'prototype', + 'protoxide', + 'protoxylem', + 'protozoal', + 'protozoan', + 'protozoology', + 'protozoon', + 'protract', + 'protractile', + 'protraction', + 'protractor', + 'protrude', + 'protrusile', + 'protrusion', + 'protrusive', + 'protuberance', + 'protuberancy', + 'protuberant', + 'protuberate', + 'proud', + 'proustite', + 'prove', + 'proven', + 'provenance', + 'provender', + 'provenience', + 'proverb', + 'proverbial', + 'provide', + 'provided', + 'providence', + 'provident', + 'providential', + 'providing', + 'province', + 'provincial', + 'provincialism', + 'provinciality', + 'provision', + 'provisional', + 'proviso', + 'provisory', + 'provitamin', + 'provocation', + 'provocative', + 'provoke', + 'provolone', + 'provost', + 'prow', + 'prowess', + 'prowl', + 'prowler', + 'proximal', + 'proximate', + 'proximity', + 'proximo', + 'proxy', + 'prude', + 'prudence', + 'prudent', + 'prudential', + 'prudery', + 'prudish', + 'pruinose', + 'prune', + 'prunella', + 'prunelle', + 'prurient', + 'prurigo', + 'pruritus', + 'prussiate', + 'pry', + 'pryer', + 'prying', + 'prytaneum', + 'psalm', + 'psalmbook', + 'psalmist', + 'psalmody', + 'psalterium', + 'psaltery', + 'psephology', + 'pseudaxis', + 'pseudo', + 'pseudocarp', + 'pseudohemophilia', + 'pseudohermaphrodite', + 'pseudohermaphroditism', + 'pseudonym', + 'pseudonymous', + 'pseudoscope', + 'psf', + 'pshaw', + 'psi', + 'psia', + 'psid', + 'psilocybin', + 'psilomelane', + 'psittacine', + 'psittacosis', + 'psoas', + 'psoriasis', + 'psych', + 'psychasthenia', + 'psyche', + 'psychedelic', + 'psychiatrist', + 'psychiatry', + 'psychic', + 'psycho', + 'psychoactive', + 'psychoanalysis', + 'psychobiology', + 'psychochemical', + 'psychodiagnosis', + 'psychodiagnostics', + 'psychodrama', + 'psychodynamics', + 'psychogenesis', + 'psychogenic', + 'psychognosis', + 'psychographer', + 'psychokinesis', + 'psycholinguistics', + 'psychological', + 'psychologism', + 'psychologist', + 'psychologize', + 'psychology', + 'psychomancy', + 'psychometrics', + 'psychometry', + 'psychomotor', + 'psychoneurosis', + 'psychoneurotic', + 'psychopath', + 'psychopathist', + 'psychopathology', + 'psychopathy', + 'psychopharmacology', + 'psychophysics', + 'psychophysiology', + 'psychosexual', + 'psychosis', + 'psychosocial', + 'psychosomatic', + 'psychosomatics', + 'psychosurgery', + 'psychotechnics', + 'psychotechnology', + 'psychotherapy', + 'psychotic', + 'psychotomimetic', + 'psychrometer', + 'pt', + 'ptarmigan', + 'pteranodon', + 'pteridology', + 'pteridophyte', + 'pterodactyl', + 'pteropod', + 'pterosaur', + 'pteryla', + 'ptisan', + 'ptomaine', + 'ptosis', + 'ptyalin', + 'ptyalism', + 'pub', + 'puberty', + 'puberulent', + 'pubes', + 'pubescent', + 'pubis', + 'public', + 'publican', + 'publication', + 'publicist', + 'publicity', + 'publicize', + 'publicly', + 'publicness', + 'publish', + 'publisher', + 'publishing', + 'puca', + 'puccoon', + 'puce', + 'puck', + 'pucka', + 'pucker', + 'puckery', + 'pudding', + 'puddle', + 'puddling', + 'pudency', + 'pudendum', + 'pudgy', + 'pueblo', + 'puerile', + 'puerilism', + 'puerility', + 'puerperal', + 'puerperium', + 'puff', + 'puffball', + 'puffer', + 'puffery', + 'puffin', + 'puffy', + 'pug', + 'pugging', + 'puggree', + 'pugilism', + 'pugilist', + 'pugnacious', + 'puisne', + 'puissance', + 'puissant', + 'puke', + 'pukka', + 'pul', + 'pulchritude', + 'pulchritudinous', + 'pule', + 'puli', + 'puling', + 'pull', + 'pullet', + 'pulley', + 'pullover', + 'pullulate', + 'pulmonary', + 'pulmonate', + 'pulmonic', + 'pulp', + 'pulpboard', + 'pulpit', + 'pulpiteer', + 'pulpwood', + 'pulpy', + 'pulque', + 'pulsar', + 'pulsate', + 'pulsatile', + 'pulsation', + 'pulsatory', + 'pulse', + 'pulsimeter', + 'pulsometer', + 'pulverable', + 'pulverize', + 'pulverulent', + 'pulvinate', + 'pulvinus', + 'puma', + 'pumice', + 'pummel', + 'pump', + 'pumpernickel', + 'pumping', + 'pumpkin', + 'pumpkinseed', + 'pun', + 'punch', + 'punchball', + 'punchboard', + 'puncheon', + 'punchy', + 'punctate', + 'punctilio', + 'punctilious', + 'punctual', + 'punctuality', + 'punctuate', + 'punctuation', + 'puncture', + 'pundit', + 'pung', + 'pungent', + 'pungy', + 'punish', + 'punishable', + 'punishment', + 'punitive', + 'punk', + 'punkah', + 'punkie', + 'punner', + 'punnet', + 'punster', + 'punt', + 'puny', + 'pup', + 'pupa', + 'puparium', + 'pupil', + 'pupillary', + 'pupiparous', + 'puppet', + 'puppetry', + 'puppy', + 'purblind', + 'purchasable', + 'purchase', + 'purdah', + 'pure', + 'purebred', + 'puree', + 'purehearted', + 'purely', + 'purgation', + 'purgative', + 'purgatorial', + 'purgatory', + 'purge', + 'purificator', + 'purify', + 'purine', + 'purism', + 'puritan', + 'puritanical', + 'purity', + 'purl', + 'purlieu', + 'purlin', + 'purloin', + 'purple', + 'purpleness', + 'purplish', + 'purport', + 'purpose', + 'purposeful', + 'purposeless', + 'purposely', + 'purposive', + 'purpura', + 'purpure', + 'purpurin', + 'purr', + 'purree', + 'purse', + 'purser', + 'purslane', + 'pursuance', + 'pursuant', + 'pursue', + 'pursuer', + 'pursuit', + 'pursuivant', + 'pursy', + 'purtenance', + 'purulence', + 'purulent', + 'purusha', + 'purvey', + 'purveyance', + 'purveyor', + 'purview', + 'pus', + 'push', + 'pushball', + 'pushcart', + 'pushed', + 'pusher', + 'pushing', + 'pushover', + 'pushy', + 'pusillanimity', + 'pusillanimous', + 'puss', + 'pussy', + 'pussyfoot', + 'pustulant', + 'pustulate', + 'pustule', + 'put', + 'putamen', + 'putative', + 'putrefaction', + 'putrefy', + 'putrescent', + 'putrescible', + 'putrescine', + 'putrid', + 'putsch', + 'putt', + 'puttee', + 'putter', + 'puttier', + 'putto', + 'putty', + 'puttyroot', + 'puzzle', + 'puzzlement', + 'puzzler', + 'pya', + 'pyaemia', + 'pycnidium', + 'pycnometer', + 'pye', + 'pyelitis', + 'pyelography', + 'pyelonephritis', + 'pyemia', + 'pygidium', + 'pygmy', + 'pyjamas', + 'pyknic', + 'pylon', + 'pylorectomy', + 'pylorus', + 'pyoid', + 'pyonephritis', + 'pyorrhea', + 'pyosis', + 'pyralid', + 'pyramid', + 'pyramidal', + 'pyrargyrite', + 'pyrazole', + 'pyre', + 'pyrene', + 'pyrethrin', + 'pyrethrum', + 'pyretic', + 'pyretotherapy', + 'pyrexia', + 'pyridine', + 'pyridoxine', + 'pyriform', + 'pyrimidine', + 'pyrite', + 'pyrites', + 'pyrochemical', + 'pyroclastic', + 'pyroconductivity', + 'pyroelectric', + 'pyroelectricity', + 'pyrogallate', + 'pyrogallol', + 'pyrogen', + 'pyrogenic', + 'pyrogenous', + 'pyrognostics', + 'pyrography', + 'pyroligneous', + 'pyrology', + 'pyrolysis', + 'pyromagnetic', + 'pyromancy', + 'pyromania', + 'pyrometallurgy', + 'pyrometer', + 'pyromorphite', + 'pyrone', + 'pyrope', + 'pyrophoric', + 'pyrophosphate', + 'pyrophotometer', + 'pyrophyllite', + 'pyrosis', + 'pyrostat', + 'pyrotechnic', + 'pyrotechnics', + 'pyroxene', + 'pyroxenite', + 'pyroxylin', + 'pyrrhic', + 'pyrrhotite', + 'pyrrhuloxia', + 'pyrrolidine', + 'python', + 'pythoness', + 'pyuria', + 'pyx', + 'pyxidium', + 'pyxie', + 'q', + 'qadi', + 'qibla', + 'qintar', + 'qoph', + 'qua', + 'quack', + 'quackery', + 'quacksalver', + 'quad', + 'quadrangle', + 'quadrangular', + 'quadrant', + 'quadrat', + 'quadrate', + 'quadratic', + 'quadratics', + 'quadrature', + 'quadrennial', + 'quadrennium', + 'quadric', + 'quadriceps', + 'quadricycle', + 'quadrifid', + 'quadriga', + 'quadrilateral', + 'quadrille', + 'quadrillion', + 'quadrinomial', + 'quadripartite', + 'quadriplegia', + 'quadriplegic', + 'quadrireme', + 'quadrisect', + 'quadrivalent', + 'quadrivial', + 'quadrivium', + 'quadroon', + 'quadrumanous', + 'quadruped', + 'quadruple', + 'quadruplet', + 'quadruplex', + 'quadruplicate', + 'quaff', + 'quag', + 'quagga', + 'quaggy', + 'quagmire', + 'quahog', + 'quail', + 'quaint', + 'quake', + 'quaky', + 'qualification', + 'qualified', + 'qualifier', + 'qualify', + 'qualitative', + 'quality', + 'qualm', + 'qualmish', + 'quamash', + 'quandary', + 'quant', + 'quanta', + 'quantic', + 'quantifier', + 'quantify', + 'quantitative', + 'quantity', + 'quantize', + 'quantum', + 'quaquaversal', + 'quarantine', + 'quark', + 'quarrel', + 'quarrelsome', + 'quarrier', + 'quarry', + 'quart', + 'quartan', + 'quarter', + 'quarterage', + 'quarterback', + 'quarterdeck', + 'quartered', + 'quartering', + 'quarterly', + 'quartermaster', + 'quartern', + 'quarters', + 'quartersaw', + 'quarterstaff', + 'quartet', + 'quartic', + 'quartile', + 'quarto', + 'quartz', + 'quartziferous', + 'quartzite', + 'quasar', + 'quash', + 'quasi', + 'quass', + 'quassia', + 'quaternary', + 'quaternion', + 'quaternity', + 'quatrain', + 'quatre', + 'quatrefoil', + 'quattrocento', + 'quaver', + 'quay', + 'quean', + 'queasy', + 'queen', + 'queenhood', + 'queenly', + 'queer', + 'quell', + 'quench', + 'quenchless', + 'quenelle', + 'quercetin', + 'querist', + 'quern', + 'querulous', + 'query', + 'quest', + 'question', + 'questionable', + 'questionary', + 'questioning', + 'questionless', + 'questionnaire', + 'questor', + 'quetzal', + 'queue', + 'quibble', + 'quibbling', + 'quiche', + 'quick', + 'quicken', + 'quickie', + 'quicklime', + 'quickly', + 'quicksand', + 'quicksilver', + 'quickstep', + 'quid', + 'quiddity', + 'quidnunc', + 'quiescent', + 'quiet', + 'quieten', + 'quietism', + 'quietly', + 'quietude', + 'quietus', + 'quiff', + 'quill', + 'quillet', + 'quillon', + 'quilt', + 'quilting', + 'quinacrine', + 'quinary', + 'quinate', + 'quince', + 'quincentenary', + 'quincuncial', + 'quincunx', + 'quindecagon', + 'quindecennial', + 'quinidine', + 'quinine', + 'quinol', + 'quinone', + 'quinonoid', + 'quinquefid', + 'quinquennial', + 'quinquennium', + 'quinquepartite', + 'quinquereme', + 'quinquevalent', + 'quinsy', + 'quint', + 'quintain', + 'quintal', + 'quintan', + 'quinte', + 'quintessence', + 'quintet', + 'quintic', + 'quintile', + 'quintillion', + 'quintuple', + 'quintuplet', + 'quintuplicate', + 'quinze', + 'quip', + 'quipster', + 'quipu', + 'quire', + 'quirk', + 'quirt', + 'quisling', + 'quit', + 'quitclaim', + 'quite', + 'quitrent', + 'quits', + 'quittance', + 'quittor', + 'quiver', + 'quixotic', + 'quixotism', + 'quiz', + 'quizmaster', + 'quizzical', + 'quod', + 'quodlibet', + 'quoin', + 'quoit', + 'quoits', + 'quondam', + 'quorum', + 'quota', + 'quotable', + 'quotation', + 'quote', + 'quoth', + 'quotha', + 'quotidian', + 'quotient', + 'r', + 'rabato', + 'rabbet', + 'rabbi', + 'rabbin', + 'rabbinate', + 'rabbinical', + 'rabbinism', + 'rabbit', + 'rabbitfish', + 'rabbitry', + 'rabble', + 'rabblement', + 'rabid', + 'rabies', + 'raccoon', + 'race', + 'racecourse', + 'racehorse', + 'raceme', + 'racemic', + 'racemose', + 'racer', + 'raceway', + 'rachis', + 'rachitis', + 'racial', + 'racialism', + 'racing', + 'racism', + 'rack', + 'racket', + 'racketeer', + 'rackety', + 'racon', + 'raconteur', + 'racoon', + 'racquet', + 'racy', + 'rad', + 'radar', + 'radarman', + 'radarscope', + 'raddle', + 'raddled', + 'radial', + 'radian', + 'radiance', + 'radiancy', + 'radiant', + 'radiate', + 'radiation', + 'radiative', + 'radiator', + 'radical', + 'radicalism', + 'radically', + 'radicand', + 'radicel', + 'radices', + 'radicle', + 'radiculitis', + 'radii', + 'radio', + 'radioactivate', + 'radioactive', + 'radioactivity', + 'radiobiology', + 'radiobroadcast', + 'radiocarbon', + 'radiochemical', + 'radiochemistry', + 'radiocommunication', + 'radioelement', + 'radiogram', + 'radiograph', + 'radiography', + 'radioisotope', + 'radiolarian', + 'radiolocation', + 'radiology', + 'radiolucent', + 'radioluminescence', + 'radioman', + 'radiometeorograph', + 'radiometer', + 'radiomicrometer', + 'radionuclide', + 'radiopaque', + 'radiophone', + 'radiophotograph', + 'radioscope', + 'radioscopy', + 'radiosensitive', + 'radiosonde', + 'radiosurgery', + 'radiotelegram', + 'radiotelegraph', + 'radiotelegraphy', + 'radiotelephone', + 'radiotelephony', + 'radiotherapy', + 'radiothermy', + 'radiothorium', + 'radiotransparent', + 'radish', + 'radium', + 'radius', + 'radix', + 'radome', + 'radon', + 'raff', + 'raffia', + 'raffinate', + 'raffinose', + 'raffish', + 'raffle', + 'rafflesia', + 'raft', + 'rafter', + 'rag', + 'ragamuffin', + 'rage', + 'ragged', + 'raggedy', + 'ragi', + 'raglan', + 'ragman', + 'ragout', + 'rags', + 'ragtime', + 'ragweed', + 'ragwort', + 'rah', + 'raid', + 'rail', + 'railhead', + 'railing', + 'raillery', + 'railroad', + 'railroader', + 'railway', + 'raiment', + 'rain', + 'rainband', + 'rainbow', + 'raincoat', + 'raindrop', + 'rainfall', + 'rainmaker', + 'rainout', + 'rainproof', + 'rains', + 'rainstorm', + 'rainwater', + 'rainy', + 'raise', + 'raised', + 'raisin', + 'raising', + 'raja', + 'rajah', + 'rake', + 'rakehell', + 'raker', + 'raki', + 'rakish', + 'rale', + 'rallentando', + 'ralline', + 'rally', + 'ram', + 'ramble', + 'rambler', + 'rambling', + 'rambunctious', + 'rambutan', + 'ramekin', + 'ramentum', + 'ramie', + 'ramification', + 'ramiform', + 'ramify', + 'ramjet', + 'rammer', + 'rammish', + 'ramose', + 'ramp', + 'rampage', + 'rampageous', + 'rampant', + 'rampart', + 'ramrod', + 'ramshackle', + 'ramtil', + 'ramulose', + 'ran', + 'rance', + 'ranch', + 'rancher', + 'ranchero', + 'ranchman', + 'rancho', + 'rancid', + 'rancidity', + 'rancor', + 'rancorous', + 'rand', + 'randan', + 'random', + 'randy', + 'ranee', + 'rang', + 'range', + 'ranged', + 'rangefinder', + 'ranger', + 'rangy', + 'rani', + 'rank', + 'ranket', + 'ranking', + 'rankle', + 'ransack', + 'ransom', + 'rant', + 'ranunculaceous', + 'ranunculus', + 'rap', + 'rapacious', + 'rape', + 'rapeseed', + 'rapid', + 'rapids', + 'rapier', + 'rapine', + 'rapparee', + 'rappee', + 'rappel', + 'rapper', + 'rapping', + 'rapport', + 'rapprochement', + 'rapscallion', + 'rapt', + 'raptor', + 'raptorial', + 'rapture', + 'rapturous', + 'rare', + 'rarebit', + 'rarefaction', + 'rarefied', + 'rarefy', + 'rarely', + 'rarity', + 'rasbora', + 'rascal', + 'rascality', + 'rascally', + 'rase', + 'rash', + 'rasher', + 'rasorial', + 'rasp', + 'raspberry', + 'rasping', + 'raspings', + 'raspy', + 'raster', + 'rat', + 'rata', + 'ratable', + 'ratafia', + 'ratal', + 'ratan', + 'rataplan', + 'ratchet', + 'rate', + 'rateable', + 'ratel', + 'ratepayer', + 'ratfink', + 'rath', + 'rathe', + 'rather', + 'rathskeller', + 'ratify', + 'rating', + 'ratio', + 'ratiocinate', + 'ratiocination', + 'ration', + 'rational', + 'rationale', + 'rationalism', + 'rationality', + 'rationalize', + 'rations', + 'ratite', + 'ratline', + 'ratoon', + 'ratsbane', + 'rattan', + 'ratter', + 'rattish', + 'rattle', + 'rattlebox', + 'rattlebrain', + 'rattlebrained', + 'rattlehead', + 'rattlepate', + 'rattler', + 'rattlesnake', + 'rattletrap', + 'rattling', + 'rattly', + 'rattoon', + 'rattrap', + 'ratty', + 'raucous', + 'rauwolfia', + 'ravage', + 'rave', + 'ravel', + 'ravelin', + 'ravelment', + 'raven', + 'ravening', + 'ravenous', + 'raver', + 'ravin', + 'ravine', + 'raving', + 'ravioli', + 'ravish', + 'ravishing', + 'ravishment', + 'raw', + 'rawboned', + 'rawhide', + 'rawinsonde', + 'ray', + 'rayless', + 'rayon', + 'raze', + 'razee', + 'razor', + 'razorback', + 'razorbill', + 'razz', + 'razzia', + 're', + 'reach', + 'react', + 'reactance', + 'reactant', + 'reaction', + 'reactionary', + 'reactivate', + 'reactive', + 'reactor', + 'read', + 'readability', + 'readable', + 'reader', + 'readership', + 'readily', + 'readiness', + 'reading', + 'readjust', + 'readjustment', + 'ready', + 'reagent', + 'real', + 'realgar', + 'realism', + 'realist', + 'realistic', + 'reality', + 'realize', + 'really', + 'realm', + 'realtor', + 'realty', + 'ream', + 'reamer', + 'reap', + 'reaper', + 'rear', + 'rearm', + 'rearmost', + 'rearrange', + 'rearward', + 'reason', + 'reasonable', + 'reasoned', + 'reasoning', + 'reasonless', + 'reassure', + 'reata', + 'reave', + 'rebarbative', + 'rebate', + 'rebatement', + 'rebato', + 'rebec', + 'rebel', + 'rebellion', + 'rebellious', + 'rebirth', + 'reboant', + 'reborn', + 'rebound', + 'rebozo', + 'rebroadcast', + 'rebuff', + 'rebuild', + 'rebuke', + 'rebus', + 'rebut', + 'rebuttal', + 'rebutter', + 'recalcitrant', + 'recalcitrate', + 'recalesce', + 'recalescence', + 'recall', + 'recant', + 'recap', + 'recapitulate', + 'recapitulation', + 'recaption', + 'recapture', + 'recce', + 'recede', + 'receipt', + 'receiptor', + 'receivable', + 'receive', + 'receiver', + 'receivership', + 'recency', + 'recension', + 'recent', + 'recept', + 'receptacle', + 'reception', + 'receptionist', + 'receptive', + 'receptor', + 'recess', + 'recession', + 'recessional', + 'recessive', + 'recidivate', + 'recidivism', + 'recipe', + 'recipience', + 'recipient', + 'reciprocal', + 'reciprocate', + 'reciprocation', + 'reciprocity', + 'recital', + 'recitation', + 'recitative', + 'recitativo', + 'recite', + 'reck', + 'reckless', + 'reckon', + 'reckoner', + 'reckoning', + 'reclaim', + 'reclamation', + 'reclinate', + 'recline', + 'recliner', + 'recluse', + 'reclusion', + 'recognition', + 'recognizance', + 'recognize', + 'recognizee', + 'recognizor', + 'recoil', + 'recollect', + 'recollected', + 'recollection', + 'recombination', + 'recommend', + 'recommendation', + 'recommendatory', + 'recommit', + 'recompense', + 'reconcilable', + 'reconcile', + 'reconciliatory', + 'recondite', + 'recondition', + 'reconnaissance', + 'reconnoiter', + 'reconnoitre', + 'reconsider', + 'reconstitute', + 'reconstruct', + 'reconstruction', + 'reconstructive', + 'reconvert', + 'record', + 'recorder', + 'recording', + 'recount', + 'recountal', + 'recoup', + 'recourse', + 'recover', + 'recoverable', + 'recovery', + 'recreant', + 'recreate', + 'recreation', + 'recrement', + 'recriminate', + 'recrimination', + 'recrudesce', + 'recrudescence', + 'recruit', + 'recruitment', + 'recrystallize', + 'rectal', + 'rectangle', + 'rectangular', + 'recti', + 'rectifier', + 'rectify', + 'rectilinear', + 'rectitude', + 'recto', + 'rectocele', + 'rector', + 'rectory', + 'rectrix', + 'rectum', + 'rectus', + 'recumbent', + 'recuperate', + 'recuperative', + 'recuperator', + 'recur', + 'recurrence', + 'recurrent', + 'recursion', + 'recurvate', + 'recurve', + 'recurved', + 'recusancy', + 'recusant', + 'recycle', + 'red', + 'redact', + 'redan', + 'redbird', + 'redbreast', + 'redbud', + 'redbug', + 'redcap', + 'redcoat', + 'redd', + 'redden', + 'reddish', + 'rede', + 'redeem', + 'redeemable', + 'redeemer', + 'redeeming', + 'redemption', + 'redemptioner', + 'redeploy', + 'redevelop', + 'redfin', + 'redfish', + 'redhead', + 'redingote', + 'redintegrate', + 'redintegration', + 'redistrict', + 'redivivus', + 'redneck', + 'redness', + 'redo', + 'redolent', + 'redouble', + 'redoubt', + 'redoubtable', + 'redound', + 'redpoll', + 'redraft', + 'redress', + 'redroot', + 'redshank', + 'redskin', + 'redstart', + 'redtop', + 'reduce', + 'reduced', + 'reducer', + 'reductase', + 'reduction', + 'reductive', + 'redundancy', + 'redundant', + 'reduplicate', + 'reduplication', + 'reduplicative', + 'redware', + 'redwing', + 'redwood', + 'reed', + 'reedbird', + 'reedbuck', + 'reeding', + 'reeducate', + 'reedy', + 'reef', + 'reefer', + 'reek', + 'reel', + 'reenforce', + 'reenter', + 'reentry', + 'reest', + 'reeve', + 'ref', + 'reface', + 'refection', + 'refectory', + 'refer', + 'referee', + 'reference', + 'referendum', + 'referent', + 'referential', + 'refill', + 'refine', + 'refined', + 'refinement', + 'refinery', + 'refit', + 'reflate', + 'reflation', + 'reflect', + 'reflectance', + 'reflection', + 'reflective', + 'reflector', + 'reflex', + 'reflexion', + 'reflexive', + 'refluent', + 'reflux', + 'reforest', + 'reform', + 'reformation', + 'reformatory', + 'reformed', + 'reformer', + 'reformism', + 'refract', + 'refraction', + 'refractive', + 'refractometer', + 'refractor', + 'refractory', + 'refrain', + 'refrangible', + 'refresh', + 'refresher', + 'refreshing', + 'refreshment', + 'refrigerant', + 'refrigerate', + 'refrigeration', + 'refrigerator', + 'reft', + 'refuel', + 'refuge', + 'refugee', + 'refulgence', + 'refulgent', + 'refund', + 'refurbish', + 'refusal', + 'refuse', + 'refutation', + 'refutative', + 'refute', + 'regain', + 'regal', + 'regale', + 'regalia', + 'regality', + 'regard', + 'regardant', + 'regardful', + 'regarding', + 'regardless', + 'regatta', + 'regelate', + 'regelation', + 'regency', + 'regeneracy', + 'regenerate', + 'regeneration', + 'regenerative', + 'regenerator', + 'regent', + 'regicide', + 'regime', + 'regimen', + 'regiment', + 'regimentals', + 'region', + 'regional', + 'regionalism', + 'register', + 'registered', + 'registrant', + 'registrar', + 'registration', + 'registry', + 'reglet', + 'regnal', + 'regnant', + 'regolith', + 'regorge', + 'regrate', + 'regress', + 'regression', + 'regressive', + 'regret', + 'regretful', + 'regulable', + 'regular', + 'regularize', + 'regularly', + 'regulate', + 'regulation', + 'regulator', + 'regulus', + 'regurgitate', + 'regurgitation', + 'rehabilitate', + 'rehabilitation', + 'rehash', + 'rehearing', + 'rehearsal', + 'rehearse', + 'reheat', + 'reify', + 'reign', + 'reimburse', + 'reimport', + 'reimpression', + 'rein', + 'reincarnate', + 'reincarnation', + 'reindeer', + 'reinforce', + 'reinforcement', + 'reins', + 'reinstate', + 'reinsure', + 'reis', + 'reiterant', + 'reiterate', + 'reive', + 'reject', + 'rejection', + 'rejoice', + 'rejoin', + 'rejoinder', + 'rejuvenate', + 'relapse', + 'relate', + 'related', + 'relation', + 'relational', + 'relations', + 'relationship', + 'relative', + 'relativistic', + 'relativity', + 'relativize', + 'relator', + 'relax', + 'relaxation', + 'relay', + 'release', + 'relegate', + 'relent', + 'relentless', + 'relevance', + 'relevant', + 'reliable', + 'reliance', + 'reliant', + 'relic', + 'relict', + 'relief', + 'relieve', + 'religieuse', + 'religieux', + 'religion', + 'religionism', + 'religiose', + 'religiosity', + 'religious', + 'relinquish', + 'reliquary', + 'relique', + 'reliquiae', + 'relish', + 'relive', + 'relucent', + 'reluct', + 'reluctance', + 'reluctant', + 'reluctivity', + 'relume', + 'rely', + 'remain', + 'remainder', + 'remainderman', + 'remains', + 'remake', + 'remand', + 'remanence', + 'remanent', + 'remark', + 'remarkable', + 'remarque', + 'rematch', + 'remediable', + 'remedial', + 'remediless', + 'remedy', + 'remember', + 'remembrance', + 'remembrancer', + 'remex', + 'remind', + 'remindful', + 'reminisce', + 'reminiscence', + 'reminiscent', + 'remise', + 'remiss', + 'remissible', + 'remission', + 'remit', + 'remittance', + 'remittee', + 'remittent', + 'remitter', + 'remnant', + 'remodel', + 'remonetize', + 'remonstrance', + 'remonstrant', + 'remonstrate', + 'remontant', + 'remora', + 'remorse', + 'remorseful', + 'remorseless', + 'remote', + 'remotion', + 'remount', + 'removable', + 'removal', + 'remove', + 'removed', + 'remunerate', + 'remuneration', + 'remunerative', + 'renaissance', + 'renal', + 'renascence', + 'renascent', + 'rencontre', + 'rend', + 'render', + 'rendering', + 'rendezvous', + 'rendition', + 'renegade', + 'renegado', + 'renege', + 'renew', + 'renewal', + 'renin', + 'renitent', + 'rennet', + 'rennin', + 'renounce', + 'renovate', + 'renown', + 'renowned', + 'rensselaerite', + 'rent', + 'rental', + 'renter', + 'rentier', + 'renunciation', + 'renvoi', + 'reopen', + 'reorder', + 'reorganization', + 'reorganize', + 'reorientation', + 'rep', + 'repair', + 'repairer', + 'repairman', + 'repand', + 'reparable', + 'reparation', + 'reparative', + 'repartee', + 'repartition', + 'repast', + 'repatriate', + 'repay', + 'repeal', + 'repeat', + 'repeated', + 'repeater', + 'repel', + 'repellent', + 'repent', + 'repentance', + 'repentant', + 'repercussion', + 'repertoire', + 'repertory', + 'repetend', + 'repetition', + 'repetitious', + 'repetitive', + 'rephrase', + 'repine', + 'replace', + 'replacement', + 'replay', + 'replenish', + 'replete', + 'repletion', + 'replevin', + 'replevy', + 'replica', + 'replicate', + 'replication', + 'reply', + 'report', + 'reportage', + 'reporter', + 'reportorial', + 'repose', + 'reposeful', + 'reposit', + 'reposition', + 'repository', + 'repossess', + 'repp', + 'reprehend', + 'reprehensible', + 'reprehension', + 'represent', + 'representation', + 'representational', + 'representationalism', + 'representative', + 'repress', + 'repression', + 'repressive', + 'reprieve', + 'reprimand', + 'reprint', + 'reprisal', + 'reprise', + 'repro', + 'reproach', + 'reproachful', + 'reproachless', + 'reprobate', + 'reprobation', + 'reprobative', + 'reproduce', + 'reproduction', + 'reproductive', + 'reprography', + 'reproof', + 'reprovable', + 'reproval', + 'reprove', + 'reptant', + 'reptile', + 'reptilian', + 'republic', + 'republican', + 'republicanism', + 'republicanize', + 'repudiate', + 'repudiation', + 'repugn', + 'repugnance', + 'repugnant', + 'repulse', + 'repulsion', + 'repulsive', + 'repurchase', + 'reputable', + 'reputation', + 'repute', + 'reputed', + 'request', + 'requiem', + 'requiescat', + 'require', + 'requirement', + 'requisite', + 'requisition', + 'requital', + 'requite', + 'reredos', + 'reremouse', + 'rerun', + 'resale', + 'rescind', + 'rescission', + 'rescissory', + 'rescript', + 'rescue', + 'research', + 'reseat', + 'reseau', + 'resect', + 'resection', + 'reseda', + 'resemblance', + 'resemble', + 'resent', + 'resentful', + 'resentment', + 'reserpine', + 'reservation', + 'reserve', + 'reserved', + 'reservist', + 'reservoir', + 'reset', + 'resh', + 'reshape', + 'reside', + 'residence', + 'residency', + 'resident', + 'residential', + 'residentiary', + 'residual', + 'residuary', + 'residue', + 'residuum', + 'resign', + 'resignation', + 'resigned', + 'resile', + 'resilience', + 'resilient', + 'resin', + 'resinate', + 'resiniferous', + 'resinoid', + 'resinous', + 'resist', + 'resistance', + 'resistant', + 'resistive', + 'resistless', + 'resistor', + 'resnatron', + 'resoluble', + 'resolute', + 'resolution', + 'resolutive', + 'resolvable', + 'resolve', + 'resolved', + 'resolvent', + 'resonance', + 'resonant', + 'resonate', + 'resonator', + 'resorcinol', + 'resort', + 'resound', + 'resource', + 'resourceful', + 'respect', + 'respectability', + 'respectable', + 'respectful', + 'respecting', + 'respective', + 'respectively', + 'respirable', + 'respiration', + 'respirator', + 'respiratory', + 'respire', + 'respite', + 'resplendence', + 'resplendent', + 'respond', + 'respondence', + 'respondent', + 'response', + 'responser', + 'responsibility', + 'responsible', + 'responsion', + 'responsive', + 'responsiveness', + 'responsory', + 'responsum', + 'rest', + 'restate', + 'restaurant', + 'restaurateur', + 'restful', + 'restharrow', + 'resting', + 'restitution', + 'restive', + 'restless', + 'restoration', + 'restorative', + 'restore', + 'restrain', + 'restrained', + 'restrainer', + 'restraint', + 'restrict', + 'restricted', + 'restriction', + 'restrictive', + 'result', + 'resultant', + 'resume', + 'resumption', + 'resupinate', + 'resupine', + 'resurge', + 'resurgent', + 'resurrect', + 'resurrection', + 'resurrectionism', + 'resurrectionist', + 'resuscitate', + 'resuscitator', + 'ret', + 'retable', + 'retail', + 'retain', + 'retainer', + 'retake', + 'retaliate', + 'retaliation', + 'retard', + 'retardant', + 'retardation', + 'retarded', + 'retarder', + 'retardment', + 'retch', + 'rete', + 'retene', + 'retention', + 'retentive', + 'retentivity', + 'rethink', + 'retiarius', + 'retiary', + 'reticent', + 'reticle', + 'reticular', + 'reticulate', + 'reticulation', + 'reticule', + 'reticulum', + 'retiform', + 'retina', + 'retinite', + 'retinitis', + 'retinol', + 'retinoscope', + 'retinoscopy', + 'retinue', + 'retire', + 'retired', + 'retirement', + 'retiring', + 'retool', + 'retorsion', + 'retort', + 'retortion', + 'retouch', + 'retrace', + 'retract', + 'retractile', + 'retraction', + 'retractor', + 'retrad', + 'retral', + 'retread', + 'retreat', + 'retrench', + 'retrenchment', + 'retribution', + 'retributive', + 'retrieval', + 'retrieve', + 'retriever', + 'retroact', + 'retroaction', + 'retroactive', + 'retrocede', + 'retrochoir', + 'retroflex', + 'retroflexion', + 'retrogradation', + 'retrograde', + 'retrogress', + 'retrogression', + 'retrogressive', + 'retrorocket', + 'retrorse', + 'retrospect', + 'retrospection', + 'retrospective', + 'retroversion', + 'retrusion', + 'retsina', + 'return', + 'returnable', + 'returnee', + 'retuse', + 'reunion', + 'reunionist', + 'reunite', + 'rev', + 'revalue', + 'revamp', + 'revanche', + 'revanchism', + 'reveal', + 'revealment', + 'revegetate', + 'reveille', + 'revel', + 'revelation', + 'revelationist', + 'revelatory', + 'revelry', + 'revenant', + 'revenge', + 'revengeful', + 'revenue', + 'revenuer', + 'reverberate', + 'reverberation', + 'reverberator', + 'reverberatory', + 'revere', + 'reverence', + 'reverend', + 'reverent', + 'reverential', + 'reverie', + 'revers', + 'reversal', + 'reverse', + 'reversible', + 'reversion', + 'reversioner', + 'reverso', + 'revert', + 'revest', + 'revet', + 'revetment', + 'review', + 'reviewer', + 'revile', + 'revisal', + 'revise', + 'revision', + 'revisionism', + 'revisionist', + 'revisory', + 'revitalize', + 'revival', + 'revivalism', + 'revivalist', + 'revive', + 'revivify', + 'reviviscence', + 'revocable', + 'revocation', + 'revoice', + 'revoke', + 'revolt', + 'revolting', + 'revolute', + 'revolution', + 'revolutionary', + 'revolutionist', + 'revolutionize', + 'revolve', + 'revolver', + 'revolving', + 'revue', + 'revulsion', + 'revulsive', + 'reward', + 'rewarding', + 'rewire', + 'reword', + 'rework', + 'rewrite', + 'rhabdomancy', + 'rhachis', + 'rhamnaceous', + 'rhapsodic', + 'rhapsodist', + 'rhapsodize', + 'rhapsody', + 'rhatany', + 'rhea', + 'rhenium', + 'rheology', + 'rheometer', + 'rheostat', + 'rheotaxis', + 'rheotropism', + 'rhesus', + 'rhetor', + 'rhetoric', + 'rhetorical', + 'rhetorician', + 'rheum', + 'rheumatic', + 'rheumatism', + 'rheumatoid', + 'rheumy', + 'rhigolene', + 'rhinal', + 'rhinarium', + 'rhinencephalon', + 'rhinestone', + 'rhinitis', + 'rhino', + 'rhinoceros', + 'rhinology', + 'rhinoplasty', + 'rhinoscopy', + 'rhizobium', + 'rhizocarpous', + 'rhizogenic', + 'rhizoid', + 'rhizome', + 'rhizomorphous', + 'rhizopod', + 'rhizotomy', + 'rhodamine', + 'rhodic', + 'rhodium', + 'rhododendron', + 'rhodolite', + 'rhodonite', + 'rhomb', + 'rhombencephalon', + 'rhombic', + 'rhombohedral', + 'rhombohedron', + 'rhomboid', + 'rhombus', + 'rhonchus', + 'rhotacism', + 'rhubarb', + 'rhumb', + 'rhyme', + 'rhymester', + 'rhynchocephalian', + 'rhyolite', + 'rhythm', + 'rhythmical', + 'rhythmics', + 'rhythmist', + 'rhyton', + 'ria', + 'rial', + 'rialto', + 'riant', + 'riata', + 'rib', + 'ribald', + 'ribaldry', + 'riband', + 'ribband', + 'ribbing', + 'ribbon', + 'ribbonfish', + 'ribbonwood', + 'riboflavin', + 'ribonuclease', + 'ribose', + 'ribosome', + 'ribwort', + 'rice', + 'ricebird', + 'ricer', + 'ricercar', + 'ricercare', + 'rich', + 'riches', + 'richly', + 'rick', + 'rickets', + 'rickettsia', + 'rickety', + 'rickey', + 'rickrack', + 'ricochet', + 'ricotta', + 'rictus', + 'rid', + 'riddance', + 'ridden', + 'riddle', + 'ride', + 'rident', + 'rider', + 'ridge', + 'ridgeling', + 'ridgepole', + 'ridicule', + 'ridiculous', + 'riding', + 'ridotto', + 'riel', + 'rife', + 'riff', + 'riffle', + 'riffraff', + 'rifle', + 'rifleman', + 'riflery', + 'rifling', + 'rift', + 'rig', + 'rigadoon', + 'rigamarole', + 'rigatoni', + 'rigger', + 'rigging', + 'right', + 'righteous', + 'righteousness', + 'rightful', + 'rightism', + 'rightist', + 'rightly', + 'rightness', + 'rights', + 'rightward', + 'rightwards', + 'rigid', + 'rigidify', + 'rigmarole', + 'rigor', + 'rigorism', + 'rigorous', + 'rigsdaler', + 'rile', + 'rilievo', + 'rill', + 'rillet', + 'rim', + 'rime', + 'rimester', + 'rimose', + 'rimple', + 'rimrock', + 'rind', + 'rinderpest', + 'ring', + 'ringdove', + 'ringed', + 'ringent', + 'ringer', + 'ringhals', + 'ringleader', + 'ringlet', + 'ringmaster', + 'ringside', + 'ringster', + 'ringtail', + 'ringworm', + 'rink', + 'rinse', + 'riot', + 'riotous', + 'rip', + 'riparian', + 'ripe', + 'ripen', + 'ripieno', + 'riposte', + 'ripping', + 'ripple', + 'ripplet', + 'ripply', + 'ripsaw', + 'riptide', + 'rise', + 'riser', + 'rishi', + 'risibility', + 'risible', + 'rising', + 'risk', + 'risky', + 'risotto', + 'rissole', + 'ritardando', + 'rite', + 'ritenuto', + 'ritornello', + 'ritual', + 'ritualism', + 'ritualist', + 'ritualize', + 'ritzy', + 'rivage', + 'rival', + 'rivalry', + 'rive', + 'riven', + 'river', + 'riverhead', + 'riverine', + 'riverside', + 'rivet', + 'rivulet', + 'riyal', + 'rms', + 'roach', + 'road', + 'roadability', + 'roadbed', + 'roadblock', + 'roadhouse', + 'roadrunner', + 'roadside', + 'roadstead', + 'roadster', + 'roadway', + 'roadwork', + 'roam', + 'roan', + 'roar', + 'roaring', + 'roast', + 'roaster', + 'roasting', + 'rob', + 'robalo', + 'roband', + 'robber', + 'robbery', + 'robbin', + 'robe', + 'robin', + 'robinia', + 'roble', + 'robomb', + 'roborant', + 'robot', + 'robotize', + 'robust', + 'robustious', + 'roc', + 'rocaille', + 'rocambole', + 'rochet', + 'rock', + 'rockabilly', + 'rockaway', + 'rockbound', + 'rocker', + 'rockery', + 'rocket', + 'rocketeer', + 'rocketry', + 'rockfish', + 'rockling', + 'rockoon', + 'rockrose', + 'rockweed', + 'rocky', + 'rococo', + 'rod', + 'rode', + 'rodent', + 'rodenticide', + 'rodeo', + 'rodomontade', + 'roe', + 'roebuck', + 'roentgenogram', + 'roentgenograph', + 'roentgenology', + 'roentgenoscope', + 'roentgenotherapy', + 'rogation', + 'rogatory', + 'roger', + 'rogue', + 'roguery', + 'roguish', + 'roil', + 'roily', + 'roister', + 'role', + 'roll', + 'rollaway', + 'rollback', + 'roller', + 'rollick', + 'rollicking', + 'rolling', + 'rollmop', + 'rollway', + 'romaine', + 'roman', + 'romance', + 'romantic', + 'romanticism', + 'romanticist', + 'romanticize', + 'romp', + 'rompers', + 'rompish', + 'rondeau', + 'rondel', + 'rondelet', + 'rondelle', + 'rondo', + 'rondure', + 'roo', + 'rood', + 'roof', + 'roofer', + 'roofing', + 'rooftop', + 'rooftree', + 'rook', + 'rookery', + 'rookie', + 'rooky', + 'room', + 'roomer', + 'roomette', + 'roomful', + 'roommate', + 'roomy', + 'roorback', + 'roose', + 'roost', + 'rooster', + 'root', + 'rooted', + 'rootless', + 'rootlet', + 'rootstock', + 'ropable', + 'rope', + 'ropedancer', + 'ropeway', + 'roping', + 'ropy', + 'roque', + 'roquelaure', + 'rorqual', + 'rosaceous', + 'rosaniline', + 'rosarium', + 'rosary', + 'rose', + 'roseate', + 'rosebay', + 'rosebud', + 'rosefish', + 'rosemary', + 'roseola', + 'rosette', + 'rosewood', + 'rosily', + 'rosin', + 'rosinweed', + 'rostellum', + 'roster', + 'rostrum', + 'rosy', + 'rot', + 'rota', + 'rotary', + 'rotate', + 'rotation', + 'rotative', + 'rotator', + 'rotatory', + 'rote', + 'rotenone', + 'rotgut', + 'rotifer', + 'rotl', + 'rotogravure', + 'rotor', + 'rotten', + 'rottenstone', + 'rotter', + 'rotund', + 'rotunda', + 'roturier', + 'rouble', + 'roue', + 'rouge', + 'rough', + 'roughage', + 'roughcast', + 'roughen', + 'roughhew', + 'roughhouse', + 'roughish', + 'roughneck', + 'roughrider', + 'roughshod', + 'roulade', + 'rouleau', + 'roulette', + 'rounce', + 'round', + 'roundabout', + 'rounded', + 'roundel', + 'roundelay', + 'rounder', + 'rounders', + 'roundhouse', + 'rounding', + 'roundish', + 'roundlet', + 'roundly', + 'roundsman', + 'roundup', + 'roundworm', + 'roup', + 'rouse', + 'rousing', + 'roustabout', + 'rout', + 'route', + 'router', + 'routine', + 'routinize', + 'roux', + 'rove', + 'rover', + 'roving', + 'row', + 'rowan', + 'rowboat', + 'rowdy', + 'rowdyish', + 'rowdyism', + 'rowel', + 'rowlock', + 'royal', + 'royalist', + 'royalty', + 'rub', + 'rubato', + 'rubber', + 'rubberize', + 'rubberneck', + 'rubbery', + 'rubbing', + 'rubbish', + 'rubble', + 'rubdown', + 'rube', + 'rubefaction', + 'rubella', + 'rubellite', + 'rubeola', + 'rubescent', + 'rubiaceous', + 'rubicund', + 'rubidium', + 'rubiginous', + 'rubious', + 'ruble', + 'rubric', + 'rubricate', + 'rubrician', + 'rubstone', + 'ruby', + 'ruche', + 'ruching', + 'ruck', + 'rucksack', + 'ruckus', + 'ruction', + 'rudbeckia', + 'rudd', + 'rudder', + 'rudderhead', + 'rudderpost', + 'ruddle', + 'ruddock', + 'ruddy', + 'rude', + 'ruderal', + 'rudiment', + 'rudimentary', + 'rue', + 'rueful', + 'rufescent', + 'ruff', + 'ruffian', + 'ruffianism', + 'ruffle', + 'ruffled', + 'rufous', + 'rug', + 'rugged', + 'rugger', + 'rugging', + 'rugose', + 'ruin', + 'ruination', + 'ruinous', + 'rule', + 'ruler', + 'ruling', + 'rum', + 'rumal', + 'rumba', + 'rumble', + 'rumen', + 'ruminant', + 'ruminate', + 'rummage', + 'rummer', + 'rummy', + 'rumor', + 'rumormonger', + 'rump', + 'rumple', + 'rumpus', + 'rumrunner', + 'run', + 'runabout', + 'runagate', + 'runaway', + 'rundle', + 'rundlet', + 'rundown', + 'rune', + 'runesmith', + 'rung', + 'runic', + 'runlet', + 'runnel', + 'runner', + 'running', + 'runny', + 'runoff', + 'runt', + 'runty', + 'runway', + 'rupee', + 'rupiah', + 'rupture', + 'rural', + 'ruralize', + 'ruse', + 'rush', + 'rushing', + 'rushy', + 'rusk', + 'russet', + 'rust', + 'rustic', + 'rusticate', + 'rustication', + 'rustle', + 'rustler', + 'rustproof', + 'rusty', + 'rut', + 'rutabaga', + 'rutaceous', + 'ruth', + 'ruthenic', + 'ruthenious', + 'ruthenium', + 'rutherfordium', + 'ruthful', + 'ruthless', + 'rutilant', + 'rutile', + 'ruttish', + 'rutty', + 'rye', + 's', + 'sabadilla', + 'sabayon', + 'sabbatical', + 'saber', + 'sabin', + 'sable', + 'sabotage', + 'saboteur', + 'sabra', + 'sabre', + 'sabulous', + 'sac', + 'sacaton', + 'saccharase', + 'saccharate', + 'saccharide', + 'sacchariferous', + 'saccharify', + 'saccharin', + 'saccharine', + 'saccharoid', + 'saccharometer', + 'saccharose', + 'saccular', + 'sacculate', + 'saccule', + 'sacculus', + 'sacellum', + 'sacerdotal', + 'sacerdotalism', + 'sachem', + 'sachet', + 'sack', + 'sackbut', + 'sackcloth', + 'sacker', + 'sacking', + 'sacral', + 'sacrament', + 'sacramental', + 'sacramentalism', + 'sacramentalist', + 'sacrarium', + 'sacred', + 'sacrifice', + 'sacrificial', + 'sacrilege', + 'sacrilegious', + 'sacring', + 'sacristan', + 'sacristy', + 'sacroiliac', + 'sacrosanct', + 'sacrum', + 'sad', + 'sadden', + 'saddle', + 'saddleback', + 'saddlebag', + 'saddlebow', + 'saddlecloth', + 'saddler', + 'saddlery', + 'saddletree', + 'sadiron', + 'sadism', + 'sadness', + 'sadomasochism', + 'safari', + 'safe', + 'safeguard', + 'safekeeping', + 'safelight', + 'safety', + 'saffron', + 'safranine', + 'sag', + 'saga', + 'sagacious', + 'sagacity', + 'sagamore', + 'sage', + 'sagebrush', + 'sagittal', + 'sagittate', + 'sago', + 'saguaro', + 'sahib', + 'said', + 'saiga', + 'sail', + 'sailboat', + 'sailcloth', + 'sailer', + 'sailfish', + 'sailing', + 'sailmaker', + 'sailor', + 'sailplane', + 'sain', + 'sainfoin', + 'saint', + 'sainted', + 'sainthood', + 'saintly', + 'saith', + 'sake', + 'saker', + 'saki', + 'salaam', + 'salable', + 'salacious', + 'salad', + 'salade', + 'salamander', + 'salami', + 'salaried', + 'salary', + 'sale', + 'saleable', + 'salep', + 'saleratus', + 'sales', + 'salesclerk', + 'salesgirl', + 'salesman', + 'salesmanship', + 'salespeople', + 'salesperson', + 'salesroom', + 'saleswoman', + 'salicaceous', + 'salicin', + 'salicylate', + 'salience', + 'salient', + 'salientian', + 'saliferous', + 'salify', + 'salimeter', + 'salina', + 'saline', + 'salinometer', + 'saliva', + 'salivate', + 'salivation', + 'sallet', + 'sallow', + 'sally', + 'salmagundi', + 'salmi', + 'salmon', + 'salmonberry', + 'salmonella', + 'salmonoid', + 'salol', + 'salon', + 'saloon', + 'saloop', + 'salpa', + 'salpiglossis', + 'salpingectomy', + 'salpingitis', + 'salpingotomy', + 'salpinx', + 'salsify', + 'salt', + 'saltant', + 'saltarello', + 'saltation', + 'saltatorial', + 'saltatory', + 'saltcellar', + 'salted', + 'salter', + 'saltern', + 'saltigrade', + 'saltine', + 'saltire', + 'saltish', + 'saltpeter', + 'salts', + 'saltus', + 'saltwater', + 'saltworks', + 'saltwort', + 'salty', + 'salubrious', + 'salutary', + 'salutation', + 'salutatory', + 'salute', + 'salvage', + 'salvation', + 'salve', + 'salver', + 'salverform', + 'salvia', + 'salvo', + 'samadhi', + 'samarium', + 'samarskite', + 'samba', + 'sambar', + 'sambo', + 'same', + 'samekh', + 'sameness', + 'samiel', + 'samisen', + 'samite', + 'samovar', + 'sampan', + 'samphire', + 'sample', + 'sampler', + 'sampling', + 'samsara', + 'samurai', + 'sanative', + 'sanatorium', + 'sanatory', + 'sanbenito', + 'sanctified', + 'sanctify', + 'sanctimonious', + 'sanctimony', + 'sanction', + 'sanctitude', + 'sanctity', + 'sanctuary', + 'sanctum', + 'sand', + 'sandal', + 'sandalwood', + 'sandarac', + 'sandbag', + 'sandbank', + 'sandblast', + 'sandbox', + 'sander', + 'sanderling', + 'sandfly', + 'sandglass', + 'sandhi', + 'sandhog', + 'sandman', + 'sandpaper', + 'sandpiper', + 'sandpit', + 'sandstone', + 'sandstorm', + 'sandwich', + 'sandy', + 'sane', + 'sang', + 'sangria', + 'sanguinaria', + 'sanguinary', + 'sanguine', + 'sanguineous', + 'sanguinolent', + 'sanies', + 'sanious', + 'sanitarian', + 'sanitarium', + 'sanitary', + 'sanitation', + 'sanitize', + 'sanity', + 'sanjak', + 'sank', + 'sannyasi', + 'sans', + 'santalaceous', + 'santonica', + 'santonin', + 'sap', + 'sapajou', + 'sapanwood', + 'sapele', + 'saphead', + 'sapheaded', + 'saphena', + 'sapid', + 'sapient', + 'sapiential', + 'sapindaceous', + 'sapless', + 'sapling', + 'sapodilla', + 'saponaceous', + 'saponify', + 'saponin', + 'sapor', + 'saporific', + 'saporous', + 'sapota', + 'sapotaceous', + 'sappanwood', + 'sapper', + 'sapphire', + 'sapphirine', + 'sapphism', + 'sappy', + 'saprogenic', + 'saprolite', + 'saprophagous', + 'saprophyte', + 'sapsago', + 'sapsucker', + 'sapwood', + 'saraband', + 'saran', + 'sarangi', + 'sarcasm', + 'sarcastic', + 'sarcenet', + 'sarcocarp', + 'sarcoid', + 'sarcoma', + 'sarcomatosis', + 'sarcophagus', + 'sarcous', + 'sard', + 'sardine', + 'sardius', + 'sardonic', + 'sardonyx', + 'sargasso', + 'sargassum', + 'sari', + 'sarmentose', + 'sarmentum', + 'sarong', + 'saros', + 'sarracenia', + 'sarraceniaceous', + 'sarrusophone', + 'sarsaparilla', + 'sarsen', + 'sarsenet', + 'sartor', + 'sartorial', + 'sartorius', + 'sash', + 'sashay', + 'sasin', + 'saskatoon', + 'sass', + 'sassaby', + 'sassafras', + 'sassy', + 'sastruga', + 'sat', + 'satang', + 'satanic', + 'satchel', + 'sate', + 'sateen', + 'satellite', + 'satem', + 'satiable', + 'satiate', + 'satiated', + 'satiety', + 'satin', + 'satinet', + 'satinwood', + 'satiny', + 'satire', + 'satirical', + 'satirist', + 'satirize', + 'satisfaction', + 'satisfactory', + 'satisfied', + 'satisfy', + 'satori', + 'satrap', + 'saturable', + 'saturant', + 'saturate', + 'saturated', + 'saturation', + 'saturniid', + 'saturnine', + 'satyr', + 'satyriasis', + 'sauce', + 'saucepan', + 'saucer', + 'saucy', + 'sauerbraten', + 'sauerkraut', + 'sauger', + 'sauna', + 'saunter', + 'saurel', + 'saurian', + 'saurischian', + 'sauropod', + 'saury', + 'sausage', + 'sauterne', + 'savage', + 'savagery', + 'savagism', + 'savanna', + 'savant', + 'savarin', + 'savate', + 'save', + 'saveloy', + 'saving', + 'savior', + 'saviour', + 'savor', + 'savory', + 'savour', + 'savoury', + 'savoy', + 'saw', + 'sawbuck', + 'sawdust', + 'sawfish', + 'sawfly', + 'sawhorse', + 'sawmill', + 'sawn', + 'sawyer', + 'sax', + 'saxhorn', + 'saxophone', + 'saxtuba', + 'say', + 'saying', + 'sayyid', + 'scab', + 'scabbard', + 'scabble', + 'scabby', + 'scabies', + 'scabious', + 'scabrous', + 'scad', + 'scaffold', + 'scaffolding', + 'scag', + 'scagliola', + 'scalable', + 'scalade', + 'scalage', + 'scalar', + 'scalariform', + 'scalawag', + 'scald', + 'scale', + 'scaleboard', + 'scalene', + 'scalenus', + 'scaler', + 'scallion', + 'scallop', + 'scalp', + 'scalpel', + 'scalping', + 'scaly', + 'scammony', + 'scamp', + 'scamper', + 'scampi', + 'scan', + 'scandal', + 'scandalize', + 'scandalmonger', + 'scandent', + 'scandic', + 'scandium', + 'scanner', + 'scansion', + 'scansorial', + 'scant', + 'scanties', + 'scantling', + 'scanty', + 'scape', + 'scapegoat', + 'scapegrace', + 'scaphoid', + 'scapolite', + 'scapula', + 'scapular', + 'scar', + 'scarab', + 'scarabaeid', + 'scarabaeoid', + 'scarabaeus', + 'scarce', + 'scarcely', + 'scarcity', + 'scare', + 'scarecrow', + 'scaremonger', + 'scarf', + 'scarfskin', + 'scarification', + 'scarificator', + 'scarify', + 'scarlatina', + 'scarlet', + 'scarp', + 'scarper', + 'scary', + 'scat', + 'scathe', + 'scathing', + 'scatology', + 'scatter', + 'scatterbrain', + 'scattering', + 'scauper', + 'scavenge', + 'scavenger', + 'scenario', + 'scenarist', + 'scend', + 'scene', + 'scenery', + 'scenic', + 'scenography', + 'scent', + 'scepter', + 'sceptic', + 'sceptre', + 'schappe', + 'schedule', + 'scheelite', + 'schema', + 'schematic', + 'schematism', + 'schematize', + 'scheme', + 'scheming', + 'scherzando', + 'scherzo', + 'schiller', + 'schilling', + 'schipperke', + 'schism', + 'schismatic', + 'schist', + 'schistosome', + 'schistosomiasis', + 'schizo', + 'schizogenesis', + 'schizogony', + 'schizoid', + 'schizomycete', + 'schizont', + 'schizophrenia', + 'schizophyceous', + 'schizopod', + 'schizothymia', + 'schlemiel', + 'schlep', + 'schlieren', + 'schlimazel', + 'schlock', + 'schmaltz', + 'schmaltzy', + 'schmo', + 'schmooze', + 'schmuck', + 'schnapps', + 'schnauzer', + 'schnitzel', + 'schnook', + 'schnorkle', + 'schnorrer', + 'schnozzle', + 'scholar', + 'scholarship', + 'scholastic', + 'scholasticate', + 'scholasticism', + 'scholiast', + 'scholium', + 'school', + 'schoolbag', + 'schoolbook', + 'schoolboy', + 'schoolfellow', + 'schoolgirl', + 'schoolhouse', + 'schooling', + 'schoolman', + 'schoolmarm', + 'schoolmaster', + 'schoolmate', + 'schoolmistress', + 'schoolroom', + 'schoolteacher', + 'schooner', + 'schorl', + 'schottische', + 'schuss', + 'schwa', + 'sciamachy', + 'sciatic', + 'sciatica', + 'science', + 'sciential', + 'scientific', + 'scientism', + 'scientist', + 'scientistic', + 'scilicet', + 'scilla', + 'scimitar', + 'scincoid', + 'scintilla', + 'scintillant', + 'scintillate', + 'scintillation', + 'scintillator', + 'scintillometer', + 'sciolism', + 'sciomachy', + 'sciomancy', + 'scion', + 'scirrhous', + 'scirrhus', + 'scissel', + 'scissile', + 'scission', + 'scissor', + 'scissors', + 'scissure', + 'sciurine', + 'sciuroid', + 'sclaff', + 'sclera', + 'sclerenchyma', + 'sclerite', + 'scleritis', + 'scleroderma', + 'sclerodermatous', + 'scleroma', + 'sclerometer', + 'sclerophyll', + 'scleroprotein', + 'sclerosed', + 'sclerosis', + 'sclerotic', + 'sclerotomy', + 'sclerous', + 'scoff', + 'scofflaw', + 'scold', + 'scolecite', + 'scolex', + 'scoliosis', + 'scolopendrid', + 'sconce', + 'scone', + 'scoop', + 'scoot', + 'scooter', + 'scop', + 'scope', + 'scopolamine', + 'scopoline', + 'scopophilia', + 'scopula', + 'scorbutic', + 'scorch', + 'scorcher', + 'score', + 'scoreboard', + 'scorecard', + 'scorekeeper', + 'scoria', + 'scorify', + 'scorn', + 'scornful', + 'scorpaenid', + 'scorpaenoid', + 'scorper', + 'scorpion', + 'scot', + 'scotch', + 'scoter', + 'scotia', + 'scotopia', + 'scoundrel', + 'scoundrelly', + 'scour', + 'scourge', + 'scouring', + 'scourings', + 'scout', + 'scouting', + 'scoutmaster', + 'scow', + 'scowl', + 'scrabble', + 'scrag', + 'scraggly', + 'scraggy', + 'scram', + 'scramble', + 'scrambler', + 'scrannel', + 'scrap', + 'scrapbook', + 'scrape', + 'scraper', + 'scraperboard', + 'scrapple', + 'scrappy', + 'scratch', + 'scratchboard', + 'scratches', + 'scratchy', + 'scrawl', + 'scrawly', + 'scrawny', + 'screak', + 'scream', + 'screamer', + 'scree', + 'screech', + 'screeching', + 'screed', + 'screen', + 'screening', + 'screenplay', + 'screw', + 'screwball', + 'screwdriver', + 'screwed', + 'screwworm', + 'screwy', + 'scribble', + 'scribbler', + 'scribe', + 'scriber', + 'scrim', + 'scrimmage', + 'scrimp', + 'scrimpy', + 'scrimshaw', + 'scrip', + 'script', + 'scriptorium', + 'scriptural', + 'scripture', + 'scriptwriter', + 'scrivener', + 'scrobiculate', + 'scrod', + 'scrofula', + 'scrofulous', + 'scroll', + 'scroop', + 'scrophulariaceous', + 'scrotum', + 'scrouge', + 'scrounge', + 'scrub', + 'scrubber', + 'scrubby', + 'scrubland', + 'scruff', + 'scruffy', + 'scrummage', + 'scrumptious', + 'scrunch', + 'scruple', + 'scrupulous', + 'scrutable', + 'scrutator', + 'scrutineer', + 'scrutinize', + 'scrutiny', + 'scuba', + 'scud', + 'scudo', + 'scuff', + 'scuffle', + 'scull', + 'scullery', + 'scullion', + 'sculpin', + 'sculpsit', + 'sculpt', + 'sculptor', + 'sculptress', + 'sculpture', + 'sculpturesque', + 'scum', + 'scumble', + 'scummy', + 'scup', + 'scupper', + 'scuppernong', + 'scurf', + 'scurrile', + 'scurrility', + 'scurrilous', + 'scurry', + 'scurvy', + 'scut', + 'scuta', + 'scutage', + 'scutate', + 'scutch', + 'scutcheon', + 'scute', + 'scutellation', + 'scutiform', + 'scutter', + 'scuttle', + 'scuttlebutt', + 'scutum', + 'scyphate', + 'scyphozoan', + 'scyphus', + 'scythe', + 'sea', + 'seaboard', + 'seaborne', + 'seacoast', + 'seacock', + 'seadog', + 'seafarer', + 'seafaring', + 'seafood', + 'seagirt', + 'seagoing', + 'seal', + 'sealed', + 'sealer', + 'sealskin', + 'seam', + 'seaman', + 'seamanlike', + 'seamanship', + 'seamark', + 'seamount', + 'seamstress', + 'seamy', + 'seaplane', + 'seaport', + 'seaquake', + 'sear', + 'search', + 'searching', + 'searchlight', + 'seascape', + 'seashore', + 'seasick', + 'seasickness', + 'seaside', + 'season', + 'seasonable', + 'seasonal', + 'seasoning', + 'seat', + 'seating', + 'seaward', + 'seawards', + 'seaware', + 'seaway', + 'seaweed', + 'seaworthy', + 'sebaceous', + 'sebiferous', + 'sebum', + 'sec', + 'secant', + 'secateurs', + 'secco', + 'secede', + 'secern', + 'secession', + 'secessionist', + 'sech', + 'seclude', + 'secluded', + 'seclusion', + 'seclusive', + 'second', + 'secondary', + 'secondhand', + 'secondly', + 'secrecy', + 'secret', + 'secretarial', + 'secretariat', + 'secretary', + 'secrete', + 'secretin', + 'secretion', + 'secretive', + 'secretory', + 'sect', + 'sectarian', + 'sectarianism', + 'sectarianize', + 'sectary', + 'section', + 'sectional', + 'sectionalism', + 'sectionalize', + 'sector', + 'sectorial', + 'secular', + 'secularism', + 'secularity', + 'secund', + 'secundine', + 'secundines', + 'secure', + 'security', + 'sedan', + 'sedate', + 'sedation', + 'sedative', + 'sedentary', + 'sedge', + 'sediment', + 'sedimentary', + 'sedimentation', + 'sedimentology', + 'sedition', + 'seditious', + 'seduce', + 'seducer', + 'seduction', + 'seductive', + 'seductress', + 'sedulity', + 'sedulous', + 'sedum', + 'see', + 'seed', + 'seedbed', + 'seedcase', + 'seeder', + 'seedling', + 'seedtime', + 'seedy', + 'seeing', + 'seek', + 'seeker', + 'seel', + 'seem', + 'seeming', + 'seemly', + 'seen', + 'seep', + 'seepage', + 'seer', + 'seeress', + 'seersucker', + 'seesaw', + 'seethe', + 'segment', + 'segmental', + 'segmentation', + 'segno', + 'segregate', + 'segregation', + 'segregationist', + 'seguidilla', + 'seicento', + 'seigneur', + 'seigneury', + 'seignior', + 'seigniorage', + 'seigniory', + 'seine', + 'seise', + 'seisin', + 'seism', + 'seismic', + 'seismism', + 'seismograph', + 'seismography', + 'seismology', + 'seismoscope', + 'seize', + 'seizing', + 'seizure', + 'sejant', + 'selachian', + 'selaginella', + 'selah', + 'seldom', + 'select', + 'selectee', + 'selection', + 'selective', + 'selectivity', + 'selectman', + 'selector', + 'selenate', + 'selenious', + 'selenite', + 'selenium', + 'selenodont', + 'selenography', + 'self', + 'selfheal', + 'selfhood', + 'selfish', + 'selfless', + 'selfness', + 'selfsame', + 'sell', + 'seller', + 'selsyn', + 'selvage', + 'selves', + 'semanteme', + 'semantic', + 'semantics', + 'semaphore', + 'semasiology', + 'sematic', + 'semblable', + 'semblance', + 'semeiology', + 'sememe', + 'semen', + 'semester', + 'semi', + 'semiannual', + 'semiaquatic', + 'semiautomatic', + 'semibreve', + 'semicentennial', + 'semicircle', + 'semicolon', + 'semiconductor', + 'semiconscious', + 'semidiurnal', + 'semidome', + 'semifinal', + 'semifinalist', + 'semifluid', + 'semiliquid', + 'semiliterate', + 'semilunar', + 'semimonthly', + 'seminal', + 'seminar', + 'seminarian', + 'seminary', + 'semination', + 'semiology', + 'semiotic', + 'semiotics', + 'semipalmate', + 'semipermeable', + 'semiporcelain', + 'semipostal', + 'semipro', + 'semiprofessional', + 'semiquaver', + 'semirigid', + 'semiskilled', + 'semitone', + 'semitrailer', + 'semitropical', + 'semivitreous', + 'semivowel', + 'semiweekly', + 'semiyearly', + 'semolina', + 'sempiternal', + 'sempstress', + 'sen', + 'senarmontite', + 'senary', + 'senate', + 'senator', + 'senatorial', + 'send', + 'sendal', + 'sender', + 'senega', + 'senescent', + 'seneschal', + 'senhor', + 'senhorita', + 'senile', + 'senility', + 'senior', + 'seniority', + 'senna', + 'sennet', + 'sennight', + 'sennit', + 'sensate', + 'sensation', + 'sensational', + 'sensationalism', + 'sense', + 'senseless', + 'sensibility', + 'sensible', + 'sensillum', + 'sensitive', + 'sensitivity', + 'sensitize', + 'sensitometer', + 'sensor', + 'sensorimotor', + 'sensorium', + 'sensory', + 'sensual', + 'sensualism', + 'sensualist', + 'sensuality', + 'sensuous', + 'sent', + 'sentence', + 'sententious', + 'sentience', + 'sentient', + 'sentiment', + 'sentimental', + 'sentimentalism', + 'sentimentality', + 'sentimentalize', + 'sentinel', + 'sentry', + 'sepal', + 'sepaloid', + 'separable', + 'separate', + 'separates', + 'separation', + 'separatist', + 'separative', + 'separator', + 'separatrix', + 'sepia', + 'sepoy', + 'seppuku', + 'sepsis', + 'sept', + 'septa', + 'septal', + 'septarium', + 'septate', + 'septavalent', + 'septempartite', + 'septenary', + 'septennial', + 'septet', + 'septic', + 'septicemia', + 'septicidal', + 'septilateral', + 'septillion', + 'septimal', + 'septime', + 'septivalent', + 'septuagenarian', + 'septum', + 'septuor', + 'septuple', + 'septuplet', + 'septuplicate', + 'sepulcher', + 'sepulchral', + 'sepulchre', + 'sepulture', + 'sequacious', + 'sequel', + 'sequela', + 'sequence', + 'sequent', + 'sequential', + 'sequester', + 'sequestered', + 'sequestrate', + 'sequestration', + 'sequin', + 'sequoia', + 'ser', + 'sera', + 'seraglio', + 'serai', + 'seraph', + 'seraphic', + 'serdab', + 'sere', + 'serena', + 'serenade', + 'serenata', + 'serendipity', + 'serene', + 'serenity', + 'serf', + 'serge', + 'sergeant', + 'serial', + 'serialize', + 'seriate', + 'seriatim', + 'sericeous', + 'sericin', + 'seriema', + 'series', + 'serif', + 'serigraph', + 'serin', + 'serine', + 'seringa', + 'seriocomic', + 'serious', + 'serjeant', + 'sermon', + 'sermonize', + 'serology', + 'serosa', + 'serotherapy', + 'serotine', + 'serotonin', + 'serous', + 'serow', + 'serpent', + 'serpentiform', + 'serpentine', + 'serpigo', + 'serranid', + 'serrate', + 'serrated', + 'serration', + 'serried', + 'serriform', + 'serrulate', + 'serrulation', + 'serum', + 'serval', + 'servant', + 'serve', + 'server', + 'service', + 'serviceable', + 'serviceberry', + 'serviceman', + 'serviette', + 'servile', + 'servility', + 'serving', + 'servitor', + 'servitude', + 'servo', + 'servomechanical', + 'servomechanism', + 'servomotor', + 'sesame', + 'sesquialtera', + 'sesquicarbonate', + 'sesquicentennial', + 'sesquioxide', + 'sesquipedalian', + 'sesquiplane', + 'sessile', + 'session', + 'sessions', + 'sesterce', + 'sestertium', + 'sestet', + 'sestina', + 'set', + 'seta', + 'setaceous', + 'setback', + 'setiform', + 'setose', + 'setscrew', + 'sett', + 'settee', + 'setter', + 'setting', + 'settle', + 'settlement', + 'settler', + 'settling', + 'settlings', + 'setula', + 'setup', + 'seven', + 'sevenfold', + 'seventeen', + 'seventeenth', + 'seventh', + 'seventieth', + 'seventy', + 'sever', + 'severable', + 'several', + 'severally', + 'severalty', + 'severance', + 'severe', + 'severity', + 'sew', + 'sewage', + 'sewan', + 'sewellel', + 'sewer', + 'sewerage', + 'sewing', + 'sewn', + 'sex', + 'sexagenarian', + 'sexagenary', + 'sexagesimal', + 'sexcentenary', + 'sexdecillion', + 'sexed', + 'sexennial', + 'sexism', + 'sexist', + 'sexivalent', + 'sexless', + 'sexology', + 'sexpartite', + 'sexpot', + 'sext', + 'sextain', + 'sextan', + 'sextant', + 'sextet', + 'sextillion', + 'sextodecimo', + 'sexton', + 'sextuple', + 'sextuplet', + 'sextuplicate', + 'sexual', + 'sexuality', + 'sexy', + 'sf', + 'sferics', + 'sfumato', + 'sgraffito', + 'shabby', + 'shack', + 'shackle', + 'shad', + 'shadberry', + 'shadbush', + 'shadchan', + 'shaddock', + 'shade', + 'shading', + 'shadoof', + 'shadow', + 'shadowgraph', + 'shadowy', + 'shaduf', + 'shady', + 'shaft', + 'shafting', + 'shag', + 'shagbark', + 'shaggy', + 'shagreen', + 'shah', + 'shake', + 'shakedown', + 'shaker', + 'shaking', + 'shako', + 'shaky', + 'shale', + 'shall', + 'shalloon', + 'shallop', + 'shallot', + 'shallow', + 'shalt', + 'sham', + 'shaman', + 'shamanism', + 'shamble', + 'shambles', + 'shame', + 'shamefaced', + 'shameful', + 'shameless', + 'shammer', + 'shammy', + 'shampoo', + 'shamrock', + 'shamus', + 'shandrydan', + 'shandy', + 'shanghai', + 'shank', + 'shanny', + 'shantung', + 'shanty', + 'shape', + 'shaped', + 'shapeless', + 'shapely', + 'shard', + 'share', + 'sharecrop', + 'sharecropper', + 'shareholder', + 'shark', + 'sharkskin', + 'sharp', + 'sharpen', + 'sharper', + 'sharpie', + 'sharpshooter', + 'shashlik', + 'shastra', + 'shatter', + 'shatterproof', + 'shave', + 'shaveling', + 'shaven', + 'shaver', + 'shaving', + 'shaw', + 'shawl', + 'shawm', + 'shay', + 'she', + 'sheaf', + 'shear', + 'sheared', + 'shears', + 'shearwater', + 'sheatfish', + 'sheath', + 'sheathbill', + 'sheathe', + 'sheathing', + 'sheave', + 'sheaves', + 'shebang', + 'shebeen', + 'shed', + 'sheen', + 'sheeny', + 'sheep', + 'sheepcote', + 'sheepdog', + 'sheepfold', + 'sheepherder', + 'sheepish', + 'sheepshank', + 'sheepshead', + 'sheepshearing', + 'sheepskin', + 'sheepwalk', + 'sheer', + 'sheerlegs', + 'sheers', + 'sheet', + 'sheeting', + 'sheik', + 'sheikdom', + 'sheikh', + 'shekel', + 'shelduck', + 'shelf', + 'shell', + 'shellac', + 'shellacking', + 'shellback', + 'shellbark', + 'shelled', + 'shellfire', + 'shellfish', + 'shellproof', + 'shelter', + 'shelty', + 'shelve', + 'shelves', + 'shelving', + 'shend', + 'shepherd', + 'sherbet', + 'sherd', + 'sherif', + 'sheriff', + 'sherry', + 'sheugh', + 'shew', + 'shibboleth', + 'shied', + 'shield', + 'shier', + 'shiest', + 'shift', + 'shiftless', + 'shifty', + 'shigella', + 'shikari', + 'shiksa', + 'shill', + 'shillelagh', + 'shilling', + 'shimmer', + 'shimmery', + 'shimmy', + 'shin', + 'shinbone', + 'shindig', + 'shine', + 'shiner', + 'shingle', + 'shingles', + 'shingly', + 'shinleaf', + 'shinny', + 'shiny', + 'ship', + 'shipboard', + 'shipentine', + 'shipload', + 'shipman', + 'shipmaster', + 'shipmate', + 'shipment', + 'shipowner', + 'shippen', + 'shipper', + 'shipping', + 'shipshape', + 'shipway', + 'shipworm', + 'shipwreck', + 'shipwright', + 'shipyard', + 'shire', + 'shirk', + 'shirker', + 'shirr', + 'shirring', + 'shirt', + 'shirting', + 'shirtmaker', + 'shirtwaist', + 'shirty', + 'shit', + 'shithead', + 'shitty', + 'shiv', + 'shivaree', + 'shive', + 'shiver', + 'shivery', + 'shoal', + 'shoat', + 'shock', + 'shocker', + 'shockheaded', + 'shocking', + 'shockproof', + 'shod', + 'shoddy', + 'shoe', + 'shoebill', + 'shoeblack', + 'shoelace', + 'shoemaker', + 'shoer', + 'shoeshine', + 'shoestring', + 'shofar', + 'shogun', + 'shogunate', + 'shone', + 'shoo', + 'shook', + 'shool', + 'shoon', + 'shoot', + 'shooter', + 'shop', + 'shophar', + 'shopkeeper', + 'shoplifter', + 'shopper', + 'shopping', + 'shopwindow', + 'shopworn', + 'shoran', + 'shore', + 'shoreless', + 'shoreline', + 'shoreward', + 'shoring', + 'shorn', + 'short', + 'shortage', + 'shortbread', + 'shortcake', + 'shortcoming', + 'shortcut', + 'shorten', + 'shortening', + 'shortfall', + 'shorthand', + 'shorthanded', + 'shorthorn', + 'shortie', + 'shortly', + 'shorts', + 'shortsighted', + 'shortstop', + 'shortwave', + 'shot', + 'shote', + 'shotgun', + 'shotten', + 'should', + 'shoulder', + 'shouldst', + 'shout', + 'shove', + 'shovel', + 'shovelboard', + 'shoveler', + 'shovelhead', + 'shovelnose', + 'show', + 'showboat', + 'showbread', + 'showcase', + 'showdown', + 'shower', + 'showery', + 'showily', + 'showiness', + 'showing', + 'showman', + 'showmanship', + 'shown', + 'showpiece', + 'showplace', + 'showroom', + 'showy', + 'shrapnel', + 'shred', + 'shredding', + 'shrew', + 'shrewd', + 'shrewish', + 'shrewmouse', + 'shriek', + 'shrieval', + 'shrievalty', + 'shrieve', + 'shrift', + 'shrike', + 'shrill', + 'shrimp', + 'shrine', + 'shrink', + 'shrinkage', + 'shrive', + 'shrivel', + 'shroff', + 'shroud', + 'shrove', + 'shrub', + 'shrubbery', + 'shrubby', + 'shrug', + 'shrunk', + 'shrunken', + 'shuck', + 'shudder', + 'shuddering', + 'shuffle', + 'shuffleboard', + 'shul', + 'shun', + 'shunt', + 'shush', + 'shut', + 'shutdown', + 'shutout', + 'shutter', + 'shuttering', + 'shuttle', + 'shuttlecock', + 'shwa', + 'shy', + 'shyster', + 'si', + 'sialagogue', + 'sialoid', + 'siamang', + 'sib', + 'sibilant', + 'sibilate', + 'sibling', + 'sibship', + 'sibyl', + 'sic', + 'siccative', + 'sick', + 'sicken', + 'sickener', + 'sickening', + 'sickle', + 'sicklebill', + 'sickly', + 'sickness', + 'sickroom', + 'siddur', + 'side', + 'sideband', + 'sideboard', + 'sideburns', + 'sidecar', + 'sidekick', + 'sidelight', + 'sideline', + 'sideling', + 'sidelong', + 'sideman', + 'sidereal', + 'siderite', + 'siderolite', + 'siderosis', + 'siderostat', + 'sidesaddle', + 'sideshow', + 'sideslip', + 'sidesman', + 'sidestep', + 'sidestroke', + 'sideswipe', + 'sidetrack', + 'sidewalk', + 'sideward', + 'sideway', + 'sideways', + 'sidewheel', + 'sidewinder', + 'siding', + 'sidle', + 'siege', + 'siemens', + 'sienna', + 'sierra', + 'siesta', + 'sieve', + 'sift', + 'siftings', + 'sigh', + 'sight', + 'sighted', + 'sightless', + 'sightly', + 'sigil', + 'siglos', + 'sigma', + 'sigmatism', + 'sigmoid', + 'sign', + 'signal', + 'signalize', + 'signally', + 'signalman', + 'signalment', + 'signatory', + 'signature', + 'signboard', + 'signet', + 'significance', + 'significancy', + 'significant', + 'signification', + 'significative', + 'significs', + 'signify', + 'signor', + 'signora', + 'signore', + 'signorina', + 'signorino', + 'signory', + 'signpost', + 'sika', + 'sike', + 'silage', + 'silence', + 'silencer', + 'silent', + 'silesia', + 'silhouette', + 'silica', + 'silicate', + 'siliceous', + 'silicic', + 'silicify', + 'silicious', + 'silicium', + 'silicle', + 'silicon', + 'silicone', + 'silicosis', + 'siliculose', + 'siliqua', + 'silique', + 'silk', + 'silkaline', + 'silken', + 'silkweed', + 'silkworm', + 'silky', + 'sill', + 'sillabub', + 'sillimanite', + 'silly', + 'silo', + 'siloxane', + 'silt', + 'siltstone', + 'silurid', + 'silva', + 'silvan', + 'silver', + 'silverfish', + 'silvern', + 'silverpoint', + 'silverside', + 'silversmith', + 'silverware', + 'silverweed', + 'silvery', + 'silviculture', + 'sima', + 'simar', + 'simarouba', + 'simaroubaceous', + 'simba', + 'simian', + 'similar', + 'similarity', + 'simile', + 'similitude', + 'simitar', + 'simmer', + 'simoniac', + 'simonize', + 'simony', + 'simoom', + 'simp', + 'simpatico', + 'simper', + 'simple', + 'simpleton', + 'simplex', + 'simplicidentate', + 'simplicity', + 'simplify', + 'simplism', + 'simplistic', + 'simply', + 'simulacrum', + 'simulant', + 'simulate', + 'simulated', + 'simulation', + 'simulator', + 'simulcast', + 'simultaneous', + 'sin', + 'sinapism', + 'since', + 'sincere', + 'sincerity', + 'sinciput', + 'sine', + 'sinecure', + 'sinew', + 'sinewy', + 'sinfonia', + 'sinfonietta', + 'sinful', + 'sing', + 'singe', + 'singer', + 'single', + 'singleness', + 'singles', + 'singlestick', + 'singlet', + 'singleton', + 'singletree', + 'singly', + 'singsong', + 'singular', + 'singularity', + 'singularize', + 'singultus', + 'sinh', + 'sinister', + 'sinistrad', + 'sinistral', + 'sinistrality', + 'sinistrocular', + 'sinistrodextral', + 'sinistrorse', + 'sinistrous', + 'sink', + 'sinkage', + 'sinker', + 'sinkhole', + 'sinking', + 'sinless', + 'sinner', + 'sinter', + 'sinuate', + 'sinuation', + 'sinuosity', + 'sinuous', + 'sinus', + 'sinusitis', + 'sinusoid', + 'sinusoidal', + 'sip', + 'siphon', + 'siphonophore', + 'siphonostele', + 'sipper', + 'sippet', + 'sir', + 'sirdar', + 'sire', + 'siren', + 'sirenic', + 'siriasis', + 'sirloin', + 'sirocco', + 'sirrah', + 'sirree', + 'sirup', + 'sis', + 'sisal', + 'siskin', + 'sissified', + 'sissy', + 'sister', + 'sisterhood', + 'sisterly', + 'sit', + 'sitar', + 'site', + 'sitology', + 'sitter', + 'sitting', + 'situate', + 'situated', + 'situation', + 'situla', + 'situs', + 'sitzmark', + 'six', + 'sixfold', + 'sixpence', + 'sixpenny', + 'sixteen', + 'sixteenmo', + 'sixteenth', + 'sixth', + 'sixtieth', + 'sixty', + 'sizable', + 'sizar', + 'size', + 'sizeable', + 'sized', + 'sizing', + 'sizzle', + 'sizzler', + 'sjambok', + 'skald', + 'skat', + 'skate', + 'skateboard', + 'skater', + 'skatole', + 'skean', + 'skedaddle', + 'skeet', + 'skeg', + 'skein', + 'skeleton', + 'skellum', + 'skelp', + 'skep', + 'skepful', + 'skeptic', + 'skeptical', + 'skepticism', + 'skerrick', + 'skerry', + 'sketch', + 'sketchbook', + 'sketchy', + 'skew', + 'skewback', + 'skewbald', + 'skewer', + 'skewness', + 'ski', + 'skiagraph', + 'skiascope', + 'skid', + 'skidproof', + 'skidway', + 'skied', + 'skiff', + 'skiffle', + 'skiing', + 'skijoring', + 'skilful', + 'skill', + 'skilled', + 'skillet', + 'skillful', + 'skilling', + 'skim', + 'skimmer', + 'skimmia', + 'skimp', + 'skimpy', + 'skin', + 'skinflint', + 'skinhead', + 'skink', + 'skinned', + 'skinny', + 'skintight', + 'skip', + 'skipjack', + 'skiplane', + 'skipper', + 'skippet', + 'skirl', + 'skirling', + 'skirmish', + 'skirr', + 'skirret', + 'skirt', + 'skirting', + 'skit', + 'skite', + 'skitter', + 'skittish', + 'skittle', + 'skive', + 'skiver', + 'skivvy', + 'skulduggery', + 'skulk', + 'skull', + 'skullcap', + 'skunk', + 'sky', + 'skycap', + 'skydive', + 'skyjack', + 'skylark', + 'skylight', + 'skyline', + 'skyrocket', + 'skysail', + 'skyscape', + 'skyscraper', + 'skysweeper', + 'skyward', + 'skyway', + 'skywriting', + 'slab', + 'slabber', + 'slack', + 'slacken', + 'slacker', + 'slacks', + 'slag', + 'slain', + 'slake', + 'slalom', + 'slam', + 'slander', + 'slang', + 'slangy', + 'slant', + 'slantwise', + 'slap', + 'slapdash', + 'slaphappy', + 'slapjack', + 'slapstick', + 'slash', + 'slashing', + 'slat', + 'slate', + 'slater', + 'slather', + 'slating', + 'slattern', + 'slatternly', + 'slaty', + 'slaughter', + 'slaughterhouse', + 'slave', + 'slaveholder', + 'slaver', + 'slavery', + 'slavey', + 'slavish', + 'slavocracy', + 'slaw', + 'slay', + 'sleave', + 'sleazy', + 'sled', + 'sledge', + 'sledgehammer', + 'sleek', + 'sleekit', + 'sleep', + 'sleeper', + 'sleeping', + 'sleepless', + 'sleepwalk', + 'sleepy', + 'sleepyhead', + 'sleet', + 'sleety', + 'sleeve', + 'sleigh', + 'sleight', + 'slender', + 'slenderize', + 'sleuth', + 'sleuthhound', + 'slew', + 'slice', + 'slicer', + 'slick', + 'slickenside', + 'slicker', + 'slide', + 'slider', + 'sliding', + 'slier', + 'sliest', + 'slight', + 'slighting', + 'slightly', + 'slily', + 'slim', + 'slime', + 'slimsy', + 'slimy', + 'sling', + 'slingshot', + 'slink', + 'slinky', + 'slip', + 'slipcase', + 'slipcover', + 'slipknot', + 'slipnoose', + 'slipover', + 'slippage', + 'slipper', + 'slipperwort', + 'slippery', + 'slippy', + 'slipsheet', + 'slipshod', + 'slipslop', + 'slipstream', + 'slipway', + 'slit', + 'slither', + 'sliver', + 'slivovitz', + 'slob', + 'slobber', + 'slobbery', + 'sloe', + 'slog', + 'slogan', + 'sloganeer', + 'sloop', + 'slop', + 'slope', + 'sloppy', + 'slopwork', + 'slosh', + 'sloshy', + 'slot', + 'sloth', + 'slothful', + 'slotter', + 'slouch', + 'slough', + 'sloven', + 'slovenly', + 'slow', + 'slowdown', + 'slowpoke', + 'slowworm', + 'slub', + 'sludge', + 'sludgy', + 'slue', + 'sluff', + 'slug', + 'slugabed', + 'sluggard', + 'sluggish', + 'sluice', + 'slum', + 'slumber', + 'slumberland', + 'slumberous', + 'slumgullion', + 'slumlord', + 'slump', + 'slung', + 'slunk', + 'slur', + 'slurp', + 'slurry', + 'slush', + 'slushy', + 'slut', + 'sly', + 'slype', + 'smack', + 'smacker', + 'smacking', + 'small', + 'smallage', + 'smallclothes', + 'smallish', + 'smallpox', + 'smallsword', + 'smalt', + 'smaltite', + 'smalto', + 'smaragd', + 'smaragdine', + 'smaragdite', + 'smarm', + 'smarmy', + 'smart', + 'smarten', + 'smash', + 'smashed', + 'smasher', + 'smashing', + 'smatter', + 'smattering', + 'smaze', + 'smear', + 'smearcase', + 'smectic', + 'smegma', + 'smell', + 'smelly', + 'smelt', + 'smelter', + 'smew', + 'smidgen', + 'smilacaceous', + 'smilax', + 'smile', + 'smirch', + 'smirk', + 'smite', + 'smith', + 'smithereens', + 'smithery', + 'smithsonite', + 'smithy', + 'smitten', + 'smock', + 'smocking', + 'smog', + 'smoke', + 'smokechaser', + 'smokejumper', + 'smokeless', + 'smokeproof', + 'smoker', + 'smokestack', + 'smoking', + 'smoko', + 'smoky', + 'smolder', + 'smolt', + 'smoodge', + 'smooth', + 'smoothbore', + 'smoothen', + 'smoothie', + 'smorgasbord', + 'smote', + 'smother', + 'smoulder', + 'smriti', + 'smudge', + 'smug', + 'smuggle', + 'smut', + 'smutch', + 'smutchy', + 'smutty', + 'snack', + 'snaffle', + 'snafu', + 'snag', + 'snaggletooth', + 'snaggy', + 'snail', + 'snailfish', + 'snake', + 'snakebird', + 'snakebite', + 'snakemouth', + 'snakeroot', + 'snaky', + 'snap', + 'snapback', + 'snapdragon', + 'snapper', + 'snappish', + 'snappy', + 'snapshot', + 'snare', + 'snarl', + 'snatch', + 'snatchy', + 'snath', + 'snazzy', + 'sneak', + 'sneakbox', + 'sneaker', + 'sneakers', + 'sneaking', + 'sneaky', + 'sneck', + 'sneer', + 'sneeze', + 'snick', + 'snicker', + 'snide', + 'sniff', + 'sniffle', + 'sniffy', + 'snifter', + 'snigger', + 'sniggle', + 'snip', + 'snipe', + 'sniper', + 'sniperscope', + 'snippet', + 'snippy', + 'snips', + 'snitch', + 'snivel', + 'snob', + 'snobbery', + 'snobbish', + 'snood', + 'snook', + 'snooker', + 'snoop', + 'snooperscope', + 'snoopy', + 'snooty', + 'snooze', + 'snore', + 'snorkel', + 'snort', + 'snorter', + 'snot', + 'snotty', + 'snout', + 'snow', + 'snowball', + 'snowberry', + 'snowbird', + 'snowblink', + 'snowbound', + 'snowcap', + 'snowdrift', + 'snowdrop', + 'snowfall', + 'snowfield', + 'snowflake', + 'snowman', + 'snowmobile', + 'snowplow', + 'snowshed', + 'snowshoe', + 'snowslide', + 'snowstorm', + 'snowy', + 'snub', + 'snuck', + 'snuff', + 'snuffbox', + 'snuffer', + 'snuffle', + 'snuffy', + 'snug', + 'snuggery', + 'snuggle', + 'so', + 'soak', + 'soakage', + 'soap', + 'soapbark', + 'soapberry', + 'soapbox', + 'soapstone', + 'soapsuds', + 'soapwort', + 'soapy', + 'soar', + 'soaring', + 'soave', + 'sob', + 'sober', + 'sobersided', + 'sobriety', + 'sobriquet', + 'socage', + 'soccer', + 'sociability', + 'sociable', + 'social', + 'socialism', + 'socialist', + 'socialistic', + 'socialite', + 'sociality', + 'socialization', + 'socialize', + 'societal', + 'society', + 'socioeconomic', + 'sociolinguistics', + 'sociology', + 'sociometry', + 'sociopath', + 'sock', + 'socket', + 'socle', + 'socman', + 'sod', + 'soda', + 'sodalite', + 'sodality', + 'sodamide', + 'sodden', + 'sodium', + 'sodomite', + 'sodomy', + 'soever', + 'sofa', + 'sofar', + 'soffit', + 'soft', + 'softa', + 'softball', + 'soften', + 'softener', + 'softhearted', + 'software', + 'softwood', + 'softy', + 'soggy', + 'soil', + 'soilage', + 'soilure', + 'soiree', + 'sojourn', + 'soke', + 'sol', + 'sola', + 'solace', + 'solan', + 'solanaceous', + 'solander', + 'solano', + 'solanum', + 'solar', + 'solarism', + 'solarium', + 'solarize', + 'solatium', + 'sold', + 'solder', + 'soldier', + 'soldierly', + 'soldiery', + 'soldo', + 'sole', + 'solecism', + 'solely', + 'solemn', + 'solemnity', + 'solemnize', + 'solenoid', + 'solfatara', + 'solfeggio', + 'solferino', + 'solicit', + 'solicitor', + 'solicitous', + 'solicitude', + 'solid', + 'solidago', + 'solidarity', + 'solidary', + 'solidify', + 'solidus', + 'solifidian', + 'solifluction', + 'soliloquize', + 'soliloquy', + 'solipsism', + 'solitaire', + 'solitary', + 'solitude', + 'solleret', + 'solmization', + 'solo', + 'soloist', + 'solstice', + 'solubility', + 'solubilize', + 'soluble', + 'solus', + 'solute', + 'solution', + 'solvable', + 'solve', + 'solvency', + 'solvent', + 'solvolysis', + 'soma', + 'somatic', + 'somatist', + 'somatology', + 'somatoplasm', + 'somatotype', + 'somber', + 'sombrero', + 'sombrous', + 'some', + 'somebody', + 'someday', + 'somehow', + 'someone', + 'someplace', + 'somersault', + 'somerset', + 'something', + 'sometime', + 'sometimes', + 'someway', + 'somewhat', + 'somewhere', + 'somewise', + 'somite', + 'sommelier', + 'somnambulate', + 'somnambulation', + 'somnambulism', + 'somnifacient', + 'somniferous', + 'somniloquy', + 'somnolent', + 'son', + 'sonant', + 'sonar', + 'sonata', + 'sonatina', + 'sonde', + 'sone', + 'song', + 'songbird', + 'songful', + 'songster', + 'songstress', + 'songwriter', + 'sonic', + 'sonics', + 'soniferous', + 'sonnet', + 'sonneteer', + 'sonny', + 'sonobuoy', + 'sonometer', + 'sonorant', + 'sonority', + 'sonorous', + 'soon', + 'sooner', + 'soot', + 'sooth', + 'soothe', + 'soothfast', + 'soothsay', + 'soothsayer', + 'sooty', + 'sop', + 'sophism', + 'sophist', + 'sophister', + 'sophistic', + 'sophisticate', + 'sophisticated', + 'sophistication', + 'sophistry', + 'sophomore', + 'sophrosyne', + 'sopor', + 'soporific', + 'sopping', + 'soppy', + 'soprano', + 'sora', + 'sorb', + 'sorbitol', + 'sorbose', + 'sorcerer', + 'sorcery', + 'sordid', + 'sordino', + 'sore', + 'soredium', + 'sorehead', + 'sorely', + 'sorghum', + 'sorgo', + 'sori', + 'soricine', + 'sorites', + 'sorn', + 'sororate', + 'sororicide', + 'sorority', + 'sorosis', + 'sorption', + 'sorrel', + 'sorrow', + 'sorry', + 'sort', + 'sortie', + 'sortilege', + 'sortition', + 'sorus', + 'sostenuto', + 'sot', + 'soteriology', + 'sotted', + 'sottish', + 'sou', + 'soubise', + 'soubrette', + 'soubriquet', + 'souffle', + 'sough', + 'sought', + 'soul', + 'soulful', + 'soulless', + 'sound', + 'soundboard', + 'sounder', + 'sounding', + 'soundless', + 'soundproof', + 'soup', + 'soupspoon', + 'soupy', + 'sour', + 'source', + 'sourdine', + 'sourdough', + 'sourpuss', + 'soursop', + 'sourwood', + 'sousaphone', + 'souse', + 'soutache', + 'soutane', + 'souter', + 'souterrain', + 'south', + 'southbound', + 'southeast', + 'southeaster', + 'southeasterly', + 'southeastward', + 'southeastwardly', + 'southeastwards', + 'souther', + 'southerly', + 'southern', + 'southernly', + 'southernmost', + 'southing', + 'southland', + 'southpaw', + 'southward', + 'southwards', + 'southwest', + 'southwester', + 'southwesterly', + 'southwestward', + 'southwestwardly', + 'southwestwards', + 'souvenir', + 'sovereign', + 'sovereignty', + 'soviet', + 'sovran', + 'sow', + 'sowens', + 'sox', + 'soy', + 'soybean', + 'spa', + 'space', + 'spaceband', + 'spacecraft', + 'spaceless', + 'spaceman', + 'spaceport', + 'spaceship', + 'spacesuit', + 'spacial', + 'spacing', + 'spacious', + 'spade', + 'spadefish', + 'spadework', + 'spadiceous', + 'spadix', + 'spae', + 'spaetzle', + 'spaghetti', + 'spagyric', + 'spahi', + 'spake', + 'spall', + 'spallation', + 'span', + 'spancel', + 'spandex', + 'spandrel', + 'spang', + 'spangle', + 'spaniel', + 'spank', + 'spanker', + 'spanking', + 'spanner', + 'spar', + 'spare', + 'sparerib', + 'sparge', + 'sparid', + 'sparing', + 'spark', + 'sparker', + 'sparkle', + 'sparkler', + 'sparks', + 'sparling', + 'sparoid', + 'sparrow', + 'sparrowgrass', + 'sparry', + 'sparse', + 'sparteine', + 'spasm', + 'spasmodic', + 'spastic', + 'spat', + 'spate', + 'spathe', + 'spathic', + 'spathose', + 'spatial', + 'spatiotemporal', + 'spatter', + 'spatterdash', + 'spatula', + 'spavin', + 'spavined', + 'spawn', + 'spay', + 'speak', + 'speakeasy', + 'speaker', + 'speaking', + 'spear', + 'spearhead', + 'spearman', + 'spearmint', + 'spearwort', + 'spec', + 'special', + 'specialism', + 'specialist', + 'specialistic', + 'speciality', + 'specialize', + 'specialty', + 'speciation', + 'specie', + 'species', + 'specific', + 'specification', + 'specify', + 'specimen', + 'speciosity', + 'specious', + 'speck', + 'speckle', + 'specs', + 'spectacle', + 'spectacled', + 'spectacles', + 'spectacular', + 'spectator', + 'spectatress', + 'specter', + 'spectra', + 'spectral', + 'spectre', + 'spectrochemistry', + 'spectrogram', + 'spectrograph', + 'spectroheliograph', + 'spectrohelioscope', + 'spectrometer', + 'spectrophotometer', + 'spectroradiometer', + 'spectroscope', + 'spectroscopy', + 'spectrum', + 'specular', + 'speculate', + 'speculation', + 'speculative', + 'speculator', + 'speculum', + 'sped', + 'speech', + 'speechless', + 'speechmaker', + 'speechmaking', + 'speed', + 'speedball', + 'speedboat', + 'speedometer', + 'speedway', + 'speedwell', + 'speedy', + 'speiss', + 'spelaean', + 'speleology', + 'spell', + 'spellbind', + 'spellbinder', + 'spellbound', + 'spelldown', + 'speller', + 'spelling', + 'spelt', + 'spelter', + 'spelunker', + 'spence', + 'spencer', + 'spend', + 'spendable', + 'spender', + 'spendthrift', + 'spent', + 'speos', + 'sperm', + 'spermaceti', + 'spermary', + 'spermatic', + 'spermatid', + 'spermatium', + 'spermatocyte', + 'spermatogonium', + 'spermatophore', + 'spermatophyte', + 'spermatozoid', + 'spermatozoon', + 'spermic', + 'spermicide', + 'spermine', + 'spermiogenesis', + 'spermogonium', + 'spermophile', + 'spermophyte', + 'spermous', + 'sperrylite', + 'spessartite', + 'spew', + 'sphacelus', + 'sphagnum', + 'sphalerite', + 'sphene', + 'sphenic', + 'sphenogram', + 'sphenoid', + 'sphere', + 'spherical', + 'sphericity', + 'spherics', + 'spheroid', + 'spheroidal', + 'spheroidicity', + 'spherule', + 'spherulite', + 'sphery', + 'sphincter', + 'sphingosine', + 'sphinx', + 'sphygmic', + 'sphygmograph', + 'sphygmoid', + 'sphygmomanometer', + 'spic', + 'spica', + 'spicate', + 'spiccato', + 'spice', + 'spiceberry', + 'spicebush', + 'spiculate', + 'spicule', + 'spiculum', + 'spicy', + 'spider', + 'spiderwort', + 'spidery', + 'spiegeleisen', + 'spiel', + 'spieler', + 'spier', + 'spiffing', + 'spiffy', + 'spigot', + 'spike', + 'spikelet', + 'spikenard', + 'spiky', + 'spile', + 'spill', + 'spillage', + 'spillway', + 'spilt', + 'spin', + 'spinach', + 'spinal', + 'spindle', + 'spindlelegs', + 'spindling', + 'spindly', + 'spindrift', + 'spine', + 'spinel', + 'spineless', + 'spinescent', + 'spinet', + 'spiniferous', + 'spinifex', + 'spinnaker', + 'spinner', + 'spinneret', + 'spinney', + 'spinning', + 'spinode', + 'spinose', + 'spinous', + 'spinster', + 'spinthariscope', + 'spinule', + 'spiny', + 'spiracle', + 'spiraea', + 'spiral', + 'spirant', + 'spire', + 'spirelet', + 'spireme', + 'spirillum', + 'spirit', + 'spirited', + 'spiritism', + 'spiritless', + 'spiritoso', + 'spiritual', + 'spiritualism', + 'spiritualist', + 'spirituality', + 'spiritualize', + 'spiritualty', + 'spirituel', + 'spirituous', + 'spirketing', + 'spirochaetosis', + 'spirochete', + 'spirograph', + 'spirogyra', + 'spiroid', + 'spirometer', + 'spirt', + 'spirula', + 'spiry', + 'spit', + 'spital', + 'spitball', + 'spite', + 'spiteful', + 'spitfire', + 'spitter', + 'spittle', + 'spittoon', + 'spitz', + 'spiv', + 'splanchnic', + 'splanchnology', + 'splash', + 'splashboard', + 'splashdown', + 'splasher', + 'splashy', + 'splat', + 'splatter', + 'splay', + 'splayfoot', + 'spleen', + 'spleenful', + 'spleenwort', + 'spleeny', + 'splendent', + 'splendid', + 'splendiferous', + 'splendor', + 'splenectomy', + 'splenetic', + 'splenic', + 'splenitis', + 'splenius', + 'splenomegaly', + 'splice', + 'spline', + 'splint', + 'splinter', + 'split', + 'splitting', + 'splore', + 'splotch', + 'splurge', + 'splutter', + 'spodumene', + 'spoil', + 'spoilage', + 'spoiler', + 'spoilfive', + 'spoils', + 'spoilsman', + 'spoilsport', + 'spoilt', + 'spoke', + 'spoken', + 'spokeshave', + 'spokesman', + 'spokeswoman', + 'spoliate', + 'spoliation', + 'spondaic', + 'spondee', + 'spondylitis', + 'sponge', + 'sponger', + 'spongin', + 'spongioblast', + 'spongy', + 'sponson', + 'sponsor', + 'spontaneity', + 'spontaneous', + 'spontoon', + 'spoof', + 'spoofery', + 'spook', + 'spooky', + 'spool', + 'spoon', + 'spoonbill', + 'spoondrift', + 'spoonerism', + 'spoonful', + 'spoony', + 'spoor', + 'sporadic', + 'sporangium', + 'spore', + 'sporocarp', + 'sporocyst', + 'sporocyte', + 'sporogenesis', + 'sporogonium', + 'sporogony', + 'sporophore', + 'sporophyll', + 'sporophyte', + 'sporozoite', + 'sporran', + 'sport', + 'sporting', + 'sportive', + 'sports', + 'sportscast', + 'sportsman', + 'sportsmanship', + 'sportswear', + 'sportswoman', + 'sporty', + 'sporulate', + 'sporule', + 'spot', + 'spotless', + 'spotlight', + 'spotted', + 'spotter', + 'spotty', + 'spousal', + 'spouse', + 'spout', + 'spraddle', + 'sprag', + 'sprain', + 'sprang', + 'sprat', + 'sprawl', + 'spray', + 'spread', + 'spreader', + 'spree', + 'sprig', + 'sprightly', + 'spring', + 'springboard', + 'springbok', + 'springe', + 'springer', + 'springhalt', + 'springhead', + 'springhouse', + 'springing', + 'springlet', + 'springtail', + 'springtime', + 'springwood', + 'springy', + 'sprinkle', + 'sprinkler', + 'sprinkling', + 'sprint', + 'sprit', + 'sprite', + 'spritsail', + 'sprocket', + 'sprout', + 'spruce', + 'sprue', + 'spruik', + 'sprung', + 'spry', + 'spud', + 'spue', + 'spume', + 'spumescent', + 'spun', + 'spunk', + 'spunky', + 'spur', + 'spurge', + 'spurious', + 'spurn', + 'spurrier', + 'spurry', + 'spurt', + 'spurtle', + 'sputnik', + 'sputter', + 'sputum', + 'spy', + 'spyglass', + 'squab', + 'squabble', + 'squad', + 'squadron', + 'squalene', + 'squalid', + 'squall', + 'squally', + 'squalor', + 'squama', + 'squamation', + 'squamosal', + 'squamous', + 'squamulose', + 'squander', + 'square', + 'squarely', + 'squarrose', + 'squash', + 'squashy', + 'squat', + 'squatness', + 'squatter', + 'squaw', + 'squawk', + 'squeak', + 'squeaky', + 'squeal', + 'squeamish', + 'squeegee', + 'squeeze', + 'squelch', + 'squeteague', + 'squib', + 'squid', + 'squiffy', + 'squiggle', + 'squilgee', + 'squill', + 'squinch', + 'squint', + 'squinty', + 'squire', + 'squirearchy', + 'squireen', + 'squirm', + 'squirmy', + 'squirrel', + 'squirt', + 'squish', + 'squishy', + 'sri', + 'sruti', + 'stab', + 'stabile', + 'stability', + 'stabilize', + 'stabilizer', + 'stable', + 'stableboy', + 'stableman', + 'stablish', + 'staccato', + 'stack', + 'stacked', + 'stacte', + 'stadholder', + 'stadia', + 'stadiometer', + 'stadium', + 'stadtholder', + 'staff', + 'staffer', + 'staffman', + 'stag', + 'stage', + 'stagecoach', + 'stagecraft', + 'stagehand', + 'stagey', + 'staggard', + 'stagger', + 'staggers', + 'staghound', + 'staging', + 'stagnant', + 'stagnate', + 'stagy', + 'staid', + 'stain', + 'stainless', + 'stair', + 'staircase', + 'stairhead', + 'stairs', + 'stairway', + 'stairwell', + 'stake', + 'stakeout', + 'stalactite', + 'stalag', + 'stalagmite', + 'stale', + 'stalemate', + 'stalk', + 'stalky', + 'stall', + 'stallion', + 'stalwart', + 'stamen', + 'stamin', + 'stamina', + 'staminody', + 'stammel', + 'stammer', + 'stamp', + 'stampede', + 'stance', + 'stanch', + 'stanchion', + 'stand', + 'standard', + 'standardize', + 'standby', + 'standee', + 'standfast', + 'standing', + 'standoff', + 'standoffish', + 'standpipe', + 'standpoint', + 'standstill', + 'stane', + 'stang', + 'stanhope', + 'stank', + 'stannary', + 'stannic', + 'stannite', + 'stannum', + 'stanza', + 'stapes', + 'staphylococcus', + 'staphyloplasty', + 'staphylorrhaphy', + 'staple', + 'stapler', + 'star', + 'starboard', + 'starch', + 'starchy', + 'stardom', + 'stare', + 'starfish', + 'starflower', + 'stark', + 'starlet', + 'starlight', + 'starlike', + 'starling', + 'starred', + 'starry', + 'start', + 'starter', + 'startle', + 'startling', + 'starvation', + 'starve', + 'starveling', + 'starwort', + 'stash', + 'stasis', + 'statampere', + 'statant', + 'state', + 'statecraft', + 'stated', + 'statehood', + 'stateless', + 'stately', + 'statement', + 'stater', + 'stateroom', + 'statesman', + 'statesmanship', + 'statfarad', + 'static', + 'statics', + 'station', + 'stationary', + 'stationer', + 'stationery', + 'stationmaster', + 'statism', + 'statist', + 'statistical', + 'statistician', + 'statistics', + 'stative', + 'statocyst', + 'statolatry', + 'statolith', + 'stator', + 'statuary', + 'statue', + 'statued', + 'statuesque', + 'statuette', + 'stature', + 'status', + 'statutable', + 'statute', + 'statutory', + 'statvolt', + 'staunch', + 'staurolite', + 'stave', + 'staves', + 'stay', + 'stays', + 'staysail', + 'stead', + 'steadfast', + 'steading', + 'steady', + 'steak', + 'steakhouse', + 'steal', + 'stealage', + 'stealer', + 'stealing', + 'stealth', + 'stealthy', + 'steam', + 'steamboat', + 'steamer', + 'steamroller', + 'steamship', + 'steamtight', + 'steamy', + 'steapsin', + 'stearic', + 'stearin', + 'stearoptene', + 'steatite', + 'steatopygia', + 'stedfast', + 'steed', + 'steel', + 'steelhead', + 'steelmaker', + 'steels', + 'steelwork', + 'steelworker', + 'steelworks', + 'steelyard', + 'steenbok', + 'steep', + 'steepen', + 'steeple', + 'steeplebush', + 'steeplechase', + 'steeplejack', + 'steer', + 'steerage', + 'steerageway', + 'steersman', + 'steeve', + 'stegodon', + 'stegosaur', + 'stein', + 'steinbok', + 'stela', + 'stele', + 'stellar', + 'stellarator', + 'stellate', + 'stelliform', + 'stellular', + 'stem', + 'stemma', + 'stemson', + 'stemware', + 'stench', + 'stencil', + 'stenograph', + 'stenographer', + 'stenography', + 'stenopetalous', + 'stenophagous', + 'stenophyllous', + 'stenosis', + 'stenotype', + 'stenotypy', + 'stentor', + 'stentorian', + 'step', + 'stepbrother', + 'stepchild', + 'stepdame', + 'stepdaughter', + 'stepfather', + 'stephanotis', + 'stepladder', + 'stepmother', + 'stepparent', + 'steppe', + 'stepper', + 'stepsister', + 'stepson', + 'steradian', + 'stercoraceous', + 'stercoricolous', + 'sterculiaceous', + 'stere', + 'stereo', + 'stereobate', + 'stereochemistry', + 'stereochrome', + 'stereochromy', + 'stereogram', + 'stereograph', + 'stereography', + 'stereoisomer', + 'stereoisomerism', + 'stereometry', + 'stereophonic', + 'stereophotography', + 'stereopticon', + 'stereoscope', + 'stereoscopic', + 'stereoscopy', + 'stereotaxis', + 'stereotomy', + 'stereotropism', + 'stereotype', + 'stereotyped', + 'stereotypy', + 'steric', + 'sterigma', + 'sterilant', + 'sterile', + 'sterilization', + 'sterilize', + 'sterling', + 'stern', + 'sternforemost', + 'sternmost', + 'sternpost', + 'sternson', + 'sternum', + 'sternutation', + 'sternutatory', + 'sternway', + 'steroid', + 'sterol', + 'stertor', + 'stertorous', + 'stet', + 'stethoscope', + 'stevedore', + 'stew', + 'steward', + 'stewardess', + 'stewed', + 'stewpan', + 'sthenic', + 'stibine', + 'stibnite', + 'stich', + 'stichometry', + 'stichomythia', + 'stick', + 'sticker', + 'stickle', + 'stickleback', + 'stickler', + 'stickpin', + 'stickseed', + 'sticktight', + 'stickup', + 'stickweed', + 'sticky', + 'stickybeak', + 'stiff', + 'stiffen', + 'stifle', + 'stifling', + 'stigma', + 'stigmasterol', + 'stigmatic', + 'stigmatism', + 'stigmatize', + 'stilbestrol', + 'stilbite', + 'stile', + 'stiletto', + 'still', + 'stillage', + 'stillbirth', + 'stillborn', + 'stilliform', + 'stillness', + 'stilly', + 'stilt', + 'stilted', + 'stimulant', + 'stimulate', + 'stimulative', + 'stimulus', + 'sting', + 'stingaree', + 'stinger', + 'stingo', + 'stingy', + 'stink', + 'stinker', + 'stinkhorn', + 'stinking', + 'stinko', + 'stinkpot', + 'stinkstone', + 'stinkweed', + 'stinkwood', + 'stint', + 'stipe', + 'stipel', + 'stipend', + 'stipendiary', + 'stipitate', + 'stipple', + 'stipulate', + 'stipulation', + 'stipule', + 'stir', + 'stirk', + 'stirpiculture', + 'stirps', + 'stirring', + 'stirrup', + 'stitch', + 'stitching', + 'stithy', + 'stiver', + 'stoa', + 'stoat', + 'stob', + 'stochastic', + 'stock', + 'stockade', + 'stockbreeder', + 'stockbroker', + 'stockholder', + 'stockinet', + 'stocking', + 'stockish', + 'stockist', + 'stockjobber', + 'stockman', + 'stockpile', + 'stockroom', + 'stocks', + 'stocktaking', + 'stocky', + 'stockyard', + 'stodge', + 'stodgy', + 'stogy', + 'stoic', + 'stoical', + 'stoichiometric', + 'stoichiometry', + 'stoicism', + 'stoke', + 'stokehold', + 'stokehole', + 'stoker', + 'stole', + 'stolen', + 'stolid', + 'stolon', + 'stoma', + 'stomach', + 'stomachache', + 'stomacher', + 'stomachic', + 'stomatal', + 'stomatic', + 'stomatitis', + 'stomatology', + 'stomodaeum', + 'stone', + 'stonechat', + 'stonecrop', + 'stonecutter', + 'stoned', + 'stonefish', + 'stonefly', + 'stonemason', + 'stonewall', + 'stoneware', + 'stonework', + 'stonewort', + 'stony', + 'stood', + 'stooge', + 'stook', + 'stool', + 'stoop', + 'stop', + 'stopcock', + 'stope', + 'stopgap', + 'stoplight', + 'stopover', + 'stoppage', + 'stopped', + 'stopper', + 'stopping', + 'stopple', + 'stopwatch', + 'storage', + 'storax', + 'store', + 'storehouse', + 'storekeeper', + 'storeroom', + 'stores', + 'storey', + 'storied', + 'storiette', + 'stork', + 'storm', + 'stormproof', + 'stormy', + 'story', + 'storybook', + 'storyteller', + 'storytelling', + 'stoss', + 'stotinka', + 'stound', + 'stoup', + 'stour', + 'stoush', + 'stout', + 'stouthearted', + 'stove', + 'stovepipe', + 'stover', + 'stow', + 'stowage', + 'stowaway', + 'strabismus', + 'straddle', + 'strafe', + 'straggle', + 'straight', + 'straightaway', + 'straightedge', + 'straighten', + 'straightforward', + 'straightjacket', + 'straightway', + 'strain', + 'strained', + 'strainer', + 'strait', + 'straiten', + 'straitjacket', + 'straitlaced', + 'strake', + 'stramonium', + 'strand', + 'strange', + 'strangeness', + 'stranger', + 'strangle', + 'stranglehold', + 'strangles', + 'strangulate', + 'strangulation', + 'strangury', + 'strap', + 'straphanger', + 'strapless', + 'strappado', + 'strapped', + 'strapper', + 'strapping', + 'strata', + 'stratagem', + 'strategic', + 'strategist', + 'strategy', + 'strath', + 'strathspey', + 'straticulate', + 'stratification', + 'stratiform', + 'stratify', + 'stratigraphy', + 'stratocracy', + 'stratocumulus', + 'stratopause', + 'stratosphere', + 'stratovision', + 'stratum', + 'stratus', + 'straw', + 'strawberry', + 'strawboard', + 'strawflower', + 'strawworm', + 'stray', + 'streak', + 'streaky', + 'stream', + 'streamer', + 'streaming', + 'streamlet', + 'streamline', + 'streamlined', + 'streamliner', + 'streamway', + 'streamy', + 'street', + 'streetcar', + 'streetlight', + 'streetwalker', + 'strength', + 'strengthen', + 'strenuous', + 'strep', + 'strepitous', + 'streptococcus', + 'streptokinase', + 'streptomycin', + 'streptothricin', + 'stress', + 'stressful', + 'stretch', + 'stretcher', + 'stretchy', + 'stretto', + 'streusel', + 'strew', + 'stria', + 'striate', + 'striated', + 'striation', + 'strick', + 'stricken', + 'strickle', + 'strict', + 'striction', + 'strictly', + 'stricture', + 'stride', + 'strident', + 'stridor', + 'stridulate', + 'stridulous', + 'strife', + 'strigil', + 'strigose', + 'strike', + 'strikebound', + 'strikebreaker', + 'striker', + 'striking', + 'string', + 'stringboard', + 'stringed', + 'stringency', + 'stringendo', + 'stringent', + 'stringer', + 'stringhalt', + 'stringpiece', + 'stringy', + 'strip', + 'stripe', + 'striped', + 'striper', + 'stripling', + 'stripper', + 'striptease', + 'stripteaser', + 'stripy', + 'strive', + 'strobe', + 'strobila', + 'strobilaceous', + 'strobile', + 'stroboscope', + 'strobotron', + 'strode', + 'stroganoff', + 'stroke', + 'stroll', + 'stroller', + 'strong', + 'strongbox', + 'stronghold', + 'strongroom', + 'strontia', + 'strontian', + 'strontianite', + 'strontium', + 'strop', + 'strophanthin', + 'strophanthus', + 'strophe', + 'strophic', + 'stroud', + 'strove', + 'strow', + 'stroy', + 'struck', + 'structural', + 'structuralism', + 'structure', + 'strudel', + 'struggle', + 'strum', + 'struma', + 'strumpet', + 'strung', + 'strut', + 'struthious', + 'strutting', + 'strychnic', + 'strychnine', + 'strychninism', + 'stub', + 'stubbed', + 'stubble', + 'stubborn', + 'stubby', + 'stucco', + 'stuccowork', + 'stuck', + 'stud', + 'studbook', + 'studding', + 'studdingsail', + 'student', + 'studhorse', + 'studied', + 'studio', + 'studious', + 'study', + 'stuff', + 'stuffed', + 'stuffing', + 'stuffy', + 'stull', + 'stultify', + 'stumble', + 'stumer', + 'stump', + 'stumpage', + 'stumper', + 'stumpy', + 'stun', + 'stung', + 'stunk', + 'stunner', + 'stunning', + 'stunsail', + 'stunt', + 'stupa', + 'stupe', + 'stupefacient', + 'stupefaction', + 'stupefy', + 'stupendous', + 'stupid', + 'stupidity', + 'stupor', + 'sturdy', + 'sturgeon', + 'stutter', + 'sty', + 'style', + 'stylet', + 'styliform', + 'stylish', + 'stylist', + 'stylistic', + 'stylite', + 'stylize', + 'stylobate', + 'stylograph', + 'stylographic', + 'stylography', + 'stylolite', + 'stylopodium', + 'stylus', + 'stymie', + 'stypsis', + 'styptic', + 'styracaceous', + 'styrax', + 'styrene', + 'suable', + 'suasion', + 'suave', + 'suavity', + 'sub', + 'subacid', + 'subacute', + 'subadar', + 'subalpine', + 'subaltern', + 'subalternate', + 'subantarctic', + 'subaquatic', + 'subaqueous', + 'subarctic', + 'subarid', + 'subassembly', + 'subastral', + 'subatomic', + 'subaudition', + 'subauricular', + 'subaxillary', + 'subbase', + 'subbasement', + 'subcartilaginous', + 'subcelestial', + 'subchaser', + 'subchloride', + 'subclass', + 'subclavian', + 'subclavius', + 'subclimax', + 'subclinical', + 'subcommittee', + 'subconscious', + 'subcontinent', + 'subcontract', + 'subcontraoctave', + 'subcortex', + 'subcritical', + 'subcutaneous', + 'subdeacon', + 'subdebutante', + 'subdelirium', + 'subdiaconate', + 'subdivide', + 'subdivision', + 'subdominant', + 'subdual', + 'subduct', + 'subdue', + 'subdued', + 'subedit', + 'subeditor', + 'subequatorial', + 'suberin', + 'subfamily', + 'subfloor', + 'subfusc', + 'subgenus', + 'subglacial', + 'subgroup', + 'subhead', + 'subheading', + 'subhuman', + 'subinfeudate', + 'subinfeudation', + 'subirrigate', + 'subito', + 'subjacent', + 'subject', + 'subjectify', + 'subjection', + 'subjective', + 'subjectivism', + 'subjoin', + 'subjoinder', + 'subjugate', + 'subjunction', + 'subjunctive', + 'subkingdom', + 'sublapsarianism', + 'sublease', + 'sublet', + 'sublieutenant', + 'sublimate', + 'sublimation', + 'sublime', + 'subliminal', + 'sublimity', + 'sublingual', + 'sublittoral', + 'sublunar', + 'sublunary', + 'submarginal', + 'submarine', + 'submariner', + 'submaxillary', + 'submediant', + 'submerge', + 'submerged', + 'submergible', + 'submerse', + 'submersed', + 'submersible', + 'submicroscopic', + 'subminiature', + 'subminiaturize', + 'submiss', + 'submission', + 'submissive', + 'submit', + 'submultiple', + 'subnormal', + 'suboceanic', + 'suborbital', + 'suborder', + 'subordinary', + 'subordinate', + 'suborn', + 'suboxide', + 'subphylum', + 'subplot', + 'subpoena', + 'subprincipal', + 'subreption', + 'subrogate', + 'subrogation', + 'subroutine', + 'subscapular', + 'subscribe', + 'subscript', + 'subscription', + 'subsellium', + 'subsequence', + 'subsequent', + 'subserve', + 'subservience', + 'subservient', + 'subset', + 'subshrub', + 'subside', + 'subsidence', + 'subsidiary', + 'subsidize', + 'subsidy', + 'subsist', + 'subsistence', + 'subsistent', + 'subsocial', + 'subsoil', + 'subsolar', + 'subsonic', + 'subspecies', + 'substage', + 'substance', + 'substandard', + 'substantial', + 'substantialism', + 'substantialize', + 'substantiate', + 'substantive', + 'substation', + 'substituent', + 'substitute', + 'substitution', + 'substitutive', + 'substrate', + 'substratosphere', + 'substratum', + 'substructure', + 'subsume', + 'subsumption', + 'subtangent', + 'subteen', + 'subtemperate', + 'subtenant', + 'subtend', + 'subterfuge', + 'subternatural', + 'subterrane', + 'subterranean', + 'subtile', + 'subtilize', + 'subtitle', + 'subtle', + 'subtlety', + 'subtonic', + 'subtorrid', + 'subtotal', + 'subtract', + 'subtraction', + 'subtractive', + 'subtrahend', + 'subtreasury', + 'subtropical', + 'subtropics', + 'subtype', + 'subulate', + 'suburb', + 'suburban', + 'suburbanite', + 'suburbanize', + 'suburbia', + 'suburbicarian', + 'subvene', + 'subvention', + 'subversion', + 'subversive', + 'subvert', + 'subway', + 'subzero', + 'succedaneum', + 'succeed', + 'succentor', + 'success', + 'successful', + 'succession', + 'successive', + 'successor', + 'succinate', + 'succinct', + 'succinctorium', + 'succinic', + 'succinylsulfathiazole', + 'succor', + 'succory', + 'succotash', + 'succubus', + 'succulent', + 'succumb', + 'succursal', + 'succuss', + 'succussion', + 'such', + 'suchlike', + 'suck', + 'sucker', + 'suckerfish', + 'sucking', + 'suckle', + 'suckling', + 'sucrase', + 'sucre', + 'sucrose', + 'suction', + 'suctorial', + 'sudarium', + 'sudatorium', + 'sudatory', + 'sudd', + 'sudden', + 'sudor', + 'sudoriferous', + 'sudorific', + 'suds', + 'sue', + 'suede', + 'suet', + 'suffer', + 'sufferable', + 'sufferance', + 'suffering', + 'suffice', + 'sufficiency', + 'sufficient', + 'suffix', + 'sufflate', + 'suffocate', + 'suffragan', + 'suffrage', + 'suffragette', + 'suffragist', + 'suffruticose', + 'suffumigate', + 'suffuse', + 'sugar', + 'sugared', + 'sugarplum', + 'sugary', + 'suggest', + 'suggestibility', + 'suggestible', + 'suggestion', + 'suggestive', + 'suicidal', + 'suicide', + 'suint', + 'suit', + 'suitable', + 'suitcase', + 'suite', + 'suited', + 'suiting', + 'suitor', + 'sukiyaki', + 'sukkah', + 'sulcate', + 'sulcus', + 'sulfa', + 'sulfaguanidine', + 'sulfamerazine', + 'sulfanilamide', + 'sulfapyrazine', + 'sulfapyridine', + 'sulfate', + 'sulfathiazole', + 'sulfatize', + 'sulfide', + 'sulfite', + 'sulfonate', + 'sulfonation', + 'sulfonmethane', + 'sulfur', + 'sulfuric', + 'sulfurous', + 'sulk', + 'sulky', + 'sullage', + 'sullen', + 'sully', + 'sulphanilamide', + 'sulphate', + 'sulphathiazole', + 'sulphide', + 'sulphonamide', + 'sulphonate', + 'sulphone', + 'sulphur', + 'sulphurate', + 'sulphuric', + 'sulphurize', + 'sulphurous', + 'sulphuryl', + 'sultan', + 'sultana', + 'sultanate', + 'sultry', + 'sum', + 'sumac', + 'sumach', + 'summa', + 'summand', + 'summarize', + 'summary', + 'summation', + 'summer', + 'summerhouse', + 'summerly', + 'summersault', + 'summertime', + 'summertree', + 'summerwood', + 'summit', + 'summitry', + 'summon', + 'summons', + 'sumo', + 'sump', + 'sumpter', + 'sumption', + 'sumptuary', + 'sumptuous', + 'sun', + 'sunbaked', + 'sunbathe', + 'sunbeam', + 'sunbonnet', + 'sunbow', + 'sunbreak', + 'sunburn', + 'sunburst', + 'sundae', + 'sunder', + 'sunderance', + 'sundew', + 'sundial', + 'sundog', + 'sundown', + 'sundowner', + 'sundries', + 'sundry', + 'sunfast', + 'sunfish', + 'sunflower', + 'sung', + 'sunglass', + 'sunglasses', + 'sunk', + 'sunken', + 'sunless', + 'sunlight', + 'sunlit', + 'sunn', + 'sunny', + 'sunproof', + 'sunrise', + 'sunroom', + 'sunset', + 'sunshade', + 'sunshine', + 'sunspot', + 'sunstone', + 'sunstroke', + 'suntan', + 'sunup', + 'sunward', + 'sunwise', + 'sup', + 'super', + 'superable', + 'superabound', + 'superabundant', + 'superadd', + 'superaltar', + 'superannuate', + 'superannuated', + 'superannuation', + 'superb', + 'superbomb', + 'supercargo', + 'supercharge', + 'supercharger', + 'supercilious', + 'superclass', + 'supercolumnar', + 'superconductivity', + 'supercool', + 'superdominant', + 'superdreadnought', + 'superego', + 'superelevation', + 'supereminent', + 'supererogate', + 'supererogation', + 'supererogatory', + 'superfamily', + 'superfecundation', + 'superfetation', + 'superficial', + 'superficies', + 'superfine', + 'superfluid', + 'superfluity', + 'superfluous', + 'superfuse', + 'supergalaxy', + 'superheat', + 'superheterodyne', + 'superhighway', + 'superhuman', + 'superimpose', + 'superimposed', + 'superincumbent', + 'superinduce', + 'superintend', + 'superintendency', + 'superintendent', + 'superior', + 'superiority', + 'superjacent', + 'superlative', + 'superload', + 'superman', + 'supermarket', + 'supermundane', + 'supernal', + 'supernatant', + 'supernational', + 'supernatural', + 'supernaturalism', + 'supernormal', + 'supernova', + 'supernumerary', + 'superorder', + 'superordinate', + 'superorganic', + 'superpatriot', + 'superphosphate', + 'superphysical', + 'superpose', + 'superposition', + 'superpower', + 'supersaturate', + 'supersaturated', + 'superscribe', + 'superscription', + 'supersede', + 'supersedure', + 'supersensible', + 'supersensitive', + 'supersensual', + 'supersession', + 'supersonic', + 'supersonics', + 'superstar', + 'superstition', + 'superstitious', + 'superstratum', + 'superstructure', + 'supertanker', + 'supertax', + 'supertonic', + 'supervene', + 'supervise', + 'supervision', + 'supervisor', + 'supervisory', + 'supinate', + 'supination', + 'supinator', + 'supine', + 'supper', + 'supplant', + 'supple', + 'supplejack', + 'supplement', + 'supplemental', + 'supplementary', + 'suppletion', + 'suppletory', + 'suppliant', + 'supplicant', + 'supplicate', + 'supplication', + 'supplicatory', + 'supply', + 'support', + 'supportable', + 'supporter', + 'supporting', + 'supportive', + 'supposal', + 'suppose', + 'supposed', + 'supposing', + 'supposition', + 'suppositious', + 'supposititious', + 'suppositive', + 'suppository', + 'suppress', + 'suppression', + 'suppressive', + 'suppurate', + 'suppuration', + 'suppurative', + 'supra', + 'supralapsarian', + 'supraliminal', + 'supramolecular', + 'supranational', + 'supranatural', + 'supraorbital', + 'suprarenal', + 'suprasegmental', + 'supremacist', + 'supremacy', + 'supreme', + 'surah', + 'sural', + 'surbase', + 'surbased', + 'surcease', + 'surcharge', + 'surcingle', + 'surculose', + 'surd', + 'sure', + 'surefire', + 'surely', + 'surety', + 'surf', + 'surface', + 'surfactant', + 'surfbird', + 'surfboard', + 'surfboarding', + 'surfboat', + 'surfeit', + 'surfing', + 'surfperch', + 'surge', + 'surgeon', + 'surgeonfish', + 'surgery', + 'surgical', + 'surgy', + 'suricate', + 'surly', + 'surmise', + 'surmount', + 'surmullet', + 'surname', + 'surpass', + 'surpassing', + 'surplice', + 'surplus', + 'surplusage', + 'surprint', + 'surprisal', + 'surprise', + 'surprising', + 'surra', + 'surrealism', + 'surrebuttal', + 'surrebutter', + 'surrejoinder', + 'surrender', + 'surreptitious', + 'surrey', + 'surrogate', + 'surround', + 'surrounding', + 'surroundings', + 'surtax', + 'surtout', + 'surveillance', + 'survey', + 'surveying', + 'surveyor', + 'survival', + 'survive', + 'survivor', + 'susceptibility', + 'susceptible', + 'susceptive', + 'sushi', + 'suslik', + 'suspect', + 'suspend', + 'suspender', + 'suspense', + 'suspension', + 'suspensive', + 'suspensoid', + 'suspensor', + 'suspensory', + 'suspicion', + 'suspicious', + 'suspiration', + 'suspire', + 'sustain', + 'sustainer', + 'sustenance', + 'sustentacular', + 'sustentation', + 'susurrant', + 'susurrate', + 'susurration', + 'susurrous', + 'susurrus', + 'sutler', + 'sutra', + 'suttee', + 'suture', + 'suzerain', + 'suzerainty', + 'svelte', + 'swab', + 'swabber', + 'swacked', + 'swaddle', + 'swag', + 'swage', + 'swagger', + 'swaggering', + 'swagman', + 'swagsman', + 'swain', + 'swale', + 'swallow', + 'swallowtail', + 'swam', + 'swami', + 'swamp', + 'swamper', + 'swampland', + 'swampy', + 'swan', + 'swanherd', + 'swank', + 'swanky', + 'swansdown', + 'swanskin', + 'swap', + 'swaraj', + 'sward', + 'swarm', + 'swart', + 'swarth', + 'swarthy', + 'swash', + 'swashbuckler', + 'swashbuckling', + 'swastika', + 'swat', + 'swatch', + 'swath', + 'swathe', + 'swats', + 'swatter', + 'sway', + 'swear', + 'swearword', + 'sweat', + 'sweatband', + 'sweatbox', + 'sweated', + 'sweater', + 'sweatshop', + 'sweaty', + 'swede', + 'sweeny', + 'sweep', + 'sweepback', + 'sweeper', + 'sweeping', + 'sweepings', + 'sweeps', + 'sweepstake', + 'sweepstakes', + 'sweet', + 'sweetbread', + 'sweetbrier', + 'sweeten', + 'sweetener', + 'sweetening', + 'sweetheart', + 'sweetie', + 'sweeting', + 'sweetmeat', + 'sweetsop', + 'swell', + 'swellfish', + 'swellhead', + 'swelling', + 'swelter', + 'sweltering', + 'swept', + 'sweptback', + 'sweptwing', + 'swerve', + 'sweven', + 'swift', + 'swifter', + 'swiftlet', + 'swig', + 'swill', + 'swim', + 'swimming', + 'swimmingly', + 'swindle', + 'swine', + 'swineherd', + 'swing', + 'swinge', + 'swingeing', + 'swinger', + 'swingle', + 'swingletree', + 'swinish', + 'swink', + 'swipe', + 'swipple', + 'swirl', + 'swirly', + 'swish', + 'switch', + 'switchback', + 'switchblade', + 'switchboard', + 'switcheroo', + 'switchman', + 'swivel', + 'swivet', + 'swizzle', + 'swob', + 'swollen', + 'swoon', + 'swoop', + 'swoosh', + 'swop', + 'sword', + 'swordbill', + 'swordcraft', + 'swordfish', + 'swordplay', + 'swordsman', + 'swordtail', + 'swore', + 'sworn', + 'swot', + 'swound', + 'swum', + 'swung', + 'sybarite', + 'sycamine', + 'sycamore', + 'syce', + 'sycee', + 'syconium', + 'sycophancy', + 'sycophant', + 'sycosis', + 'syllabary', + 'syllabi', + 'syllabic', + 'syllabify', + 'syllabism', + 'syllabize', + 'syllable', + 'syllabogram', + 'syllabub', + 'syllabus', + 'syllepsis', + 'syllogism', + 'syllogistic', + 'syllogize', + 'sylph', + 'sylphid', + 'sylvan', + 'sylvanite', + 'sylviculture', + 'sylvite', + 'symbiosis', + 'symbol', + 'symbolic', + 'symbolics', + 'symbolism', + 'symbolist', + 'symbolize', + 'symbology', + 'symmetrical', + 'symmetrize', + 'symmetry', + 'sympathetic', + 'sympathin', + 'sympathize', + 'sympathizer', + 'sympathy', + 'sympetalous', + 'symphonia', + 'symphonic', + 'symphonious', + 'symphonist', + 'symphonize', + 'symphony', + 'symphysis', + 'symploce', + 'symposiac', + 'symposiarch', + 'symposium', + 'symptom', + 'symptomatic', + 'symptomatology', + 'synaeresis', + 'synaesthesia', + 'synagogue', + 'synapse', + 'synapsis', + 'sync', + 'syncarpous', + 'synchro', + 'synchrocyclotron', + 'synchroflash', + 'synchromesh', + 'synchronic', + 'synchronism', + 'synchronize', + 'synchronous', + 'synchroscope', + 'synchrotron', + 'synclastic', + 'syncopate', + 'syncopated', + 'syncopation', + 'syncope', + 'syncretism', + 'syncretize', + 'syncrisis', + 'syncytium', + 'syndactyl', + 'syndesis', + 'syndesmosis', + 'syndetic', + 'syndic', + 'syndicalism', + 'syndicate', + 'syndrome', + 'syne', + 'synecdoche', + 'synecious', + 'synecology', + 'synectics', + 'syneresis', + 'synergetic', + 'synergism', + 'synergist', + 'synergistic', + 'synergy', + 'synesthesia', + 'syngamy', + 'synod', + 'synodic', + 'synonym', + 'synonymize', + 'synonymous', + 'synonymy', + 'synopsis', + 'synopsize', + 'synoptic', + 'synovia', + 'synovitis', + 'synsepalous', + 'syntactics', + 'syntax', + 'synthesis', + 'synthesize', + 'synthetic', + 'syntonic', + 'sypher', + 'syphilis', + 'syphilology', + 'syphon', + 'syringa', + 'syringe', + 'syringomyelia', + 'syrinx', + 'syrup', + 'syrupy', + 'systaltic', + 'system', + 'systematic', + 'systematics', + 'systematism', + 'systematist', + 'systematize', + 'systematology', + 'systemic', + 'systemize', + 'systole', + 'syzygy', + 't', + 'ta', + 'tab', + 'tabanid', + 'tabard', + 'tabaret', + 'tabby', + 'tabernacle', + 'tabes', + 'tabescent', + 'tablature', + 'table', + 'tableau', + 'tablecloth', + 'tableland', + 'tablespoon', + 'tablet', + 'tableware', + 'tabling', + 'tabloid', + 'taboo', + 'tabor', + 'taboret', + 'tabret', + 'tabu', + 'tabular', + 'tabulate', + 'tabulator', + 'tace', + 'tacet', + 'tache', + 'tacheometer', + 'tachistoscope', + 'tachograph', + 'tachometer', + 'tachycardia', + 'tachygraphy', + 'tachylyte', + 'tachymetry', + 'tachyphylaxis', + 'tacit', + 'taciturn', + 'taciturnity', + 'tack', + 'tacket', + 'tackle', + 'tackling', + 'tacky', + 'tacmahack', + 'tacnode', + 'taco', + 'taconite', + 'tact', + 'tactful', + 'tactic', + 'tactical', + 'tactician', + 'tactics', + 'tactile', + 'taction', + 'tactless', + 'tactual', + 'tad', + 'tadpole', + 'tael', + 'taenia', + 'taeniacide', + 'taeniafuge', + 'taeniasis', + 'taffeta', + 'taffrail', + 'taffy', + 'tafia', + 'tag', + 'tagliatelle', + 'tagmeme', + 'tagmemic', + 'tagmemics', + 'tahr', + 'tahsildar', + 'taiga', + 'tail', + 'tailback', + 'tailband', + 'tailgate', + 'tailing', + 'taille', + 'taillight', + 'tailor', + 'tailorbird', + 'tailored', + 'tailpiece', + 'tailpipe', + 'tailrace', + 'tails', + 'tailspin', + 'tailstock', + 'tailwind', + 'tain', + 'taint', + 'taintless', + 'taipan', + 'take', + 'taken', + 'takeoff', + 'takeover', + 'taker', + 'takin', + 'taking', + 'talapoin', + 'talaria', + 'talc', + 'tale', + 'talebearer', + 'talent', + 'talented', + 'taler', + 'tales', + 'talesman', + 'taligrade', + 'talion', + 'taliped', + 'talipes', + 'talisman', + 'talk', + 'talkathon', + 'talkative', + 'talkfest', + 'talkie', + 'talky', + 'tall', + 'tallage', + 'tallboy', + 'tallith', + 'tallow', + 'tallowy', + 'tally', + 'tallyho', + 'tallyman', + 'talon', + 'taluk', + 'talus', + 'tam', + 'tamable', + 'tamale', + 'tamandua', + 'tamarack', + 'tamarau', + 'tamarin', + 'tamarind', + 'tamarisk', + 'tamasha', + 'tambac', + 'tambour', + 'tamboura', + 'tambourin', + 'tambourine', + 'tame', + 'tameless', + 'tamis', + 'tammy', + 'tamp', + 'tamper', + 'tampon', + 'tan', + 'tana', + 'tanager', + 'tanbark', + 'tandem', + 'tang', + 'tangelo', + 'tangency', + 'tangent', + 'tangential', + 'tangerine', + 'tangible', + 'tangle', + 'tangleberry', + 'tangled', + 'tango', + 'tangram', + 'tangy', + 'tanh', + 'tank', + 'tanka', + 'tankage', + 'tankard', + 'tanked', + 'tanker', + 'tannage', + 'tannate', + 'tanner', + 'tannery', + 'tannic', + 'tannin', + 'tanning', + 'tansy', + 'tantalate', + 'tantalic', + 'tantalite', + 'tantalize', + 'tantalizing', + 'tantalous', + 'tantalum', + 'tantamount', + 'tantara', + 'tantivy', + 'tanto', + 'tantrum', + 'tap', + 'tape', + 'taper', + 'tapestry', + 'tapetum', + 'tapeworm', + 'taphole', + 'taphouse', + 'tapioca', + 'tapir', + 'tapis', + 'tappet', + 'tapping', + 'taproom', + 'taproot', + 'taps', + 'tapster', + 'tar', + 'taradiddle', + 'tarantass', + 'tarantella', + 'tarantula', + 'taraxacum', + 'tarboosh', + 'tardigrade', + 'tardy', + 'tare', + 'targe', + 'target', + 'tariff', + 'tarlatan', + 'tarn', + 'tarnation', + 'tarnish', + 'taro', + 'tarp', + 'tarpan', + 'tarpaulin', + 'tarpon', + 'tarradiddle', + 'tarragon', + 'tarriance', + 'tarry', + 'tarsal', + 'tarsia', + 'tarsier', + 'tarsometatarsus', + 'tarsus', + 'tart', + 'tartan', + 'tartar', + 'tartaric', + 'tartarous', + 'tartlet', + 'tartrate', + 'tartrazine', + 'tarweed', + 'tasimeter', + 'task', + 'taskmaster', + 'taskwork', + 'tass', + 'tasse', + 'tassel', + 'tasset', + 'taste', + 'tasteful', + 'tasteless', + 'taster', + 'tasty', + 'tat', + 'tater', + 'tatouay', + 'tatter', + 'tatterdemalion', + 'tattered', + 'tatting', + 'tattle', + 'tattler', + 'tattletale', + 'tattoo', + 'tatty', + 'tau', + 'taught', + 'taunt', + 'taupe', + 'taurine', + 'tauromachy', + 'taut', + 'tauten', + 'tautog', + 'tautologism', + 'tautologize', + 'tautology', + 'tautomer', + 'tautomerism', + 'tautonym', + 'tavern', + 'taverner', + 'taw', + 'tawdry', + 'tawny', + 'tax', + 'taxable', + 'taxaceous', + 'taxation', + 'taxeme', + 'taxi', + 'taxicab', + 'taxidermy', + 'taxiplane', + 'taxis', + 'taxiway', + 'taxonomy', + 'taxpayer', + 'tayra', + 'tazza', + 'tea', + 'teacake', + 'teacart', + 'teach', + 'teacher', + 'teaching', + 'teacup', + 'teahouse', + 'teak', + 'teakettle', + 'teakwood', + 'teal', + 'team', + 'teammate', + 'teamster', + 'teamwork', + 'teapot', + 'tear', + 'tearful', + 'tearing', + 'tearoom', + 'tears', + 'teary', + 'tease', + 'teasel', + 'teaser', + 'teaspoon', + 'teat', + 'teatime', + 'teazel', + 'technetium', + 'technic', + 'technical', + 'technicality', + 'technician', + 'technics', + 'technique', + 'technocracy', + 'technology', + 'techy', + 'tectonic', + 'tectonics', + 'tectrix', + 'ted', + 'tedder', + 'tedious', + 'tedium', + 'tee', + 'teem', + 'teeming', + 'teen', + 'teenager', + 'teens', + 'teeny', + 'teenybopper', + 'teepee', + 'teeter', + 'teeterboard', + 'teeth', + 'teethe', + 'teetotal', + 'teetotaler', + 'teetotalism', + 'teetotum', + 'tefillin', + 'tegmen', + 'tegular', + 'tegument', + 'tektite', + 'telamon', + 'telangiectasis', + 'telecast', + 'telecommunication', + 'teledu', + 'telefilm', + 'telega', + 'telegenic', + 'telegony', + 'telegram', + 'telegraph', + 'telegraphese', + 'telegraphic', + 'telegraphone', + 'telegraphy', + 'telekinesis', + 'telemark', + 'telemechanics', + 'telemeter', + 'telemetry', + 'telemotor', + 'telencephalon', + 'teleology', + 'teleost', + 'telepathist', + 'telepathy', + 'telephone', + 'telephonic', + 'telephonist', + 'telephony', + 'telephoto', + 'telephotography', + 'teleplay', + 'teleprinter', + 'teleran', + 'telescope', + 'telescopic', + 'telescopy', + 'telesis', + 'telespectroscope', + 'telesthesia', + 'telestich', + 'telethermometer', + 'telethon', + 'teletypewriter', + 'teleutospore', + 'teleview', + 'televise', + 'television', + 'televisor', + 'telex', + 'telfer', + 'telic', + 'teliospore', + 'telium', + 'tell', + 'teller', + 'telling', + 'telltale', + 'tellurate', + 'tellurian', + 'telluric', + 'telluride', + 'tellurion', + 'tellurite', + 'tellurium', + 'tellurize', + 'telly', + 'telophase', + 'telpher', + 'telpherage', + 'telson', + 'temblor', + 'temerity', + 'temper', + 'tempera', + 'temperament', + 'temperamental', + 'temperance', + 'temperate', + 'temperature', + 'tempered', + 'tempest', + 'tempestuous', + 'tempi', + 'template', + 'temple', + 'templet', + 'tempo', + 'temporal', + 'temporary', + 'temporize', + 'tempt', + 'temptation', + 'tempting', + 'temptress', + 'tempura', + 'ten', + 'tenable', + 'tenace', + 'tenacious', + 'tenaculum', + 'tenaille', + 'tenancy', + 'tenant', + 'tenantry', + 'tench', + 'tend', + 'tendance', + 'tendency', + 'tendentious', + 'tender', + 'tenderfoot', + 'tenderhearted', + 'tenderize', + 'tenderloin', + 'tendinous', + 'tendon', + 'tendril', + 'tenebrific', + 'tenebrous', + 'tenement', + 'tenesmus', + 'tenet', + 'tenfold', + 'tenia', + 'teniacide', + 'teniafuge', + 'tenne', + 'tennis', + 'tenno', + 'tenon', + 'tenor', + 'tenorite', + 'tenorrhaphy', + 'tenotomy', + 'tenpenny', + 'tenpin', + 'tenpins', + 'tenrec', + 'tense', + 'tensible', + 'tensile', + 'tensimeter', + 'tensiometer', + 'tension', + 'tensity', + 'tensive', + 'tensor', + 'tent', + 'tentacle', + 'tentage', + 'tentation', + 'tentative', + 'tented', + 'tenter', + 'tenterhook', + 'tenth', + 'tentmaker', + 'tenuis', + 'tenuous', + 'tenure', + 'tenuto', + 'teocalli', + 'teosinte', + 'tepee', + 'tepefy', + 'tephra', + 'tephrite', + 'tepid', + 'tequila', + 'teratism', + 'teratogenic', + 'teratoid', + 'teratology', + 'terbia', + 'terbium', + 'terce', + 'tercel', + 'tercentenary', + 'tercet', + 'terebene', + 'terebinthine', + 'teredo', + 'terefah', + 'terete', + 'tergal', + 'tergiversate', + 'tergum', + 'teriyaki', + 'term', + 'termagant', + 'terminable', + 'terminal', + 'terminate', + 'termination', + 'terminator', + 'terminology', + 'terminus', + 'termitarium', + 'termite', + 'termless', + 'termor', + 'terms', + 'tern', + 'ternary', + 'ternate', + 'ternion', + 'terpene', + 'terpineol', + 'terpsichorean', + 'terra', + 'terrace', + 'terrain', + 'terrane', + 'terrapin', + 'terraqueous', + 'terrarium', + 'terrazzo', + 'terrene', + 'terrestrial', + 'terret', + 'terrible', + 'terribly', + 'terricolous', + 'terrier', + 'terrific', + 'terrify', + 'terrigenous', + 'terrine', + 'territorial', + 'territorialism', + 'territoriality', + 'territorialize', + 'territory', + 'terror', + 'terrorism', + 'terrorist', + 'terrorize', + 'terry', + 'terse', + 'tertial', + 'tertian', + 'tertiary', + 'tervalent', + 'terzetto', + 'tesla', + 'tessellate', + 'tessellated', + 'tessellation', + 'tessera', + 'tessitura', + 'test', + 'testa', + 'testaceous', + 'testament', + 'testamentary', + 'testate', + 'testator', + 'testee', + 'tester', + 'testes', + 'testicle', + 'testify', + 'testimonial', + 'testimony', + 'testis', + 'teston', + 'testosterone', + 'testudinal', + 'testudo', + 'testy', + 'tetanic', + 'tetanize', + 'tetanus', + 'tetany', + 'tetartohedral', + 'tetchy', + 'teth', + 'tether', + 'tetherball', + 'tetra', + 'tetrabasic', + 'tetrabrach', + 'tetrabranchiate', + 'tetracaine', + 'tetrachloride', + 'tetrachord', + 'tetracycline', + 'tetrad', + 'tetradymite', + 'tetrafluoroethylene', + 'tetragon', + 'tetragonal', + 'tetragram', + 'tetrahedral', + 'tetrahedron', + 'tetralogy', + 'tetrameter', + 'tetramethyldiarsine', + 'tetraploid', + 'tetrapod', + 'tetrapody', + 'tetrapterous', + 'tetrarch', + 'tetraspore', + 'tetrastich', + 'tetrastichous', + 'tetrasyllable', + 'tetratomic', + 'tetravalent', + 'tetrode', + 'tetroxide', + 'tetryl', + 'tetter', + 'text', + 'textbook', + 'textile', + 'textual', + 'textualism', + 'textualist', + 'textuary', + 'texture', + 'thalamencephalon', + 'thalamus', + 'thalassic', + 'thalassography', + 'thaler', + 'thalidomide', + 'thallic', + 'thallium', + 'thallophyte', + 'thallus', + 'thalweg', + 'than', + 'thanatopsis', + 'thane', + 'thank', + 'thankful', + 'thankless', + 'thanks', + 'thanksgiving', + 'thar', + 'that', + 'thatch', + 'thaumatology', + 'thaumatrope', + 'thaumaturge', + 'thaumaturgy', + 'thaw', + 'the', + 'theaceous', + 'thearchy', + 'theater', + 'theatre', + 'theatrical', + 'theatricalize', + 'theatricals', + 'theatrician', + 'theatrics', + 'thebaine', + 'theca', + 'thee', + 'theft', + 'thegn', + 'theine', + 'their', + 'theirs', + 'theism', + 'them', + 'thematic', + 'theme', + 'themselves', + 'then', + 'thenar', + 'thence', + 'thenceforth', + 'thenceforward', + 'theocentric', + 'theocracy', + 'theocrasy', + 'theodicy', + 'theodolite', + 'theogony', + 'theologian', + 'theological', + 'theologize', + 'theologue', + 'theology', + 'theomachy', + 'theomancy', + 'theomania', + 'theomorphic', + 'theophany', + 'theophylline', + 'theorbo', + 'theorem', + 'theoretical', + 'theoretician', + 'theoretics', + 'theorist', + 'theorize', + 'theory', + 'theosophy', + 'therapeutic', + 'therapeutics', + 'therapist', + 'therapsid', + 'therapy', + 'there', + 'thereabout', + 'thereabouts', + 'thereafter', + 'thereat', + 'thereby', + 'therefor', + 'therefore', + 'therefrom', + 'therein', + 'thereinafter', + 'thereinto', + 'thereof', + 'thereon', + 'thereto', + 'theretofore', + 'thereunder', + 'thereupon', + 'therewith', + 'therewithal', + 'therianthropic', + 'therm', + 'thermae', + 'thermaesthesia', + 'thermal', + 'thermel', + 'thermic', + 'thermion', + 'thermionic', + 'thermionics', + 'thermistor', + 'thermobarograph', + 'thermobarometer', + 'thermochemistry', + 'thermocline', + 'thermocouple', + 'thermodynamic', + 'thermodynamics', + 'thermoelectric', + 'thermoelectricity', + 'thermoelectrometer', + 'thermogenesis', + 'thermograph', + 'thermography', + 'thermolabile', + 'thermoluminescence', + 'thermoluminescent', + 'thermolysis', + 'thermomagnetic', + 'thermometer', + 'thermometry', + 'thermomotor', + 'thermonuclear', + 'thermophone', + 'thermopile', + 'thermoplastic', + 'thermoscope', + 'thermosetting', + 'thermosiphon', + 'thermosphere', + 'thermostat', + 'thermostatics', + 'thermotaxis', + 'thermotensile', + 'thermotherapy', + 'theroid', + 'theropod', + 'thesaurus', + 'these', + 'thesis', + 'thespian', + 'theta', + 'thetic', + 'theurgy', + 'thew', + 'they', + 'thiamine', + 'thiazine', + 'thiazole', + 'thick', + 'thicken', + 'thickening', + 'thicket', + 'thickhead', + 'thickleaf', + 'thickness', + 'thickset', + 'thief', + 'thieve', + 'thievery', + 'thievish', + 'thigh', + 'thighbone', + 'thigmotaxis', + 'thigmotropism', + 'thill', + 'thimble', + 'thimbleful', + 'thimblerig', + 'thimbleweed', + 'thimerosal', + 'thin', + 'thine', + 'thing', + 'thingumabob', + 'thingumajig', + 'think', + 'thinkable', + 'thinker', + 'thinking', + 'thinner', + 'thinnish', + 'thiol', + 'thionate', + 'thionic', + 'thiosinamine', + 'thiouracil', + 'thiourea', + 'third', + 'thirlage', + 'thirst', + 'thirsty', + 'thirteen', + 'thirteenth', + 'thirtieth', + 'thirty', + 'this', + 'thistle', + 'thistledown', + 'thistly', + 'thither', + 'thitherto', + 'tho', + 'thole', + 'tholos', + 'thong', + 'thoracic', + 'thoracoplasty', + 'thoracotomy', + 'thorax', + 'thoria', + 'thorianite', + 'thorite', + 'thorium', + 'thorn', + 'thorny', + 'thoron', + 'thorough', + 'thoroughbred', + 'thoroughfare', + 'thoroughgoing', + 'thoroughpaced', + 'thoroughwort', + 'thorp', + 'those', + 'thou', + 'though', + 'thought', + 'thoughtful', + 'thoughtless', + 'thousand', + 'thousandfold', + 'thousandth', + 'thrall', + 'thralldom', + 'thrash', + 'thrasher', + 'thrashing', + 'thrasonical', + 'thrave', + 'thrawn', + 'thread', + 'threadbare', + 'threadfin', + 'thready', + 'threap', + 'threat', + 'threaten', + 'three', + 'threefold', + 'threepence', + 'threescore', + 'threesome', + 'thremmatology', + 'threnode', + 'threnody', + 'threonine', + 'thresh', + 'thresher', + 'threshold', + 'threw', + 'thrice', + 'thrift', + 'thriftless', + 'thrifty', + 'thrill', + 'thriller', + 'thrilling', + 'thrippence', + 'thrips', + 'thrive', + 'throat', + 'throaty', + 'throb', + 'throe', + 'throes', + 'thrombin', + 'thrombocyte', + 'thromboembolism', + 'thrombokinase', + 'thrombophlebitis', + 'thromboplastic', + 'thromboplastin', + 'thrombosis', + 'thrombus', + 'throne', + 'throng', + 'throstle', + 'throttle', + 'through', + 'throughout', + 'throughput', + 'throughway', + 'throve', + 'throw', + 'throwaway', + 'throwback', + 'thrower', + 'thrown', + 'thrum', + 'thrush', + 'thrust', + 'thruster', + 'thruway', + 'thud', + 'thug', + 'thuggee', + 'thuja', + 'thulium', + 'thumb', + 'thumbnail', + 'thumbprint', + 'thumbscrew', + 'thumbstall', + 'thumbtack', + 'thump', + 'thumping', + 'thunder', + 'thunderbolt', + 'thunderclap', + 'thundercloud', + 'thunderhead', + 'thundering', + 'thunderous', + 'thunderpeal', + 'thundershower', + 'thundersquall', + 'thunderstone', + 'thunderstorm', + 'thunderstruck', + 'thundery', + 'thurible', + 'thurifer', + 'thus', + 'thusly', + 'thuya', + 'thwack', + 'thwart', + 'thy', + 'thylacine', + 'thyme', + 'thymelaeaceous', + 'thymic', + 'thymol', + 'thymus', + 'thyratron', + 'thyroid', + 'thyroiditis', + 'thyrotoxicosis', + 'thyroxine', + 'thyrse', + 'thyrsus', + 'thyself', + 'ti', + 'tiara', + 'tibia', + 'tibiotarsus', + 'tic', + 'tical', + 'tick', + 'ticker', + 'ticket', + 'ticking', + 'tickle', + 'tickler', + 'ticklish', + 'ticktack', + 'ticktock', + 'tidal', + 'tidbit', + 'tiddly', + 'tiddlywinks', + 'tide', + 'tideland', + 'tidemark', + 'tidewaiter', + 'tidewater', + 'tideway', + 'tidings', + 'tidy', + 'tie', + 'tieback', + 'tied', + 'tiemannite', + 'tier', + 'tierce', + 'tiercel', + 'tiff', + 'tiffin', + 'tiger', + 'tigerish', + 'tight', + 'tighten', + 'tightfisted', + 'tightrope', + 'tights', + 'tightwad', + 'tigon', + 'tigress', + 'tike', + 'tiki', + 'til', + 'tilbury', + 'tilde', + 'tile', + 'tilefish', + 'tiliaceous', + 'tiling', + 'till', + 'tillage', + 'tillandsia', + 'tiller', + 'tilt', + 'tilth', + 'tiltyard', + 'timbal', + 'timbale', + 'timber', + 'timbered', + 'timberhead', + 'timbering', + 'timberland', + 'timberwork', + 'timbre', + 'timbrel', + 'time', + 'timecard', + 'timekeeper', + 'timeless', + 'timely', + 'timeous', + 'timepiece', + 'timepleaser', + 'timer', + 'timeserver', + 'timetable', + 'timework', + 'timeworn', + 'timid', + 'timing', + 'timocracy', + 'timorous', + 'timothy', + 'timpani', + 'tin', + 'tinamou', + 'tincal', + 'tinct', + 'tinctorial', + 'tincture', + 'tinder', + 'tinderbox', + 'tine', + 'tinea', + 'tineid', + 'tinfoil', + 'ting', + 'tinge', + 'tingle', + 'tingly', + 'tinhorn', + 'tinker', + 'tinkle', + 'tinkling', + 'tinned', + 'tinner', + 'tinnitus', + 'tinny', + 'tinsel', + 'tinsmith', + 'tinstone', + 'tint', + 'tintinnabulation', + 'tintinnabulum', + 'tintometer', + 'tintype', + 'tinware', + 'tinworks', + 'tiny', + 'tip', + 'tipcat', + 'tipi', + 'tipper', + 'tippet', + 'tipple', + 'tippler', + 'tipstaff', + 'tipster', + 'tipsy', + 'tiptoe', + 'tiptop', + 'tirade', + 'tire', + 'tired', + 'tireless', + 'tiresome', + 'tirewoman', + 'tiro', + 'tisane', + 'tissue', + 'tit', + 'titan', + 'titanate', + 'titania', + 'titanic', + 'titanite', + 'titanium', + 'titanothere', + 'titbit', + 'titer', + 'titfer', + 'tithable', + 'tithe', + 'tithing', + 'titi', + 'titillate', + 'titivate', + 'titlark', + 'title', + 'titled', + 'titleholder', + 'titmouse', + 'titrant', + 'titrate', + 'titration', + 'titre', + 'titter', + 'tittivate', + 'tittle', + 'tittup', + 'titty', + 'titular', + 'titulary', + 'tizzy', + 'tmesis', + 'to', + 'toad', + 'toadeater', + 'toadfish', + 'toadflax', + 'toadstool', + 'toady', + 'toast', + 'toaster', + 'toastmaster', + 'tobacco', + 'tobacconist', + 'toboggan', + 'toccata', + 'tocology', + 'tocopherol', + 'tocsin', + 'tod', + 'today', + 'toddle', + 'toddler', + 'toddy', + 'tody', + 'toe', + 'toed', + 'toehold', + 'toenail', + 'toffee', + 'toft', + 'tog', + 'toga', + 'together', + 'togetherness', + 'toggery', + 'toggle', + 'togs', + 'tohubohu', + 'toil', + 'toile', + 'toilet', + 'toiletry', + 'toilette', + 'toilsome', + 'toilworn', + 'tokay', + 'token', + 'tokenism', + 'tokoloshe', + 'tola', + 'tolan', + 'tolbooth', + 'tolbutamide', + 'told', + 'tole', + 'tolerable', + 'tolerance', + 'tolerant', + 'tolerate', + 'toleration', + 'tolidine', + 'toll', + 'tollbooth', + 'tollgate', + 'tollhouse', + 'tolly', + 'tolu', + 'toluate', + 'toluene', + 'toluidine', + 'toluol', + 'tolyl', + 'tom', + 'tomahawk', + 'tomato', + 'tomb', + 'tombac', + 'tombola', + 'tombolo', + 'tomboy', + 'tombstone', + 'tomcat', + 'tome', + 'tomfool', + 'tomfoolery', + 'tommyrot', + 'tomorrow', + 'tompion', + 'tomtit', + 'ton', + 'tonal', + 'tonality', + 'tone', + 'toneless', + 'toneme', + 'tonetic', + 'tong', + 'tonga', + 'tongs', + 'tongue', + 'tonguing', + 'tonic', + 'tonicity', + 'tonight', + 'tonnage', + 'tonne', + 'tonneau', + 'tonometer', + 'tonsil', + 'tonsillectomy', + 'tonsillitis', + 'tonsillotomy', + 'tonsorial', + 'tonsure', + 'tontine', + 'tonus', + 'tony', + 'too', + 'took', + 'tool', + 'tooling', + 'toolmaker', + 'toot', + 'tooth', + 'toothache', + 'toothbrush', + 'toothed', + 'toothless', + 'toothlike', + 'toothpaste', + 'toothpick', + 'toothsome', + 'toothwort', + 'toothy', + 'tootle', + 'toots', + 'tootsy', + 'top', + 'topaz', + 'topazolite', + 'topcoat', + 'tope', + 'topee', + 'toper', + 'topflight', + 'topfull', + 'topgallant', + 'tophus', + 'topi', + 'topic', + 'topical', + 'topknot', + 'topless', + 'toplofty', + 'topmast', + 'topminnow', + 'topmost', + 'topnotch', + 'topographer', + 'topography', + 'topology', + 'toponym', + 'toponymy', + 'topotype', + 'topper', + 'topping', + 'topple', + 'tops', + 'topsail', + 'topside', + 'topsoil', + 'toque', + 'tor', + 'torbernite', + 'torch', + 'torchbearer', + 'torchier', + 'torchwood', + 'tore', + 'toreador', + 'torero', + 'toreutic', + 'toreutics', + 'torii', + 'torment', + 'tormentil', + 'tormentor', + 'torn', + 'tornado', + 'torose', + 'torpedo', + 'torpedoman', + 'torpid', + 'torpor', + 'torque', + 'torques', + 'torr', + 'torrefy', + 'torrent', + 'torrential', + 'torrid', + 'torse', + 'torsi', + 'torsibility', + 'torsion', + 'torsk', + 'torso', + 'tort', + 'torticollis', + 'tortile', + 'tortilla', + 'tortious', + 'tortoise', + 'tortoni', + 'tortricid', + 'tortuosity', + 'tortuous', + 'torture', + 'torus', + 'tosh', + 'toss', + 'tosspot', + 'tot', + 'total', + 'totalitarian', + 'totalitarianism', + 'totality', + 'totalizator', + 'totalizer', + 'totally', + 'totaquine', + 'tote', + 'totem', + 'totemism', + 'tother', + 'toting', + 'totipalmate', + 'totter', + 'tottering', + 'toucan', + 'touch', + 'touchback', + 'touchdown', + 'touched', + 'touchhole', + 'touching', + 'touchline', + 'touchstone', + 'touchwood', + 'touchy', + 'tough', + 'toughen', + 'toughie', + 'toupee', + 'tour', + 'touraco', + 'tourbillion', + 'tourer', + 'tourism', + 'tourist', + 'touristy', + 'tourmaline', + 'tournament', + 'tournedos', + 'tourney', + 'tourniquet', + 'tousle', + 'tout', + 'touter', + 'touzle', + 'tow', + 'towage', + 'toward', + 'towardly', + 'towards', + 'towboat', + 'towel', + 'toweling', + 'towelling', + 'tower', + 'towering', + 'towery', + 'towhead', + 'towhee', + 'towline', + 'town', + 'townscape', + 'townsfolk', + 'township', + 'townsman', + 'townspeople', + 'townswoman', + 'towpath', + 'towrope', + 'toxemia', + 'toxic', + 'toxicant', + 'toxicity', + 'toxicogenic', + 'toxicology', + 'toxicosis', + 'toxin', + 'toxoid', + 'toxophilite', + 'toxoplasmosis', + 'toy', + 'trabeated', + 'trace', + 'traceable', + 'tracer', + 'tracery', + 'trachea', + 'tracheid', + 'tracheitis', + 'tracheostomy', + 'tracheotomy', + 'trachoma', + 'trachyte', + 'trachytic', + 'tracing', + 'track', + 'trackless', + 'trackman', + 'tract', + 'tractable', + 'tractate', + 'tractile', + 'traction', + 'tractor', + 'trade', + 'trademark', + 'trader', + 'tradescantia', + 'tradesfolk', + 'tradesman', + 'tradespeople', + 'tradeswoman', + 'tradition', + 'traditional', + 'traditionalism', + 'traditor', + 'traduce', + 'traffic', + 'trafficator', + 'tragacanth', + 'tragedian', + 'tragedienne', + 'tragedy', + 'tragic', + 'tragicomedy', + 'tragopan', + 'tragus', + 'trail', + 'trailblazer', + 'trailer', + 'train', + 'trainband', + 'trainbearer', + 'trainee', + 'trainer', + 'training', + 'trainload', + 'trainman', + 'traipse', + 'trait', + 'traitor', + 'traitorous', + 'traject', + 'trajectory', + 'tram', + 'tramline', + 'trammel', + 'tramontane', + 'tramp', + 'trample', + 'trampoline', + 'tramroad', + 'tramway', + 'trance', + 'tranche', + 'tranquil', + 'tranquilize', + 'tranquilizer', + 'tranquillity', + 'tranquillize', + 'transact', + 'transaction', + 'transalpine', + 'transarctic', + 'transatlantic', + 'transcalent', + 'transceiver', + 'transcend', + 'transcendence', + 'transcendent', + 'transcendental', + 'transcendentalism', + 'transcendentalistic', + 'transcontinental', + 'transcribe', + 'transcript', + 'transcription', + 'transcurrent', + 'transducer', + 'transduction', + 'transect', + 'transept', + 'transeunt', + 'transfer', + 'transferase', + 'transference', + 'transferor', + 'transfiguration', + 'transfigure', + 'transfinite', + 'transfix', + 'transform', + 'transformation', + 'transformer', + 'transformism', + 'transfuse', + 'transfusion', + 'transgress', + 'transgression', + 'tranship', + 'transhumance', + 'transience', + 'transient', + 'transilient', + 'transilluminate', + 'transistor', + 'transistorize', + 'transit', + 'transition', + 'transitive', + 'transitory', + 'translatable', + 'translate', + 'translation', + 'translative', + 'translator', + 'transliterate', + 'translocate', + 'translocation', + 'translucent', + 'translucid', + 'translunar', + 'transmarine', + 'transmigrant', + 'transmigrate', + 'transmissible', + 'transmission', + 'transmit', + 'transmittal', + 'transmittance', + 'transmitter', + 'transmogrify', + 'transmontane', + 'transmundane', + 'transmutation', + 'transmute', + 'transnational', + 'transoceanic', + 'transom', + 'transonic', + 'transpacific', + 'transpadane', + 'transparency', + 'transparent', + 'transpicuous', + 'transpierce', + 'transpire', + 'transplant', + 'transpolar', + 'transponder', + 'transpontine', + 'transport', + 'transportation', + 'transported', + 'transposal', + 'transpose', + 'transposition', + 'transship', + 'transsonic', + 'transubstantiate', + 'transubstantiation', + 'transudate', + 'transudation', + 'transude', + 'transvalue', + 'transversal', + 'transverse', + 'transvestite', + 'trap', + 'trapan', + 'trapes', + 'trapeze', + 'trapeziform', + 'trapezium', + 'trapezius', + 'trapezohedron', + 'trapezoid', + 'trapper', + 'trappings', + 'traprock', + 'traps', + 'trapshooting', + 'trash', + 'trashy', + 'trass', + 'trattoria', + 'trauma', + 'traumatism', + 'traumatize', + 'travail', + 'trave', + 'travel', + 'traveled', + 'traveler', + 'travelled', + 'traveller', + 'traverse', + 'travertine', + 'travesty', + 'trawl', + 'trawler', + 'tray', + 'treacherous', + 'treachery', + 'treacle', + 'tread', + 'treadle', + 'treadmill', + 'treason', + 'treasonable', + 'treasonous', + 'treasure', + 'treasurer', + 'treasury', + 'treat', + 'treatise', + 'treatment', + 'treaty', + 'treble', + 'trebuchet', + 'tredecillion', + 'tree', + 'treed', + 'treehopper', + 'treen', + 'treenail', + 'treenware', + 'tref', + 'trefoil', + 'trehala', + 'trehalose', + 'treillage', + 'trek', + 'trellis', + 'trelliswork', + 'trematode', + 'tremble', + 'trembles', + 'trembly', + 'tremendous', + 'tremolant', + 'tremolite', + 'tremolo', + 'tremor', + 'tremulant', + 'tremulous', + 'trenail', + 'trench', + 'trenchant', + 'trencher', + 'trencherman', + 'trend', + 'trepan', + 'trepang', + 'trephine', + 'trepidation', + 'treponema', + 'trespass', + 'tress', + 'tressure', + 'trestle', + 'trestlework', + 'tret', + 'trews', + 'trey', + 'triable', + 'triacid', + 'triad', + 'triadelphous', + 'triage', + 'trial', + 'triangle', + 'triangular', + 'triangulate', + 'triangulation', + 'triarchy', + 'triatomic', + 'triaxial', + 'triazine', + 'tribade', + 'tribadism', + 'tribal', + 'tribalism', + 'tribasic', + 'tribe', + 'tribesman', + 'triboelectricity', + 'triboluminescence', + 'triboluminescent', + 'tribrach', + 'tribromoethanol', + 'tribulation', + 'tribunal', + 'tribunate', + 'tribune', + 'tributary', + 'tribute', + 'trice', + 'triceps', + 'triceratops', + 'trichiasis', + 'trichina', + 'trichinize', + 'trichinosis', + 'trichite', + 'trichloride', + 'trichloroethylene', + 'trichloromethane', + 'trichocyst', + 'trichoid', + 'trichology', + 'trichome', + 'trichomonad', + 'trichomoniasis', + 'trichosis', + 'trichotomy', + 'trichroism', + 'trichromat', + 'trichromatic', + 'trichromatism', + 'trick', + 'trickery', + 'trickish', + 'trickle', + 'trickster', + 'tricksy', + 'tricky', + 'triclinic', + 'triclinium', + 'tricolor', + 'tricorn', + 'tricornered', + 'tricostate', + 'tricot', + 'tricotine', + 'tricrotic', + 'trictrac', + 'tricuspid', + 'tricycle', + 'tricyclic', + 'tridactyl', + 'trident', + 'tridimensional', + 'triecious', + 'tried', + 'triennial', + 'triennium', + 'trier', + 'trierarch', + 'trifacial', + 'trifid', + 'trifle', + 'trifling', + 'trifocal', + 'trifocals', + 'trifoliate', + 'trifolium', + 'triforium', + 'triform', + 'trifurcate', + 'trig', + 'trigeminal', + 'trigger', + 'triggerfish', + 'triglyceride', + 'triglyph', + 'trigon', + 'trigonal', + 'trigonometry', + 'trigonous', + 'trigraph', + 'trihedral', + 'trihedron', + 'trihydric', + 'triiodomethane', + 'trike', + 'trilateral', + 'trilateration', + 'trilby', + 'trilemma', + 'trilinear', + 'trilingual', + 'triliteral', + 'trill', + 'trillion', + 'trillium', + 'trilobate', + 'trilobite', + 'trilogy', + 'trim', + 'trimaran', + 'trimer', + 'trimerous', + 'trimester', + 'trimetallic', + 'trimeter', + 'trimetric', + 'trimetrogon', + 'trimly', + 'trimmer', + 'trimming', + 'trimolecular', + 'trimorphism', + 'trinal', + 'trinary', + 'trine', + 'trinitrobenzene', + 'trinitrocresol', + 'trinitroglycerin', + 'trinitrophenol', + 'trinitrotoluene', + 'trinity', + 'trinket', + 'trinomial', + 'trio', + 'triode', + 'trioecious', + 'triolein', + 'triolet', + 'trioxide', + 'trip', + 'tripalmitin', + 'triparted', + 'tripartite', + 'tripartition', + 'tripe', + 'tripedal', + 'tripersonal', + 'tripetalous', + 'triphammer', + 'triphibious', + 'triphthong', + 'triphylite', + 'tripinnate', + 'triplane', + 'triple', + 'triplet', + 'tripletail', + 'triplex', + 'triplicate', + 'triplicity', + 'triploid', + 'tripod', + 'tripodic', + 'tripody', + 'tripoli', + 'tripos', + 'tripper', + 'trippet', + 'tripping', + 'tripterous', + 'triptych', + 'triquetrous', + 'trireme', + 'trisaccharide', + 'trisect', + 'triserial', + 'triskelion', + 'trismus', + 'trisoctahedron', + 'trisomic', + 'triste', + 'tristich', + 'tristichous', + 'trisyllable', + 'tritanopia', + 'trite', + 'tritheism', + 'tritium', + 'triton', + 'triturable', + 'triturate', + 'trituration', + 'triumph', + 'triumphal', + 'triumphant', + 'triumvir', + 'triumvirate', + 'triune', + 'trivalent', + 'trivet', + 'trivia', + 'trivial', + 'triviality', + 'trivium', + 'troat', + 'trocar', + 'trochaic', + 'trochal', + 'trochanter', + 'troche', + 'trochee', + 'trochelminth', + 'trochilus', + 'trochlear', + 'trochophore', + 'trod', + 'trodden', + 'troglodyte', + 'trogon', + 'troika', + 'troll', + 'trolley', + 'trollop', + 'trolly', + 'trombidiasis', + 'trombone', + 'trommel', + 'trompe', + 'trona', + 'troop', + 'trooper', + 'troopship', + 'troostite', + 'tropaeolin', + 'trope', + 'trophic', + 'trophoblast', + 'trophoplasm', + 'trophozoite', + 'trophy', + 'tropic', + 'tropical', + 'tropicalize', + 'tropine', + 'tropism', + 'tropology', + 'tropopause', + 'tropophilous', + 'troposphere', + 'trot', + 'troth', + 'trothplight', + 'trotline', + 'trotter', + 'trotyl', + 'troubadour', + 'trouble', + 'troublemaker', + 'troublesome', + 'troublous', + 'trough', + 'trounce', + 'troupe', + 'trouper', + 'trousers', + 'trousseau', + 'trout', + 'trouvaille', + 'trouveur', + 'trove', + 'trover', + 'trow', + 'trowel', + 'troy', + 'truancy', + 'truant', + 'truce', + 'truck', + 'truckage', + 'trucker', + 'trucking', + 'truckle', + 'truckload', + 'truculent', + 'trudge', + 'true', + 'truehearted', + 'truelove', + 'truffle', + 'trug', + 'truism', + 'trull', + 'truly', + 'trump', + 'trumpery', + 'trumpet', + 'trumpeter', + 'trumpetweed', + 'truncate', + 'truncated', + 'truncation', + 'truncheon', + 'trundle', + 'trunk', + 'trunkfish', + 'trunks', + 'trunnel', + 'trunnion', + 'truss', + 'trussing', + 'trust', + 'trustbuster', + 'trustee', + 'trusteeship', + 'trustful', + 'trusting', + 'trustless', + 'trustworthy', + 'trusty', + 'truth', + 'truthful', + 'try', + 'trying', + 'tryma', + 'tryout', + 'trypanosome', + 'trypanosomiasis', + 'tryparsamide', + 'trypsin', + 'tryptophan', + 'trysail', + 'tryst', + 'tsar', + 'tsarevitch', + 'tsarevna', + 'tsarina', + 'tsarism', + 'tsunami', + 'tuatara', + 'tub', + 'tuba', + 'tubate', + 'tubby', + 'tube', + 'tuber', + 'tubercle', + 'tubercular', + 'tuberculate', + 'tuberculin', + 'tuberculosis', + 'tuberculous', + 'tuberose', + 'tuberosity', + 'tuberous', + 'tubing', + 'tubular', + 'tubulate', + 'tubule', + 'tubuliflorous', + 'tubulure', + 'tuchun', + 'tuck', + 'tucker', + 'tucket', + 'tufa', + 'tuff', + 'tuft', + 'tufted', + 'tufthunter', + 'tug', + 'tugboat', + 'tui', + 'tuition', + 'tulip', + 'tulipwood', + 'tulle', + 'tum', + 'tumble', + 'tumblebug', + 'tumbledown', + 'tumbler', + 'tumbleweed', + 'tumbling', + 'tumbrel', + 'tumefacient', + 'tumefaction', + 'tumefy', + 'tumescent', + 'tumid', + 'tummy', + 'tumor', + 'tumpline', + 'tumular', + 'tumult', + 'tumultuous', + 'tumulus', + 'tun', + 'tuna', + 'tunable', + 'tundra', + 'tune', + 'tuneful', + 'tuneless', + 'tuner', + 'tunesmith', + 'tungstate', + 'tungsten', + 'tungstic', + 'tungstite', + 'tunic', + 'tunicate', + 'tunicle', + 'tuning', + 'tunnage', + 'tunnel', + 'tunny', + 'tupelo', + 'tuppence', + 'tuque', + 'turaco', + 'turban', + 'turbary', + 'turbellarian', + 'turbid', + 'turbidimeter', + 'turbinal', + 'turbinate', + 'turbine', + 'turbit', + 'turbofan', + 'turbojet', + 'turboprop', + 'turbosupercharger', + 'turbot', + 'turbulence', + 'turbulent', + 'turd', + 'turdine', + 'tureen', + 'turf', + 'turfman', + 'turfy', + 'turgent', + 'turgescent', + 'turgid', + 'turgite', + 'turgor', + 'turkey', + 'turmeric', + 'turmoil', + 'turn', + 'turnabout', + 'turnaround', + 'turnbuckle', + 'turncoat', + 'turner', + 'turnery', + 'turning', + 'turnip', + 'turnkey', + 'turnout', + 'turnover', + 'turnpike', + 'turnsole', + 'turnspit', + 'turnstile', + 'turnstone', + 'turntable', + 'turpentine', + 'turpeth', + 'turpitude', + 'turquoise', + 'turret', + 'turtle', + 'turtleback', + 'turtledove', + 'turtleneck', + 'turves', + 'tusche', + 'tush', + 'tushy', + 'tusk', + 'tusker', + 'tussah', + 'tussis', + 'tussle', + 'tussock', + 'tussore', + 'tut', + 'tutelage', + 'tutelary', + 'tutor', + 'tutorial', + 'tutti', + 'tutty', + 'tutu', + 'tuxedo', + 'tuyere', + 'twaddle', + 'twain', + 'twang', + 'twattle', + 'twayblade', + 'tweak', + 'tweed', + 'tweedy', + 'tweeny', + 'tweet', + 'tweeter', + 'tweeze', + 'tweezers', + 'twelfth', + 'twelve', + 'twelvemo', + 'twelvemonth', + 'twentieth', + 'twenty', + 'twerp', + 'twibill', + 'twice', + 'twiddle', + 'twig', + 'twiggy', + 'twilight', + 'twill', + 'twin', + 'twinberry', + 'twine', + 'twinflower', + 'twinge', + 'twink', + 'twinkle', + 'twinkling', + 'twinned', + 'twirl', + 'twirp', + 'twist', + 'twister', + 'twit', + 'twitch', + 'twitter', + 'twittery', + 'two', + 'twofold', + 'twopence', + 'twopenny', + 'twosome', + 'tycoon', + 'tyg', + 'tying', + 'tyke', + 'tylosis', + 'tymbal', + 'tympan', + 'tympanic', + 'tympanist', + 'tympanites', + 'tympanitis', + 'tympanum', + 'tympany', + 'typal', + 'type', + 'typebar', + 'typecase', + 'typecast', + 'typeface', + 'typescript', + 'typeset', + 'typesetter', + 'typesetting', + 'typewrite', + 'typewriter', + 'typewriting', + 'typewritten', + 'typhogenic', + 'typhoid', + 'typhoon', + 'typhus', + 'typical', + 'typify', + 'typist', + 'typo', + 'typographer', + 'typography', + 'typology', + 'tyrannical', + 'tyrannicide', + 'tyrannize', + 'tyrannosaur', + 'tyrannous', + 'tyranny', + 'tyrant', + 'tyro', + 'tyrocidine', + 'tyrosinase', + 'tyrosine', + 'tyrothricin', + 'tzar', + 'ubiety', + 'ubiquitous', + 'udder', + 'udo', + 'udometer', + 'ugh', + 'uglify', + 'ugly', + 'uhlan', + 'uintathere', + 'uitlander', + 'ukase', + 'ukulele', + 'ulcer', + 'ulcerate', + 'ulceration', + 'ulcerative', + 'ulcerous', + 'ulema', + 'ullage', + 'ulmaceous', + 'ulna', + 'ulotrichous', + 'ulster', + 'ulterior', + 'ultima', + 'ultimate', + 'ultimately', + 'ultimatum', + 'ultimo', + 'ultimogeniture', + 'ultra', + 'ultracentrifuge', + 'ultraconservative', + 'ultrafilter', + 'ultraism', + 'ultramarine', + 'ultramicrochemistry', + 'ultramicrometer', + 'ultramicroscope', + 'ultramicroscopic', + 'ultramodern', + 'ultramontane', + 'ultramontanism', + 'ultramundane', + 'ultranationalism', + 'ultrared', + 'ultrasonic', + 'ultrasonics', + 'ultrasound', + 'ultrastructure', + 'ultraviolet', + 'ultravirus', + 'ululant', + 'ululate', + 'umbel', + 'umbelliferous', + 'umber', + 'umbilical', + 'umbilicate', + 'umbilication', + 'umbilicus', + 'umbles', + 'umbra', + 'umbrage', + 'umbrageous', + 'umbrella', + 'umiak', + 'umlaut', + 'umpire', + 'umpteen', + 'unabated', + 'unable', + 'unabridged', + 'unaccompanied', + 'unaccomplished', + 'unaccountable', + 'unaccustomed', + 'unadvised', + 'unaesthetic', + 'unaffected', + 'unalienable', + 'unalloyed', + 'unalterable', + 'unaneled', + 'unanimity', + 'unanimous', + 'unanswerable', + 'unappealable', + 'unapproachable', + 'unapt', + 'unarm', + 'unarmed', + 'unashamed', + 'unasked', + 'unassailable', + 'unassuming', + 'unattached', + 'unattended', + 'unavailing', + 'unavoidable', + 'unaware', + 'unawares', + 'unbacked', + 'unbalance', + 'unbalanced', + 'unbar', + 'unbated', + 'unbearable', + 'unbeatable', + 'unbeaten', + 'unbecoming', + 'unbeknown', + 'unbelief', + 'unbelievable', + 'unbeliever', + 'unbelieving', + 'unbelt', + 'unbend', + 'unbending', + 'unbent', + 'unbiased', + 'unbidden', + 'unbind', + 'unblessed', + 'unblinking', + 'unblock', + 'unblown', + 'unblushing', + 'unbodied', + 'unbolt', + 'unbolted', + 'unboned', + 'unbonnet', + 'unborn', + 'unbosom', + 'unbound', + 'unbounded', + 'unbowed', + 'unbrace', + 'unbraid', + 'unbreathed', + 'unbridle', + 'unbridled', + 'unbroken', + 'unbuckle', + 'unbuild', + 'unburden', + 'unbutton', + 'uncanny', + 'uncanonical', + 'uncap', + 'uncaused', + 'unceasing', + 'unceremonious', + 'uncertain', + 'uncertainty', + 'unchain', + 'unchancy', + 'uncharitable', + 'uncharted', + 'unchartered', + 'unchaste', + 'unchristian', + 'unchurch', + 'uncial', + 'unciform', + 'uncinariasis', + 'uncinate', + 'uncinus', + 'uncircumcised', + 'uncircumcision', + 'uncivil', + 'uncivilized', + 'unclad', + 'unclasp', + 'unclassical', + 'unclassified', + 'uncle', + 'unclean', + 'uncleanly', + 'unclear', + 'unclench', + 'unclinch', + 'uncloak', + 'unclog', + 'unclose', + 'unclothe', + 'uncoil', + 'uncomfortable', + 'uncommercial', + 'uncommitted', + 'uncommon', + 'uncommonly', + 'uncommunicative', + 'uncompromising', + 'unconcern', + 'unconcerned', + 'unconditional', + 'unconditioned', + 'unconformable', + 'unconformity', + 'unconnected', + 'unconquerable', + 'unconscionable', + 'unconscious', + 'unconsidered', + 'unconstitutional', + 'uncontrollable', + 'unconventional', + 'unconventionality', + 'uncork', + 'uncounted', + 'uncouple', + 'uncourtly', + 'uncouth', + 'uncovenanted', + 'uncover', + 'uncovered', + 'uncritical', + 'uncrown', + 'uncrowned', + 'unction', + 'unctuous', + 'uncurl', + 'uncut', + 'undamped', + 'undaunted', + 'undecagon', + 'undeceive', + 'undecided', + 'undefined', + 'undemonstrative', + 'undeniable', + 'undenominational', + 'under', + 'underachieve', + 'underact', + 'underage', + 'underarm', + 'underbelly', + 'underbid', + 'underbodice', + 'underbody', + 'underbred', + 'underbrush', + 'undercarriage', + 'undercast', + 'undercharge', + 'underclassman', + 'underclay', + 'underclothes', + 'underclothing', + 'undercoat', + 'undercoating', + 'undercool', + 'undercover', + 'undercroft', + 'undercurrent', + 'undercut', + 'underdeveloped', + 'underdog', + 'underdone', + 'underdrawers', + 'underestimate', + 'underexpose', + 'underexposure', + 'underfeed', + 'underfoot', + 'underfur', + 'undergarment', + 'undergird', + 'underglaze', + 'undergo', + 'undergraduate', + 'underground', + 'undergrown', + 'undergrowth', + 'underhand', + 'underhanded', + 'underhung', + 'underlaid', + 'underlay', + 'underlayer', + 'underlet', + 'underlie', + 'underline', + 'underlinen', + 'underling', + 'underlying', + 'undermanned', + 'undermine', + 'undermost', + 'underneath', + 'undernourished', + 'underpainting', + 'underpants', + 'underpart', + 'underpass', + 'underpay', + 'underpin', + 'underpinning', + 'underpinnings', + 'underplay', + 'underplot', + 'underprivileged', + 'underproduction', + 'underproof', + 'underprop', + 'underquote', + 'underrate', + 'underscore', + 'undersea', + 'undersecretary', + 'undersell', + 'underset', + 'undersexed', + 'undersheriff', + 'undershirt', + 'undershoot', + 'undershorts', + 'undershot', + 'undershrub', + 'underside', + 'undersigned', + 'undersize', + 'undersized', + 'underskirt', + 'underslung', + 'understand', + 'understandable', + 'understanding', + 'understate', + 'understood', + 'understrapper', + 'understructure', + 'understudy', + 'undersurface', + 'undertake', + 'undertaker', + 'undertaking', + 'undertenant', + 'underthrust', + 'undertint', + 'undertone', + 'undertook', + 'undertow', + 'undertrick', + 'undertrump', + 'undervalue', + 'undervest', + 'underwaist', + 'underwater', + 'underwear', + 'underweight', + 'underwent', + 'underwing', + 'underwood', + 'underworld', + 'underwrite', + 'underwriter', + 'undesigned', + 'undesigning', + 'undesirable', + 'undetermined', + 'undeviating', + 'undies', + 'undine', + 'undirected', + 'undistinguished', + 'undo', + 'undoing', + 'undone', + 'undoubted', + 'undrape', + 'undress', + 'undressed', + 'undue', + 'undulant', + 'undulate', + 'undulation', + 'undulatory', + 'unduly', + 'undying', + 'unearned', + 'unearth', + 'unearthly', + 'uneasy', + 'uneducated', + 'unemployable', + 'unemployed', + 'unemployment', + 'unending', + 'unequal', + 'unequaled', + 'unequivocal', + 'unerring', + 'unessential', + 'uneven', + 'uneventful', + 'unexacting', + 'unexampled', + 'unexceptionable', + 'unexceptional', + 'unexpected', + 'unexperienced', + 'unexpressed', + 'unexpressive', + 'unfailing', + 'unfair', + 'unfaithful', + 'unfamiliar', + 'unfasten', + 'unfathomable', + 'unfavorable', + 'unfeeling', + 'unfeigned', + 'unfetter', + 'unfinished', + 'unfit', + 'unfix', + 'unfledged', + 'unfleshly', + 'unflinching', + 'unfold', + 'unfolded', + 'unforgettable', + 'unformed', + 'unfortunate', + 'unfounded', + 'unfreeze', + 'unfrequented', + 'unfriended', + 'unfriendly', + 'unfrock', + 'unfruitful', + 'unfurl', + 'ungainly', + 'ungenerous', + 'unglue', + 'ungodly', + 'ungotten', + 'ungovernable', + 'ungraceful', + 'ungracious', + 'ungrateful', + 'ungrounded', + 'ungrudging', + 'ungual', + 'unguarded', + 'unguent', + 'unguentum', + 'unguiculate', + 'unguinous', + 'ungula', + 'ungulate', + 'unhair', + 'unhallow', + 'unhallowed', + 'unhand', + 'unhandled', + 'unhandsome', + 'unhandy', + 'unhappy', + 'unharness', + 'unhealthy', + 'unheard', + 'unhelm', + 'unhesitating', + 'unhinge', + 'unhitch', + 'unholy', + 'unhook', + 'unhorse', + 'unhouse', + 'unhurried', + 'uniaxial', + 'unicameral', + 'unicellular', + 'unicorn', + 'unicuspid', + 'unicycle', + 'unideaed', + 'unidirectional', + 'unific', + 'unification', + 'unifilar', + 'uniflorous', + 'unifoliate', + 'unifoliolate', + 'uniform', + 'uniformed', + 'uniformitarian', + 'uniformity', + 'uniformize', + 'unify', + 'unijugate', + 'unilateral', + 'unilingual', + 'uniliteral', + 'unilobed', + 'unilocular', + 'unimpeachable', + 'unimposing', + 'unimproved', + 'uninhibited', + 'uninspired', + 'uninstructed', + 'unintelligent', + 'unintelligible', + 'unintentional', + 'uninterested', + 'uninterrupted', + 'uniocular', + 'union', + 'unionism', + 'unionist', + 'unionize', + 'uniparous', + 'unipersonal', + 'uniplanar', + 'unipod', + 'unipolar', + 'unique', + 'uniseptate', + 'unisexual', + 'unison', + 'unit', + 'unitary', + 'unite', + 'united', + 'unitive', + 'unity', + 'univalence', + 'univalent', + 'univalve', + 'universal', + 'universalism', + 'universalist', + 'universality', + 'universalize', + 'universally', + 'universe', + 'university', + 'univocal', + 'unjaundiced', + 'unjust', + 'unkempt', + 'unkenned', + 'unkennel', + 'unkind', + 'unkindly', + 'unknit', + 'unknot', + 'unknowable', + 'unknowing', + 'unknown', + 'unlace', + 'unlade', + 'unlash', + 'unlatch', + 'unlawful', + 'unlay', + 'unlearn', + 'unlearned', + 'unleash', + 'unleavened', + 'unless', + 'unlettered', + 'unlicensed', + 'unlike', + 'unlikelihood', + 'unlikely', + 'unlimber', + 'unlimited', + 'unlisted', + 'unlive', + 'unload', + 'unlock', + 'unloose', + 'unloosen', + 'unlovely', + 'unlucky', + 'unmade', + 'unmake', + 'unman', + 'unmanly', + 'unmanned', + 'unmannered', + 'unmannerly', + 'unmarked', + 'unmask', + 'unmeaning', + 'unmeant', + 'unmeasured', + 'unmeet', + 'unmentionable', + 'unmerciful', + 'unmeriting', + 'unmindful', + 'unmistakable', + 'unmitigated', + 'unmixed', + 'unmoor', + 'unmoral', + 'unmoved', + 'unmoving', + 'unmusical', + 'unmuzzle', + 'unnamed', + 'unnatural', + 'unnecessarily', + 'unnecessary', + 'unnerve', + 'unnumbered', + 'unobtrusive', + 'unoccupied', + 'unofficial', + 'unopened', + 'unorganized', + 'unorthodox', + 'unpack', + 'unpaged', + 'unpaid', + 'unparalleled', + 'unparliamentary', + 'unpeg', + 'unpen', + 'unpeople', + 'unpeopled', + 'unperforated', + 'unpile', + 'unpin', + 'unplaced', + 'unpleasant', + 'unpleasantness', + 'unplug', + 'unplumbed', + 'unpolite', + 'unpolitic', + 'unpolled', + 'unpopular', + 'unpractical', + 'unpracticed', + 'unprecedented', + 'unpredictable', + 'unprejudiced', + 'unpremeditated', + 'unprepared', + 'unpretentious', + 'unpriced', + 'unprincipled', + 'unprintable', + 'unproductive', + 'unprofessional', + 'unprofitable', + 'unpromising', + 'unprovided', + 'unqualified', + 'unquestionable', + 'unquestioned', + 'unquestioning', + 'unquiet', + 'unquote', + 'unravel', + 'unread', + 'unreadable', + 'unready', + 'unreal', + 'unreality', + 'unrealizable', + 'unreason', + 'unreasonable', + 'unreasoning', + 'unreconstructed', + 'unreel', + 'unreeve', + 'unrefined', + 'unreflecting', + 'unreflective', + 'unregenerate', + 'unrelenting', + 'unreliable', + 'unreligious', + 'unremitting', + 'unrepair', + 'unrequited', + 'unreserve', + 'unreserved', + 'unrest', + 'unrestrained', + 'unrestraint', + 'unriddle', + 'unrig', + 'unrighteous', + 'unripe', + 'unrivaled', + 'unrivalled', + 'unrobe', + 'unroll', + 'unroof', + 'unroot', + 'unrounded', + 'unruffled', + 'unruly', + 'unsaddle', + 'unsaid', + 'unsatisfactory', + 'unsavory', + 'unsay', + 'unscathed', + 'unschooled', + 'unscientific', + 'unscramble', + 'unscratched', + 'unscreened', + 'unscrew', + 'unscrupulous', + 'unseal', + 'unseam', + 'unsearchable', + 'unseasonable', + 'unseasoned', + 'unseat', + 'unsecured', + 'unseemly', + 'unseen', + 'unsegregated', + 'unselfish', + 'unset', + 'unsettle', + 'unsettled', + 'unsex', + 'unshackle', + 'unshakable', + 'unshaped', + 'unshapen', + 'unsheathe', + 'unship', + 'unshod', + 'unshroud', + 'unsightly', + 'unskilled', + 'unskillful', + 'unsling', + 'unsnap', + 'unsnarl', + 'unsociable', + 'unsocial', + 'unsophisticated', + 'unsought', + 'unsound', + 'unsparing', + 'unspeakable', + 'unspent', + 'unsphere', + 'unspoiled', + 'unspoken', + 'unspotted', + 'unstable', + 'unstained', + 'unsteady', + 'unsteel', + 'unstep', + 'unstick', + 'unstop', + 'unstoppable', + 'unstopped', + 'unstrained', + 'unstrap', + 'unstressed', + 'unstring', + 'unstriped', + 'unstrung', + 'unstuck', + 'unstudied', + 'unsubstantial', + 'unsuccess', + 'unsuccessful', + 'unsuitable', + 'unsung', + 'unsupportable', + 'unsure', + 'unsuspected', + 'unsuspecting', + 'unsustainable', + 'unswear', + 'unswerving', + 'untangle', + 'untaught', + 'unteach', + 'untenable', + 'unthankful', + 'unthinkable', + 'unthinking', + 'unthread', + 'unthrone', + 'untidy', + 'untie', + 'until', + 'untimely', + 'untinged', + 'untitled', + 'unto', + 'untold', + 'untouchability', + 'untouchable', + 'untouched', + 'untoward', + 'untraveled', + 'untread', + 'untried', + 'untrimmed', + 'untrue', + 'untruth', + 'untruthful', + 'untuck', + 'untune', + 'untutored', + 'untwine', + 'untwist', + 'unused', + 'unusual', + 'unutterable', + 'unvalued', + 'unvarnished', + 'unveil', + 'unveiling', + 'unvoice', + 'unvoiced', + 'unwarrantable', + 'unwarranted', + 'unwary', + 'unwashed', + 'unwatched', + 'unwearied', + 'unweave', + 'unweighed', + 'unwelcome', + 'unwell', + 'unwept', + 'unwholesome', + 'unwieldy', + 'unwilled', + 'unwilling', + 'unwind', + 'unwinking', + 'unwisdom', + 'unwise', + 'unwish', + 'unwished', + 'unwitnessed', + 'unwitting', + 'unwonted', + 'unworldly', + 'unworthy', + 'unwrap', + 'unwritten', + 'unyielding', + 'unyoke', + 'unzip', + 'up', + 'upas', + 'upbear', + 'upbeat', + 'upbraid', + 'upbraiding', + 'upbringing', + 'upbuild', + 'upcast', + 'upcoming', + 'upcountry', + 'update', + 'updo', + 'updraft', + 'upend', + 'upgrade', + 'upgrowth', + 'upheaval', + 'upheave', + 'upheld', + 'uphill', + 'uphold', + 'upholster', + 'upholsterer', + 'upholstery', + 'uphroe', + 'upkeep', + 'upland', + 'uplift', + 'upmost', + 'upon', + 'upper', + 'upperclassman', + 'uppercut', + 'uppermost', + 'uppish', + 'uppity', + 'upraise', + 'uprear', + 'upright', + 'uprise', + 'uprising', + 'uproar', + 'uproarious', + 'uproot', + 'uprush', + 'upset', + 'upsetting', + 'upshot', + 'upside', + 'upsilon', + 'upspring', + 'upstage', + 'upstairs', + 'upstanding', + 'upstart', + 'upstate', + 'upstream', + 'upstretched', + 'upstroke', + 'upsurge', + 'upsweep', + 'upswell', + 'upswing', + 'uptake', + 'upthrow', + 'upthrust', + 'uptown', + 'uptrend', + 'upturn', + 'upturned', + 'upward', + 'upwards', + 'upwind', + 'uracil', + 'uraemia', + 'uraeus', + 'uralite', + 'uranalysis', + 'uranic', + 'uraninite', + 'uranium', + 'uranography', + 'uranology', + 'uranometry', + 'uranous', + 'uranyl', + 'urban', + 'urbane', + 'urbanism', + 'urbanist', + 'urbanite', + 'urbanity', + 'urbanize', + 'urceolate', + 'urchin', + 'urea', + 'urease', + 'uredium', + 'uredo', + 'ureide', + 'uremia', + 'ureter', + 'urethra', + 'urethrectomy', + 'urethritis', + 'urethroscope', + 'uretic', + 'urge', + 'urgency', + 'urgent', + 'urger', + 'urial', + 'uric', + 'urinal', + 'urinalysis', + 'urinary', + 'urinate', + 'urine', + 'uriniferous', + 'urn', + 'urnfield', + 'urochrome', + 'urogenital', + 'urogenous', + 'urolith', + 'urology', + 'uropod', + 'uropygium', + 'uroscopy', + 'ursine', + 'urticaceous', + 'urticaria', + 'urtication', + 'urus', + 'urushiol', + 'us', + 'usable', + 'usage', + 'usance', + 'use', + 'used', + 'useful', + 'useless', + 'user', + 'usher', + 'usherette', + 'usquebaugh', + 'ustulation', + 'usual', + 'usufruct', + 'usurer', + 'usurious', + 'usurp', + 'usurpation', + 'usury', + 'ut', + 'utensil', + 'uterine', + 'uterus', + 'utile', + 'utilitarian', + 'utilitarianism', + 'utility', + 'utilize', + 'utmost', + 'utopia', + 'utopian', + 'utopianism', + 'utricle', + 'utter', + 'utterance', + 'uttermost', + 'uvarovite', + 'uvea', + 'uveitis', + 'uvula', + 'uvular', + 'uvulitis', + 'uxorial', + 'uxoricide', + 'uxorious', + 'v', + 'vacancy', + 'vacant', + 'vacate', + 'vacation', + 'vacationist', + 'vaccinate', + 'vaccination', + 'vaccine', + 'vaccinia', + 'vacillate', + 'vacillating', + 'vacillation', + 'vacillatory', + 'vacua', + 'vacuity', + 'vacuole', + 'vacuous', + 'vacuum', + 'vadose', + 'vagabond', + 'vagabondage', + 'vagal', + 'vagarious', + 'vagary', + 'vagina', + 'vaginal', + 'vaginate', + 'vaginectomy', + 'vaginismus', + 'vaginitis', + 'vagrancy', + 'vagrant', + 'vagrom', + 'vague', + 'vagus', + 'vail', + 'vain', + 'vainglorious', + 'vainglory', + 'vair', + 'vaivode', + 'valance', + 'vale', + 'valediction', + 'valedictorian', + 'valedictory', + 'valence', + 'valency', + 'valentine', + 'valerian', + 'valerianaceous', + 'valeric', + 'valet', + 'valetudinarian', + 'valetudinary', + 'valgus', + 'valiancy', + 'valiant', + 'valid', + 'validate', + 'validity', + 'valine', + 'valise', + 'vallation', + 'vallecula', + 'valley', + 'valonia', + 'valor', + 'valorization', + 'valorize', + 'valorous', + 'valse', + 'valuable', + 'valuate', + 'valuation', + 'valuator', + 'value', + 'valued', + 'valueless', + 'valuer', + 'valval', + 'valvate', + 'valve', + 'valvular', + 'valvule', + 'valvulitis', + 'vambrace', + 'vamoose', + 'vamp', + 'vampire', + 'vampirism', + 'van', + 'vanadate', + 'vanadinite', + 'vanadium', + 'vanadous', + 'vanda', + 'vandal', + 'vandalism', + 'vandalize', + 'vane', + 'vang', + 'vanguard', + 'vanilla', + 'vanillic', + 'vanillin', + 'vanish', + 'vanity', + 'vanquish', + 'vantage', + 'vanward', + 'vapid', + 'vapor', + 'vaporescence', + 'vaporetto', + 'vaporific', + 'vaporimeter', + 'vaporing', + 'vaporish', + 'vaporization', + 'vaporize', + 'vaporizer', + 'vaporous', + 'vapory', + 'vaquero', + 'vara', + 'vargueno', + 'varia', + 'variable', + 'variance', + 'variant', + 'variate', + 'variation', + 'varicella', + 'varicelloid', + 'varices', + 'varicocele', + 'varicolored', + 'varicose', + 'varicotomy', + 'varied', + 'variegate', + 'variegated', + 'variegation', + 'varietal', + 'variety', + 'variform', + 'variola', + 'variole', + 'variolite', + 'varioloid', + 'variolous', + 'variometer', + 'variorum', + 'various', + 'variscite', + 'varistor', + 'varitype', + 'varix', + 'varlet', + 'varletry', + 'varmint', + 'varnish', + 'varsity', + 'varus', + 'varve', + 'vary', + 'vas', + 'vascular', + 'vasculum', + 'vase', + 'vasectomy', + 'vasoconstrictor', + 'vasodilator', + 'vasoinhibitor', + 'vasomotor', + 'vassal', + 'vassalage', + 'vassalize', + 'vast', + 'vastitude', + 'vasty', + 'vat', + 'vatic', + 'vaticide', + 'vaticinal', + 'vaticinate', + 'vaticination', + 'vaudeville', + 'vaudevillian', + 'vault', + 'vaulted', + 'vaulting', + 'vaunt', + 'vaunting', + 'vav', + 'veal', + 'vector', + 'vedalia', + 'vedette', + 'veer', + 'veery', + 'veg', + 'vegetable', + 'vegetal', + 'vegetarian', + 'vegetarianism', + 'vegetate', + 'vegetation', + 'vegetative', + 'vehemence', + 'vehement', + 'vehicle', + 'vehicular', + 'veil', + 'veiled', + 'veiling', + 'vein', + 'veinlet', + 'veinstone', + 'veinule', + 'velamen', + 'velar', + 'velarium', + 'velarize', + 'velate', + 'veld', + 'veliger', + 'velites', + 'velleity', + 'vellicate', + 'vellum', + 'veloce', + 'velocipede', + 'velocity', + 'velodrome', + 'velour', + 'velours', + 'velum', + 'velure', + 'velutinous', + 'velvet', + 'velveteen', + 'velvety', + 'vena', + 'venal', + 'venality', + 'venatic', + 'venation', + 'vend', + 'vendace', + 'vendee', + 'vender', + 'vendetta', + 'vendible', + 'vendor', + 'vendue', + 'veneer', + 'veneering', + 'venenose', + 'venepuncture', + 'venerable', + 'venerate', + 'veneration', + 'venereal', + 'venery', + 'venesection', + 'venge', + 'vengeance', + 'vengeful', + 'venial', + 'venin', + 'venipuncture', + 'venireman', + 'venison', + 'venom', + 'venomous', + 'venose', + 'venosity', + 'venous', + 'vent', + 'ventage', + 'ventail', + 'venter', + 'ventilate', + 'ventilation', + 'ventilator', + 'ventose', + 'ventral', + 'ventricle', + 'ventricose', + 'ventricular', + 'ventriculus', + 'ventriloquism', + 'ventriloquist', + 'ventriloquize', + 'ventriloquy', + 'venture', + 'venturesome', + 'venturous', + 'venue', + 'venule', + 'veracious', + 'veracity', + 'veranda', + 'veratridine', + 'veratrine', + 'verb', + 'verbal', + 'verbalism', + 'verbality', + 'verbalize', + 'verbatim', + 'verbena', + 'verbenaceous', + 'verbiage', + 'verbid', + 'verbify', + 'verbose', + 'verbosity', + 'verboten', + 'verdant', + 'verderer', + 'verdict', + 'verdigris', + 'verdin', + 'verditer', + 'verdure', + 'verecund', + 'verge', + 'vergeboard', + 'verger', + 'veridical', + 'verified', + 'verify', + 'verily', + 'verisimilar', + 'verisimilitude', + 'verism', + 'veritable', + 'verity', + 'verjuice', + 'vermeil', + 'vermicelli', + 'vermicide', + 'vermicular', + 'vermiculate', + 'vermiculation', + 'vermiculite', + 'vermiform', + 'vermifuge', + 'vermilion', + 'vermin', + 'vermination', + 'verminous', + 'vermis', + 'vermouth', + 'vernacular', + 'vernacularism', + 'vernacularize', + 'vernal', + 'vernalize', + 'vernation', + 'vernier', + 'vernissage', + 'veronica', + 'verruca', + 'verrucose', + 'versatile', + 'verse', + 'versed', + 'versicle', + 'versicolor', + 'versicular', + 'versification', + 'versify', + 'version', + 'verso', + 'verst', + 'versus', + 'vert', + 'vertebra', + 'vertebral', + 'vertebrate', + 'vertex', + 'vertical', + 'verticillaster', + 'verticillate', + 'vertiginous', + 'vertigo', + 'vertu', + 'vervain', + 'verve', + 'vervet', + 'very', + 'vesica', + 'vesical', + 'vesicant', + 'vesicate', + 'vesicatory', + 'vesicle', + 'vesiculate', + 'vesper', + 'vesperal', + 'vespers', + 'vespertilionine', + 'vespertine', + 'vespiary', + 'vespid', + 'vespine', + 'vessel', + 'vest', + 'vesta', + 'vestal', + 'vested', + 'vestiary', + 'vestibule', + 'vestige', + 'vestigial', + 'vesting', + 'vestment', + 'vestry', + 'vestryman', + 'vesture', + 'vesuvian', + 'vesuvianite', + 'vet', + 'vetch', + 'vetchling', + 'veteran', + 'veterinarian', + 'veterinary', + 'vetiver', + 'veto', + 'vex', + 'vexation', + 'vexatious', + 'vexed', + 'vexillum', + 'via', + 'viable', + 'viaduct', + 'vial', + 'viand', + 'viaticum', + 'viator', + 'vibes', + 'vibraculum', + 'vibraharp', + 'vibrant', + 'vibraphone', + 'vibrate', + 'vibratile', + 'vibration', + 'vibrations', + 'vibrato', + 'vibrator', + 'vibratory', + 'vibrio', + 'vibrissa', + 'viburnum', + 'vicar', + 'vicarage', + 'vicarial', + 'vicariate', + 'vicarious', + 'vice', + 'vicegerent', + 'vicenary', + 'vicennial', + 'viceregal', + 'vicereine', + 'viceroy', + 'vichyssoise', + 'vicinage', + 'vicinal', + 'vicinity', + 'vicious', + 'vicissitude', + 'victim', + 'victimize', + 'victor', + 'victoria', + 'victorious', + 'victory', + 'victual', + 'victualage', + 'victualer', + 'victualler', + 'victuals', + 'vide', + 'videlicet', + 'video', + 'videogenic', + 'vidette', + 'vidicon', + 'vie', + 'view', + 'viewable', + 'viewer', + 'viewfinder', + 'viewing', + 'viewless', + 'viewpoint', + 'viewy', + 'vigesimal', + 'vigil', + 'vigilance', + 'vigilant', + 'vigilante', + 'vigilantism', + 'vignette', + 'vigor', + 'vigorous', + 'vilayet', + 'vile', + 'vilify', + 'vilipend', + 'villa', + 'village', + 'villager', + 'villain', + 'villainage', + 'villainous', + 'villainy', + 'villanelle', + 'villein', + 'villeinage', + 'villenage', + 'villiform', + 'villose', + 'villosity', + 'villous', + 'villus', + 'vim', + 'vimen', + 'vimineous', + 'vina', + 'vinaceous', + 'vinaigrette', + 'vinasse', + 'vincible', + 'vinculum', + 'vindicable', + 'vindicate', + 'vindication', + 'vindictive', + 'vine', + 'vinegar', + 'vinegarette', + 'vinegarish', + 'vinegarroon', + 'vinegary', + 'vinery', + 'vineyard', + 'vinic', + 'viniculture', + 'viniferous', + 'vinificator', + 'vino', + 'vinosity', + 'vinous', + 'vintage', + 'vintager', + 'vintner', + 'vinyl', + 'vinylidene', + 'viol', + 'viola', + 'violable', + 'violaceous', + 'violate', + 'violation', + 'violative', + 'violence', + 'violent', + 'violet', + 'violin', + 'violinist', + 'violist', + 'violoncellist', + 'violoncello', + 'violone', + 'viosterol', + 'viper', + 'viperine', + 'viperish', + 'viperous', + 'virago', + 'viral', + 'virelay', + 'vireo', + 'virescence', + 'virescent', + 'virga', + 'virgate', + 'virgin', + 'virginal', + 'virginity', + 'virginium', + 'virgulate', + 'virgule', + 'viridescent', + 'viridian', + 'viridity', + 'virile', + 'virilism', + 'virility', + 'virology', + 'virtu', + 'virtual', + 'virtually', + 'virtue', + 'virtues', + 'virtuosic', + 'virtuosity', + 'virtuoso', + 'virtuous', + 'virulence', + 'virulent', + 'virus', + 'visa', + 'visage', + 'viscacha', + 'viscera', + 'visceral', + 'viscid', + 'viscoid', + 'viscometer', + 'viscose', + 'viscosity', + 'viscount', + 'viscountcy', + 'viscountess', + 'viscounty', + 'viscous', + 'viscus', + 'vise', + 'visibility', + 'visible', + 'vision', + 'visional', + 'visionary', + 'visit', + 'visitant', + 'visitation', + 'visitor', + 'visor', + 'vista', + 'visual', + 'visualize', + 'visually', + 'vita', + 'vitaceous', + 'vital', + 'vitalism', + 'vitality', + 'vitalize', + 'vitals', + 'vitamin', + 'vitascope', + 'vitellin', + 'vitelline', + 'vitellus', + 'vitiate', + 'vitiated', + 'viticulture', + 'vitiligo', + 'vitrain', + 'vitreous', + 'vitrescence', + 'vitrescent', + 'vitric', + 'vitrics', + 'vitrification', + 'vitriform', + 'vitrify', + 'vitrine', + 'vitriol', + 'vitriolic', + 'vitriolize', + 'vitta', + 'vittle', + 'vituline', + 'vituperate', + 'vituperation', + 'viva', + 'vivace', + 'vivacious', + 'vivacity', + 'vivarium', + 'vive', + 'vivid', + 'vivify', + 'viviparous', + 'vivisect', + 'vivisection', + 'vivisectionist', + 'vixen', + 'vizard', + 'vizcacha', + 'vizier', + 'vizierate', + 'vizor', + 'vocable', + 'vocabulary', + 'vocal', + 'vocalic', + 'vocalise', + 'vocalism', + 'vocalist', + 'vocalize', + 'vocation', + 'vocational', + 'vocative', + 'vociferance', + 'vociferant', + 'vociferate', + 'vociferation', + 'vociferous', + 'vocoid', + 'vodka', + 'vogue', + 'voguish', + 'voice', + 'voiced', + 'voiceful', + 'voiceless', + 'void', + 'voidable', + 'voidance', + 'voile', + 'voiture', + 'volant', + 'volar', + 'volatile', + 'volatilize', + 'volcanic', + 'volcanism', + 'volcano', + 'volcanology', + 'vole', + 'volitant', + 'volition', + 'volitive', + 'volley', + 'volleyball', + 'volost', + 'volplane', + 'volt', + 'voltage', + 'voltaic', + 'voltaism', + 'voltameter', + 'voltammeter', + 'voltmeter', + 'voluble', + 'volume', + 'volumed', + 'volumeter', + 'volumetric', + 'voluminous', + 'voluntarism', + 'voluntary', + 'voluntaryism', + 'volunteer', + 'voluptuary', + 'voluptuous', + 'volute', + 'volution', + 'volva', + 'volvox', + 'volvulus', + 'vomer', + 'vomit', + 'vomitory', + 'vomiturition', + 'voodoo', + 'voodooism', + 'voracious', + 'voracity', + 'vortex', + 'vortical', + 'vorticella', + 'votary', + 'vote', + 'voter', + 'votive', + 'vouch', + 'voucher', + 'vouchsafe', + 'vouge', + 'voussoir', + 'vow', + 'vowel', + 'vowelize', + 'voyage', + 'voyageur', + 'voyeur', + 'voyeurism', + 'vraisemblance', + 'vulcanism', + 'vulcanite', + 'vulcanize', + 'vulcanology', + 'vulgar', + 'vulgarian', + 'vulgarism', + 'vulgarity', + 'vulgarize', + 'vulgate', + 'vulgus', + 'vulnerable', + 'vulnerary', + 'vulpine', + 'vulture', + 'vulturine', + 'vulva', + 'vulvitis', + 'vying', + 'w', + 'wabble', + 'wack', + 'wacke', + 'wacky', + 'wad', + 'wadding', + 'waddle', + 'wade', + 'wader', + 'wadi', + 'wadmal', + 'wafer', + 'waffle', + 'waft', + 'waftage', + 'wafture', + 'wag', + 'wage', + 'wager', + 'wageworker', + 'waggery', + 'waggish', + 'waggle', + 'waggon', + 'wagon', + 'wagonage', + 'wagoner', + 'wagonette', + 'wagtail', + 'wahoo', + 'waif', + 'wail', + 'wailful', + 'wain', + 'wainscot', + 'wainscoting', + 'wainwright', + 'waist', + 'waistband', + 'waistcloth', + 'waistcoat', + 'waisted', + 'waistline', + 'wait', + 'waiter', + 'waitress', + 'waive', + 'waiver', + 'wake', + 'wakeful', + 'wakeless', + 'waken', + 'wakerife', + 'waldgrave', + 'wale', + 'walk', + 'walkabout', + 'walker', + 'walking', + 'walkout', + 'walkover', + 'walkway', + 'wall', + 'wallaby', + 'wallah', + 'wallaroo', + 'wallboard', + 'wallet', + 'walleye', + 'walleyed', + 'wallflower', + 'wallop', + 'walloper', + 'walloping', + 'wallow', + 'wallpaper', + 'wally', + 'walnut', + 'walrus', + 'waltz', + 'wamble', + 'wame', + 'wampum', + 'wampumpeag', + 'wan', + 'wand', + 'wander', + 'wandering', + 'wanderlust', + 'wanderoo', + 'wane', + 'wangle', + 'wanigan', + 'want', + 'wantage', + 'wanting', + 'wanton', + 'wapentake', + 'wapiti', + 'war', + 'warble', + 'warbler', + 'ward', + 'warden', + 'warder', + 'wardmote', + 'wardrobe', + 'wardroom', + 'wardship', + 'ware', + 'warehouse', + 'warehouseman', + 'wareroom', + 'wares', + 'warfare', + 'warfarin', + 'warhead', + 'warily', + 'wariness', + 'warison', + 'warlike', + 'warlock', + 'warlord', + 'warm', + 'warmhearted', + 'warmonger', + 'warmongering', + 'warmth', + 'warn', + 'warning', + 'warp', + 'warpath', + 'warplane', + 'warrant', + 'warrantable', + 'warrantee', + 'warrantor', + 'warranty', + 'warren', + 'warrener', + 'warrigal', + 'warrior', + 'warship', + 'warsle', + 'wart', + 'warthog', + 'wartime', + 'warty', + 'wary', + 'was', + 'wash', + 'washable', + 'washbasin', + 'washboard', + 'washbowl', + 'washcloth', + 'washday', + 'washer', + 'washerman', + 'washerwoman', + 'washery', + 'washhouse', + 'washin', + 'washing', + 'washout', + 'washrag', + 'washroom', + 'washstand', + 'washtub', + 'washwoman', + 'washy', + 'wasp', + 'waspish', + 'wassail', + 'wast', + 'wastage', + 'waste', + 'wastebasket', + 'wasteful', + 'wasteland', + 'wastepaper', + 'wasting', + 'wastrel', + 'wat', + 'watch', + 'watchband', + 'watchcase', + 'watchdog', + 'watcher', + 'watchful', + 'watchmaker', + 'watchman', + 'watchtower', + 'watchword', + 'water', + 'waterage', + 'waterborne', + 'waterbuck', + 'watercolor', + 'watercourse', + 'watercraft', + 'watercress', + 'waterfall', + 'waterfowl', + 'waterfront', + 'wateriness', + 'watering', + 'waterish', + 'waterless', + 'waterline', + 'waterlog', + 'waterlogged', + 'waterman', + 'watermark', + 'watermelon', + 'waterproof', + 'waterscape', + 'watershed', + 'waterside', + 'waterspout', + 'watertight', + 'waterway', + 'waterworks', + 'watery', + 'watt', + 'wattage', + 'wattle', + 'wattmeter', + 'wave', + 'wavelength', + 'wavelet', + 'wavellite', + 'wavemeter', + 'waver', + 'wavy', + 'waw', + 'wax', + 'waxbill', + 'waxen', + 'waxplant', + 'waxwing', + 'waxwork', + 'waxy', + 'way', + 'waybill', + 'wayfarer', + 'wayfaring', + 'waylay', + 'wayless', + 'ways', + 'wayside', + 'wayward', + 'wayworn', + 'wayzgoose', + 'we', + 'weak', + 'weaken', + 'weakfish', + 'weakling', + 'weakly', + 'weakness', + 'weal', + 'weald', + 'wealth', + 'wealthy', + 'wean', + 'weaner', + 'weanling', + 'weapon', + 'weaponeer', + 'weaponless', + 'weaponry', + 'wear', + 'wearable', + 'weariful', + 'weariless', + 'wearing', + 'wearisome', + 'wearproof', + 'weary', + 'weasand', + 'weasel', + 'weather', + 'weatherboard', + 'weatherboarding', + 'weathercock', + 'weathered', + 'weatherglass', + 'weathering', + 'weatherly', + 'weatherman', + 'weatherproof', + 'weathertight', + 'weatherworn', + 'weave', + 'weaver', + 'weaverbird', + 'web', + 'webbed', + 'webbing', + 'webby', + 'weber', + 'webfoot', + 'webworm', + 'wed', + 'wedded', + 'wedding', + 'wedge', + 'wedged', + 'wedlock', + 'wee', + 'weed', + 'weeds', + 'weedy', + 'week', + 'weekday', + 'weekend', + 'weekender', + 'weekly', + 'ween', + 'weeny', + 'weep', + 'weeper', + 'weeping', + 'weepy', + 'weever', + 'weevil', + 'weevily', + 'weft', + 'weigela', + 'weigh', + 'weighbridge', + 'weight', + 'weighted', + 'weighting', + 'weightless', + 'weightlessness', + 'weighty', + 'weir', + 'weird', + 'weirdie', + 'weirdo', + 'weka', + 'welch', + 'welcome', + 'weld', + 'welfare', + 'welfarism', + 'welkin', + 'well', + 'wellborn', + 'wellhead', + 'wellspring', + 'welsh', + 'welt', + 'welter', + 'welterweight', + 'wen', + 'wench', + 'wend', + 'went', + 'wentletrap', + 'wept', + 'were', + 'werewolf', + 'wergild', + 'wernerite', + 'wersh', + 'wert', + 'west', + 'westbound', + 'wester', + 'westering', + 'westerly', + 'western', + 'westernism', + 'westernize', + 'westernmost', + 'westing', + 'westward', + 'westwardly', + 'wet', + 'wether', + 'whack', + 'whacking', + 'whacky', + 'whale', + 'whaleback', + 'whaleboat', + 'whalebone', + 'whaler', + 'whaling', + 'wham', + 'whangee', + 'whap', + 'wharf', + 'wharfage', + 'wharfinger', + 'wharve', + 'what', + 'whatever', + 'whatnot', + 'whatsoever', + 'wheal', + 'wheat', + 'wheatear', + 'wheaten', + 'wheatworm', + 'wheedle', + 'wheel', + 'wheelbarrow', + 'wheelbase', + 'wheelchair', + 'wheeled', + 'wheeler', + 'wheelhorse', + 'wheelhouse', + 'wheeling', + 'wheelman', + 'wheels', + 'wheelsman', + 'wheelwork', + 'wheelwright', + 'wheen', + 'wheeze', + 'wheezy', + 'whelk', + 'whelm', + 'whelp', + 'when', + 'whenas', + 'whence', + 'whencesoever', + 'whenever', + 'whensoever', + 'where', + 'whereabouts', + 'whereas', + 'whereat', + 'whereby', + 'wherefore', + 'wherefrom', + 'wherein', + 'whereinto', + 'whereof', + 'whereon', + 'wheresoever', + 'whereto', + 'whereunto', + 'whereupon', + 'wherever', + 'wherewith', + 'wherewithal', + 'wherry', + 'whet', + 'whether', + 'whetstone', + 'whew', + 'whey', + 'which', + 'whichever', + 'whichsoever', + 'whicker', + 'whidah', + 'whiff', + 'whiffet', + 'whiffle', + 'whiffler', + 'whiffletree', + 'while', + 'whiles', + 'whilom', + 'whilst', + 'whim', + 'whimper', + 'whimsey', + 'whimsical', + 'whimsicality', + 'whimsy', + 'whin', + 'whinchat', + 'whine', + 'whinny', + 'whinstone', + 'whiny', + 'whip', + 'whipcord', + 'whiplash', + 'whippersnapper', + 'whippet', + 'whipping', + 'whippletree', + 'whippoorwill', + 'whipsaw', + 'whipstall', + 'whipstitch', + 'whipstock', + 'whirl', + 'whirlabout', + 'whirligig', + 'whirlpool', + 'whirlwind', + 'whirly', + 'whirlybird', + 'whish', + 'whisk', + 'whisker', + 'whiskey', + 'whisky', + 'whisper', + 'whispering', + 'whist', + 'whistle', + 'whistler', + 'whistling', + 'whit', + 'white', + 'whitebait', + 'whitebeam', + 'whitecap', + 'whitefish', + 'whitefly', + 'whiten', + 'whiteness', + 'whitening', + 'whitesmith', + 'whitethorn', + 'whitethroat', + 'whitewall', + 'whitewash', + 'whitewing', + 'whitewood', + 'whither', + 'whithersoever', + 'whitherward', + 'whiting', + 'whitish', + 'whitleather', + 'whitlow', + 'whittle', + 'whittling', + 'whity', + 'whiz', + 'who', + 'whoa', + 'whodunit', + 'whoever', + 'whole', + 'wholehearted', + 'wholesale', + 'wholesome', + 'wholism', + 'wholly', + 'whom', + 'whomever', + 'whomp', + 'whomsoever', + 'whoop', + 'whoopee', + 'whooper', + 'whoops', + 'whoosh', + 'whop', + 'whopper', + 'whopping', + 'whore', + 'whoredom', + 'whorehouse', + 'whoremaster', + 'whoreson', + 'whorish', + 'whorl', + 'whorled', + 'whortleberry', + 'whose', + 'whoso', + 'whosoever', + 'why', + 'whydah', + 'wick', + 'wicked', + 'wickedness', + 'wicker', + 'wickerwork', + 'wicket', + 'wicketkeeper', + 'wickiup', + 'wicopy', + 'widdershins', + 'wide', + 'widely', + 'widen', + 'widespread', + 'widgeon', + 'widget', + 'widow', + 'widower', + 'width', + 'widthwise', + 'wield', + 'wieldy', + 'wiener', + 'wife', + 'wifehood', + 'wifeless', + 'wifely', + 'wig', + 'wigeon', + 'wigging', + 'wiggle', + 'wiggler', + 'wiggly', + 'wight', + 'wigwag', + 'wigwam', + 'wikiup', + 'wild', + 'wildcat', + 'wildebeest', + 'wilder', + 'wilderness', + 'wildfire', + 'wildfowl', + 'wilding', + 'wildlife', + 'wildwood', + 'wile', + 'wilful', + 'wiliness', + 'will', + 'willable', + 'willed', + 'willet', + 'willful', + 'willies', + 'willing', + 'williwaw', + 'willow', + 'willowy', + 'willpower', + 'wilt', + 'wily', + 'wimble', + 'wimple', + 'win', + 'wince', + 'winch', + 'wind', + 'windage', + 'windbag', + 'windblown', + 'windbound', + 'windbreak', + 'windburn', + 'windcheater', + 'winded', + 'winder', + 'windfall', + 'windflower', + 'windgall', + 'windhover', + 'winding', + 'windjammer', + 'windlass', + 'windmill', + 'window', + 'windowlight', + 'windowpane', + 'windowsill', + 'windpipe', + 'windproof', + 'windrow', + 'windsail', + 'windshield', + 'windstorm', + 'windswept', + 'windtight', + 'windup', + 'windward', + 'windy', + 'wine', + 'winebibber', + 'wineglass', + 'winegrower', + 'winepress', + 'winery', + 'wineshop', + 'wineskin', + 'wing', + 'wingback', + 'wingding', + 'winged', + 'winger', + 'wingless', + 'winglet', + 'wingover', + 'wingspan', + 'wingspread', + 'wink', + 'winker', + 'winkle', + 'winner', + 'winning', + 'winnow', + 'wino', + 'winsome', + 'winter', + 'winterfeed', + 'wintergreen', + 'winterize', + 'winterkill', + 'wintertide', + 'wintertime', + 'wintery', + 'wintry', + 'winy', + 'winze', + 'wipe', + 'wiper', + 'wire', + 'wiredraw', + 'wireless', + 'wireman', + 'wirer', + 'wiretap', + 'wirework', + 'wireworm', + 'wiring', + 'wirra', + 'wiry', + 'wisdom', + 'wise', + 'wiseacre', + 'wisecrack', + 'wisent', + 'wish', + 'wishbone', + 'wishful', + 'wisp', + 'wispy', + 'wist', + 'wisteria', + 'wistful', + 'wit', + 'witch', + 'witchcraft', + 'witchery', + 'witching', + 'witchy', + 'wite', + 'witenagemot', + 'with', + 'withal', + 'withdraw', + 'withdrawal', + 'withdrawn', + 'withdrew', + 'withe', + 'wither', + 'witherite', + 'withers', + 'withershins', + 'withhold', + 'within', + 'withindoors', + 'without', + 'withoutdoors', + 'withstand', + 'withy', + 'witless', + 'witling', + 'witness', + 'wits', + 'witted', + 'witticism', + 'witting', + 'wittol', + 'witty', + 'wive', + 'wivern', + 'wives', + 'wizard', + 'wizardly', + 'wizardry', + 'wizen', + 'wizened', + 'wo', + 'woad', + 'woaded', + 'woadwaxen', + 'woald', + 'wobble', + 'wobbling', + 'wobbly', + 'wodge', + 'woe', + 'woebegone', + 'woeful', + 'woke', + 'woken', + 'wold', + 'wolf', + 'wolffish', + 'wolfhound', + 'wolfish', + 'wolfram', + 'wolframite', + 'wolfsbane', + 'wollastonite', + 'wolver', + 'wolverine', + 'wolves', + 'woman', + 'womanhood', + 'womanish', + 'womanize', + 'womanizer', + 'womankind', + 'womanlike', + 'womanly', + 'womb', + 'wombat', + 'women', + 'womenfolk', + 'womera', + 'wommera', + 'won', + 'wonder', + 'wonderful', + 'wondering', + 'wonderland', + 'wonderment', + 'wonderwork', + 'wondrous', + 'wonky', + 'wont', + 'wonted', + 'woo', + 'wood', + 'woodbine', + 'woodborer', + 'woodchopper', + 'woodchuck', + 'woodcock', + 'woodcraft', + 'woodcut', + 'woodcutter', + 'wooded', + 'wooden', + 'woodenhead', + 'woodenware', + 'woodland', + 'woodman', + 'woodnote', + 'woodpecker', + 'woodpile', + 'woodprint', + 'woodruff', + 'woods', + 'woodshed', + 'woodsia', + 'woodsman', + 'woodsy', + 'woodwaxen', + 'woodwind', + 'woodwork', + 'woodworker', + 'woodworking', + 'woodworm', + 'woody', + 'wooer', + 'woof', + 'woofer', + 'wool', + 'woolfell', + 'woolgathering', + 'woolgrower', + 'woollen', + 'woolly', + 'woolpack', + 'woolsack', + 'woorali', + 'woozy', + 'wop', + 'word', + 'wordage', + 'wordbook', + 'wording', + 'wordless', + 'wordplay', + 'words', + 'wordsmith', + 'wordy', + 'wore', + 'work', + 'workable', + 'workaday', + 'workbag', + 'workbench', + 'workbook', + 'workday', + 'worked', + 'worker', + 'workhorse', + 'workhouse', + 'working', + 'workingman', + 'workingwoman', + 'workman', + 'workmanlike', + 'workmanship', + 'workout', + 'workroom', + 'works', + 'workshop', + 'worktable', + 'workwoman', + 'world', + 'worldling', + 'worldly', + 'worldwide', + 'worm', + 'wormhole', + 'wormseed', + 'wormwood', + 'wormy', + 'worn', + 'worried', + 'worriment', + 'worrisome', + 'worry', + 'worrywart', + 'worse', + 'worsen', + 'worser', + 'worship', + 'worshipful', + 'worst', + 'worsted', + 'wort', + 'worth', + 'worthless', + 'worthwhile', + 'worthy', + 'wot', + 'would', + 'wouldst', + 'wound', + 'wounded', + 'woundwort', + 'wove', + 'woven', + 'wow', + 'wowser', + 'wrack', + 'wraith', + 'wrangle', + 'wrangler', + 'wrap', + 'wraparound', + 'wrapped', + 'wrapper', + 'wrapping', + 'wrasse', + 'wrath', + 'wrathful', + 'wreak', + 'wreath', + 'wreathe', + 'wreck', + 'wreckage', + 'wrecker', + 'wreckfish', + 'wreckful', + 'wren', + 'wrench', + 'wrest', + 'wrestle', + 'wrestling', + 'wretch', + 'wretched', + 'wrier', + 'wriest', + 'wriggle', + 'wriggler', + 'wriggly', + 'wright', + 'wring', + 'wringer', + 'wrinkle', + 'wrinkly', + 'wrist', + 'wristband', + 'wristlet', + 'wristwatch', + 'writ', + 'write', + 'writer', + 'writhe', + 'writhen', + 'writing', + 'written', + 'wrong', + 'wrongdoer', + 'wrongdoing', + 'wrongful', + 'wrongheaded', + 'wrongly', + 'wrote', + 'wroth', + 'wrought', + 'wrung', + 'wry', + 'wryneck', + 'wulfenite', + 'wurst', + 'wynd', + 'x', + 'xanthate', + 'xanthein', + 'xanthene', + 'xanthic', + 'xanthin', + 'xanthine', + 'xanthochroid', + 'xanthochroism', + 'xanthophyll', + 'xanthous', + 'xebec', + 'xenia', + 'xenocryst', + 'xenogamy', + 'xenogenesis', + 'xenolith', + 'xenomorphic', + 'xenon', + 'xenophobe', + 'xenophobia', + 'xerarch', + 'xeric', + 'xeroderma', + 'xerography', + 'xerophagy', + 'xerophilous', + 'xerophthalmia', + 'xerophyte', + 'xerosere', + 'xerosis', + 'xi', + 'xiphisternum', + 'xiphoid', + 'xylem', + 'xylene', + 'xylidine', + 'xylograph', + 'xylography', + 'xyloid', + 'xylol', + 'xylophagous', + 'xylophone', + 'xylotomous', + 'xylotomy', + 'xyster', + 'y', + 'yabber', + 'yacht', + 'yachting', + 'yachtsman', + 'yah', + 'yahoo', + 'yak', + 'yakka', + 'yam', + 'yamen', + 'yammer', + 'yank', + 'yap', + 'yapok', + 'yapon', + 'yard', + 'yardage', + 'yardarm', + 'yardman', + 'yardmaster', + 'yardstick', + 'yare', + 'yarn', + 'yarrow', + 'yashmak', + 'yataghan', + 'yaupon', + 'yautia', + 'yaw', + 'yawl', + 'yawmeter', + 'yawn', + 'yawning', + 'yawp', + 'yaws', + 'yclept', + 'ye', + 'yea', + 'yeah', + 'yean', + 'yeanling', + 'year', + 'yearbook', + 'yearling', + 'yearlong', + 'yearly', + 'yearn', + 'yearning', + 'yeast', + 'yeasty', + 'yegg', + 'yeld', + 'yell', + 'yellow', + 'yellowbird', + 'yellowhammer', + 'yellowish', + 'yellowlegs', + 'yellows', + 'yellowtail', + 'yellowthroat', + 'yellowweed', + 'yellowwood', + 'yelp', + 'yen', + 'yenta', + 'yeoman', + 'yeomanly', + 'yeomanry', + 'yep', + 'yes', + 'yeshiva', + 'yester', + 'yesterday', + 'yesteryear', + 'yestreen', + 'yet', + 'yeti', + 'yew', + 'yid', + 'yield', + 'yielding', + 'yip', + 'yippee', + 'yippie', + 'ylem', + 'yod', + 'yodel', + 'yodle', + 'yoga', + 'yogh', + 'yoghurt', + 'yogi', + 'yogini', + 'yogurt', + 'yoicks', + 'yoke', + 'yokefellow', + 'yokel', + 'yolk', + 'yon', + 'yonder', + 'yoni', + 'yore', + 'you', + 'young', + 'youngling', + 'youngster', + 'younker', + 'your', + 'yours', + 'yourself', + 'youth', + 'youthen', + 'youthful', + 'yowl', + 'ytterbia', + 'ytterbite', + 'ytterbium', + 'yttria', + 'yttriferous', + 'yttrium', + 'yuan', + 'yucca', + 'yuk', + 'yulan', + 'yule', + 'yuletide', + 'yurt', + 'ywis', + 'z', + 'zabaglione', + 'zaffer', + 'zaibatsu', + 'zamia', + 'zamindar', + 'zanthoxylum', + 'zany', + 'zap', + 'zapateado', + 'zaratite', + 'zareba', + 'zarf', + 'zarzuela', + 'zayin', + 'zeal', + 'zealot', + 'zealotry', + 'zealous', + 'zebec', + 'zebra', + 'zebrass', + 'zebrawood', + 'zebu', + 'zecchino', + 'zed', + 'zedoary', + 'zee', + 'zemstvo', + 'zenana', + 'zenith', + 'zenithal', + 'zeolite', + 'zephyr', + 'zeppelin', + 'zero', + 'zest', + 'zestful', + 'zeta', + 'zeugma', + 'zibeline', + 'zibet', + 'zig', + 'zigzag', + 'zigzagger', + 'zillion', + 'zinc', + 'zincate', + 'zinciferous', + 'zincograph', + 'zincography', + 'zing', + 'zingaro', + 'zinkenite', + 'zinnia', + 'zip', + 'zipper', + 'zippy', + 'zircon', + 'zirconia', + 'zirconium', + 'zither', + 'zizith', + 'zloty', + 'zoa', + 'zodiac', + 'zombie', + 'zonal', + 'zonate', + 'zonation', + 'zone', + 'zoning', + 'zonked', + 'zoo', + 'zoochemistry', + 'zoochore', + 'zoogeography', + 'zoogloea', + 'zoography', + 'zooid', + 'zoolatry', + 'zoologist', + 'zoology', + 'zoom', + 'zoometry', + 'zoomorphism', + 'zoon', + 'zoonosis', + 'zoophilia', + 'zoophilous', + 'zoophobia', + 'zoophyte', + 'zooplankton', + 'zooplasty', + 'zoosperm', + 'zoosporangium', + 'zoospore', + 'zootechnics', + 'zootomy', + 'zootoxin', + 'zoril', + 'zoster', + 'zounds', + 'zucchetto', + 'zugzwang', + 'zwieback', + 'zygapophysis', + 'zygodactyl', + 'zygoma', + 'zygophyllaceous', + 'zygophyte', + 'zygosis', + 'zygospore', + 'zygote', + 'zygotene', + 'zymase', + 'zymogen', + 'zymogenesis', + 'zymogenic', + 'zymolysis', + 'zymometer', + 'zymosis', + 'zymotic' +] diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index fa72e42ef..e38c9fe89 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1,10 +1,12 @@ const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers') +const Random = require('random-seed') +const {getRandomBufferRange, buildRandomLines} = require('./helpers/random') const TextEditorComponent = require('../src/text-editor-component') const TextEditorElement = require('../src/text-editor-element') const TextEditor = require('../src/text-editor') const TextBuffer = require('text-buffer') -const {Point} = TextBuffer +const {Point, Range} = TextBuffer const fs = require('fs') const path = require('path') const Grim = require('grim') @@ -894,6 +896,73 @@ describe('TextEditorComponent', () => { expect(component.getGutterContainerWidth()).toBe(originalGutterContainerWidth) expect(component.getLineNumberGutterWidth()).toBe(originalLineNumberGutterWidth) }) + + describe('randomized test', () => { + let originalTimeout + + beforeEach(() => { + originalTimeout = jasmine.getEnv().defaultTimeoutInterval + jasmine.getEnv().defaultTimeoutInterval = 60 * 1000 + }) + + afterEach(() => { + jasmine.getEnv().defaultTimeoutInterval = originalTimeout + }) + + it('randomized insertions and deletions', async () => { + const initialSeed = Date.now() + for (var i = 0; i < 50; i++) { + let seed = initialSeed + i + // seed = 1507195048481 + const failureMessage = 'Randomized test failed with seed: ' + seed + const random = Random(seed) + + const rowsPerTile = random.intBetween(1, 6) + const {component, element, editor} = buildComponent({rowsPerTile, autoHeight: false}) + editor.setSoftWrapped(Boolean(random(2))) + await setEditorWidthInCharacters(component, random(20)) + await setEditorHeightInLines(component, random(10)) + element.focus() + + for (var j = 0; j < 5; j++) { + const k = random(10) + const range = getRandomBufferRange(random, editor.buffer) + + if (k < 1) { + editor.setSoftWrapped(!editor.isSoftWrapped()) + } else if (k < 4) { + editor.setSelectedBufferRange(range) + editor.backspace() + } else if (k < 8) { + const linesToInsert = buildRandomLines(random, 5) + editor.setCursorBufferPosition(range.start) + editor.insertText(linesToInsert) + } else { + editor.setSelectedBufferRange(range) + } + + component.scheduleUpdate() + await component.getNextUpdatePromise() + + const renderedLines = queryOnScreenLineElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow) + const renderedLineNumbers = queryOnScreenLineNumberElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow) + const renderedStartRow = component.getRenderedStartRow() + const actualLines = editor.displayLayer.getScreenLines(renderedStartRow, component.getRenderedEndRow()) + + expect(renderedLines.length).toBe(actualLines.length, failureMessage) + expect(renderedLineNumbers.length).toBe(actualLines.length, failureMessage) + for (let i = 0; i < renderedLines.length; i++) { + expect(renderedLines[i].textContent).toBe(actualLines[i].lineText || ' ', failureMessage) + expect(parseInt(renderedLines[i].dataset.screenRow)).toBe(renderedStartRow + i, failureMessage) + expect(parseInt(renderedLineNumbers[i].dataset.screenRow)).toBe(renderedStartRow + i, failureMessage) + } + } + + element.remove() + editor.destroy() + } + }) + }) }) describe('mini editors', () => { @@ -4504,3 +4573,11 @@ function queryOnScreenLineNumberElements (element) { function queryOnScreenLineElements (element) { return Array.from(element.querySelectorAll('.line:not(.dummy):not([data-off-screen])')) } + +function arraysEqual (a, b) { + if (a.length !== b.length) return false + for (let i = 0, length = a.length; i < length; i++) { + if (a[i] !== b[i]) return false + } + return true +} From 8759f9dac7cf0a7532c916ad02d05c045206a3bd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 Oct 2017 12:08:36 +0200 Subject: [PATCH 306/448] Clear dimensions cache when approximate screen line count changes --- src/text-editor-component.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 3060b6857..0a9d8c805 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2884,10 +2884,25 @@ class TextEditorComponent { // Ensure the spatial index is populated with rows that are currently visible populateVisibleRowRange (renderedStartRow) { - const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() - const visibleTileCount = Math.ceil(editorHeightInTiles) + 1 - const lastRenderedRow = renderedStartRow + (visibleTileCount * this.getRowsPerTile()) - this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, lastRenderedRow) + const {model} = this.props + const previousScreenLineCount = model.getApproximateScreenLineCount() + + const renderedEndRow = renderedStartRow + (this.getVisibleTileCount() * this.getRowsPerTile()) + this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, renderedEndRow) + + // It is possible for the approximate screen line count to exceed the actual + // number of lines. This may happen if there are many soft-wraps and the + // display layer hasn't indexed the entire contents of the buffer. In that + // circumstance, if a user attempts to scroll to a position that does not + // exist, we are unable to clip it correctly in `setScrollTop` because the + // line count approximation is also wrong. Therefore, after populating the + // spatial index, we will make sure that the approximate screen line count + // did not change. If it did, we need to clear the derived dimensions cache + // (which contains, for example, the rendered row range) because it could + // contain incorrect values. + if (model.getApproximateScreenLineCount() !== previousScreenLineCount) { + this.derivedDimensionsCache = {} + } } populateVisibleTiles () { From 1ca49d8f1764ddf25d674a457ba118cb38818be5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 Oct 2017 13:08:57 +0200 Subject: [PATCH 307/448] Move random-seed into script/package.json ...so that it doesn't get included in the final application bundle. --- package.json | 3 --- script/package.json | 1 + spec/text-editor-component-spec.js | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 8ddd8cfab..418fa0b75 100644 --- a/package.json +++ b/package.json @@ -186,8 +186,5 @@ "atom", "snapshotResult" ] - }, - "devDependencies": { - "random-seed": "^0.3.0" } } diff --git a/script/package.json b/script/package.json index 6ff678be0..c766806a1 100644 --- a/script/package.json +++ b/script/package.json @@ -26,6 +26,7 @@ "npm": "5.3.0", "passwd-user": "2.1.0", "pegjs": "0.9.0", + "random-seed": "^0.3.0", "season": "5.3.0", "semver": "5.3.0", "standard": "8.4.0", diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index e38c9fe89..3cee4af53 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1,6 +1,6 @@ const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers') -const Random = require('random-seed') +const Random = require('../script/node_modules/random-seed') const {getRandomBufferRange, buildRandomLines} = require('./helpers/random') const TextEditorComponent = require('../src/text-editor-component') const TextEditorElement = require('../src/text-editor-element') From ae57cd268e759c019ff5f54fc27bb9cba91eba08 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 Oct 2017 13:12:37 +0200 Subject: [PATCH 308/448] Delete unnecessary test code --- spec/text-editor-component-spec.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 3cee4af53..4e13d855c 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -6,7 +6,7 @@ const TextEditorComponent = require('../src/text-editor-component') const TextEditorElement = require('../src/text-editor-element') const TextEditor = require('../src/text-editor') const TextBuffer = require('text-buffer') -const {Point, Range} = TextBuffer +const {Point} = TextBuffer const fs = require('fs') const path = require('path') const Grim = require('grim') @@ -4573,11 +4573,3 @@ function queryOnScreenLineNumberElements (element) { function queryOnScreenLineElements (element) { return Array.from(element.querySelectorAll('.line:not(.dummy):not([data-off-screen])')) } - -function arraysEqual (a, b) { - if (a.length !== b.length) return false - for (let i = 0, length = a.length; i < length; i++) { - if (a[i] !== b[i]) return false - } - return true -} From ec14125ecb55f0e299ffe7018ae236e3e7429b6c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 Oct 2017 13:14:08 +0200 Subject: [PATCH 309/448] :art: --- spec/text-editor-component-spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 4e13d855c..23a29fc5e 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -897,7 +897,7 @@ describe('TextEditorComponent', () => { expect(component.getLineNumberGutterWidth()).toBe(originalLineNumberGutterWidth) }) - describe('randomized test', () => { + describe('randomized tests', () => { let originalTimeout beforeEach(() => { @@ -909,7 +909,7 @@ describe('TextEditorComponent', () => { jasmine.getEnv().defaultTimeoutInterval = originalTimeout }) - it('randomized insertions and deletions', async () => { + it('renders the visible rows correctly after randomly mutating the editor', async () => { const initialSeed = Date.now() for (var i = 0; i < 50; i++) { let seed = initialSeed + i From 785ade897ecda6a73d87b9a95b6e3f83aaf97eb7 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Thu, 5 Oct 2017 09:55:33 -0400 Subject: [PATCH 310/448] =?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 311/448] =?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 f640e9339d722dbd18180857359449a8bca5d88b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 Oct 2017 18:54:26 +0200 Subject: [PATCH 312/448] Make comment more succinct Signed-off-by: Nathan Sobo --- src/text-editor-component.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 0a9d8c805..5667a733e 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -2890,16 +2890,8 @@ class TextEditorComponent { const renderedEndRow = renderedStartRow + (this.getVisibleTileCount() * this.getRowsPerTile()) this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, renderedEndRow) - // It is possible for the approximate screen line count to exceed the actual - // number of lines. This may happen if there are many soft-wraps and the - // display layer hasn't indexed the entire contents of the buffer. In that - // circumstance, if a user attempts to scroll to a position that does not - // exist, we are unable to clip it correctly in `setScrollTop` because the - // line count approximation is also wrong. Therefore, after populating the - // spatial index, we will make sure that the approximate screen line count - // did not change. If it did, we need to clear the derived dimensions cache - // (which contains, for example, the rendered row range) because it could - // contain incorrect values. + // If the approximate screen line count changes, previously-cached derived + // dimensions could now be out of date. if (model.getApproximateScreenLineCount() !== previousScreenLineCount) { this.derivedDimensionsCache = {} } From 07c4b32689eb96ad50b84f6b11b0a10e12130819 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 Oct 2017 19:07:29 +0200 Subject: [PATCH 313/448] :arrow_up: text-buffer Signed-off-by: Nathan Sobo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 418fa0b75..f420deba9 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.2", + "text-buffer": "13.5.3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From d83304cbddbdf661f656b04137ee10b66121db14 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 5 Oct 2017 19:41:28 +0200 Subject: [PATCH 314/448] Exercise more features in the randomized test Signed-off-by: Nathan Sobo --- spec/text-editor-component-spec.js | 53 +++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 23a29fc5e..029cfee19 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -14,7 +14,6 @@ const electron = require('electron') const clipboard = require('../src/safe-clipboard') const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8') -const NBSP_CHARACTER = '\u00a0' document.registerElement('text-editor-component-test-element', { prototype: Object.create(HTMLElement.prototype, { @@ -911,9 +910,9 @@ describe('TextEditorComponent', () => { it('renders the visible rows correctly after randomly mutating the editor', async () => { const initialSeed = Date.now() - for (var i = 0; i < 50; i++) { + for (var i = 0; i < 20; i++) { let seed = initialSeed + i - // seed = 1507195048481 + // seed = 1507224195357 const failureMessage = 'Randomized test failed with seed: ' + seed const random = Random(seed) @@ -925,20 +924,32 @@ describe('TextEditorComponent', () => { element.focus() for (var j = 0; j < 5; j++) { - const k = random(10) + const k = random(100) const range = getRandomBufferRange(random, editor.buffer) - if (k < 1) { + if (k < 10) { editor.setSoftWrapped(!editor.isSoftWrapped()) - } else if (k < 4) { + } else if (k < 15) { + if (random(2)) setEditorWidthInCharacters(component, random(20)) + if (random(2)) setEditorHeightInLines(component, random(10)) + } else if (k < 40) { editor.setSelectedBufferRange(range) editor.backspace() - } else if (k < 8) { + } else if (k < 80) { const linesToInsert = buildRandomLines(random, 5) editor.setCursorBufferPosition(range.start) editor.insertText(linesToInsert) - } else { + } else if (k < 90) { + if (random(2)) { + editor.foldBufferRange(range) + } else { + editor.destroyFoldsIntersectingBufferRange(range) + } + } else if (k < 95) { editor.setSelectedBufferRange(range) + } else { + if (random(2)) component.setScrollTop(random(component.getScrollHeight())) + if (random(2)) component.setScrollLeft(random(component.getScrollWidth())) } component.scheduleUpdate() @@ -947,14 +958,26 @@ describe('TextEditorComponent', () => { const renderedLines = queryOnScreenLineElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow) const renderedLineNumbers = queryOnScreenLineNumberElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow) const renderedStartRow = component.getRenderedStartRow() - const actualLines = editor.displayLayer.getScreenLines(renderedStartRow, component.getRenderedEndRow()) + const expectedLines = editor.displayLayer.getScreenLines(renderedStartRow, component.getRenderedEndRow()) - expect(renderedLines.length).toBe(actualLines.length, failureMessage) - expect(renderedLineNumbers.length).toBe(actualLines.length, failureMessage) - for (let i = 0; i < renderedLines.length; i++) { - expect(renderedLines[i].textContent).toBe(actualLines[i].lineText || ' ', failureMessage) - expect(parseInt(renderedLines[i].dataset.screenRow)).toBe(renderedStartRow + i, failureMessage) - expect(parseInt(renderedLineNumbers[i].dataset.screenRow)).toBe(renderedStartRow + i, failureMessage) + expect(renderedLines.length).toBe(expectedLines.length, failureMessage) + expect(renderedLineNumbers.length).toBe(expectedLines.length, failureMessage) + for (let k = 0; k < renderedLines.length; k++) { + const expectedLine = expectedLines[k] + const expectedText = expectedLine.lineText || ' ' + + const renderedLine = renderedLines[k] + const renderedLineNumber = renderedLineNumbers[k] + let renderedText = renderedLine.textContent + // We append zero width NBSPs after folds at the end of the + // line in order to support measurement. + if (expectedText.endsWith(editor.displayLayer.foldCharacter)) { + renderedText = renderedText.substring(0, renderedText.length - 1) + } + + expect(renderedText).toBe(expectedText, failureMessage) + expect(parseInt(renderedLine.dataset.screenRow)).toBe(renderedStartRow + k, failureMessage) + expect(parseInt(renderedLineNumber.dataset.screenRow)).toBe(renderedStartRow + k, failureMessage) } } From 6015ba0096efbac4caac59954dfbf60230b4a451 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Thu, 5 Oct 2017 13:46:38 -0600 Subject: [PATCH 315/448] :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 316/448] :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 317/448] :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 318/448] :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 319/448] 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 320/448] 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 321/448] =?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 322/448] =?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 323/448] :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 324/448] 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 325/448] :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 326/448] =?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 327/448] 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 328/448] :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 329/448] :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 330/448] 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 331/448] :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 332/448] :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 333/448] :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 334/448] :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 335/448] :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 336/448] 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 337/448] :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 338/448] :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 339/448] :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 340/448] 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 341/448] 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 342/448] :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 343/448] =?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 344/448] :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 345/448] 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 346/448] :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 347/448] 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 348/448] 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 349/448] 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 350/448] 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 351/448] 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 352/448] :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 353/448] :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 354/448] :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 355/448] 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 356/448] =?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 357/448] 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 358/448] 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 359/448] 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 360/448] :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 361/448] 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 362/448] 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 363/448] :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 364/448] 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 365/448] :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 366/448] :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 367/448] 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 368/448] 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 369/448] 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 370/448] 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 371/448] :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 372/448] :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 c632e6ca58f6c965956d1e75e9bc0c78134a8e6e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 20:22:02 -0400 Subject: [PATCH 373/448] =?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 374/448] 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 375/448] 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 376/448] 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 377/448] :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 378/448] :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 379/448] =?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 d79e6c4b6352e708d97b1d23b29856c9af06c857 Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Wed, 18 Oct 2017 17:52:03 -0700 Subject: [PATCH 380/448] :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 381/448] :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 382/448] :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 383/448] 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 384/448] :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 385/448] 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 386/448] :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 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 387/448] 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 0f89211d55cbcc5f4fdbfa6f76ed6cb709c98783 Mon Sep 17 00:00:00 2001 From: Indrek Ardel Date: Fri, 20 Oct 2017 13:34:15 +0300 Subject: [PATCH 388/448] 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 389/448] =?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 d0bdbb861ba8b0234135460bd86cc968316a3e14 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 20 Oct 2017 11:30:50 -0600 Subject: [PATCH 390/448] 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 391/448] 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 392/448] =?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 393/448] =?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 394/448] =?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 395/448] =?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 396/448] =?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 397/448] =?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 398/448] =?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 399/448] 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 400/448] =?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 401/448] :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 402/448] :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 403/448] :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 404/448] :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 405/448] =?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 406/448] :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 407/448] 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 408/448] :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 409/448] :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 410/448] :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 411/448] :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 412/448] 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 413/448] :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 414/448] :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 415/448] :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 416/448] :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 417/448] :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 418/448] :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 419/448] 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 420/448] 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 421/448] :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 422/448] 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 423/448] 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 424/448] :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 425/448] 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 426/448] 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 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 427/448] :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 428/448] :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 429/448] :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 430/448] :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 431/448] :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 432/448] :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 433/448] :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 434/448] :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 435/448] :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 436/448] :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 437/448] :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 438/448] :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 439/448] :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 440/448] :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 441/448] :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 442/448] :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 443/448] :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 444/448] 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 445/448] :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 446/448] 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 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 447/448] :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 1722273630bf8b306ac34bb7b4b38ffa40135cab Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 25 Oct 2017 12:52:31 -0700 Subject: [PATCH 448/448] :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",