diff --git a/.github/stale.yml b/.github/stale.yml index 415a830c2..4888a3bb6 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -13,9 +13,20 @@ 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: + + 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 # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 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/ diff --git a/apm/package.json b/apm/package.json index 8b8cf4c73..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.5" + "atom-package-manager": "1.18.10" } } 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/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/ 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/package.json b/package.json index a297b552f..6c1d675cc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.21.0-dev", + "version": "1.23.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { @@ -12,11 +12,11 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.6.9", + "electronVersion": "1.6.15", "dependencies": { "@atom/source-map-support": "^0.3.4", "async": "0.2.6", - "atom-keymap": "8.2.4", + "atom-keymap": "8.2.8", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", @@ -26,18 +26,18 @@ "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.3.0", + "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.7", + "first-mate": "7.0.10", "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", - "git-utils": "5.0.0", + "git-utils": "5.1.0", "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", @@ -65,12 +65,12 @@ "scandal": "^3.1.0", "scoped-property-store": "^0.17.0", "scrollbar-style": "^3.2", - "season": "^6.0.0", + "season": "^6.0.2", "semver": "^4.3.3", "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.1.14", + "text-buffer": "13.5.8", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -89,86 +89,86 @@ "one-light-syntax": "1.8.0", "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", - "about": "1.7.6", - "archive-view": "0.63.3", + "about": "1.7.8", + "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", - "autocomplete-html": "0.8.1", - "autocomplete-plus": "2.35.8", - "autocomplete-snippets": "1.11.1", + "autocomplete-html": "0.8.2", + "autocomplete-plus": "2.37.0", + "autocomplete-snippets": "1.11.2", "autoflow": "0.29.0", - "autosave": "0.24.3", + "autosave": "0.24.6", "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.7", + "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", - "encoding-selector": "0.23.4", + "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", - "find-and-replace": "0.212.0", - "fuzzy-finder": "1.5.8", - "github": "0.5.0", + "find-and-replace": "0.212.3", + "fuzzy-finder": "1.6.1", + "github": "0.7.0", "git-diff": "1.3.6", "go-to-line": "0.32.1", - "grammar-selector": "0.49.5", - "image-view": "0.62.3", + "grammar-selector": "0.49.8", + "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", - "line-ending-selector": "0.7.3", + "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.13", + "markdown-preview": "0.159.16", "metrics": "1.2.6", - "notifications": "0.69.0", + "notifications": "0.69.2", "open-on-github": "1.2.1", "package-generator": "1.1.1", - "settings-view": "0.251.5", - "snippets": "1.1.4", - "spell-check": "0.72.2", - "status-bar": "1.8.11", - "styleguide": "0.49.6", - "symbols-view": "0.117.1", - "tabs": "0.107.1", + "settings-view": "0.252.1", + "snippets": "1.1.6", + "spell-check": "0.72.3", + "status-bar": "1.8.13", + "styleguide": "0.49.8", + "symbols-view": "0.118.1", + "tabs": "0.108.0", "timecop": "0.36.0", - "tree-view": "0.217.8", + "tree-view": "0.220.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", - "whitespace": "0.37.2", + "whitespace": "0.37.4", "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4", - "language-coffee-script": "0.49.0", - "language-csharp": "0.14.2", - "language-css": "0.42.5", - "language-gfm": "0.90.1", + "language-coffee-script": "0.49.2", + "language-csharp": "0.14.3", + "language-css": "0.42.7", + "language-gfm": "0.90.2", "language-git": "0.19.1", - "language-go": "0.44.2", - "language-html": "0.47.7", - "language-hyperlink": "0.16.2", - "language-java": "0.27.4", - "language-javascript": "0.127.3", + "language-go": "0.44.3", + "language-html": "0.48.2", + "language-hyperlink": "0.16.3", + "language-java": "0.27.5", + "language-javascript": "0.127.6", "language-json": "0.19.1", - "language-less": "0.33.0", + "language-less": "0.34.0", "language-make": "0.22.3", - "language-mustache": "0.14.1", + "language-mustache": "0.14.4", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.41.0", + "language-php": "0.42.2", "language-property-list": "0.9.1", - "language-python": "0.45.4", - "language-ruby": "0.71.3", + "language-python": "0.45.5", + "language-ruby": "0.71.4", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.61.1", - "language-shellscript": "0.25.3", + "language-sass": "0.61.2", + "language-shellscript": "0.25.4", "language-source": "0.9.0", "language-sql": "0.25.8", "language-text": "0.7.3", - "language-todo": "0.29.2", + "language-todo": "0.29.3", "language-toml": "0.18.1", - "language-typescript": "0.1.0", + "language-typescript": "0.2.2", "language-xml": "0.35.2", - "language-yaml": "0.30.2" + "language-yaml": "0.31.1" }, "private": true, "scripts": { diff --git a/resources/app-icons/beta/atom.icns b/resources/app-icons/beta/atom.icns index 69abe031e..6737fa27f 100644 Binary files a/resources/app-icons/beta/atom.icns and b/resources/app-icons/beta/atom.icns differ diff --git a/resources/app-icons/dev/atom.icns b/resources/app-icons/dev/atom.icns index 84319be3d..b006a1a83 100644 Binary files a/resources/app-icons/dev/atom.icns and b/resources/app-icons/dev/atom.icns differ diff --git a/resources/app-icons/stable/atom.icns b/resources/app-icons/stable/atom.icns index 2f3246bb8..73ef96330 100644 Binary files a/resources/app-icons/stable/atom.icns and b/resources/app-icons/stable/atom.icns differ 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.`) } diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 471bd1201..333acdc0a 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') @@ -28,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', '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') || - 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) => { @@ -76,7 +65,21 @@ 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') { + 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 { + 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/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 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)) { diff --git a/script/package.json b/script/package.json index 59ca93375..4cf1bfb8c 100644 --- a/script/package.json +++ b/script/package.json @@ -9,10 +9,10 @@ "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.2", + "electron-winstaller": "2.6.3", "fs-admin": "^0.1.5", "fs-extra": "0.30.0", "glob": "7.0.3", @@ -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/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}) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 8a3e4e0fb..f178bbb6c 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 -> diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 552f44bf9..bcf50c268 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -13,6 +13,8 @@ describe "Config", -> dotAtomPath = temp.path('atom-spec-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 -> @@ -877,7 +879,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 @@ -895,16 +897,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 - 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 -> + jasmine.useRealClock() + atom.config.setSchema 'foo', type: 'object' properties: @@ -920,7 +921,7 @@ describe "Config", -> default: 12 expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy() - fs.writeFileSync atom.config.configFilePath, """ + writeConfigFile """ '*': foo: bar: 'baz' @@ -930,26 +931,32 @@ describe "Config", -> scoped: true """ atom.config.loadUserConfig() - atom.config.observeUserConfig() - updatedHandler = jasmine.createSpy("updatedHandler") - atom.config.onDidChange updatedHandler + + waitsForPromise -> atom.config.observeUserConfig() + + runs -> + updatedHandler = jasmine.createSpy "updatedHandler" + atom.config.onDidChange updatedHandler afterEach -> atom.config.unobserveUserConfig() fs.removeSync(dotAtomPath) describe "when the config file changes to contain valid cson", -> + it "updates the config data", -> - writeConfigFile("foo: { bar: 'quux', baz: 'bar'}") + 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' 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'}") + writeConfigFile "foo: { bar: 'baz', baz: 'ok'}", 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> @@ -964,15 +971,16 @@ 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() 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'}") + writeConfigFile "foo: { bar: ['baz', 'ok'], baz: 'another'}", 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> @@ -989,7 +997,7 @@ describe "Config", -> '*': foo: scoped: false - """ + """, 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> @@ -997,7 +1005,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 """ @@ -1007,7 +1015,7 @@ describe "Config", -> '.source.ruby': foo: scoped: true - """ + """, 2 waitsFor 'update event', -> updatedHandler.callCount > 0 runs -> @@ -1017,7 +1025,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' @@ -1025,20 +1033,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", -> @@ -1047,25 +1055,26 @@ describe "Config", -> describe "when the config file changes to contain invalid cson", -> addErrorHandler = null beforeEach -> - atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy() - writeConfigFile("}}}") + atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy "error handler" + 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 -> @@ -1741,3 +1750,35 @@ 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", -> + beforeEach -> + atom.config.settingsLoaded = false + fs.writeFileSync atom.config.configFilePath, """ + '*': + foo: + bar: 'baz' + do: + ray: 'me' + """ + + 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() + + 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' 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/fixtures/packages/package-with-uri-handler/index.js b/spec/fixtures/packages/package-with-uri-handler/index.js new file mode 100644 index 000000000..5d31dca98 --- /dev/null +++ b/spec/fixtures/packages/package-with-uri-handler/index.js @@ -0,0 +1,5 @@ +module.exports = { + activate: () => null, + deactivate: () => 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/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee deleted file mode 100644 index 186bc7f63..000000000 --- a/spec/git-repository-provider-spec.coffee +++ /dev/null @@ -1,101 +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 -> - if provider? - provider.pathToRepository[key].destroy() for key in Object.keys(provider.pathToRepository) - - 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..24993fe9b --- /dev/null +++ b/spec/git-repository-provider-spec.js @@ -0,0 +1,111 @@ +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) + }) + + afterEach(() => { + if (provider) { + Object.keys(provider.pathToRepository).forEach(key => { + provider.pathToRepository[key].destroy() + }) + } + }) + + 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') + }) + }) + }) +}) 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() diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index 7d4754397..db716528d 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", -> @@ -118,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/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([]) + }) +) +}) 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() + }) + }) +}) 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/language-mode-spec.coffee b/spec/language-mode-spec.coffee deleted file mode 100644 index 4025472af..000000000 --- a/spec/language-mode-spec.coffee +++ /dev/null @@ -1,490 +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 -> - atom.packages.deactivatePackages() - 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 -> - atom.packages.deactivatePackages() - 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 -> - atom.packages.deactivatePackages() - 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 -> - 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`", -> - 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 -> - atom.packages.deactivatePackages() - 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 -> - atom.packages.deactivatePackages() - 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 syntatic 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)", -> - 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 syntatic 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 -> - atom.packages.deactivatePackages() - 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 -> - 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/main-process/parse-command-line.test.js b/spec/main-process/parse-command-line.test.js new file mode 100644 index 000000000..0cd1f5b13 --- /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 --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) + 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 --uri-handler is passed', function () { + it('ignores other arguments and limits to one URL', function () { + 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) + assert.deepEqual(args.urlsToOpen, ['atom://test/url']) + assert.deepEqual(args.pathsToOpen, []) + }) + }) +}) 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') + }) + }) +}) 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) + }) + ) + }) +}) diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee deleted file mode 100644 index 88efff4ae..000000000 --- a/spec/package-manager-spec.coffee +++ /dev/null @@ -1,1356 +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 -> - atom.packages.deactivatePackages() - 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 -> - atom.packages.deactivatePackage('package-with-view-providers') - 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 - 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() - - 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") - atom.packages.deactivatePackage("package-with-serialization") - 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") - - 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() - - atom.packages.deactivatePackage("package-with-provided-services") - - expect(firstServiceV3Disposed).toBe true - expect(firstServiceV4Disposed).toBe true - expect(secondServiceDisposed).toBe true - - 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') - 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", -> - 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() - - atom.packages.deactivatePackage("package-with-deactivate") - 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() - - 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", -> - spyOn(console, 'error') - - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-deactivate") - - runs -> - expect(-> atom.packages.deactivatePackage("package-that-throws-on-deactivate")).not.toThrow() - expect(console.error).toHaveBeenCalled() - - it "removes the package's grammars", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-grammars') - - runs -> - 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') - - runs -> - 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') - - runs -> - atom.packages.deactivatePackage('package-with-styles') - 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' - 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") - - runs -> - deactivatedPackage = null - atom.packages.onDidDeactivatePackage (pack) -> deactivatedPackage = pack - atom.packages.deactivatePackage("package-with-main") - 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 -> - atom.packages.deactivatePackages() - 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' - waitsForPromise -> - atom.packages.activatePackage(packageName) - - runs -> - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - pack = atom.packages.disablePackage(packageName) - - activatedPackages = atom.packages.getActivePackages() - 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 -> - 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..0b26bf839 --- /dev/null +++ b/spec/package-manager-spec.js @@ -0,0 +1,1354 @@ +const path = require('path') +const url = require('url') +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') +const {it, fit, ffit, beforeEach, afterEach} = require('./async-spec-helpers') + +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(async () => { + await atom.packages.deactivatePackages() + 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(async () => { + await atom.packages.deactivatePackage('package-with-view-providers') + 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', async () => { + atom.packages.loadPackage('package-with-view-providers') + + await atom.packages.activatePackage('package-with-view-providers') + + 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', async () => { + const pack = await atom.packages.activatePackage('package-with-main') + 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', async () => { + spyOn(Package.prototype, 'activateNow').andCallThrough() + await atom.packages.activatePackage('package-with-index') + await atom.packages.activatePackage('package-with-index') + await atom.packages.activatePackage('package-with-index') + + 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', async () => { + const mainModule = require('./fixtures/packages/package-with-main/main-module') + spyOn(mainModule, 'activate') + + 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', async () => { + const indexModule = require('./fixtures/packages/package-with-index/index') + spyOn(indexModule, 'activate') + + 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', async () => { + expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined() + + 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`', () => { + 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', async () => { + expect(Package.prototype.requireMainModule.callCount).toBe(0) + + atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', {bubbles: true})) + + await promise + expect(Package.prototype.requireMainModule.callCount).toBe(1) + }) + + it('triggers the activation event on all handlers registered during activation', async () => { + await atom.workspace.open() + + 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', async () => { + mainModule = require('./fixtures/packages/package-with-empty-activation-commands/index') + spyOn(mainModule, 'activate').andCallThrough() + + atom.packages.activatePackage('package-with-empty-activation-commands') + + 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', async () => { + let notificationEvent + + await new Promise(resolve => { + const subscription = atom.notifications.onDidAddNotification(event => { + notificationEvent = event + subscription.dispose() + resolve() + }) + + 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', async () => { + let notificationEvent + + await new Promise(resolve => { + const subscription = atom.notifications.onDidAddNotification(event => { + notificationEvent = event + subscription.dispose() + resolve() + }) + + 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') + }) + }) + }) + + 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', 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() + + await promise + expect(Package.prototype.requireMainModule.callCount).toBe(1) + }) + + 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() + + await promise + expect(mainModule.activate.callCount).toBe(1) + + await 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() + + await promise + expect(mainModule.activate.callCount).toBe(2) + }) + + it('activates the package immediately when activationHooks is empty', async () => { + mainModule = require('./fixtures/packages/package-with-empty-activation-hooks/index') + spyOn(mainModule, 'activate').andCallThrough() + + expect(Package.prototype.requireMainModule.callCount).toBe(0) + + 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', async () => { + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + expect(Package.prototype.requireMainModule.callCount).toBe(0) + + await atom.packages.activatePackage('package-with-activation-hooks') + 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', async () => { + spyOn(console, 'warn') + 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", 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', async () => { + let activatedPackage + atom.packages.onDidActivatePackage(pack => { + activatedPackage = pack + }) + + 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", () => { + 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', async () => { + spyOn(console, 'warn') + atom.config.set('core.disabledPackages', []) + + try { + await atom.packages.activatePackage('this-doesnt-exist') + expect('Error to be thrown').toBe('') + } catch (error) { + expect(console.warn.callCount).toBe(1) + 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', 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) + + 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', async () => { + const element1 = createTestElement('test-1') + const element3 = createTestElement('test-3') + 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})[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', 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', 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']) + await atom.packages.activatePackage('package-with-keymaps-manifest') + 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', async () => { + const element1 = createTestElement('test-1') + atom.packages.observePackagesWithKeymapsDisabled() + + await atom.packages.activatePackage('package-with-keymaps-manifest') + + 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", async () => { + fs.writeFileSync(userKeymapPath, `".test-1": {"ctrl-z": "user-command"}`) + atom.keymaps.loadUserKeymap() + + 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') + + 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') + }) + }) + }) + + 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', async () => { + const element = createTestElement('test-1') + expect(atom.contextMenu.templateForElement(element)).toEqual([]) + + 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', async () => { + const element = createTestElement('test-1') + expect(atom.contextMenu.templateForElement(element)).toEqual([]) + + 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', 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', 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') + + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + + 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', 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') + 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() + + 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", async () => { + await atom.packages.activatePackage('package-with-styles') + + 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", 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', async () => { + await atom.packages.activatePackage('package-with-settings') + expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a') + }) + }) + + + 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-uri-handler') + atom.dispatchURIMessage(uri) + await activationPromise + expect(mod.handleURI).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') + + 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 })) + + 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') + + consumerModule.consumeFirstServiceV3.reset() + consumerModule.consumeFirstServiceV4.reset() + consumerModule.consumeSecondService.reset() + + await atom.packages.deactivatePackage('package-with-provided-services') + expect(firstServiceV3Disposed).toBe(true) + expect(firstServiceV4Disposed).toBe(true) + expect(secondServiceDisposed).toBe(true) + + 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', async () => { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + + 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', async () => { + spyOn(atom, 'inSpecMode').andReturn(false) + spyOn(console, 'warn') + + const badPack = await atom.packages.activatePackage('package-that-throws-on-activate') + 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", async () => { + spyOn(console, 'error') + + 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', async () => { + const pack1 = await atom.packages.activatePackage('package-with-deactivate') + const pack2 = await atom.packages.activatePackage('package-with-serialization') + + 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", async () => { + spyOn(atom, 'inSpecMode').andReturn(false) + + const pack = await atom.packages.activatePackage('package-with-deactivate') + 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() + + 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() + + 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", async () => { + spyOn(console, 'error') + 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", 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", 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", async () => { + await atom.packages.activatePackage('package-with-styles') + await atom.packages.deactivatePackage('package-with-styles') + + 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", async () => { + await atom.packages.activatePackage('package-with-settings') + expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a') + + 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', async () => { + await atom.packages.activatePackage('package-with-main') + + let deactivatedPackage + atom.packages.onDidDeactivatePackage(pack => { + deactivatedPackage = pack + }) + + await atom.packages.deactivatePackage('package-with-main') + 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(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + jasmine.restoreDeprecationsSnapshot() + }) + + it('sets hasActivatedInitialPackages', async () => { + spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) + spyOn(atom.packages, 'activatePackages') + expect(atom.packages.hasActivatedInitialPackages()).toBe(false) + + await atom.packages.activate() + 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', 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') + + atom.packages.activate() + 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', 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) + await new Promise(resolve => atom.packages.onDidActivatePackage(resolve)) + + 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', async () => { + const packageName = 'package-with-main' + const pack = await atom.packages.activatePackage(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) + }) + + expect(atom.packages.getActivePackages()).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', () => { + beforeEach(() => atom.themes.activateThemes()) + afterEach(() => atom.themes.deactivateThemes()) + + 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 + 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) + + await new Promise(resolve => { + atom.themes.onDidChangeActiveThemes(resolve) + atom.packages.disablePackage(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) + }) + }) + }) +}) diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index 5018d2eb4..e7bfd0249 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 -> + Promise.resolve(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 -> + Promise.resolve(theme.deactivate()) + runs -> + expect(spy).toHaveBeenCalled() describe ".loadMetadata()", -> [packagePath, metadata] = [] 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] 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) + }) + ) + }) +}) diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index ff7634734..af34681a6 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -113,6 +113,53 @@ 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) -> + @path = path + callback() for callback in @changePathCallbacks + return + item1.getPath = -> @path + item1.onDidChangePath = (callback) -> + @changePathCallbacks.push callback + return dispose: => + @changePathCallbacks = @changePathCallbacks.filter (f) -> f isnt 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", -> diff --git a/spec/pane-spec.js b/spec/pane-spec.js index 41946a6c9..e448f992f 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,16 +491,31 @@ describe('Pane', () => { expect(pane.getActiveItem()).toBeUndefined() }) - it('invokes ::onWillDestroyItem() observers before destroying the item', () => { + it('invokes ::onWillDestroyItem() and PaneContainer::onWillDestroyPaneItem observers before destroying the item', async () => { + jasmine.useRealClock() + pane.container = new PaneContainer({config: atom.config, confirm}) 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) + events.push(['will-destroy-item', event]) }) - pane.destroyItem(item2) + 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(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/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() diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee deleted file mode 100644 index 6ce13c3f4..000000000 --- a/spec/project-spec.coffee +++ /dev/null @@ -1,716 +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 - - afterEach -> - deserializedProject?.destroy() - - it "does not deserialize paths to non directories", -> - deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm}) - state = atom.project.serialize() - state.paths.push('/directory/that/does/not/exist') - - waitsForPromise -> - deserializedProject.deserialize(state, atom.deserializers) - - runs -> - expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths()) - - 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 a directory that exists", -> - 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}) - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - 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}) - deserializedProject.deserialize(atom.project.serialize({isUnloading: false})) - expect(deserializedProject.getBuffers().length).toBe 0 - - it "serializes marker layers and history only if Atom is quitting", -> - waitsForPromise -> - atom.workspace.open('a') - - notQuittingProject = null - quittingProject = null - bufferA = null - layerA = null - markerA = null - - runs -> - bufferA = atom.project.getBuffers()[0] - layerA = bufferA.addMarkerLayer(persistent: true) - markerA = layerA.markPosition([0, 3]) - bufferA.append('!') - - waitsForPromise -> - notQuittingProject?.destroy() - 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) - - waitsForPromise -> - quittingProject?.destroy() - 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) - - 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)", -> - 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') - 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) - - 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)", -> - 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) - - 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..63c065fa6 --- /dev/null +++ b/spec/project-spec.js @@ -0,0 +1,927 @@ +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', () => { + beforeEach(() => { + 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) + }) + + describe('serialization', () => { + let deserializedProject = null + let notQuittingProject = null + let quittingProject = null + + afterEach(() => { + if (deserializedProject != null) { + deserializedProject.destroy() + } + if (notQuittingProject != null) { + notQuittingProject.destroy() + } + if (quittingProject != null) { + 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}) + 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 }) + ) + + 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', () => { + 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 }) + ) + + 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', () => { + const 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', () => { + 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(() => { + 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', () => { + const 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', () => { + const 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')) + + let bufferA = null + let layerA = null + let 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), 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}) + }) + + waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true}))) + + runs(() => { + expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.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", () => { + 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)) + + runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile))) + }) + ) + + describe('before and after saving a buffer', () => { + let 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', () => { + let editor = null + beforeEach(() => + waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => { editor = o })) + ) + + it('creates a warning notification', () => { + 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`') + expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a')) + }) + }) + + describe('when a custom repository-provider service is provided', () => { + let fakeRepositoryProvider, fakeRepository + + beforeEach(() => { + fakeRepository = {destroy () { return null }} + fakeRepositoryProvider = { + repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) }, + repositoryForDirectorySync (directory) { return fakeRepository } + } + }) + + 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]) + + atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider) + waitsFor(() => atom.project.repositoryProviders.length > 1) + runs(() => atom.project.getRepositories()[0] === fakeRepository) + }) + + 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() + + 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([]) + + const 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 (aPath) { + this.path = aPath + } + 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(() => { + serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', { + directoryForURISync (uri) { + if (uri.startsWith('ssh://')) { + return new DummyDirectory(uri) + } else { + return null + } + } + }) + + waitsFor(() => atom.project.directoryProviders.length > 0) + }) + + 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' + + 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) + 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)', () => { + let 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'", () => { + let 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'", () => { + let 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', () => { + 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)) + ) + + 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'", () => { + let editor = null + waitsForPromise(() => atom.workspace.open().then(o => { editor = o })) + + runs(() => { + expect(editor.buffer.getPath()).toBeUndefined() + expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer) + }) + }) + ) + }) + + describe('.bufferForPath(path)', () => { + let 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'))) + return atom.project.bufferForPath('b') + }) + + waitsForPromise({shouldReject: false}, () => { + TextBuffer.load.andCallThrough() + return 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(() => { + const directory = new Directory('/tmp') + return 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(() => { + const directory = new Directory(path.join(__dirname, '..')) + const promise = atom.project.repositoryForDirectory(directory) + return promise.then((result) => { + expect(result).toBeInstanceOf(GitRepository) + const 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', () => { + let repository = null + const 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", () => { + const 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', () => { + 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] = 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', () => { + 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) + expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths) + }) + + 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 { + 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', () => { + const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + + const [oldPath] = atom.project.getPaths() + + const 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", () => { + const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy') + atom.project.onDidChangePaths(onDidChangePathsSpy) + const [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 + const 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", () => { + const 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)', () => { + let onDidChangePathsSpy = null + + beforeEach(() => { + onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener') + atom.project.onDidChangePaths(onDidChangePathsSpy) + }) + + 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([]) + 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", () => { + const originalPaths = atom.project.getPaths() + const 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) { + 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) + expect(atom.project.getPaths()).toEqual([]) + }) + }) + + describe('.onDidChangeFiles()', () => { + let sub = [] + const events = [] + let checkCallback = () => {} + + beforeEach(() => { + sub = atom.project.onDidChangeFiles((incoming) => { + events.push(...incoming) + checkCallback() + }) + }) + + afterEach(() => sub.dispose()) + + const waitForEvents = (paths) => { + const remaining = new Set(paths.map((p) => fs.realpathSync(p))) + return new Promise((resolve, reject) => { + checkCallback = () => { + for (let event of events) { remaining.delete(event.path) } + if (remaining.size === 0) { resolve() } + } + + const expire = () => { + checkCallback = () => {} + console.error('Paths not seen:', remaining) + reject(new Error('Expired before all expected events were delivered.')) + } + + checkCallback() + setTimeout(expire, 2000) + }) + } + + 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') + 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(() => { + 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 === fileThree)).toBeFalsy()) + }) + }) + + describe('.onDidAddBuffer()', () => + it('invokes the callback with added text buffers', () => { + const buffers = [] + const 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', () => { + 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(() => { + 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')) + + 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') + 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', () => { + const 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')) + + 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') + 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', () => { + 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', () => { + 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', () => { + 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) + 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', () => { + 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') + 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/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') 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/spec/spec-helper.coffee b/spec/spec-helper.coffee index eec8ce5fb..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]) @@ -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/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 311759bac..5f0a28883 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1,5 +1,7 @@ const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers') +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') const TextEditor = require('../src/text-editor') @@ -12,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, { @@ -286,6 +287,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) @@ -378,35 +404,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 () => { @@ -854,6 +895,97 @@ describe('TextEditorComponent', () => { expect(component.getGutterContainerWidth()).toBe(originalGutterContainerWidth) expect(component.getLineNumberGutterWidth()).toBe(originalLineNumberGutterWidth) }) + + describe('randomized tests', () => { + let originalTimeout + + beforeEach(() => { + originalTimeout = jasmine.getEnv().defaultTimeoutInterval + jasmine.getEnv().defaultTimeoutInterval = 60 * 1000 + }) + + afterEach(() => { + jasmine.getEnv().defaultTimeoutInterval = originalTimeout + }) + + it('renders the visible rows correctly after randomly mutating the editor', async () => { + const initialSeed = Date.now() + for (var i = 0; i < 20; i++) { + let seed = initialSeed + i + // seed = 1507224195357 + 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(100) + const range = getRandomBufferRange(random, editor.buffer) + + if (k < 10) { + editor.setSoftWrapped(!editor.isSoftWrapped()) + } 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 < 80) { + const linesToInsert = buildRandomLines(random, 5) + editor.setCursorBufferPosition(range.start) + editor.insertText(linesToInsert) + } 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() + 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 expectedLines = editor.displayLayer.getScreenLines(renderedStartRow, component.getRenderedEndRow()) + + 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) + } + } + + element.remove() + editor.destroy() + } + }) + }) }) describe('mini editors', () => { @@ -1142,7 +1274,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())) @@ -1601,7 +1733,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) @@ -1764,6 +1896,8 @@ describe('TextEditorComponent', () => { const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'}) await component.getNextUpdatePromise() + const overlayComponent = component.overlayComponents.values().next().value + const overlayWrapper = overlayElement.parentElement expect(overlayWrapper.classList.contains('a')).toBe(true) expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) @@ -1794,12 +1928,12 @@ describe('TextEditorComponent', () => { await setScrollTop(component, 20) expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) overlayElement.style.height = 60 + 'px' - await component.getNextUpdatePromise() + await overlayComponent.getNextUpdatePromise() expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4)) // Does not flip the overlay vertically if it would overflow the top of the window overlayElement.style.height = 80 + 'px' - await component.getNextUpdatePromise() + await overlayComponent.getNextUpdatePromise() expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5)) // Can update overlay wrapper class @@ -2284,6 +2418,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'}) @@ -2388,6 +2543,49 @@ 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) + 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}) @@ -2706,6 +2904,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 +2914,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 +2924,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 +2962,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 +2992,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 +3001,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 +3028,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 +3045,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 +3068,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 +3086,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 +3101,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', () => { @@ -3272,9 +3480,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 () => { @@ -4216,7 +4424,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 @@ -4229,8 +4437,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) @@ -4250,7 +4464,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/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js index 79d575a5f..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) @@ -685,7 +700,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) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index fc67881ab..0dce1acaf 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -74,6 +74,16 @@ 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()) + + 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", -> @@ -145,7 +155,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()) @@ -620,7 +630,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() @@ -1158,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]) @@ -1364,7 +1426,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]) @@ -1397,7 +1459,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 +1469,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 +1484,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 +1503,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 +1545,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 +1564,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 +1583,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] @@ -1800,7 +1862,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 +1911,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]]]) @@ -2258,7 +2320,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 " };" @@ -3517,7 +3579,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) {' @@ -3704,7 +3766,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 +3789,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") @@ -4160,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", -> @@ -4301,108 +4376,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") @@ -4820,7 +4793,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() @@ -4992,7 +4965,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') @@ -5262,37 +5235,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') @@ -5918,3 +5860,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/spec/text-editor-spec.js b/spec/text-editor-spec.js new file mode 100644 index 000000000..d10efa695 --- /dev/null +++ b/spec/text-editor-spec.js @@ -0,0 +1,541 @@ +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 + + afterEach(() => { + 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('.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') + }) + + 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', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(1) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + 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', 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) + 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('.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}) + + 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) + }) + + it('folds every foldable range at a given indentLevel', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + + 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', 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]) + }) + }) + + describe('.isFoldableAtBufferRow(bufferRow)', () => { + 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) + 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', 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", 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) + }) + }) + }) +}) 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/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee deleted file mode 100644 index 07e7e80e6..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.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 contigous 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..b1574673a --- /dev/null +++ b/spec/tokenized-buffer-spec.js @@ -0,0 +1,904 @@ +const NullGrammar = require('../src/null-grammar') +const TokenizedBuffer = require('../src/tokenized-buffer') +const TextBuffer = require('text-buffer') +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 + + beforeEach(async () => { + // enable async tokenization + TokenizedBuffer.prototype.chunkSize = 5 + jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(() => { + buffer && buffer.destroy() + 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(async () => { + buffer = atom.project.bufferForPathSync('sample.js') + await 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('tokenizing', () => { + 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', async () => { + await atom.packages.activatePackage('language-gfm') + + 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(async () => { + atom.packages.activatePackage('language-coffee-script') + + 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 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'] + }) + + await atom.packages.activatePackage('language-html') + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: '<', + scopes: ['text.html.ruby', 'meta.tag.block.div.html', 'punctuation.definition.tag.begin.html'] + }) + }) + }) + + 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('.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('.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('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', async () => { + await atom.packages.activatePackage('language-coffee-script') + + 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']) + }) + }) + }) + + 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') + 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() + } + `) + }) + + 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) { + buffer.transact(() => { + for (const range of ranges.reverse()) { + buffer.setTextInRange(range, '⋯') + } + }) + let text = buffer.getText() + buffer.undo() + return text + } +}) 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() diff --git a/spec/uri-handler-registry-spec.js b/spec/uri-handler-registry-spec.js new file mode 100644 index 000000000..d2da93087 --- /dev/null +++ b/spec/uri-handler-registry-spec.js @@ -0,0 +1,75 @@ +/** @babel */ + +import url from 'url' + +import {it} from './async-spec-helpers' + +import URIHandlerRegistry from '../src/uri-handler-registry' + +describe('URIHandlerRegistry', () => { + let registry + + beforeEach(() => { + registry = new URIHandlerRegistry(5) + }) + + 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.handleURI('atom://yet-another-package/path') + expect(testPackageSpy).not.toHaveBeenCalled() + expect(otherPackageSpy).not.toHaveBeenCalled() + + 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.handleURI('atom://other-package/path') + 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 uris = [ + 'atom://one/something?asdf=1', + 'atom://fake/nothing', + 'atom://two/other/stuff', + 'atom://one/more/thing', + 'atom://two/more/stuff' + ] + + uris.forEach(u => registry.handleURI(u)) + + expect(changeSpy.callCount).toBe(5) + 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.handleURI('atom://another/url') + expect(changeSpy.callCount).toBe(6) + const history = registry.getRecentlyHandledURIs() + expect(history.length).toBe(5) + expect(history[0].uri).toBe('atom://another/url') + expect(history[4].uri).toBe(uris[1]) + }) + + 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.handleURI(uri)).toThrow() + }) + }) +}) 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..db8b077f1 --- /dev/null +++ b/spec/view-registry-spec.js @@ -0,0 +1,216 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ViewRegistry = require('../src/view-registry') + +describe('ViewRegistry', () => { + let registry = null + + beforeEach(() => { + registry = new ViewRegistry() + }) + + afterEach(() => { + registry.clearDocumentRequests() + }) + + describe('::getView(object)', () => { + describe('when passed a DOM node', () => + it('returns the given DOM node', () => { + const node = document.createElement('div') + expect(registry.getView(node)).toBe(node) + }) + ) + + describe('when passed an object with an element property', () => + it("returns the element property if it's an instance of HTMLElement", () => { + class TestComponent { + constructor () { + this.element = document.createElement('div') + } + } + + const component = new TestComponent() + expect(registry.getView(component)).toBe(component.element) + }) + ) + + describe('when passed an object with a getElement function', () => + it("returns the return value of getElement if it's an instance of HTMLElement", () => { + class TestComponent { + getElement () { + if (this.myElement == null) { + this.myElement = document.createElement('div') + } + return this.myElement + } + } + + const component = new TestComponent() + expect(registry.getView(component)).toBe(component.myElement) + }) + ) + + describe('when passed a model object', () => { + describe("when a view provider is registered matching the object's constructor", () => + it('constructs a view element and assigns the model on it', () => { + class TestModel {} + + class TestModelSubclass extends TestModel {} + + class TestView { + initialize (model) { + this.model = model + return this + } + } + + const model = new TestModel() + + registry.addViewProvider(TestModel, (model) => + new TestView().initialize(model) + ) + + const view = registry.getView(model) + expect(view instanceof TestView).toBe(true) + expect(view.model).toBe(model) + + const subclassModel = new TestModelSubclass() + const view2 = registry.getView(subclassModel) + expect(view2 instanceof TestView).toBe(true) + expect(view2.model).toBe(subclassModel) + }) + ) + + describe('when a view provider is registered generically, and works with the object', () => + it('constructs a view element and assigns the model on it', () => { + registry.addViewProvider((model) => { + if (model.a === 'b') { + const element = document.createElement('div') + element.className = 'test-element' + return element + } + }) + + const view = registry.getView({a: 'b'}) + expect(view.className).toBe('test-element') + + expect(() => registry.getView({a: 'c'})).toThrow() + }) + ) + + describe("when no view provider is registered for the object's constructor", () => + it('throws an exception', () => { + expect(() => registry.getView({})).toThrow() + }) + ) + }) + }) + + describe('::addViewProvider(providerSpec)', () => + it('returns a disposable that can be used to remove the provider', () => { + class TestModel {} + class TestView { + initialize (model) { + this.model = model + return this + } + } + + const disposable = registry.addViewProvider(TestModel, (model) => + new TestView().initialize(model) + ) + + expect(registry.getView(new TestModel()) instanceof TestView).toBe(true) + disposable.dispose() + expect(() => registry.getView(new TestModel())).toThrow() + }) + ) + + describe('::updateDocument(fn) and ::readDocument(fn)', () => { + let frameRequests = null + + beforeEach(() => { + frameRequests = [] + spyOn(window, 'requestAnimationFrame').andCallFake(fn => frameRequests.push(fn)) + }) + + it('performs all pending writes before all pending reads on the next animation frame', () => { + let events = [] + + registry.updateDocument(() => events.push('write 1')) + registry.readDocument(() => events.push('read 1')) + registry.readDocument(() => events.push('read 2')) + registry.updateDocument(() => events.push('write 2')) + + expect(events).toEqual([]) + + expect(frameRequests.length).toBe(1) + frameRequests[0]() + expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2']) + + frameRequests = [] + events = [] + const disposable = registry.updateDocument(() => events.push('write 3')) + registry.updateDocument(() => events.push('write 4')) + registry.readDocument(() => events.push('read 3')) + + disposable.dispose() + + expect(frameRequests.length).toBe(1) + frameRequests[0]() + expect(events).toEqual(['write 4', 'read 3']) + }) + + it('performs writes requested from read callbacks in the same animation frame', () => { + spyOn(window, 'setInterval').andCallFake(fakeSetInterval) + spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) + const events = [] + + registry.updateDocument(() => events.push('write 1')) + registry.readDocument(() => { + registry.updateDocument(() => events.push('write from read 1')) + events.push('read 1') + }) + registry.readDocument(() => { + registry.updateDocument(() => events.push('write from read 2')) + events.push('read 2') + }) + registry.updateDocument(() => events.push('write 2')) + + expect(frameRequests.length).toBe(1) + frameRequests[0]() + expect(frameRequests.length).toBe(1) + + expect(events).toEqual([ + 'write 1', + 'write 2', + 'read 1', + 'read 2', + 'write from read 1', + 'write from read 2' + ]) + }) + }) + + describe('::getNextUpdatePromise()', () => + it('returns a promise that resolves at the end of the next update cycle', () => { + let updateCalled = false + let readCalled = false + + waitsFor('getNextUpdatePromise to resolve', (done) => { + registry.getNextUpdatePromise().then(() => { + expect(updateCalled).toBe(true) + expect(readCalled).toBe(true) + done() + }) + + registry.updateDocument(() => { updateCalled = true }) + registry.readDocument(() => { readCalled = true }) + }) + }) + ) +}) 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() + }) + ) +}) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 476a4ba5b..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() @@ -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) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 78ea42087..55c27eb61 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -233,6 +233,14 @@ class ApplicationDelegate new Disposable -> ipcRenderer.removeListener('context-command', outerCallback) + onURIMessage: (callback) -> + outerCallback = (event, args...) -> + callback(args...) + + ipcRenderer.on('uri-message', outerCallback) + new Disposable -> + ipcRenderer.removeListener('uri-message', outerCallback) + onDidRequestUnload: (callback) -> outerCallback = (event, message) -> callback(event).then (shouldUnload) -> diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index d777ecc3e..af61ffb36 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' +URIHandlerRegistry = require './uri-handler-registry' GrammarRegistry = require './grammar-registry' {HistoryManager, HistoryProject} = require './history-manager' ReopenProjectMenuManager = require './reopen-project-menu-manager' @@ -31,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' @@ -146,12 +148,14 @@ class AtomEnvironment extends Model @keymaps = new KeymapManager({notificationManager: @notifications}) @tooltips = new TooltipManager(keymapManager: @keymaps, viewRegistry: @views) @commands = new CommandRegistry + @uriHandlerRegistry = new URIHandlerRegistry @grammars = new GrammarRegistry({@config}) @styles = new StyleManager() @packages = new PackageManager({ @config, styleManager: @styles, commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications, - grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views + grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views, + uriHandlerRegistry: @uriHandlerRegistry }) @themes = new ThemeManager({ packageManager: @packages, @config, styleManager: @styles, @@ -165,6 +169,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), @@ -231,6 +236,7 @@ class AtomEnvironment extends Model @themes.initialize({@configDirPath, resourcePath, safeMode, devMode}) @commandInstaller.initialize(@getVersion()) + @protocolHandlerInstaller.initialize(@config, @notifications) @autoUpdater.initialize() @config.load() @@ -327,20 +333,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 @@ -355,6 +355,7 @@ class AtomEnvironment extends Model @stylesElement.remove() @config.unobserveUserConfig() @autoUpdater.destroy() + @uriHandlerRegistry.destroy() @uninstallWindowEventHandler() @@ -444,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 @@ -693,6 +696,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.onURIMessage(@dispatchURIMessage.bind(this))) @disposables.add @applicationDelegate.onDidRequestUnload => @saveState({isUnloading: true}) .catch(console.error) @@ -701,6 +705,11 @@ class AtomEnvironment extends Model windowCloseRequested: true, projectHasPaths: @project.getPaths().length > 0 }) + .then (closing) => + if closing + @packages.deactivatePackages().then -> closing + else + closing @listenForUpdates() @@ -757,7 +766,6 @@ class AtomEnvironment extends Model return if not @project @storeWindowBackground() - @packages.deactivatePackages() @saveBlobStoreSync() @unloaded = true @@ -826,6 +834,9 @@ class AtomEnvironment extends Model # Essential: A flexible way to open a dialog akin to an alert dialog. # + # If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button + # the first button will be clicked unless a "Cancel" or "No" button is provided. + # # ## Examples # # ```coffee @@ -843,7 +854,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) @@ -997,11 +1008,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", description: err.message, stack: err.stack else projectPromise = Promise.resolve() @@ -1014,6 +1032,19 @@ class AtomEnvironment extends Model @workspace.deserialize(state.workspace, @deserializers) if state.workspace? @deserializeTimings.workspace = Date.now() - startTime + if missingProjectPaths.length > 0 + count = if missingProjectPaths.length is 1 then '' else missingProjectPaths.length + ' ' + noun = if missingProjectPaths.length is 1 then 'directory' else 'directories' + toBe = if missingProjectPaths.length is 1 then 'is' else 'are' + escaped = missingProjectPaths.map (projectPath) -> "`#{projectPath}`" + group = switch escaped.length + when 1 then escaped[0] + when 2 then "#{escaped[0]} and #{escaped[1]}" + else escaped[..-2].join(", ") + ", and #{escaped[escaped.length - 1]}" + + @notifications.addError "Unable to open #{count}project #{noun}", + description: "Project #{noun} #{group} #{toBe} no longer on disk." + getStateKey: (paths) -> if paths?.length > 0 sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex') @@ -1068,6 +1099,14 @@ class AtomEnvironment extends Model dispatchContextMenuCommand: (command, args...) -> @commands.dispatch(@contextMenu.activeElement, command, args) + dispatchURIMessage: (uri) -> + if @packages.hasLoadedInitialPackages() + @uriHandlerRegistry.handleURI(uri) + else + sub = @packages.onDidLoadInitialPackages -> + sub.dispose() + @uriHandlerRegistry.handleURI(uri) + openLocations: (locations) -> needsProjectPaths = @project?.getPaths().length is 0 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 } 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 diff --git a/src/config-schema.js b/src/config-schema.js index fb0164766..2ff68be86 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', @@ -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'], @@ -409,6 +428,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/config.coffee b/src/config.coffee index f0628ffee..b8bf8a76f 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, @@ -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 @@ -417,17 +417,24 @@ class Config @defaultSettings = {} @settings = {} @scopedSettingsStore = new ScopedPropertyStore + + @settingsLoaded = false + @savePending = false @configFileHasErrors = false @transactDepth = 0 - @savePending = false - @requestLoad = _.debounce(@loadUserConfig, 100) - @requestSave = => - @savePending = true - debouncedSave.call(this) - save = => + @pendingOperations = [] + + @requestLoad = _.debounce => + @loadUserConfig() + , 100 + + debouncedSave = _.debounce => @savePending = false @save() - debouncedSave = _.debounce(save, 100) + , 100 + @requestSave = => + @savePending = true + debouncedSave() shouldNotAccessFileSystem: -> not @enablePersistence @@ -647,6 +654,10 @@ class Config # * `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.call(this, keyPath, value, options) + scopeSelector = options?.scopeSelector source = options?.source shouldSave = options?.save ? true @@ -667,7 +678,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. @@ -677,6 +689,9 @@ class Config # * `scopeSelector` (optional) {String}. See {::set} # * `source` (optional) {String}. See {::set} unset: (keyPath, options) -> + unless @settingsLoaded + @pendingOperations.push => @unset.call(this, keyPath, options) + {scopeSelector, source} = options ? {} source ?= @getUserConfigPath() @@ -688,7 +703,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 + @requestSave() else @scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector) @emitChangeEvent() @@ -848,21 +864,27 @@ class Config loadUserConfig: -> return if @shouldNotAccessFileSystem() + return if @savePending try unless fs.existsSync(@configFilePath) fs.makeTreeSync(path.dirname(@configFilePath)) - CSON.writeFileSync(@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 - userConfig = CSON.readFileSync(@configFilePath) - @resetUserSettings(userConfig) - @configFileHasErrors = false + 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 @configFileHasErrors = true message = "Failed to load `#{path.basename(@configFilePath)}`" @@ -880,8 +902,10 @@ class Config return if @shouldNotAccessFileSystem() try - @watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) => - @requestLoad() if eventType is 'change' and @watchSubscription? + @watchSubscriptionPromise ?= watchPath @configFilePath, {}, (events) => + for {action} in events + if action in ['created', 'modified', 'renamed'] and @watchSubscriptionPromise? + @requestLoad() catch error @notifyFailure """ Unable to watch path: `#{path.basename(@configFilePath)}`. Make sure you have permissions to @@ -890,9 +914,11 @@ class Config [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path """ + @watchSubscriptionPromise + unobserveUserConfig: -> - @watchSubscription?.close() - @watchSubscription = null + @watchSubscriptionPromise?.then (watcher) -> watcher?.dispose() + @watchSubscriptionPromise = null notifyFailure: (errorMessage, detail) -> @notificationManager?.addError(errorMessage, {detail, dismissable: true}) @@ -915,11 +941,6 @@ class Config ### resetUserSettings: (newSettings) -> - unless isPlainObject(newSettings) - @settings = {} - @emitChangeEvent() - return - if newSettings.global? newSettings['*'] = newSettings.global delete newSettings.global @@ -932,8 +953,11 @@ class Config @transact => @settings = {} + @settingsLoaded = true @set(key, value, save: false) for key, value of newSettings - return + if @pendingOperations.length + op() for op in @pendingOperations + @pendingOperations = [] getRawValue: (keyPath, options) -> unless options?.excludeSources?.indexOf(@getUserConfigPath()) >= 0 diff --git a/src/cursor.coffee b/src/cursor.coffee deleted file mode 100644 index 128fe7ff5..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.languageMode.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..6cd0cc623 --- /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 position = this.getBufferPosition() + + const scanRange = allowPrevious + ? new Range(new Point(position.row - 1, 0), position) + : new Range(new Point(position.row, 0), position) + + const ranges = this.editor.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + scanRange + ) + + let result + for (let range of ranges) { + if (position.isLessThanOrEqual(range.start)) break + if (allowPrevious || position.isLessThanOrEqual(range.end)) result = Point.fromObject(range.start) + } + + return result || (allowPrevious ? new Point(0, 0) : position) + } + + // 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 position = this.getBufferPosition() + + const scanRange = allowNext + ? new Range(position, new Point(position.row + 2, 0)) + : new Range(position, new Point(position.row, Infinity)) + + const ranges = this.editor.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + scanRange + ) + + for (let range of ranges) { + if (position.isLessThan(range.start) && !allowNext) break + if (position.isLessThan(range.end)) return Point.fromObject(range.end) + } + + return allowNext ? this.editor.getEofBufferPosition() : position + } + + // 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 position = this.getBufferPosition() + const ranges = this.editor.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + new Range(new Point(position.row, 0), new Point(position.row, Infinity)) + ) + const range = ranges.find(range => + range.end.column >= position.column && range.start.column <= position.column + ) + return range ? Range.fromObject(range) : new Range(position, position) + } + + // 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\r ]*$|[^\\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 + } +} 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..731935506 --- /dev/null +++ b/src/decoration.js @@ -0,0 +1,205 @@ +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') + } +} 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)) 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. 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..057c5fcb7 --- /dev/null +++ b/src/git-repository.js @@ -0,0 +1,603 @@ +/* + * 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') + +let nextId = 0 + +// 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.id = nextId++ + 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.statusRefreshCount = 0 + 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() + 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 () { + this.repo = null + + if (this.emitter) { + this.emitter.emit('did-destroy') + this.emitter.dispose() + this.emitter = null + } + + if (this.subscriptions) { + 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. + async refreshStatus () { + const statusRefreshCount = ++this.statusRefreshCount + const repo = this.getRepo() + + const relativeProjectPaths = this.project && this.project.getPaths() + .map(projectPath => this.relativize(projectPath)) + .filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath)) + + const branch = await repo.getHeadAsync() + const upstream = await repo.getAheadBehindCountAsync() + + const statuses = {} + const repoStatus = relativeProjectPaths.length > 0 + ? await repo.getStatusAsync(relativeProjectPaths) + : await repo.getStatusAsync() + for (let filePath in repoStatus) { + statuses[filePath] = repoStatus[filePath] + } + + const submodules = {} + for (let submodulePath in repo.submodules) { + const submoduleRepo = repo.submodules[submodulePath] + submodules[submodulePath] = { + branch: await submoduleRepo.getHeadAsync(), + upstream: await submoduleRepo.getAheadBehindCountAsync() + } + + 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 (this.statusRefreshCount !== statusRefreshCount || this.isDestroyed()) return + + const statusesUnchanged = + _.isEqual(branch, this.branch) && + _.isEqual(statuses, this.statuses) && + _.isEqual(upstream, this.upstream) && + _.isEqual(submodules, this.submodules) + + this.branch = branch + this.statuses = statuses + this.upstream = upstream + this.submodules = submodules + + for (let submodulePath in repo.submodules) { + repo.submodules[submodulePath].upstream = submodules[submodulePath].upstream + } + + if (!statusesUnchanged) this.emitter.emit('did-change-statuses') + } +} 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..f2994acf1 --- /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.125 + } + if (this.grammarMatchesContents(grammar, contents)) { + score += 0.25 + } + 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) + } +} 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) + } +} 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 + } +} diff --git a/src/language-mode.coffee b/src/language-mode.coffee deleted file mode 100644 index 1839f1c59..000000000 --- a/src/language-mode.coffee +++ /dev/null @@ -1,350 +0,0 @@ -{Range} = 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 = {} - - 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 - - # 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})") - - 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] - - startRow = bufferRow - while startRow > firstRow - break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment - break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1))) - 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))) - endRow++ - - new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length]) - - # 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. - 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. - # - # 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)) - - foldEndRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getFoldEndPattern(scopeDescriptor)) diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index dcc7c6513..0c587020e 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -648,6 +648,41 @@ 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) + return unless parsedUrl.protocol is "atom:" + + pack = @findPackageWithName(parsedUrl.host, devMode) + if pack?.urlMain + @openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env) + else + @openPackageUriHandler(urlToOpen, devMode, safeMode, env) + + openPackageUriHandler: (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.sendURIMessage url + else + windowDimensions = @getDimensionsForNewWindow() + @lastFocusedWindow = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env}) + @lastFocusedWindow.on 'window:loaded', => + @lastFocusedWindow.sendURIMessage url + + findPackageWithName: (packageName, devMode) -> + _.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName + + openPackageUrlMain: (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) -> + packagePath = @getPackageManager(devMode).resolvePackagePath(packageName) + windowInitializationScript = path.resolve(packagePath, packageUrlMain) + windowDimensions = @getDimensionsForNewWindow() + new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env}) + + getPackageManager: (devMode) -> unless @packages? PackageManager = require '../package-manager' @packages = new PackageManager({}) @@ -656,18 +691,8 @@ class AtomApplication devMode: devMode resourcePath: @resourcePath - packageName = url.parse(urlToOpen).host - 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}" + @packages + # 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 9bbdcfc25..ca3995c05 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -232,6 +232,9 @@ class AtomWindow unless @atomApplication.sendCommandToFirstResponder(command) @sendCommandToBrowserWindow(command, args...) + sendURIMessage: (uri) -> + @browserWindow.webContents.send 'uri-message', uri + sendCommandToBrowserWindow: (command, args...) -> action = if args[0]?.contextCommand then 'context-command' else 'command' @browserWindow.webContents.send action, command, args... diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 7531e609b..3b0654962 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. @@ -56,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('uri-handler') - const args = options.argv + let args = options.argv + + // If --uri-handler is set, then we parse NOTHING else + if (args.uriHandler) { + args = { + uriHandler: true, + 'uri-handler': true, + _: args._.filter(str => str.startsWith('atom://')).slice(0, 1) + } + } if (args.help) { process.stdout.write(options.help()) @@ -76,7 +88,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 +111,20 @@ module.exports = function parseCommandLine (processArgs) { const userDataDir = args['user-data-dir'] const profileStartup = args['profile-startup'] const clearWindowState = args['clear-window-state'] - 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 + 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'] diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js index 798ff0619..e63ac6cda 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 @@ -323,13 +323,13 @@ 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 { // Private: Instantiate a new {MissingResult}. // - // * `lastParent` the final succesfully traversed {RegistryNode}. + // * `lastParent` the final successfully traversed {RegistryNode}. constructor (lastParent) { this.lastParent = lastParent } 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..df5e5fb42 --- /dev/null +++ b/src/notification-manager.js @@ -0,0 +1,206 @@ +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'`. + // + // Returns the {Notification} that was added. + 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'`. + // + // Returns the {Notification} that was added. + 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'`. + // + // Returns the {Notification} that was added. + 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. + // + // Returns the {Notification} that was added. + 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. + // + // Returns the {Notification} that was added. + 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 = [] + } +} 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' + } + } +} diff --git a/src/package-manager.js b/src/package-manager.js index 73855ae37..17a5f2214 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, + uriHandlerRegistry: this.uriHandlerRegistry } = params) this.emitter = new Emitter() @@ -77,9 +78,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 = {} @@ -647,6 +648,10 @@ module.exports = class PackageManager { }) } + registerURIHandlerForPackage (packageName, handler) { + return this.uriHandlerRegistry.registerHostHandler(packageName, handler) + } + // another type of package manager can handle other package types. // See ThemeManager registerPackageActivator (activator, types) { @@ -744,21 +749,30 @@ 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 (pack == null) { + return + } + if (!suppressSerialization && this.isPackageActive(pack.name)) { this.serializePackage(pack) } - 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 039ccf9d3..1635c75dc 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -84,6 +84,7 @@ class Package @loadMenus() @registerDeserializerMethods() @activateCoreStartupServices() + @registerURIHandler() @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @requireMainModule() @settingsPromise = @loadSettings() @@ -114,6 +115,7 @@ class Package @loadStylesheets() @registerDeserializerMethods() @activateCoreStartupServices() + @registerURIHandler() @registerTranspilerConfig() @configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata() @settingsPromise = @loadSettings() @@ -318,6 +320,19 @@ class Package @activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule)) return + registerURIHandler: -> + handlerConfig = @getURIHandler() + if methodName = handlerConfig?.method + @uriHandlerSubscription = @packageManager.registerURIHandlerForPackage @name, (args...) => + @handleURI(methodName, args) + + unregisterURIHandler: -> + @uriHandlerSubscription?.dispose() + + handleURI: (methodName, args) -> + @activate().then => @mainModule[methodName]?.apply(@mainModule, args) + @activateNow() unless @mainActivated + registerTranspilerConfig: -> if @metadata.atomTranspilers CompileCache.addTranspilerConfigForPath(@path, @name, @metadata, @metadata.atomTranspilers) @@ -504,16 +519,32 @@ class Package @activationCommandSubscriptions?.dispose() @activationHookSubscriptions?.dispose() @configSchemaRegisteredOnActivate = false + @unregisterURIHandler() @deactivateResources() @deactivateKeymaps() - if @mainActivated - try - @mainModule?.deactivate?() - @mainModule?.deactivateConfig?() - @mainActivated = false - @mainInitialized = false - catch e - console.error "Error deactivating package '#{@name}'", e.stack + + unless @mainActivated + @emitter.emit 'did-deactivate' + 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 typeof 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: -> @@ -580,7 +611,7 @@ class Package @mainModulePath = fs.resolveExtension(mainModulePath, ["", CompileCache.supportedExtensions...]) activationShouldBeDeferred: -> - @hasActivationCommands() or @hasActivationHooks() + @hasActivationCommands() or @hasActivationHooks() or @hasDeferredURIHandler() hasActivationHooks: -> @getActivationHooks()?.length > 0 @@ -590,6 +621,9 @@ class Package return true if commands.length > 0 false + hasDeferredURIHandler: -> + @getURIHandler() and @getURIHandler().deferActivation isnt false + subscribeToDeferredActivation: -> @subscribeToActivationCommands() @subscribeToActivationHooks() @@ -658,6 +692,9 @@ class Package @activationHooks = _.uniq(@activationHooks) + getURIHandler: -> + @metadata?.uriHandler + # Does the given module path contain native code? isNativeModule: (modulePath) -> try 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-element.coffee b/src/pane-element.coffee index c4866816a..d68b3b834 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) @@ -119,6 +126,7 @@ class PaneElement extends HTMLElement paneDestroyed: -> @subscriptions.dispose() + @changePathDisposable?.dispose() flexScaleChanged: (flexScale) -> @style.flexGrow = flexScale diff --git a/src/pane.coffee b/src/pane.coffee deleted file mode 100644 index 6ac3ef359..000000000 --- a/src/pane.coffee +++ /dev/null @@ -1,996 +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) - - @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 - - # 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..0305b39dd --- /dev/null +++ b/src/pane.js @@ -0,0 +1,1252 @@ +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. + // + // Returns a {Promise} that resolves when the item is destroyed. + destroyActiveItem () { + return this.destroyItem(this.activeItem) + } + + // 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) + } +} 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 // } // }) // }) diff --git a/src/project.coffee b/src/project.coffee deleted file mode 100644 index cad5f03ac..000000000 --- a/src/project.coffee +++ /dev/null @@ -1,517 +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 = {} - @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 = {} - @consumeServices(packageManager) - - destroyUnretainedBuffers: -> - buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() - return - - ### - Section: Serialization - ### - - deserialize: (state) -> - 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) => - @subscribeToBuffer(buffer) for buffer in @buffers - @setPaths(state.paths) - - 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. - setPaths: (projectPaths) -> - repository?.destroy() for repository in @repositories - @rootDirectories = [] - @repositories = [] - - watcher.then((w) -> w.dispose()) for _, watcher in @watcherPromisesByPath - @watcherPromisesByPath = {} - - @addPath(projectPath, emitEvent: false) for projectPath in projectPaths - - @emitter.emit 'did-change-paths', projectPaths - - # Public: Add a path to the project's list of root paths - # - # * `projectPath` {String} The path to the directory to add. - addPath: (projectPath, options) -> - directory = @getDirectoryForProjectPath(projectPath) - return unless directory.existsSync() - 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) - existingBuffer = @findBufferForPath(absoluteFilePath) if filePath - existingBuffer ? @buildBufferSync(absoluteFilePath) - - # Only to be used when deserializing - bufferForIdSync: (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..48541c395 --- /dev/null +++ b/src/project.js @@ -0,0 +1,705 @@ +const path = require('path') + +const _ = 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 = +class Project extends Model { + /* + Section: Construction and Destruction + */ + + constructor ({notificationManager, packageManager, config, applicationDelegate}) { + super() + 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 path in this.watcherPromisesByPath) { + this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) + } + this.rootDirectories = [] + 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() + this.consumeServices(packageManager) + } + + destroyUnretainedBuffers () { + for (let buffer of this.getBuffers()) { + if (!buffer.isRetained()) buffer.destroy() + } + } + + /* + Section: Serialization + */ + + deserialize (state) { + 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((_) => { + this.retiredBufferIDs.add(bufferState.id) + this.retiredBufferPaths.add(bufferState.filePath) + return null + }) + } + + const bufferPromises = [] + for (let bufferState of state.buffers) { + bufferPromises.push(handleBufferState(bufferState)) + } + + return Promise.all(bufferPromises).then(buffers => { + this.buffers = buffers.filter(Boolean) + for (let buffer of this.buffers) { + this.subscribeToBuffer(buffer) + } + 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) => { + 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 + // registered in the future that could supply a Repository for the + // directory. + if (repo == null) this.repositoryPromisesByPath.delete(pathForDirectory) + + if (repo && repo.onDidDestroy) { + repo.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 path in this.watcherPromisesByPath) { + this.watcherPromisesByPath[path].then(watcher => { watcher.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(...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 = (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)) { + this.emitter.emit('did-change-files', events) + } + }) + + for (let watchedPath in this.watcherPromisesByPath) { + if (!this.rootDirectories.find(dir => dir.getPath() === watchedPath)) { + this.watcherPromisesByPath[watchedPath].then(watcher => { watcher.dispose() }) + } + } + + let repo = null + for (let provider of this.repositoryProviders) { + if (provider.repositoryForDirectorySync) { + repo = provider.repositoryForDirectorySync(directory) + } + if (repo) { break } + } + this.repositories.push(repo != null ? repo : null) + + if (options.emitEvent !== false) { + this.emitter.emit('did-change-paths', this.getPaths()) + } + } + + getDirectoryForProjectPath (projectPath) { + let directory = null + for (let provider of this.directoryProviders) { + if (typeof provider.directoryForURISync === 'function') { + directory = provider.directoryForURISync(projectPath) + if (directory) 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. + if (!this.getPaths().includes(projectPath)) { + 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) { + this.rootDirectories.splice(indexToRemove, 1) + const [removedRepository] = this.repositories.splice(indexToRemove, 1) + if (!this.repositories.includes(removedRepository)) { + if (removedRepository) 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.match(/[A-Za-z0-9+-.]+:\/\//)) { // 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 < 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) { + const bufferForPath = this.findBufferForPath(this.resolvePath(filePath)) + return bufferForPath && bufferForPath.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) { + 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) { + if (this.retiredBufferIDs.has(id)) { return null } + + let existingBuffer + 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) { + const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + + let buffer + 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) { + const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + + let promise + if (absoluteFilePath != null) { + 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)) + } + 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] = 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)) { + this.setPaths([path.dirname(buffer.getPath())]) + } + }) + buffer.onWillThrowWatchError(({error, handle}) => { + handle() + 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 + }) + }) + } +} diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js new file mode 100644 index 000000000..0a55bff41 --- /dev/null +++ b/src/protocol-handler-installer.js @@ -0,0 +1,92 @@ +const {remote} = require('electron') + +const SETTING = 'core.uriHandlerRegistration' +const PROMPT = 'prompt' +const ALWAYS = 'always' +const NEVER = 'never' + +module.exports = +class ProtocolHandlerInstaller { + isSupported () { + return ['win32', 'darwin'].includes(process.platform) + } + + isDefaultProtocolClient () { + 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, ['--uri-handler']) + } + + initialize (config, notifications) { + if (!this.isSupported()) { + return + } + + if (!this.isDefaultProtocolClient()) { + const behaviorWhenNotProtocolClient = config.get(SETTING) + switch (behaviorWhenNotProtocolClient) { + case PROMPT: + this.promptToBecomeProtocolClient(config, notifications) + break + case ALWAYS: + this.setAsDefaultProtocolClient() + break + case NEVER: + default: + // Do nothing + } + } + } + + promptToBecomeProtocolClient (config, notifications) { + let notification + + const withSetting = (value, fn) => { + return function () { + config.set(SETTING, value) + fn() + } + } + + const accept = () => { + notification.dismiss() + this.setAsDefaultProtocolClient() + } + const decline = () => { + notification.dismiss() + } + + 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 ' + + 'atom:// URIs?', + buttons: [ + { + text: 'Yes', + className: 'btn btn-info btn-primary', + onDidClick: accept + }, + { + text: 'Yes, Always', + className: 'btn btn-info', + onDidClick: withSetting(ALWAYS, accept) + }, + { + text: 'No', + className: 'btn btn-info', + onDidClick: decline + }, + { + text: 'No, Never', + className: 'btn btn-info', + onDidClick: withSetting(NEVER, decline) + } + ] + }) + } +} 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() 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} 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. # diff --git a/src/selection.coffee b/src/selection.coffee index e361d0b5c..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) @@ -381,7 +387,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-component.js b/src/text-editor-component.js index 61868228b..641cdad02 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') @@ -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() @@ -804,7 +804,15 @@ class TextEditorComponent { key: overlayProps.element, overlayComponents: this.overlayComponents, measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element), - didResize: () => { this.updateSync() } + didResize: (overlayComponent) => { + this.updateOverlayToRender(overlayProps) + overlayComponent.update(Object.assign( + { + measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element) + }, + overlayProps + )) + } }, overlayProps )) @@ -1153,9 +1161,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) { @@ -1337,42 +1347,47 @@ class TextEditorComponent { }) } + updateOverlayToRender (decoration) { + const windowInnerHeight = this.getWindowInnerHeight() + const windowInnerWidth = this.getWindowInnerWidth() + const contentClientRect = this.refs.content.getBoundingClientRect() + + const {element, screenPosition, avoidOverflow} = decoration + const {row, column} = screenPosition + let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight() + let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column) + const clientRect = element.getBoundingClientRect() + this.overlayDimensionsByElement.set(element, clientRect) + + if (avoidOverflow !== false) { + const computedStyle = window.getComputedStyle(element) + const elementTop = wrapperTop + parseInt(computedStyle.marginTop) + const elementBottom = elementTop + clientRect.height + const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom) + const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft) + const elementRight = elementLeft + clientRect.width + + if (elementBottom > windowInnerHeight && flippedElementTop >= 0) { + wrapperTop -= (elementTop - flippedElementTop) + } + if (elementLeft < 0) { + wrapperLeft -= elementLeft + } else if (elementRight > windowInnerWidth) { + wrapperLeft -= (elementRight - windowInnerWidth) + } + } + + decoration.pixelTop = Math.round(wrapperTop) + decoration.pixelLeft = Math.round(wrapperLeft) + } + updateOverlaysToRender () { const overlayCount = this.decorationsToRender.overlays.length if (overlayCount === 0) return null - const windowInnerHeight = this.getWindowInnerHeight() - const windowInnerWidth = this.getWindowInnerWidth() - const contentClientRect = this.refs.content.getBoundingClientRect() for (let i = 0; i < overlayCount; i++) { const decoration = this.decorationsToRender.overlays[i] - const {element, screenPosition, avoidOverflow} = decoration - const {row, column} = screenPosition - let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight() - let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column) - const clientRect = element.getBoundingClientRect() - this.overlayDimensionsByElement.set(element, clientRect) - - if (avoidOverflow !== false) { - const computedStyle = window.getComputedStyle(element) - const elementTop = wrapperTop + parseInt(computedStyle.marginTop) - const elementBottom = elementTop + clientRect.height - const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom) - const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft) - const elementRight = elementLeft + clientRect.width - - if (elementBottom > windowInnerHeight && flippedElementTop >= 0) { - wrapperTop -= (elementTop - flippedElementTop) - } - if (elementLeft < 0) { - wrapperLeft -= elementLeft - } else if (elementRight > windowInnerWidth) { - wrapperLeft -= (elementRight - windowInnerWidth) - } - } - - decoration.pixelTop = Math.round(wrapperTop) - decoration.pixelLeft = Math.round(wrapperLeft) + this.updateOverlayToRender(decoration) } } @@ -1401,6 +1416,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() } @@ -1590,30 +1608,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 @@ -1633,6 +1651,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 @@ -1761,22 +1784,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 } @@ -1857,7 +1880,6 @@ class TextEditorComponent { handleMouseDragUntilMouseUp ({didDrag, didStopDragging}) { let dragging = false let lastMousemoveEvent - let bufferWillChangeDisposable const animationFrameLoop = () => { window.requestAnimationFrame(() => { @@ -1877,9 +1899,9 @@ class TextEditorComponent { } function didMouseUp () { + this.stopDragging = null window.removeEventListener('mousemove', didMouseMove) - window.removeEventListener('mouseup', didMouseUp) - bufferWillChangeDisposable.dispose() + window.removeEventListener('mouseup', didMouseUp, {capture: true}) if (dragging) { dragging = false didStopDragging() @@ -1888,10 +1910,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) { @@ -2091,14 +2110,30 @@ 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.derivedDimensionsCache = {} + this.suppressUpdates = false } } @@ -2501,7 +2536,8 @@ class TextEditorComponent { didUpdateDisposable.dispose() didDestroyDisposable.dispose() - if (marker.isValid()) { + if (wasValid) { + wasValid = false this.blockDecorationsToMeasure.delete(decoration) this.heightsByBlockDecoration.delete(decoration) this.blockDecorationsByElement.delete(element) @@ -2745,7 +2781,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() @@ -2862,13 +2898,19 @@ 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 () { - const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() - const visibleTileCount = Math.ceil(editorHeightInTiles) + 1 - const lastRenderedRow = this.getRenderedStartRow() + (visibleTileCount * this.getRowsPerTile()) - this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, lastRenderedRow) + // Ensure the spatial index is populated with rows that are currently visible + populateVisibleRowRange (renderedStartRow) { + const {model} = this.props + const previousScreenLineCount = model.getApproximateScreenLineCount() + + const renderedEndRow = renderedStartRow + (this.getVisibleTileCount() * this.getRowsPerTile()) + this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, renderedEndRow) + + // If the approximate screen line count changes, previously-cached derived + // dimensions could now be out of date. + if (model.getApproximateScreenLineCount() !== previousScreenLineCount) { + this.derivedDimensionsCache = {} + } } populateVisibleTiles () { @@ -4173,8 +4215,8 @@ 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() - process.nextTick(() => { this.resizeObserver.observe(this.element) }) + this.props.didResize(this) + process.nextTick(() => { this.resizeObserver.observe(this.props.element) }) } }) this.didAttach() @@ -4186,19 +4228,34 @@ class OverlayComponent { this.didDetach() } + getNextUpdatePromise () { + if (!this.nextUpdatePromise) { + this.nextUpdatePromise = new Promise((resolve) => { + this.resolveNextUpdatePromise = () => { + this.nextUpdatePromise = null + this.resolveNextUpdatePromise = null + resolve() + } + }) + } + return this.nextUpdatePromise + } + update (newProps) { const oldProps = this.props - this.props = newProps + this.props = Object.assign({}, oldProps, newProps) if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px' if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px' if (newProps.className !== oldProps.className) { if (oldProps.className != null) this.element.classList.remove(oldProps.className) if (newProps.className != null) this.element.classList.add(newProps.className) } + + if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise() } didAttach () { - this.resizeObserver.observe(this.element) + this.resizeObserver.observe(this.props.element) } didDetach () { @@ -4307,7 +4364,7 @@ class NodePool { } if (element) { - element.className = className + element.className = className || '' element.styleMap.forEach((value, key) => { if (!style || style[key] == null) element.style[key] = '' }) diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 72aa8b364..d891a5868 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'], @@ -287,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) } } @@ -428,3 +429,5 @@ class ScopedSettingsDelegate { } } } + +TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate 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 54de91054..1e3f9930d 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -4,20 +4,21 @@ fs = require 'fs-plus' Grim = require 'grim' {CompositeDisposable, Disposable, Emitter} = require 'event-kit' {Point, Range} = TextBuffer = require 'text-buffer' -LanguageMode = require './language-mode' DecorationManager = require './decoration-manager' TokenizedBuffer = require './tokenized-buffer' Cursor = require './cursor' Model = require './model' Selection = require './selection' +TextEditorUtils = require './text-editor-utils' + TextMateScopeSelector = require('first-mate').ScopeSelector GutterContainer = require './gutter-container' TextEditorComponent = null TextEditorElement = null {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' +NON_WHITESPACE_REGEXP = /\S/ ZERO_WIDTH_NBSP = '\ufeff' -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. @@ -79,7 +80,6 @@ class TextEditor extends Model serializationVersion: 1 buffer: null - languageMode: null cursors: null showCursorOnSelection: null selections: null @@ -123,13 +123,20 @@ class TextEditor extends Model this ) + Object.defineProperty(@prototype, 'languageMode', get: -> @tokenizedBuffer) + + Object.assign(@prototype, TextEditorUtils) + @deserialize: (state, atomEnvironment) -> # TODO: Return null on version mismatch when 1.8.0 has been out for a while if state.version isnt @prototype.serializationVersion and state.displayBuffer? state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer try - 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' @@ -153,12 +160,12 @@ 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, @softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength, - @showCursorOnSelection + @showCursorOnSelection, @maxScreenLineLength } = params @assert ?= (condition) -> condition @@ -183,6 +190,7 @@ class TextEditor extends Model @softWrapped ?= false @softWrapAtPreferredLineLength ?= false @preferredLineLength ?= 80 + @maxScreenLineLength ?= 500 @showLineNumbers ?= true @buffer ?= new TextBuffer({ @@ -240,8 +248,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' @@ -323,6 +329,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 +444,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 } @@ -474,7 +485,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() @@ -955,7 +965,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() @@ -1223,7 +1233,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() @@ -2202,7 +2212,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 @@ -2510,7 +2520,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 +2531,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 +2545,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 +2554,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 +2563,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 +2572,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 +2599,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() @@ -3039,7 +3049,7 @@ class TextEditor extends Model else @getEditorWidthInChars() else - MAX_SCREEN_LINE_LENGTH + @maxScreenLineLength ### Section: Indentation @@ -3241,12 +3251,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 @@ -3303,13 +3314,15 @@ 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() + if 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) + {row} = @getCursorBufferPosition() + position = Point(row, Infinity) + @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Essential: Fold the given row in buffer coordinates based on its indentation # level. @@ -3319,13 +3332,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: -> @@ -3334,18 +3360,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. # @@ -3539,6 +3572,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. @@ -3592,21 +3626,6 @@ class TextEditor extends Model getNonWordCharacters: (scopes) -> @scopedSettingsDelegate?.getNonWordCharacters?(scopes) ? @nonWordCharacters - 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 ### @@ -3842,14 +3861,49 @@ 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) if indentLevel? - toggleLineCommentForBufferRow: (row) -> @languageMode.toggleLineCommentsForBufferRow(row) + toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) - toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end) + 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))) diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 0f019dbf0..d5a2cb0d1 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 @@ -262,33 +262,31 @@ 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() @unwatchUserStylesheet() - @packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes() - null + results = @getActiveThemes().map((pack) => @packageManager.deactivatePackage(pack.name)) + Promise.all(results.filter((r) -> typeof r?.then is 'function')) isInitialLoadComplete: -> @initialLoadComplete diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee deleted file mode 100644 index 8fca6c06b..000000000 --- a/src/tokenized-buffer.coffee +++ /dev/null @@ -1,451 +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) -> - if state.bufferId - state.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) - 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..2a9446256 --- /dev/null +++ b/src/tokenized-buffer.js @@ -0,0 +1,764 @@ +const _ = require('underscore-plus') +const {CompositeDisposable, Emitter} = require('event-kit') +const {Point, Range} = require('text-buffer') +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 {OnigRegExp} = require('oniguruma') +const {toFirstMateScopeId} = require('./first-mate-helpers') + +const NON_WHITESPACE_REGEX = /\S/ + +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.regexesByPattern = {} + + this.alive = true + this.visible = false + 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.scopedSettingsDelegate = params.scopedSettingsDelegate + + 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 + } + + /* + 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. It may also return undefined if no change should + // be made. + // + // * bufferRow - The row {Number} + // + // Returns a {Number}. + suggestedIndentForEditedBufferRow (bufferRow) { + const line = this.buffer.lineForRow(bufferRow) + const currentIndentLevel = this.indentLevelForLine(line) + if (currentIndentLevel === 0) return + + const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0]) + const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) + if (!decreaseIndentRegex) return + + if (!decreaseIndentRegex.testSync(line)) return + + const precedingRow = this.buffer.previousNonBlankRow(bufferRow) + if (precedingRow == null) return + + 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 + 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) + } + + /* + Section - Comments + */ + + commentStringsForPosition (position) { + if (this.scopedSettingsDelegate) { + const scope = this.scopeDescriptorForPosition(position) + return this.scopedSettingsDelegate.getCommentStrings(scope) + } else { + return {} + } + } + + 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.buffer.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.endRowForFoldAtRow(row, 1, true) != null + } + + 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) + ] + this.tokenizedLines[bufferRow] = new TokenizedLine({ + openScopes: [], + text, + tags, + lineEnding, + tokenIterator: this.tokenIterator, + grammar: this.grammar + }) + return this.tokenizedLines[bufferRow] + } + } + } + + 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())}\`` + error.privateMetadata = { + filePath: this.buffer.getPath(), + fileContents: this.buffer.getText() + } + }) + break + } + } + } + } + } + return scopes + } + + 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)) + } + + 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, existenceOnly = false) { + if (this.isRowCommented(row)) { + return this.endRowForCommentFoldAtRow(row, existenceOnly) + } else { + return this.endRowForCodeFoldAtRow(row, tabLength, existenceOnly) + } + } + + 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, existenceOnly) { + 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 + if (existenceOnly) break + } + 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)) + } + } + + regexForPattern (pattern) { + if (pattern) { + if (!this.regexesByPattern[pattern]) { + this.regexesByPattern[pattern] = new OnigRegExp(pattern) + } + return this.regexesByPattern[pattern] + } + } + + logLines (start = 0, end = this.buffer.getLastRow()) { + for (let row = start; row <= end; row++) { + const line = this.tokenizedLines[row].text + console.log(row, line, line.length) + } + } +} + +module.exports.prototype.chunkSize = 50 + +function selectorMatchesAnyScope (selector, scopes) { + const targetClasses = selector.replace(/^\./, '').split('.') + return scopes.some((scope) => { + const scopeClasses = scope.split('.') + return _.isSubset(targetClasses, scopeClasses) + }) +} diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee deleted file mode 100644 index 03630c87f..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 interpetation 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..937f831d1 --- /dev/null +++ b/src/tooltip-manager.js @@ -0,0 +1,199 @@ +const _ = require('underscore-plus') +const {Disposable, CompositeDisposable} = require('event-kit') +let Tooltip = null + +// Essential: Associates tooltips with HTML elements. +// +// You can get the `TooltipManager` via `atom.tooltips`. +// +// ## Examples +// +// The essence of displaying a tooltip +// +// ```javascript +// // display it +// const disposable = atom.tooltips.add(div, {title: 'This is a tooltip'}) +// +// // remove it +// disposable.dispose() +// ``` +// +// In practice there are usually multiple tooltips. So we add them to a +// CompositeDisposable +// +// ```javascript +// const {CompositeDisposable} = require('atom') +// const subscriptions = new CompositeDisposable() +// +// const div1 = document.createElement('div') +// const div2 = document.createElement('div') +// subscriptions.add(atom.tooltips.add(div1, {title: 'This is a tooltip'})) +// subscriptions.add(atom.tooltips.add(div2, {title: 'Another tooltip'})) +// +// // remove them all +// subscriptions.dispose() +// ``` +// +// You can display a key binding in the tooltip as well with the +// `keyBindingCommand` option. +// +// ```javascript +// disposable = atom.tooltips.add(this.caseOptionButton, { +// title: 'Match Case', +// keyBindingCommand: 'find-and-replace:toggle-case-option', +// keyBindingTarget: this.findEditor.element +// }) +// ``` +module.exports = +class TooltipManager { + constructor ({keymapManager, viewRegistry}) { + this.defaults = { + trigger: 'hover', + container: 'body', + html: true, + placement: 'auto top', + viewportPadding: 2 + } + + this.hoverDefaults = { + delay: {show: 1000, hide: 100} + } + + this.keymapManager = keymapManager + this.viewRegistry = viewRegistry + this.tooltips = new Map() + } + + // Essential: Add a tooltip to the given element. + // + // * `target` An `HTMLElement` + // * `options` An object with one or more of the following options: + // * `title` A {String} or {Function} to use for the text in the tip. If + // a function is passed, `this` will be set to the `target` element. This + // option is mutually exclusive with the `item` option. + // * `html` A {Boolean} affecting the interpretation of the `title` option. + // If `true` (the default), the `title` string will be interpreted as HTML. + // Otherwise it will be interpreted as plain text. + // * `item` A view (object with an `.element` property) or a DOM element + // containing custom content for the tooltip. This option is mutually + // exclusive with the `title` option. + // * `class` A {String} with a class to apply to the tooltip element to + // enable custom styling. + // * `placement` A {String} or {Function} returning a string to indicate + // the position of the tooltip relative to `element`. Can be `'top'`, + // `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is + // specified, it will dynamically reorient the tooltip. For example, if + // placement is `'auto left'`, the tooltip will display to the left when + // possible, otherwise it will display right. + // When a function is used to determine the placement, it is called with + // the tooltip DOM node as its first argument and the triggering element + // DOM node as its second. The `this` context is set to the tooltip + // instance. + // * `trigger` A {String} indicating how the tooltip should be displayed. + // Choose from one of the following options: + // * `'hover'` Show the tooltip when the mouse hovers over the element. + // This is the default. + // * `'click'` Show the tooltip when the element is clicked. The tooltip + // will be hidden after clicking the element again or anywhere else + // outside of the tooltip itself. + // * `'focus'` Show the tooltip when the element is focused. + // * `'manual'` Show the tooltip immediately and only hide it when the + // returned disposable is disposed. + // * `delay` An object specifying the show and hide delay in milliseconds. + // Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and + // otherwise defaults to `0` for both values. + // * `keyBindingCommand` A {String} containing a command name. If you specify + // this option and a key binding exists that matches the command, it will + // be appended to the title or rendered alone if no title is specified. + // * `keyBindingTarget` An `HTMLElement` on which to look up the key binding. + // If this option is not supplied, the first of all matching key bindings + // for the given command will be rendered. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // tooltip. + add (target, options) { + if (target.jquery) { + const disposable = new CompositeDisposable() + for (const element of target) { disposable.add(this.add(element, options)) } + return disposable + } + + if (Tooltip == null) { Tooltip = require('./tooltip') } + + const {keyBindingCommand, keyBindingTarget} = options + + if (keyBindingCommand != null) { + const bindings = this.keymapManager.findKeyBindings({command: keyBindingCommand, target: keyBindingTarget}) + const keystroke = getKeystroke(bindings) + if ((options.title != null) && (keystroke != null)) { + options.title += ` ${getKeystroke(bindings)}` + } else if (keystroke != null) { + options.title = getKeystroke(bindings) + } + } + + delete options.selector + options = _.defaults(options, this.defaults) + if (options.trigger === 'hover') { + options = _.defaults(options, this.hoverDefaults) + } + + const tooltip = new Tooltip(target, options, this.viewRegistry) + + if (!this.tooltips.has(target)) { + this.tooltips.set(target, []) + } + this.tooltips.get(target).push(tooltip) + + const hideTooltip = function () { + tooltip.leave({currentTarget: target}) + tooltip.hide() + } + + window.addEventListener('resize', hideTooltip) + + const disposable = new Disposable(() => { + window.removeEventListener('resize', hideTooltip) + hideTooltip() + tooltip.destroy() + + if (this.tooltips.has(target)) { + const tooltipsForTarget = this.tooltips.get(target) + const index = tooltipsForTarget.indexOf(tooltip) + if (index !== -1) { + tooltipsForTarget.splice(index, 1) + } + if (tooltipsForTarget.length === 0) { + this.tooltips.delete(target) + } + } + }) + + return disposable + } + + // Extended: Find the tooltips that have been applied to the given element. + // + // * `target` The `HTMLElement` to find tooltips on. + // + // Returns an {Array} of `Tooltip` objects that match the `target`. + findTooltips (target) { + if (this.tooltips.has(target)) { + return this.tooltips.get(target).slice() + } else { + return [] + } + } +} + +function humanizeKeystrokes (keystroke) { + let keystrokes = keystroke.split(' ') + keystrokes = (keystrokes.map((stroke) => _.humanizeKeystroke(stroke))) + return keystrokes.join(' ') +} + +function getKeystroke (bindings) { + if (bindings && bindings.length) { + return `${humanizeKeystrokes(bindings[0].keystrokes)}` + } +} diff --git a/src/uri-handler-registry.js b/src/uri-handler-registry.js new file mode 100644 index 000000000..297f916eb --- /dev/null +++ b/src/uri-handler-registry.js @@ -0,0 +1,129 @@ +const url = require('url') +const {Emitter, Disposable} = require('event-kit') + +// Private: Associates listener functions with URIs from outside the application. +// +// 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 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 +// **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 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 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 URI string as the second argument. +// +// 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 "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 URI handlers +// via the `uriHandler` key. +// +// ## Example +// +// 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`: +// +// ```javascript +// { +// "name": "my-package", +// "main": "./lib/my-package.js", +// "uriHandler": { +// "method": "handleURI" +// } +// } +// ``` +// +// `lib/my-package.js` +// +// ```javascript +// module.exports = { +// activate: function() { +// // code to activate your package +// } +// +// handleURI(parsedUri, rawUri) { +// // parse and handle uri +// } +// } +// ``` +module.exports = +class URIHandlerRegistry { + constructor (maxHistoryLength = 50) { + this.registrations = new Map() + this.history = [] + this.maxHistoryLength = maxHistoryLength + this._id = 0 + + this.emitter = new Emitter() + } + + registerHostHandler (host, callback) { + if (typeof callback !== 'function') { + 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 URI host handler for the host ${host}`) + } else { + this.registrations.set(host, callback) + } + + return new Disposable(() => { + this.registrations.delete(host) + }) + } + + 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(`URIHandlerRegistry#handleURI asked to handle an invalid URI: ${uri}`) + } + + const registration = this.registrations.get(host) + const historyEntry = {id: ++this._id, uri: 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') + } + } + + getRecentlyHandledURIs () { + return this.history + } + + onHistoryChange (cb) { + return this.emitter.on('history-change', cb) + } + + destroy () { + this.emitter.dispose() + this.registrations = new Map() + this.history = [] + this._id = 0 + } +} 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..dcc1624fc --- /dev/null +++ b/src/view-registry.js @@ -0,0 +1,249 @@ +const Grim = require('grim') +const {Disposable} = require('event-kit') + +const AnyConstructor = Symbol('any-constructor') + +// Essential: `ViewRegistry` handles the association between model and view +// types in Atom. We call this association a View Provider. As in, for a given +// model, this class can provide a view via {::getView}, as long as the +// model/view association was registered via {::addViewProvider} +// +// If you're adding your own kind of pane item, a good strategy for all but the +// simplest items is to separate the model and the view. The model handles +// application logic and is the primary point of API interaction. The view +// just handles presentation. +// +// Note: Models can be any object, but must implement a `getTitle()` function +// if they are to be displayed in a {Pane} +// +// View providers inform the workspace how your model objects should be +// presented in the DOM. A view provider must always return a DOM node, which +// makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) +// an ideal tool for implementing views in Atom. +// +// You can access the `ViewRegistry` object via `atom.views`. +module.exports = +class ViewRegistry { + constructor (atomEnvironment) { + this.animationFrameRequest = null + this.documentReadInProgress = false + this.performDocumentUpdate = this.performDocumentUpdate.bind(this) + this.atomEnvironment = atomEnvironment + this.clear() + } + + clear () { + this.views = new WeakMap() + this.providers = [] + this.clearDocumentRequests() + } + + // Essential: Add a provider that will be used to construct views in the + // workspace's view layer based on model objects in its model layer. + // + // ## Examples + // + // Text editors are divided into a model and a view layer, so when you interact + // with methods like `atom.workspace.getActiveTextEditor()` you're only going + // to get the model object. We display text editors on screen by teaching the + // workspace what view constructor it should use to represent them: + // + // ```coffee + // atom.views.addViewProvider TextEditor, (textEditor) -> + // textEditorElement = new TextEditorElement + // textEditorElement.initialize(textEditor) + // textEditorElement + // ``` + // + // * `modelConstructor` (optional) Constructor {Function} for your model. If + // a constructor is given, the `createView` function will only be used + // for model objects inheriting from that constructor. Otherwise, it will + // will be called for any object. + // * `createView` Factory {Function} that is passed an instance of your model + // and must return a subclass of `HTMLElement` or `undefined`. If it returns + // `undefined`, then the registry will continue to search for other view + // providers. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // added provider. + addViewProvider (modelConstructor, createView) { + let provider + if (arguments.length === 1) { + switch (typeof modelConstructor) { + case 'function': + provider = {createView: modelConstructor, modelConstructor: AnyConstructor} + break + case 'object': + Grim.deprecate('atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.') + provider = modelConstructor + break + default: + throw new TypeError('Arguments to addViewProvider must be functions') + } + } else { + provider = {modelConstructor, createView} + } + + this.providers.push(provider) + return new Disposable(() => { + this.providers = this.providers.filter(p => p !== provider) + }) + } + + getViewProviderCount () { + return this.providers.length + } + + // Essential: Get the view associated with an object in the workspace. + // + // If you're just *using* the workspace, you shouldn't need to access the view + // layer, but view layer access may be necessary if you want to perform DOM + // manipulation that isn't supported via the model API. + // + // ## View Resolution Algorithm + // + // The view associated with the object is resolved using the following + // sequence + // + // 1. Is the object an instance of `HTMLElement`? If true, return the object. + // 2. Does the object have a method named `getElement` that returns an + // instance of `HTMLElement`? If true, return that value. + // 3. Does the object have a property named `element` with a value which is + // an instance of `HTMLElement`? If true, return the property value. + // 4. Is the object a jQuery object, indicated by the presence of a `jquery` + // property? If true, return the root DOM element (i.e. `object[0]`). + // 5. Has a view provider been registered for the object? If true, use the + // provider to create a view associated with the object, and return the + // view. + // + // If no associated view is returned by the sequence an error is thrown. + // + // Returns a DOM element. + getView (object) { + if (object == null) { return } + + let view = this.views.get(object) + if (!view) { + view = this.createView(object) + this.views.set(object, view) + } + return view + } + + createView (object) { + if (object instanceof HTMLElement) { return object } + + let element + if (object && (typeof object.getElement === 'function')) { + element = object.getElement() + if (element instanceof HTMLElement) { + return element + } + } + + if (object && object.element instanceof HTMLElement) { + return object.element + } + + if (object && object.jquery) { + return object[0] + } + + for (let provider of this.providers) { + if (provider.modelConstructor === AnyConstructor) { + element = provider.createView(object, this.atomEnvironment) + if (element) { return element } + continue + } + + if (object instanceof provider.modelConstructor) { + element = provider.createView && provider.createView(object, this.atomEnvironment) + if (element) { return element } + + let ViewConstructor = provider.viewConstructor + if (ViewConstructor) { + element = new ViewConstructor() + if (element.initialize) { + element.initialize(object) + } else if (element.setModel) { + element.setModel(object) + } + return element + } + } + } + + if (object && object.getViewClass) { + let ViewConstructor = object.getViewClass() + if (ViewConstructor) { + const view = new ViewConstructor(object) + return view[0] + } + } + + throw new Error(`Can't create a view for ${object.constructor.name} instance. Please register a view provider.`) + } + + updateDocument (fn) { + this.documentWriters.push(fn) + if (!this.documentReadInProgress) { this.requestDocumentUpdate() } + return new Disposable(() => { + this.documentWriters = this.documentWriters.filter(writer => writer !== fn) + }) + } + + readDocument (fn) { + this.documentReaders.push(fn) + this.requestDocumentUpdate() + return new Disposable(() => { + this.documentReaders = this.documentReaders.filter(reader => reader !== fn) + }) + } + + getNextUpdatePromise () { + if (this.nextUpdatePromise == null) { + this.nextUpdatePromise = new Promise(resolve => { + this.resolveNextUpdatePromise = resolve + }) + } + + return this.nextUpdatePromise + } + + clearDocumentRequests () { + this.documentReaders = [] + this.documentWriters = [] + this.nextUpdatePromise = null + this.resolveNextUpdatePromise = null + if (this.animationFrameRequest != null) { + cancelAnimationFrame(this.animationFrameRequest) + this.animationFrameRequest = null + } + } + + requestDocumentUpdate () { + if (this.animationFrameRequest == null) { + this.animationFrameRequest = requestAnimationFrame(this.performDocumentUpdate) + } + } + + performDocumentUpdate () { + const { resolveNextUpdatePromise } = this + this.animationFrameRequest = null + this.nextUpdatePromise = null + this.resolveNextUpdatePromise = null + + 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() } + } +} 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) + } +} 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(() => { 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.