Repository: jacob-lcs/atom Branch: master Commit: 8b7ea9aa2230 Files: 1292 Total size: 5.7 MB Directory structure: gitextract_ehm8a0w4/ ├── .coffeelintignore ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github/ │ ├── lock.yml │ ├── move.yml │ ├── no-response.yml │ └── stale.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── SUPPORT.md ├── apm/ │ ├── README.md │ └── package.json ├── atom.sh ├── benchmarks/ │ ├── benchmark-runner.js │ ├── text-editor-large-file-construction.bench.js │ └── text-editor-long-lines.bench.js ├── coffeelint.json ├── docs/ │ ├── README.md │ ├── apm-rest-api.md │ ├── build-instructions/ │ │ ├── build-status.md │ │ ├── linux.md │ │ ├── macOS.md │ │ └── windows.md │ ├── contributing-to-packages.md │ ├── native-profiling.md │ └── rfcs/ │ ├── 000-template.md │ ├── 001-updatable-bundled-packages.md │ ├── 002-atom-nightly-releases.md │ ├── 003-consolidate-core-packages.md │ ├── 004-decoration-ordering.md │ ├── 005-grammar-comment-delims.md │ ├── 005-pretranspile.md │ └── 005-scope-naming.md ├── dot-atom/ │ ├── .gitignore │ ├── init.coffee │ ├── keymap.cson │ ├── packages/ │ │ └── README.md │ ├── snippets.cson │ └── styles.less ├── exports/ │ ├── atom.js │ ├── clipboard.js │ ├── ipc.js │ ├── remote.js │ ├── shell.js │ └── web-frame.js ├── keymaps/ │ ├── base.cson │ ├── darwin.cson │ ├── linux.cson │ └── win32.cson ├── menus/ │ ├── darwin.cson │ ├── linux.cson │ └── win32.cson ├── package.json ├── packages/ │ ├── README.md │ ├── about/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── lib/ │ │ │ ├── about.js │ │ │ ├── components/ │ │ │ │ ├── about-status-bar.js │ │ │ │ ├── about-view.js │ │ │ │ ├── atom-logo.js │ │ │ │ └── update-view.js │ │ │ ├── etch-component.js │ │ │ ├── main.js │ │ │ └── update-manager.js │ │ ├── package.json │ │ ├── spec/ │ │ │ ├── about-spec.js │ │ │ ├── about-status-bar-spec.js │ │ │ ├── helpers/ │ │ │ │ └── async-spec-helpers.js │ │ │ ├── mocks/ │ │ │ │ └── updater.js │ │ │ ├── update-manager-spec.js │ │ │ └── update-view-spec.js │ │ └── styles/ │ │ ├── about.less │ │ └── variables.less │ ├── atom-dark-syntax/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── editor.less │ │ ├── syntax/ │ │ │ ├── base.less │ │ │ ├── css.less │ │ │ └── html.less │ │ ├── syntax-legacy/ │ │ │ └── _base.less │ │ └── syntax-variables.less │ ├── atom-dark-ui/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── atom.less │ │ ├── buttons.less │ │ ├── dropdowns.less │ │ ├── editor.less │ │ ├── git.less │ │ ├── lists.less │ │ ├── messages.less │ │ ├── nav.less │ │ ├── overlays.less │ │ ├── panels.less │ │ ├── panes.less │ │ ├── progress.less │ │ ├── sites.less │ │ ├── tabs.less │ │ ├── text.less │ │ ├── tooltips.less │ │ ├── tree-view.less │ │ ├── ui-mixins.less │ │ ├── ui-variables.less │ │ └── utilities.less │ ├── atom-light-syntax/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── editor.less │ │ ├── syntax/ │ │ │ ├── base.less │ │ │ └── css.less │ │ ├── syntax-legacy/ │ │ │ └── _base.less │ │ └── syntax-variables.less │ ├── atom-light-ui/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── atom.less │ │ ├── buttons.less │ │ ├── editor.less │ │ ├── git.less │ │ ├── lists.less │ │ ├── messages.less │ │ ├── overlays.less │ │ ├── panels.less │ │ ├── panes.less │ │ ├── progress.less │ │ ├── sites.less │ │ ├── tabs.less │ │ ├── text.less │ │ ├── tooltips.less │ │ ├── tree-view.less │ │ ├── ui-mixins.less │ │ ├── ui-variables.less │ │ └── utilities.less │ ├── autoflow/ │ │ ├── .coffeelintignore │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── coffeelint.json │ │ ├── keymaps/ │ │ │ └── autoflow.cson │ │ ├── lib/ │ │ │ └── autoflow.coffee │ │ ├── menus/ │ │ │ └── autoflow.cson │ │ ├── package.json │ │ └── spec/ │ │ └── autoflow-spec.coffee │ ├── base16-tomorrow-dark-theme/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── colors.less │ │ ├── editor.less │ │ ├── syntax/ │ │ │ ├── base.less │ │ │ └── css.less │ │ ├── syntax-legacy/ │ │ │ ├── _base.less │ │ │ ├── cs.less │ │ │ └── json.less │ │ └── syntax-variables.less │ ├── base16-tomorrow-light-theme/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── colors.less │ │ ├── editor.less │ │ ├── syntax/ │ │ │ ├── base.less │ │ │ └── css.less │ │ ├── syntax-legacy/ │ │ │ ├── _base.less │ │ │ ├── cs.less │ │ │ └── json.less │ │ └── syntax-variables.less │ ├── dalek/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── lib/ │ │ │ ├── dalek.js │ │ │ └── main.js │ │ ├── package.json │ │ └── test/ │ │ ├── dalek.test.js │ │ └── runner.js │ ├── deprecation-cop/ │ │ ├── .coffeelintignore │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── coffeelint.json │ │ ├── lib/ │ │ │ ├── deprecation-cop-status-bar-view.coffee │ │ │ ├── deprecation-cop-view.js │ │ │ └── main.js │ │ ├── package.json │ │ ├── spec/ │ │ │ ├── deprecation-cop-spec.coffee │ │ │ ├── deprecation-cop-status-bar-view-spec.coffee │ │ │ └── deprecation-cop-view-spec.coffee │ │ └── styles/ │ │ └── deprecation-cop.less │ ├── dev-live-reload/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── keymaps/ │ │ │ └── dev-live-reload.cson │ │ ├── lib/ │ │ │ ├── base-theme-watcher.js │ │ │ ├── main.js │ │ │ ├── package-watcher.js │ │ │ ├── ui-watcher.js │ │ │ └── watcher.js │ │ ├── menus/ │ │ │ └── dev-live-reload.cson │ │ ├── package.json │ │ └── spec/ │ │ ├── async-spec-helpers.js │ │ ├── dev-live-reload-spec.js │ │ ├── fixtures/ │ │ │ ├── package-with-index/ │ │ │ │ └── index.coffee │ │ │ ├── package-with-styles-folder/ │ │ │ │ ├── package.cson │ │ │ │ └── styles/ │ │ │ │ ├── 3.css │ │ │ │ └── sub/ │ │ │ │ ├── 1.css │ │ │ │ └── 2.less │ │ │ ├── package-with-styles-manifest/ │ │ │ │ ├── package.cson │ │ │ │ └── styles/ │ │ │ │ ├── 1.css │ │ │ │ ├── 2.less │ │ │ │ └── 3.css │ │ │ ├── packages/ │ │ │ │ ├── index.less │ │ │ │ ├── package.cson │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ ├── 1.css │ │ │ │ ├── 2.less │ │ │ │ ├── 3.css │ │ │ │ ├── first.less │ │ │ │ ├── last.less │ │ │ │ ├── second.less │ │ │ │ └── ui-variables.less │ │ │ ├── static/ │ │ │ │ └── atom.less │ │ │ ├── theme-with-index-less/ │ │ │ │ ├── index.less │ │ │ │ └── package.json │ │ │ ├── theme-with-multiple-imported-files/ │ │ │ │ ├── index.less │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ ├── first.less │ │ │ │ ├── last.less │ │ │ │ ├── second.less │ │ │ │ └── ui-variables.less │ │ │ ├── theme-with-package-file/ │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ ├── first.css │ │ │ │ ├── last.css │ │ │ │ └── second.less │ │ │ ├── theme-with-syntax-variables/ │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ └── editor.less │ │ │ └── theme-with-ui-variables/ │ │ │ ├── package.json │ │ │ └── styles/ │ │ │ ├── editor.less │ │ │ └── ui-variables.less │ │ └── ui-watcher-spec.js │ ├── exception-reporting/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── lib/ │ │ │ ├── main.js │ │ │ └── reporter.js │ │ ├── package.json │ │ └── spec/ │ │ └── reporter-spec.js │ ├── git-diff/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── keymaps/ │ │ │ └── git-diff.cson │ │ ├── lib/ │ │ │ ├── diff-list-view.js │ │ │ ├── git-diff-view.js │ │ │ ├── helpers.js │ │ │ └── main.js │ │ ├── menus/ │ │ │ └── git-diff.cson │ │ ├── package.json │ │ ├── spec/ │ │ │ ├── diff-list-view-spec.js │ │ │ ├── fixtures/ │ │ │ │ └── working-dir/ │ │ │ │ ├── git.git/ │ │ │ │ │ ├── HEAD │ │ │ │ │ ├── config │ │ │ │ │ ├── index │ │ │ │ │ ├── objects/ │ │ │ │ │ │ ├── 06/ │ │ │ │ │ │ │ └── 5a272b55ec2ee84530dffd60b6869f7bf5d99c │ │ │ │ │ │ ├── 3e/ │ │ │ │ │ │ │ └── 715502b971d4f8282d1e05a8ccfad6f7037910 │ │ │ │ │ │ ├── 8e/ │ │ │ │ │ │ │ └── ab2e81eb8dea81ad08694c7b30ae165af89c8e │ │ │ │ │ │ ├── 90/ │ │ │ │ │ │ │ └── 820108a054b6f49c0d21031313244b6f7d69dc │ │ │ │ │ │ ├── e7/ │ │ │ │ │ │ │ └── fd5b055dcdaa93ad8f9d63ca8db5330537105f │ │ │ │ │ │ ├── f1/ │ │ │ │ │ │ │ └── 4149b7b38a0a972c46557877caff6c9fe76476 │ │ │ │ │ │ └── fb/ │ │ │ │ │ │ └── 33b0b43b20b7f9de1bca79e192fa2e30dbeb6b │ │ │ │ │ └── refs/ │ │ │ │ │ └── heads/ │ │ │ │ │ └── master │ │ │ │ ├── sample.js │ │ │ │ └── sample.txt │ │ │ ├── git-diff-spec.js │ │ │ ├── git-diff-subfolder-spec.js │ │ │ └── init-spec.js │ │ └── styles/ │ │ └── git-diff.less │ ├── go-to-line/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── keymaps/ │ │ │ └── go-to-line.cson │ │ ├── lib/ │ │ │ └── go-to-line-view.js │ │ ├── menus/ │ │ │ └── go-to-line.cson │ │ ├── package.json │ │ └── spec/ │ │ ├── fixtures/ │ │ │ └── sample.js │ │ └── go-to-line-spec.js │ ├── grammar-selector/ │ │ ├── README.md │ │ ├── keymaps/ │ │ │ └── grammar-selector.cson │ │ ├── lib/ │ │ │ ├── grammar-list-view.js │ │ │ ├── grammar-status-view.js │ │ │ └── main.js │ │ ├── menus/ │ │ │ └── grammar-selector.cson │ │ ├── package.json │ │ ├── spec/ │ │ │ ├── fixtures/ │ │ │ │ └── language-with-no-name/ │ │ │ │ ├── grammars/ │ │ │ │ │ └── a.json │ │ │ │ └── package.json │ │ │ └── grammar-selector-spec.js │ │ └── styles/ │ │ └── grammar-selector.less │ ├── incompatible-packages/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── lib/ │ │ │ ├── incompatible-packages-component.js │ │ │ ├── main.js │ │ │ ├── status-icon-component.js │ │ │ └── view-uri.js │ │ ├── package.json │ │ ├── spec/ │ │ │ ├── fixtures/ │ │ │ │ └── incompatible-package/ │ │ │ │ ├── bad.js │ │ │ │ └── package.json │ │ │ ├── incompatible-packages-component-spec.js │ │ │ └── incompatible-packages-spec.js │ │ └── styles/ │ │ └── incompatible-packages.less │ ├── language-rust-bundled/ │ │ ├── README.md │ │ ├── grammars/ │ │ │ └── tree-sitter-rust.cson │ │ ├── lib/ │ │ │ └── main.js │ │ ├── package.json │ │ └── settings/ │ │ └── rust.cson │ ├── line-ending-selector/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── lib/ │ │ │ ├── helpers.js │ │ │ ├── main.js │ │ │ ├── selector.js │ │ │ └── status-bar-item.js │ │ ├── package.json │ │ └── spec/ │ │ ├── fixtures/ │ │ │ ├── mixed-endings.md │ │ │ ├── unix-endings.md │ │ │ └── windows-endings.md │ │ └── line-ending-selector-spec.js │ ├── link/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── keymaps/ │ │ │ └── links.cson │ │ ├── lib/ │ │ │ └── link.js │ │ ├── menus/ │ │ │ └── link.cson │ │ ├── package.json │ │ └── spec/ │ │ └── link-spec.js │ ├── one-dark-syntax/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── colors.less │ │ ├── editor.less │ │ ├── syntax/ │ │ │ ├── base.less │ │ │ └── css.less │ │ ├── syntax-legacy/ │ │ │ ├── _base.less │ │ │ ├── c.less │ │ │ ├── cpp.less │ │ │ ├── cs.less │ │ │ ├── css.less │ │ │ ├── elixir.less │ │ │ ├── gfm.less │ │ │ ├── go.less │ │ │ ├── ini.less │ │ │ ├── java.less │ │ │ ├── javascript.less │ │ │ ├── json.less │ │ │ ├── ng.less │ │ │ ├── php.less │ │ │ ├── python.less │ │ │ ├── ruby.less │ │ │ └── typescript.less │ │ └── syntax-variables.less │ ├── one-dark-ui/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── lib/ │ │ │ └── main.js │ │ ├── package.json │ │ ├── spec/ │ │ │ └── theme-spec.js │ │ └── styles/ │ │ ├── atom.less │ │ ├── badges.less │ │ ├── buttons.less │ │ ├── config.less │ │ ├── core.less │ │ ├── docks.less │ │ ├── dropdowns.less │ │ ├── editor.less │ │ ├── git.less │ │ ├── inputs.less │ │ ├── key-binding.less │ │ ├── lists.less │ │ ├── messages.less │ │ ├── modal.less │ │ ├── nav.less │ │ ├── notifications.less │ │ ├── packages.less │ │ ├── panels.less │ │ ├── panes.less │ │ ├── progress.less │ │ ├── settings.less │ │ ├── sites.less │ │ ├── status-bar.less │ │ ├── tabs.less │ │ ├── text.less │ │ ├── title-bar.less │ │ ├── tooltips.less │ │ ├── tree-view.less │ │ ├── ui-mixins.less │ │ ├── ui-variables-custom.less │ │ └── ui-variables.less │ ├── one-light-syntax/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── colors.less │ │ ├── editor.less │ │ ├── syntax/ │ │ │ ├── base.less │ │ │ └── css.less │ │ ├── syntax-legacy/ │ │ │ ├── _base.less │ │ │ ├── c.less │ │ │ ├── cpp.less │ │ │ ├── cs.less │ │ │ ├── css.less │ │ │ ├── elixir.less │ │ │ ├── gfm.less │ │ │ ├── go.less │ │ │ ├── ini.less │ │ │ ├── java.less │ │ │ ├── javascript.less │ │ │ ├── json.less │ │ │ ├── ng.less │ │ │ ├── php.less │ │ │ ├── python.less │ │ │ ├── ruby.less │ │ │ └── typescript.less │ │ └── syntax-variables.less │ ├── one-light-ui/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── lib/ │ │ │ └── main.js │ │ ├── package.json │ │ ├── spec/ │ │ │ └── theme-spec.js │ │ └── styles/ │ │ ├── atom.less │ │ ├── badges.less │ │ ├── buttons.less │ │ ├── config.less │ │ ├── core.less │ │ ├── docks.less │ │ ├── dropdowns.less │ │ ├── editor.less │ │ ├── git.less │ │ ├── inputs.less │ │ ├── key-binding.less │ │ ├── lists.less │ │ ├── messages.less │ │ ├── modal.less │ │ ├── nav.less │ │ ├── notifications.less │ │ ├── packages.less │ │ ├── panels.less │ │ ├── panes.less │ │ ├── progress.less │ │ ├── settings.less │ │ ├── sites.less │ │ ├── status-bar.less │ │ ├── tabs.less │ │ ├── text.less │ │ ├── title-bar.less │ │ ├── tooltips.less │ │ ├── tree-view.less │ │ ├── ui-mixins.less │ │ ├── ui-variables-custom.less │ │ └── ui-variables.less │ ├── solarized-dark-syntax/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── colors.less │ │ ├── editor.less │ │ ├── syntax/ │ │ │ ├── base.less │ │ │ ├── css.less │ │ │ ├── html.less │ │ │ └── js.less │ │ ├── syntax-legacy/ │ │ │ ├── _base.less │ │ │ ├── c.less │ │ │ ├── coffee.less │ │ │ ├── css.less │ │ │ ├── go.less │ │ │ ├── java.less │ │ │ ├── javascript.less │ │ │ ├── markdown.less │ │ │ ├── markup.less │ │ │ ├── php.less │ │ │ ├── python.less │ │ │ ├── ruby.less │ │ │ ├── scala.less │ │ │ └── typescript.less │ │ └── syntax-variables.less │ ├── solarized-light-syntax/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.less │ │ ├── package.json │ │ └── styles/ │ │ ├── colors.less │ │ ├── editor.less │ │ ├── syntax/ │ │ │ ├── base.less │ │ │ ├── css.less │ │ │ ├── html.less │ │ │ └── js.less │ │ ├── syntax-legacy/ │ │ │ ├── _base.less │ │ │ ├── c.less │ │ │ ├── coffee.less │ │ │ ├── css.less │ │ │ ├── go.less │ │ │ ├── java.less │ │ │ ├── javascript.less │ │ │ ├── markdown.less │ │ │ ├── markup.less │ │ │ ├── php.less │ │ │ ├── python.less │ │ │ ├── ruby.less │ │ │ ├── scala.less │ │ │ └── typescript.less │ │ └── syntax-variables.less │ ├── update-package-dependencies/ │ │ ├── .gitignore │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── lib/ │ │ │ ├── update-package-dependencies-status-view.js │ │ │ └── update-package-dependencies.js │ │ ├── package.json │ │ ├── spec/ │ │ │ └── update-package-dependencies-spec.js │ │ └── styles/ │ │ └── update-package-dependencies.less │ └── welcome/ │ ├── .gitignore │ ├── LICENSE.md │ ├── README.md │ ├── docs/ │ │ └── events.md │ ├── lib/ │ │ ├── consent-view.js │ │ ├── guide-view.js │ │ ├── main.js │ │ ├── reporter-proxy.js │ │ ├── welcome-package.js │ │ └── welcome-view.js │ ├── menus/ │ │ └── welcome.cson │ ├── package.json │ ├── styles/ │ │ └── welcome.less │ └── test/ │ ├── helpers.js │ └── welcome.test.js ├── resources/ │ ├── app-icons/ │ │ ├── beta/ │ │ │ └── atom.icns │ │ ├── dev/ │ │ │ └── atom.icns │ │ ├── nightly/ │ │ │ └── atom.icns │ │ └── stable/ │ │ └── atom.icns │ ├── linux/ │ │ ├── atom.desktop.in │ │ ├── atom.policy │ │ ├── debian/ │ │ │ └── control.in │ │ ├── desktopenviroment/ │ │ │ └── cinnamon/ │ │ │ └── atom.nemo_action │ │ └── redhat/ │ │ └── atom.spec.in │ ├── mac/ │ │ ├── atom-Info.plist │ │ ├── entitlements.plist │ │ ├── file.icns │ │ └── helper-Info.plist │ └── win/ │ ├── apm.cmd │ ├── apm.sh │ ├── atom.cmd │ ├── atom.js │ ├── atom.sh │ └── atom.visualElementsManifest.xml ├── script/ │ ├── bootstrap │ ├── bootstrap.cmd │ ├── build │ ├── build.cmd │ ├── cibuild │ ├── clean │ ├── clean.cmd │ ├── config.js │ ├── deprecated-packages.json │ ├── lib/ │ │ ├── backup-node-modules.js │ │ ├── check-chromedriver-version.js │ │ ├── clean-caches.js │ │ ├── clean-dependencies.js │ │ ├── clean-output-directory.js │ │ ├── code-sign-on-mac.js │ │ ├── code-sign-on-windows.js │ │ ├── compress-artifacts.js │ │ ├── copy-assets.js │ │ ├── create-debian-package.js │ │ ├── create-rpm-package.js │ │ ├── create-windows-installer.js │ │ ├── delete-msbuild-from-path.js │ │ ├── dependencies-fingerprint.js │ │ ├── download-file-from-github.js │ │ ├── dump-symbols.js │ │ ├── expand-glob-paths.js │ │ ├── generate-api-docs.js │ │ ├── generate-metadata.js │ │ ├── generate-module-cache.js │ │ ├── generate-startup-snapshot.js │ │ ├── get-license-text.js │ │ ├── handle-tilde.js │ │ ├── include-path-in-packaged-app.js │ │ ├── install-apm.js │ │ ├── install-application.js │ │ ├── install-script-dependencies.js │ │ ├── kill-running-atom-instances.js │ │ ├── lint-coffee-script-paths.js │ │ ├── lint-java-script-paths.js │ │ ├── lint-less-paths.js │ │ ├── notarize-on-mac.js │ │ ├── package-application.js │ │ ├── prebuild-less-cache.js │ │ ├── read-files.js │ │ ├── run-apm-install.js │ │ ├── spawn-sync.js │ │ ├── test-sign-on-mac.js │ │ ├── transpile-babel-paths.js │ │ ├── transpile-coffee-script-paths.js │ │ ├── transpile-cson-paths.js │ │ ├── transpile-packages-with-custom-transpiler-paths.js │ │ ├── transpile-peg-js-paths.js │ │ ├── update-dependency/ │ │ │ ├── fetch-outdated-dependencies.js │ │ │ ├── git.js │ │ │ ├── index.js │ │ │ ├── main.js │ │ │ ├── pull-request.js │ │ │ ├── spec/ │ │ │ │ ├── fetch-outdated-dependencies-spec.js │ │ │ │ ├── fixtures/ │ │ │ │ │ ├── create-pr-response.json │ │ │ │ │ ├── latest-package.json │ │ │ │ │ └── search-response.json │ │ │ │ ├── git-spec.js │ │ │ │ ├── helpers.js │ │ │ │ ├── pull-request-spec.js │ │ │ │ └── util-spec.js │ │ │ └── util.js │ │ └── verify-machine-requirements.js │ ├── license-overrides.js │ ├── lint │ ├── lint.cmd │ ├── package.json │ ├── postprocess-junit-results │ ├── postprocess-junit-results.cmd │ ├── redownload-electron-bins.js │ ├── test │ ├── test.cmd │ ├── update-server/ │ │ ├── README.md │ │ ├── package.json │ │ └── run-server.js │ ├── verify-snapshot-script │ └── vsts/ │ ├── README.md │ ├── get-release-version.js │ ├── lib/ │ │ ├── release-notes.js │ │ ├── upload-linux-packages.js │ │ └── upload-to-azure-blob.js │ ├── lint.yml │ ├── nightly-release.yml │ ├── package.json │ ├── platforms/ │ │ ├── linux.yml │ │ ├── macos.yml │ │ ├── templates/ │ │ │ ├── bootstrap.yml │ │ │ ├── build.yml │ │ │ ├── cache.yml │ │ │ ├── download-unzip.yml │ │ │ ├── get-release-version.yml │ │ │ ├── preparation.yml │ │ │ ├── publish.yml │ │ │ └── test.yml │ │ └── windows.yml │ ├── pull-requests.yml │ ├── release-branch-build.yml │ ├── upload-artifacts.js │ ├── upload-crash-reports.js │ ├── x64-cache-key │ └── x86-cache-key ├── spec/ │ ├── application-delegate-spec.js │ ├── async-spec-helpers.js │ ├── atom-environment-spec.js │ ├── atom-paths-spec.js │ ├── atom-protocol-handler-spec.js │ ├── atom-reporter.coffee │ ├── auto-update-manager-spec.js │ ├── babel-spec.js │ ├── buffered-node-process-spec.js │ ├── buffered-process-spec.js │ ├── clipboard-spec.js │ ├── command-installer-spec.js │ ├── command-registry-spec.js │ ├── compile-cache-spec.coffee │ ├── config-file-spec.js │ ├── config-spec.js │ ├── context-menu-manager-spec.js │ ├── decoration-manager-spec.js │ ├── default-directory-provider-spec.js │ ├── default-directory-searcher-spec.js │ ├── deserializer-manager-spec.js │ ├── dock-spec.js │ ├── file-system-blob-store-spec.js │ ├── fixtures/ │ │ ├── babel/ │ │ │ ├── babel-comment.js │ │ │ ├── babel-double-quotes.js │ │ │ ├── babel-single-quotes.js │ │ │ ├── flow-comment.js │ │ │ ├── flow-slash-comment.js │ │ │ └── invalid.js │ │ ├── coffee.coffee │ │ ├── cson.cson │ │ ├── css.css │ │ ├── dir/ │ │ │ ├── a │ │ │ ├── a-dir/ │ │ │ │ └── oh-git │ │ │ ├── b │ │ │ ├── c │ │ │ ├── file-detected-as-binary │ │ │ ├── file-with-newline-literal │ │ │ ├── file-with-unicode │ │ │ └── file1 │ │ ├── git/ │ │ │ ├── ignore.git/ │ │ │ │ ├── HEAD │ │ │ │ ├── config │ │ │ │ ├── index │ │ │ │ ├── info/ │ │ │ │ │ └── exclude │ │ │ │ ├── objects/ │ │ │ │ │ ├── 65/ │ │ │ │ │ │ └── a457425a679cbe9adf0d2741785d3ceabb44a7 │ │ │ │ │ ├── e6/ │ │ │ │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ │ │ │ └── ef/ │ │ │ │ │ └── 046e9eecaa5255ea5e9817132d4001724d6ae1 │ │ │ │ └── refs/ │ │ │ │ └── heads/ │ │ │ │ └── master │ │ │ ├── master.git/ │ │ │ │ ├── HEAD │ │ │ │ ├── config │ │ │ │ ├── index │ │ │ │ ├── objects/ │ │ │ │ │ ├── 65/ │ │ │ │ │ │ └── a457425a679cbe9adf0d2741785d3ceabb44a7 │ │ │ │ │ ├── e6/ │ │ │ │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ │ │ │ └── ef/ │ │ │ │ │ └── 046e9eecaa5255ea5e9817132d4001724d6ae1 │ │ │ │ ├── refs/ │ │ │ │ │ └── heads/ │ │ │ │ │ └── master │ │ │ │ └── worktrees/ │ │ │ │ └── worktree-dir/ │ │ │ │ ├── HEAD │ │ │ │ ├── commondir │ │ │ │ └── index │ │ │ ├── repo-with-submodules/ │ │ │ │ ├── .gitmodules │ │ │ │ ├── README │ │ │ │ ├── You-Dont-Need-jQuery/ │ │ │ │ │ ├── .babelrc │ │ │ │ │ ├── .eslintrc │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── .travis.yml │ │ │ │ │ ├── LICENSE │ │ │ │ │ ├── README-es.md │ │ │ │ │ ├── README-id.md │ │ │ │ │ ├── README-it.md │ │ │ │ │ ├── README-my.md │ │ │ │ │ ├── README-ru.md │ │ │ │ │ ├── README-tr.md │ │ │ │ │ ├── README-vi.md │ │ │ │ │ ├── README.ko-KR.md │ │ │ │ │ ├── README.md │ │ │ │ │ ├── README.pt-BR.md │ │ │ │ │ ├── README.zh-CN.md │ │ │ │ │ ├── git.git │ │ │ │ │ ├── karma.conf.js │ │ │ │ │ ├── package.json │ │ │ │ │ └── test/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── css.spec.js │ │ │ │ │ ├── dom.spec.js │ │ │ │ │ ├── query.spec.js │ │ │ │ │ └── utilities.spec.js │ │ │ │ ├── git.git/ │ │ │ │ │ ├── COMMIT_EDITMSG │ │ │ │ │ ├── HEAD │ │ │ │ │ ├── config │ │ │ │ │ ├── description │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── applypatch-msg.sample │ │ │ │ │ │ ├── commit-msg.sample │ │ │ │ │ │ ├── post-update.sample │ │ │ │ │ │ ├── pre-applypatch.sample │ │ │ │ │ │ ├── pre-commit.sample │ │ │ │ │ │ ├── pre-push.sample │ │ │ │ │ │ ├── pre-rebase.sample │ │ │ │ │ │ ├── prepare-commit-msg.sample │ │ │ │ │ │ └── update.sample │ │ │ │ │ ├── index │ │ │ │ │ ├── info/ │ │ │ │ │ │ └── exclude │ │ │ │ │ ├── logs/ │ │ │ │ │ │ ├── HEAD │ │ │ │ │ │ └── refs/ │ │ │ │ │ │ └── heads/ │ │ │ │ │ │ └── master │ │ │ │ │ ├── modules/ │ │ │ │ │ │ ├── You-Dont-Need-jQuery/ │ │ │ │ │ │ │ ├── COMMIT_EDITMSG │ │ │ │ │ │ │ ├── HEAD │ │ │ │ │ │ │ ├── ORIG_HEAD │ │ │ │ │ │ │ ├── config │ │ │ │ │ │ │ ├── description │ │ │ │ │ │ │ ├── gitdir │ │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ │ ├── applypatch-msg.sample │ │ │ │ │ │ │ │ ├── commit-msg.sample │ │ │ │ │ │ │ │ ├── post-update.sample │ │ │ │ │ │ │ │ ├── pre-applypatch.sample │ │ │ │ │ │ │ │ ├── pre-commit.sample │ │ │ │ │ │ │ │ ├── pre-push.sample │ │ │ │ │ │ │ │ ├── pre-rebase.sample │ │ │ │ │ │ │ │ ├── prepare-commit-msg.sample │ │ │ │ │ │ │ │ └── update.sample │ │ │ │ │ │ │ ├── index │ │ │ │ │ │ │ ├── info/ │ │ │ │ │ │ │ │ └── exclude │ │ │ │ │ │ │ ├── logs/ │ │ │ │ │ │ │ │ ├── HEAD │ │ │ │ │ │ │ │ └── refs/ │ │ │ │ │ │ │ │ ├── heads/ │ │ │ │ │ │ │ │ │ └── master │ │ │ │ │ │ │ │ └── remotes/ │ │ │ │ │ │ │ │ └── origin/ │ │ │ │ │ │ │ │ └── HEAD │ │ │ │ │ │ │ ├── objects/ │ │ │ │ │ │ │ │ ├── a7/ │ │ │ │ │ │ │ │ │ └── 8b35a896b890f0a2a4f1f924c5739776415250 │ │ │ │ │ │ │ │ ├── ae/ │ │ │ │ │ │ │ │ │ └── 897dce6e0590f08dddfe9a5152e237e955ca57 │ │ │ │ │ │ │ │ ├── be/ │ │ │ │ │ │ │ │ │ └── 8ed228c0a080145d28ed625a5f487caae6a3f9 │ │ │ │ │ │ │ │ └── pack/ │ │ │ │ │ │ │ │ ├── pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.idx │ │ │ │ │ │ │ │ └── pack-d38b3bc339acd655e8dae9c0dcea8bb2ec174d16.pack │ │ │ │ │ │ │ ├── packed-refs │ │ │ │ │ │ │ └── refs/ │ │ │ │ │ │ │ ├── heads/ │ │ │ │ │ │ │ │ └── master │ │ │ │ │ │ │ └── remotes/ │ │ │ │ │ │ │ └── origin/ │ │ │ │ │ │ │ └── HEAD │ │ │ │ │ │ └── jstips/ │ │ │ │ │ │ ├── COMMIT_EDITMSG │ │ │ │ │ │ ├── HEAD │ │ │ │ │ │ ├── ORIG_HEAD │ │ │ │ │ │ ├── config │ │ │ │ │ │ ├── description │ │ │ │ │ │ ├── gitdir │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ ├── applypatch-msg.sample │ │ │ │ │ │ │ ├── commit-msg.sample │ │ │ │ │ │ │ ├── post-update.sample │ │ │ │ │ │ │ ├── pre-applypatch.sample │ │ │ │ │ │ │ ├── pre-commit.sample │ │ │ │ │ │ │ ├── pre-push.sample │ │ │ │ │ │ │ ├── pre-rebase.sample │ │ │ │ │ │ │ ├── prepare-commit-msg.sample │ │ │ │ │ │ │ └── update.sample │ │ │ │ │ │ ├── index │ │ │ │ │ │ ├── info/ │ │ │ │ │ │ │ └── exclude │ │ │ │ │ │ ├── logs/ │ │ │ │ │ │ │ ├── HEAD │ │ │ │ │ │ │ └── refs/ │ │ │ │ │ │ │ ├── heads/ │ │ │ │ │ │ │ │ ├── master │ │ │ │ │ │ │ │ └── test │ │ │ │ │ │ │ └── remotes/ │ │ │ │ │ │ │ └── origin/ │ │ │ │ │ │ │ └── HEAD │ │ │ │ │ │ ├── objects/ │ │ │ │ │ │ │ ├── 05/ │ │ │ │ │ │ │ │ └── 25ef667328cb1f86b1ddf523db4a064e1590fa │ │ │ │ │ │ │ ├── 1a/ │ │ │ │ │ │ │ │ └── dd860234dad4a8bf59340363e9c88bb0457cb7 │ │ │ │ │ │ │ ├── 5b/ │ │ │ │ │ │ │ │ └── 35953562dbb4f2debe88fcc9ac1805064e1e5a │ │ │ │ │ │ │ └── pack/ │ │ │ │ │ │ │ ├── pack-e568a55e02b6b7b75582924204669e4f3ed5276f.idx │ │ │ │ │ │ │ └── pack-e568a55e02b6b7b75582924204669e4f3ed5276f.pack │ │ │ │ │ │ ├── packed-refs │ │ │ │ │ │ └── refs/ │ │ │ │ │ │ ├── heads/ │ │ │ │ │ │ │ ├── master │ │ │ │ │ │ │ └── test │ │ │ │ │ │ └── remotes/ │ │ │ │ │ │ └── origin/ │ │ │ │ │ │ └── HEAD │ │ │ │ │ ├── objects/ │ │ │ │ │ │ ├── 3e/ │ │ │ │ │ │ │ └── 2fe2f8226faab789f70d6d8a7231e4f2bd54df │ │ │ │ │ │ ├── 40/ │ │ │ │ │ │ │ └── f4e79926a85134d4c905d04e70573b6616f3bc │ │ │ │ │ │ ├── 50/ │ │ │ │ │ │ │ └── b89367d8f0acd312ef5aa012eac20a75c91351 │ │ │ │ │ │ ├── 54/ │ │ │ │ │ │ │ └── 3b9bebdc6bd5c4b22136034a95dd097a57d3dd │ │ │ │ │ │ ├── d2/ │ │ │ │ │ │ │ └── b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 │ │ │ │ │ │ ├── d3/ │ │ │ │ │ │ │ └── e073baf592c56614c68ead9e2cd0a3880140cd │ │ │ │ │ │ └── e6/ │ │ │ │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ │ │ │ └── refs/ │ │ │ │ │ ├── heads/ │ │ │ │ │ │ └── master │ │ │ │ │ └── remotes/ │ │ │ │ │ └── origin/ │ │ │ │ │ └── master │ │ │ │ └── jstips/ │ │ │ │ ├── CONTRIBUTING.md │ │ │ │ ├── README.md │ │ │ │ ├── git.git │ │ │ │ └── resources/ │ │ │ │ └── log.js │ │ │ └── working-dir/ │ │ │ ├── .gitignore │ │ │ ├── a.txt │ │ │ └── git.git/ │ │ │ ├── HEAD │ │ │ ├── config │ │ │ ├── index │ │ │ ├── objects/ │ │ │ │ ├── 06/ │ │ │ │ │ └── 15f9a45968b3515e0a202530ef9f61aba26b6c │ │ │ │ ├── 16/ │ │ │ │ │ └── 735fb793d7b038818219c4b8c6295346e20eef │ │ │ │ ├── 52/ │ │ │ │ │ └── f56457b6fca045ce41bb9d32e6ca79d23192af │ │ │ │ ├── 5b/ │ │ │ │ │ └── 24ab4c3baadf534242b1acc220c8fa051b9b20 │ │ │ │ ├── 65/ │ │ │ │ │ └── a457425a679cbe9adf0d2741785d3ceabb44a7 │ │ │ │ ├── 66/ │ │ │ │ │ └── dc9051da651c15d98d017a88658263cab28f02 │ │ │ │ ├── 8a/ │ │ │ │ │ └── 9c86f1cb1f14b8f436eb91f4b052c8802ca99e │ │ │ │ ├── e6/ │ │ │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ │ │ ├── ec/ │ │ │ │ │ └── 5e386905ff2d36e291086a1207f2585aaa8920 │ │ │ │ ├── ef/ │ │ │ │ │ └── 046e9eecaa5255ea5e9817132d4001724d6ae1 │ │ │ │ ├── fe/ │ │ │ │ │ └── bde178cdf35e9df6279d87aa27590c6d92e354 │ │ │ │ └── ff/ │ │ │ │ └── c8218bd2240a0cb92f6f02548d45784428349b │ │ │ └── refs/ │ │ │ └── heads/ │ │ │ └── master │ │ ├── indentation/ │ │ │ ├── classes.js │ │ │ ├── expressions.js │ │ │ ├── function_call.js │ │ │ ├── if_then_else.js │ │ │ ├── jsx.jsx │ │ │ ├── objects_and_array.js │ │ │ ├── switch.js │ │ │ └── while.js │ │ ├── lorem.txt │ │ ├── module-cache/ │ │ │ └── file.json │ │ ├── native-cache/ │ │ │ ├── file-1.js │ │ │ ├── file-2.js │ │ │ ├── file-3.js │ │ │ └── file-4.js │ │ ├── packages/ │ │ │ ├── folder/ │ │ │ │ └── package-symlinked/ │ │ │ │ └── package.json │ │ │ ├── package-that-throws-an-exception/ │ │ │ │ └── index.coffee │ │ │ ├── package-that-throws-on-activate/ │ │ │ │ └── index.coffee │ │ │ ├── package-that-throws-on-deactivate/ │ │ │ │ └── index.coffee │ │ │ ├── package-with-activation-commands/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.cson │ │ │ ├── package-with-activation-commands-and-deserializers/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── package-with-activation-hooks/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.cson │ │ │ ├── package-with-broken-keymap/ │ │ │ │ └── keymaps/ │ │ │ │ └── broken.json │ │ │ ├── package-with-broken-package-json/ │ │ │ │ └── package.json │ │ │ ├── package-with-cached-incompatible-native-module/ │ │ │ │ ├── main.js │ │ │ │ └── package.json │ │ │ ├── package-with-config-defaults/ │ │ │ │ └── index.coffee │ │ │ ├── package-with-config-schema/ │ │ │ │ └── index.coffee │ │ │ ├── package-with-consumed-services/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.json │ │ │ ├── package-with-deactivate/ │ │ │ │ └── index.coffee │ │ │ ├── package-with-deprecated-pane-item-method/ │ │ │ │ └── index.coffee │ │ │ ├── package-with-deserializers/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── package-with-different-directory-name/ │ │ │ │ └── package.json │ │ │ ├── package-with-directory-provider/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── package-with-empty-activation-commands/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.json │ │ │ ├── package-with-empty-activation-hooks/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.json │ │ │ ├── package-with-empty-keymap/ │ │ │ │ ├── keymaps/ │ │ │ │ │ └── keymap.cson │ │ │ │ └── package.json │ │ │ ├── package-with-empty-menu/ │ │ │ │ ├── menus/ │ │ │ │ │ └── menu.cson │ │ │ │ └── package.json │ │ │ ├── package-with-empty-workspace-openers/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.json │ │ │ ├── package-with-eval-time-api-calls/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── package-with-grammars/ │ │ │ │ └── grammars/ │ │ │ │ ├── alittle.cson │ │ │ │ └── alot.cson │ │ │ ├── package-with-ignored-incompatible-native-module/ │ │ │ │ ├── main.js │ │ │ │ └── package.json │ │ │ ├── package-with-incompatible-native-module/ │ │ │ │ ├── main.js │ │ │ │ └── package.json │ │ │ ├── package-with-incompatible-native-module-loaded-conditionally/ │ │ │ │ ├── main.js │ │ │ │ └── package.json │ │ │ ├── package-with-index/ │ │ │ │ └── index.coffee │ │ │ ├── package-with-injection-selector/ │ │ │ │ └── grammars/ │ │ │ │ └── grammar.cson │ │ │ ├── package-with-invalid-activation-commands/ │ │ │ │ └── package.json │ │ │ ├── package-with-invalid-context-menu/ │ │ │ │ ├── menus/ │ │ │ │ │ └── menu.json │ │ │ │ └── package.json │ │ │ ├── package-with-invalid-grammar/ │ │ │ │ ├── grammars/ │ │ │ │ │ └── grammar.json │ │ │ │ └── package.json │ │ │ ├── package-with-invalid-settings/ │ │ │ │ ├── package.json │ │ │ │ └── settings/ │ │ │ │ └── settings.json │ │ │ ├── package-with-invalid-styles/ │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ └── index.less │ │ │ ├── package-with-invalid-url-package-json/ │ │ │ │ └── package.json │ │ │ ├── package-with-json-config-schema/ │ │ │ │ └── package.json │ │ │ ├── package-with-keymaps/ │ │ │ │ └── keymaps/ │ │ │ │ ├── keymap-1.cson │ │ │ │ ├── keymap-2.cson │ │ │ │ └── keymap-3.cjson │ │ │ ├── package-with-keymaps-manifest/ │ │ │ │ ├── keymaps/ │ │ │ │ │ ├── keymap-1.json │ │ │ │ │ ├── keymap-2.cson │ │ │ │ │ └── keymap-3.cson │ │ │ │ └── package.cson │ │ │ ├── package-with-main/ │ │ │ │ ├── main-module.coffee │ │ │ │ └── package.cson │ │ │ ├── package-with-menus/ │ │ │ │ └── menus/ │ │ │ │ ├── menu-1.cson │ │ │ │ ├── menu-2.cson │ │ │ │ └── menu-3.cson │ │ │ ├── package-with-menus-manifest/ │ │ │ │ ├── menus/ │ │ │ │ │ ├── menu-1.cson │ │ │ │ │ ├── menu-2.cson │ │ │ │ │ └── menu-3.cson │ │ │ │ └── package.cson │ │ │ ├── package-with-missing-consumed-services/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.json │ │ │ ├── package-with-missing-provided-services/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.json │ │ │ ├── package-with-no-activate/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── package-with-prefixed-and-suffixed-repo-url/ │ │ │ │ └── package.json │ │ │ ├── package-with-provided-services/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.json │ │ │ ├── package-with-rb-filetype/ │ │ │ │ ├── grammars/ │ │ │ │ │ └── rb.cson │ │ │ │ └── package.json │ │ │ ├── package-with-serialization/ │ │ │ │ └── index.coffee │ │ │ ├── package-with-serialize-error/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.cson │ │ │ ├── package-with-settings/ │ │ │ │ └── settings/ │ │ │ │ └── omg.cson │ │ │ ├── package-with-short-url-package-json/ │ │ │ │ └── package.json │ │ │ ├── package-with-style-sheets-manifest/ │ │ │ │ ├── package.cson │ │ │ │ └── styles/ │ │ │ │ ├── 1.css │ │ │ │ ├── 2.less │ │ │ │ └── 3.css │ │ │ ├── package-with-styles/ │ │ │ │ └── styles/ │ │ │ │ ├── 1.css │ │ │ │ ├── 2.less │ │ │ │ ├── 3.test-context.css │ │ │ │ └── 4.css │ │ │ ├── package-with-stylesheets-manifest/ │ │ │ │ └── package.cson │ │ │ ├── package-with-tree-sitter-grammar/ │ │ │ │ └── grammars/ │ │ │ │ ├── fake-parser.js │ │ │ │ └── some-language.cson │ │ │ ├── package-with-uri-handler/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── package-with-url-main/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── package-with-view-providers/ │ │ │ │ ├── index.js │ │ │ │ └── package.json │ │ │ ├── package-with-workspace-openers/ │ │ │ │ ├── index.coffee │ │ │ │ └── package.cson │ │ │ ├── package-without-module/ │ │ │ │ └── package.cson │ │ │ ├── sublime-tabs/ │ │ │ │ └── package.json │ │ │ ├── theme-with-incomplete-ui-variables/ │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ ├── editor.less │ │ │ │ └── ui-variables.less │ │ │ ├── theme-with-index-css/ │ │ │ │ ├── index.css │ │ │ │ └── package.json │ │ │ ├── theme-with-index-less/ │ │ │ │ ├── index.less │ │ │ │ └── package.json │ │ │ ├── theme-with-invalid-styles/ │ │ │ │ ├── index.less │ │ │ │ └── package.json │ │ │ ├── theme-with-package-file/ │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ ├── first.css │ │ │ │ ├── last.css │ │ │ │ └── second.less │ │ │ ├── theme-with-syntax-variables/ │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ └── editor.less │ │ │ ├── theme-with-ui-variables/ │ │ │ │ ├── package.json │ │ │ │ └── styles/ │ │ │ │ ├── editor.less │ │ │ │ └── ui-variables.less │ │ │ ├── theme-without-package-file/ │ │ │ │ └── styles/ │ │ │ │ ├── a.css │ │ │ │ ├── b.css │ │ │ │ ├── c.less │ │ │ │ └── d.csv │ │ │ └── wordcount/ │ │ │ └── package.json │ │ ├── sample-with-comments.js │ │ ├── sample-with-many-folds.js │ │ ├── sample-with-tabs-and-leading-comment.coffee │ │ ├── sample-with-tabs.coffee │ │ ├── sample.js │ │ ├── sample.less │ │ ├── sample.txt │ │ ├── script-with-deprecations.js │ │ ├── script.js │ │ ├── shebang │ │ ├── task-handler-with-deprecations.coffee │ │ ├── task-spec-handler.coffee │ │ ├── testdir/ │ │ │ ├── sample-theme-1/ │ │ │ │ ├── readme │ │ │ │ └── src/ │ │ │ │ └── js/ │ │ │ │ └── main.js │ │ │ └── sample-theme-2/ │ │ │ ├── readme │ │ │ └── src/ │ │ │ └── js/ │ │ │ ├── main.js │ │ │ └── plugin/ │ │ │ └── main.js │ │ ├── two-hundred.txt │ │ └── typescript/ │ │ ├── invalid.ts │ │ └── valid.ts │ ├── git-repository-provider-spec.js │ ├── git-repository-spec.js │ ├── grammar-registry-spec.js │ ├── gutter-container-spec.js │ ├── gutter-spec.js │ ├── helpers/ │ │ ├── random.js │ │ └── words.js │ ├── history-manager-spec.js │ ├── integration/ │ │ ├── helpers/ │ │ │ ├── atom-launcher.sh │ │ │ └── start-atom.js │ │ └── smoke-spec.js │ ├── jasmine-junit-reporter.js │ ├── jasmine-list-reporter.js │ ├── jasmine-test-runner.coffee │ ├── keymap-extensions-spec.js │ ├── main-process/ │ │ ├── atom-application.test.js │ │ ├── atom-window.test.js │ │ ├── file-recovery-service.test.js │ │ ├── mocha-test-runner.js │ │ └── parse-command-line.test.js │ ├── menu-manager-spec.js │ ├── menu-sort-helpers-spec.js │ ├── module-cache-spec.js │ ├── native-compile-cache-spec.coffee │ ├── native-watcher-registry-spec.js │ ├── notification-manager-spec.js │ ├── notification-spec.js │ ├── package-manager-spec.js │ ├── package-spec.js │ ├── package-transpilation-registry-spec.js │ ├── pane-axis-element-spec.js │ ├── pane-container-element-spec.js │ ├── pane-container-spec.js │ ├── pane-element-spec.js │ ├── pane-spec.js │ ├── panel-container-element-spec.js │ ├── panel-container-spec.js │ ├── panel-spec.js │ ├── path-watcher-spec.js │ ├── project-spec.js │ ├── reopen-project-menu-manager-spec.js │ ├── selection-spec.js │ ├── spec-helper-platform.js │ ├── spec-helper.coffee │ ├── squirrel-update-spec.js │ ├── state-store-spec.js │ ├── style-manager-spec.js │ ├── styles-element-spec.js │ ├── syntax-scope-map-spec.js │ ├── task-spec.js │ ├── text-editor-component-spec.js │ ├── text-editor-element-spec.js │ ├── text-editor-registry-spec.js │ ├── text-editor-spec.js │ ├── text-mate-language-mode-spec.js │ ├── text-utils-spec.js │ ├── theme-manager-spec.js │ ├── title-bar-spec.js │ ├── tooltip-manager-spec.js │ ├── tree-indenter-spec.js │ ├── tree-sitter-language-mode-spec.js │ ├── typescript-spec.js │ ├── update-process-env-spec.js │ ├── uri-handler-registry-spec.js │ ├── view-registry-spec.js │ ├── window-event-handler-spec.js │ ├── workspace-center-spec.js │ ├── workspace-element-spec.js │ └── workspace-spec.js ├── src/ │ ├── application-delegate.js │ ├── atom-environment.js │ ├── atom-paths.js │ ├── auto-update-manager.js │ ├── babel.js │ ├── buffered-node-process.js │ ├── buffered-process.js │ ├── clipboard.js │ ├── coffee-script.js │ ├── color.js │ ├── command-installer.js │ ├── command-registry.js │ ├── compile-cache.js │ ├── config-file.js │ ├── config-schema.js │ ├── config.js │ ├── context-menu-manager.coffee │ ├── core-uri-handlers.js │ ├── crash-reporter-start.js │ ├── cursor.js │ ├── decoration-manager.js │ ├── decoration.js │ ├── default-directory-provider.coffee │ ├── default-directory-searcher.js │ ├── delegated-listener.js │ ├── deprecated-syntax-selectors.js │ ├── deserializer-manager.js │ ├── dock.js │ ├── electron-shims.js │ ├── file-system-blob-store.js │ ├── first-mate-helpers.js │ ├── get-app-name.js │ ├── get-release-channel.js │ ├── get-window-load-settings.js │ ├── git-repository-provider.js │ ├── git-repository.js │ ├── grammar-registry.js │ ├── gutter-container.js │ ├── gutter.js │ ├── history-manager.js │ ├── initialize-application-window.js │ ├── initialize-benchmark-window.js │ ├── initialize-test-window.js │ ├── ipc-helpers.js │ ├── item-registry.js │ ├── keymap-extensions.coffee │ ├── layer-decoration.coffee │ ├── less-compile-cache.coffee │ ├── main-process/ │ │ ├── application-menu.js │ │ ├── atom-application.js │ │ ├── atom-protocol-handler.js │ │ ├── atom-window.js │ │ ├── auto-update-manager.js │ │ ├── auto-updater-win32.js │ │ ├── context-menu.js │ │ ├── file-recovery-service.js │ │ ├── main.js │ │ ├── parse-command-line.js │ │ ├── spawner.js │ │ ├── squirrel-update.js │ │ ├── start.js │ │ ├── win-powershell.js │ │ └── win-shell.js │ ├── menu-helpers.js │ ├── menu-manager.coffee │ ├── menu-sort-helpers.js │ ├── model.coffee │ ├── module-cache.js │ ├── module-utils.js │ ├── native-compile-cache.js │ ├── native-watcher-registry.js │ ├── notification-manager.js │ ├── notification.js │ ├── null-grammar.js │ ├── overlay-manager.coffee │ ├── package-manager.js │ ├── package-transpilation-registry.js │ ├── package.js │ ├── pane-axis-element.js │ ├── pane-axis.js │ ├── pane-container-element.js │ ├── pane-container.js │ ├── pane-element.js │ ├── pane-resize-handle-element.js │ ├── pane.js │ ├── panel-container-element.js │ ├── panel-container.js │ ├── panel.js │ ├── path-watcher.js │ ├── project.js │ ├── protocol-handler-installer.js │ ├── register-default-commands.coffee │ ├── reopen-project-list-view.js │ ├── reopen-project-menu-manager.js │ ├── replace-handler.coffee │ ├── ripgrep-directory-searcher.js │ ├── scan-handler.coffee │ ├── scope-descriptor.js │ ├── selection.js │ ├── selectors.js │ ├── special-token-symbols.coffee │ ├── startup-time.js │ ├── state-store.js │ ├── storage-folder.js │ ├── style-manager.js │ ├── styles-element.js │ ├── syntax-scope-map.js │ ├── task-bootstrap.js │ ├── task.coffee │ ├── test.ejs │ ├── text-editor-component.js │ ├── text-editor-element.js │ ├── text-editor-registry.js │ ├── text-editor.js │ ├── text-mate-language-mode.js │ ├── text-utils.js │ ├── theme-manager.js │ ├── theme-package.js │ ├── title-bar.js │ ├── token-iterator.js │ ├── token.coffee │ ├── tokenized-line.coffee │ ├── tooltip-manager.js │ ├── tooltip.js │ ├── tree-indenter.js │ ├── tree-sitter-grammar.js │ ├── tree-sitter-language-mode.js │ ├── typescript.js │ ├── update-process-env.js │ ├── uri-handler-registry.js │ ├── view-registry.js │ ├── window-event-handler.js │ ├── window.js │ ├── workspace-center.js │ ├── workspace-element.js │ └── workspace.js ├── static/ │ ├── atom-ui/ │ │ ├── README.md │ │ ├── _index.less │ │ └── styles/ │ │ ├── badges.less │ │ ├── button-groups.less │ │ ├── buttons.less │ │ ├── git-status.less │ │ ├── icons.less │ │ ├── inputs.less │ │ ├── layout.less │ │ ├── lists.less │ │ ├── loading.less │ │ ├── messages.less │ │ ├── mixins/ │ │ │ └── mixins.less │ │ ├── modals.less │ │ ├── panels.less │ │ ├── private/ │ │ │ ├── README.md │ │ │ ├── alerts.less │ │ │ ├── close.less │ │ │ ├── code.less │ │ │ ├── forms.less │ │ │ ├── links.less │ │ │ ├── navs.less │ │ │ ├── scaffolding.less │ │ │ ├── sections.less │ │ │ ├── tables.less │ │ │ └── utilities.less │ │ ├── select-list.less │ │ ├── site-colors.less │ │ ├── text.less │ │ ├── tooltip.less │ │ └── variables/ │ │ └── variables.less │ ├── atom.less │ ├── babelrc.json │ ├── core-ui/ │ │ ├── _index.less │ │ ├── cursors.less │ │ ├── docks.less │ │ ├── panels.less │ │ ├── panes.less │ │ ├── syntax.less │ │ ├── text-editor.less │ │ ├── title-bar.less │ │ ├── utils.less │ │ └── workspace-view.less │ ├── icons/ │ │ └── octicons.less │ ├── index.html │ ├── index.js │ ├── jasmine.less │ ├── linux.less │ ├── normalize.less │ ├── scaffolding.less │ └── variables/ │ ├── octicon-mixins.less │ ├── octicon-utf-codes.less │ ├── syntax-variables.less │ └── ui-variables.less ├── stylelint.config.js └── vendor/ ├── jasmine-jquery.js └── jasmine.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coffeelintignore ================================================ spec/fixtures ================================================ FILE: .eslintignore ================================================ **/spec/fixtures/**/*.js node_modules /vendor/ /out/ ================================================ FILE: .eslintrc.json ================================================ { "extends": [ "./script/node_modules/eslint-config-standard/eslintrc.json", "./script/node_modules/eslint-config-prettier/index.js", "./script/node_modules/eslint-config-prettier/standard.js" ], "plugins": [ "prettier" ], "env": { "browser": true, "node": true }, "parser": "babel-eslint", "parserOptions": { "ecmaVersion": 8, "ecmaFeatures": { "jsx": true } }, "globals": { "atom": true, "snapshotResult": true }, "rules": { "standard/no-callback-literal": ["off"], "node/no-deprecated-api": ["off"], "prettier/prettier": ["error"] }, "overrides": [ { "files": ["spec/**", "**-spec.js", "**.test.js"], "env": { "jasmine": true }, "globals": { "advanceClock": true, "fakeClearInterval": true, "fakeSetInterval": true, "waitsForPromise": true } } ] } ================================================ FILE: .gitattributes ================================================ # Specs depend on character counts, if we don't specify the line endings the # fixtures will vary depending on platform spec/fixtures/**/*.js text eol=lf spec/fixtures/**/*.coffee text eol=lf spec/fixtures/**/*.less text eol=lf spec/fixtures/**/*.css text eol=lf spec/fixtures/**/*.txt text eol=lf spec/fixtures/dir/**/* text eol=lf # Git 1.7 does not support **/* patterns spec/fixtures/css.css text eol=lf spec/fixtures/sample.js text eol=lf spec/fixtures/sample.less text eol=lf spec/fixtures/sample.txt text eol=lf # Windows bash scripts are also Unix LF endings *.sh eol=lf # The script executables should be LF so they can be edited on Windows script/bootstrap text eol=lf script/build text eol=lf script/cibuild text eol=lf script/clean text eol=lf script/lint text eol=lf script/postprocess-junit-results text eol=lf script/test text eol=lf script/verify-snapshot-script text eol=lf ================================================ FILE: .github/lock.yml ================================================ # Configuration for lock-threads - https://github.com/dessant/lock-threads # Number of days of inactivity before a closed issue or pull request is locked daysUntilLock: 180 # Comment to post before locking. Set to `false` to disable lockComment: > This issue has been automatically locked since there has not been any recent activity after it was closed. If you can still reproduce this issue in [Safe Mode](https://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode) then please open a new issue and fill out [the entire issue template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/bug_report.md) to ensure that we have enough information to address your issue. Thanks! # Issues or pull requests with these labels will not be locked exemptLabels: - help-wanted # Limit to only `issues` or `pulls` only: issues ================================================ FILE: .github/move.yml ================================================ ================================================ FILE: .github/no-response.yml ================================================ # Configuration for probot-no-response - https://github.com/probot/no-response # Number of days of inactivity before an issue is closed for lack of response daysUntilClose: 28 # Label requiring a response responseRequiredLabel: more-information-needed # Comment to post when closing an issue for lack of response. Set to `false` to disable. closeComment: > This issue has been automatically closed because there has been no response to our request for more information from the original author. With only the information that is currently in the issue, we don't have enough information to take action. Please reach out if you have or find the answers we need so that we can investigate further. ================================================ FILE: .github/stale.yml ================================================ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 365 # Number of days of inactivity before a stale Issue or Pull Request is closed daysUntilClose: 14 # Issues or Pull Requests with these labels will never be considered stale exemptLabels: - regression - security - triaged # Label to use when marking as stale staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: > Thanks for your contribution! This issue has been automatically marked as stale because it has not had recent activity. Because the Atom team treats their issues [as their backlog](https://en.wikipedia.org/wiki/Scrum_(software_development)#Product_backlog), stale issues are closed. If you would like this issue to remain open: 1. Verify that you can still reproduce the issue in the latest version of Atom 1. Comment that the issue is still reproducible and include: * What version of Atom you reproduced the issue on * What OS and version you reproduced the issue on * What steps you followed to reproduce the issue 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 closeComment: false # Limit to only `issues` or `pulls` only: issues ================================================ FILE: .gitignore ================================================ *.swp *~ .DS_Store .eslintcache Thumbs.db .project .svn .nvm-version .vscode .python-version node_modules *.log /tags /atom-shell/ /out/ docs/output docs/includes spec/fixtures/evil-files/ !spec/fixtures/packages/package-with-incompatible-native-module-loaded-conditionally/node_modules/ out/ /electron/ ================================================ FILE: .prettierrc ================================================ { "singleQuote": true } ================================================ FILE: CHANGELOG.md ================================================ See https://atom.io/releases ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [atom@github.com](mailto:atom@github.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Atom :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: The following is a set of guidelines for contributing to Atom and its packages, which are hosted in the [Atom Organization](https://github.com/atom) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. #### Table Of Contents [Code of Conduct](#code-of-conduct) [I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question) [What should I know before I get started?](#what-should-i-know-before-i-get-started) * [Atom and Packages](#atom-and-packages) * [Atom Design Decisions](#design-decisions) [How Can I Contribute?](#how-can-i-contribute) * [Reporting Bugs](#reporting-bugs) * [Suggesting Enhancements](#suggesting-enhancements) * [Your First Code Contribution](#your-first-code-contribution) * [Pull Requests](#pull-requests) [Styleguides](#styleguides) * [Git Commit Messages](#git-commit-messages) * [JavaScript Styleguide](#javascript-styleguide) * [CoffeeScript Styleguide](#coffeescript-styleguide) * [Specs Styleguide](#specs-styleguide) * [Documentation Styleguide](#documentation-styleguide) [Additional Notes](#additional-notes) * [Issue and Pull Request Labels](#issue-and-pull-request-labels) ## Code of Conduct This project and everyone participating in it is governed by the [Atom Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [atom@github.com](mailto:atom@github.com). ## I don't want to read this whole thing I just have a question!!! > **Note:** [Please don't file an issue to ask a question.](https://blog.atom.io/2016/04/19/managing-the-deluge-of-atom-issues.html) You'll get faster results by using the resources below. We have an official message board with a detailed FAQ and where the community chimes in with helpful advice if you have questions. * [Github Discussions, the official Atom](https://github.com/atom/atom/discussions) * [Atom FAQ](https://github.com/atom/atom/discussions) ## What should I know before I get started? ### Atom and Packages Atom is a large open source project — it's made up of over [200 repositories](https://github.com/atom). When you initially consider contributing to Atom, you might be unsure about which of those 200 repositories implements the functionality you want to change or report a bug for. This section should help you with that. Atom is intentionally very modular. Nearly every non-editor UI element you interact with comes from a package, even fundamental things like tabs and the status-bar. These packages are packages in the same way that packages in the [Atom package repository](https://atom.io/packages) are packages, with one difference: they are bundled into the [default distribution](https://github.com/atom/atom/blob/10b8de6fc499a7def9b072739486e68530d67ab4/package.json#L58). ![atom-packages](https://cloud.githubusercontent.com/assets/69169/10472281/84fc9792-71d3-11e5-9fd1-19da717df079.png) To get a sense for the packages that are bundled with Atom, you can go to `Settings` > `Packages` within Atom and take a look at the Core Packages section. Here's a list of the big ones: * [atom/atom](https://github.com/atom/atom) - Atom Core! The core editor component is responsible for basic text editing (e.g. cursors, selections, scrolling), text indentation, wrapping, and folding, text rendering, editor rendering, file system operations (e.g. saving), and installation and auto-updating. You should also use this repository for feedback related to the [Atom API](https://atom.io/docs/api/latest) and for large, overarching design proposals. * [tree-view](https://github.com/atom/tree-view) - file and directory listing on the left of the UI. * [fuzzy-finder](https://github.com/atom/fuzzy-finder) - the quick file opener. * [find-and-replace](https://github.com/atom/find-and-replace) - all search and replace functionality. * [tabs](https://github.com/atom/tabs) - the tabs for open editors at the top of the UI. * [status-bar](https://github.com/atom/status-bar) - the status bar at the bottom of the UI. * [markdown-preview](https://github.com/atom/markdown-preview) - the rendered markdown pane item. * [settings-view](https://github.com/atom/settings-view) - the settings UI pane item. * [autocomplete-plus](https://github.com/atom/autocomplete-plus) - autocompletions shown while typing. Some languages have additional packages for autocompletion functionality, such as [autocomplete-html](https://github.com/atom/autocomplete-html). * [git-diff](https://github.com/atom/git-diff) - Git change indicators shown in the editor's gutter. * [language-javascript](https://github.com/atom/language-javascript) - all bundled languages are packages too, and each one has a separate package `language-[name]`. Use these for feedback on syntax highlighting issues that only appear for a specific language. * [one-dark-ui](https://github.com/atom/one-dark-ui) - the default UI styling for anything but the text editor. UI theme packages (i.e. packages with a `-ui` suffix) provide only styling and it's possible that a bundled package is responsible for a UI issue. There are other bundled UI themes, such as [one-light-ui](https://github.com/atom/one-light-ui). * [one-dark-syntax](https://github.com/atom/one-dark-syntax) - the default syntax highlighting styles applied for all languages. There are other bundled syntax themes, such as [solarized-dark-syntax](https://github.com/atom/solarized-dark-syntax). You should use these packages for reporting issues that appear in many languages, but disappear if you change to another syntax theme. * [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 for 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][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. #### Package Conventions There are a few conventions that have developed over time around packages: * Packages that add one or more syntax highlighting grammars are named `language-[language-name]` * Language packages can add other things besides just a grammar. Many offer commonly-used snippets. Try not to add too much though. * Theme packages are split into two categories: UI and Syntax themes * UI themes are named `[theme-name]-ui` * Syntax themes are named `[theme-name]-syntax` * Often themes that are designed to work together are given the same root name, for example: `one-dark-ui` and `one-dark-syntax` * UI themes style everything outside of the editor pane — all of the green areas in the [packages image above](#atom-packages-image) * Syntax themes style just the items inside the editor pane, mostly syntax highlighting * Packages that add [autocomplete providers](https://github.com/atom/autocomplete-plus/wiki/Autocomplete-Providers) are named `autocomplete-[what-they-autocomplete]` — ex: [autocomplete-css](https://github.com/atom/autocomplete-css) ### Design Decisions When we make a significant decision in how we maintain the project and what we can or cannot support, we will document it in the [atom/design-decisions repository](https://github.com/atom/design-decisions). If you have a question around how we do things, check to see if it is documented there. If it is *not* documented there, please open a new topic on [Github Discussions, the official Atom message board](https://github.com/atom/atom/discussions) and ask your question. ## How Can I Contribute? ### Reporting Bugs This section guides you through submitting a bug report for Atom. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:. Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/bug_report.md), the information it asks for helps us resolve issues faster. > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. #### Before Submitting A Bug Report * **Check the [debugging guide](https://flight-manual.atom.io/hacking-atom/sections/debugging/).** You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem [in the latest version of Atom](https://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version), if the problem happens when you run Atom in [safe mode](https://flight-manual.atom.io/hacking-atom/sections/debugging/#check-if-the-problem-shows-up-in-safe-mode), and if you can get the desired behavior by changing [Atom's or packages' config settings](https://flight-manual.atom.io/hacking-atom/sections/debugging/#check-atom-and-package-settings). * **Check the [discussions](https://github.com/atom/atom/discussions)** for a list of common questions and problems. * **Determine [which repository the problem should be reported in](#atom-and-packages)**. * **Perform a [cursory search](https://github.com/search?q=+is%3Aissue+user%3Aatom)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one. #### How Do I Submit A (Good) Bug Report? Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your bug is related to, create an issue on that repository and provide the following information by filling in [the template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). Explain the problem and include additional details to help maintainers reproduce the problem: * **Use a clear and descriptive title** for the issue to identify the problem. * **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started Atom, e.g. which command exactly you used in the terminal, or how you started Atom otherwise. When listing steps, **don't just say what you did, but explain how you did it**. For example, if you moved the cursor to the end of a line, explain if you used the mouse, or a keyboard shortcut or an Atom command, and if so which one? * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. * **Explain which behavior you expected to see instead and why.** * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. If you use the keyboard while following the steps, **record the GIF with the [Keybinding Resolver](https://github.com/atom/keybinding-resolver) shown**. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. * **If you're reporting that Atom crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. * **If the problem is related to performance or memory**, include a [CPU profile capture](https://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-runtime-performance) with your report. * **If Chrome's developer tools pane is shown without you triggering it**, that normally means that you have a syntax error in one of your themes or in your `styles.less`. Try running in [Safe Mode](https://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode) and using a different theme or comment out the contents of your `styles.less` to see if that fixes the problem. * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. Provide more context by answering these questions: * **Can you reproduce the problem in [safe mode](https://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-runtime-performance-problems-with-the-dev-tools-cpu-profiler)?** * **Did the problem start happening recently** (e.g. after updating to a new version of Atom) or was this always a problem? * If the problem started happening recently, **can you reproduce the problem in an older version of Atom?** What's the most recent version in which the problem doesn't happen? You can download older versions of Atom from [the releases page](https://github.com/atom/atom/releases). * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. * If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using? Include details about your configuration and environment: * **Which version of Atom are you using?** You can get the exact version by running `atom -v` in your terminal, or by starting Atom and running the `Application: About` command from the [Command Palette](https://github.com/atom/command-palette). * **What's the name and version of the OS you're using**? * **Are you running Atom in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest? * **Which [packages](#atom-and-packages) do you have installed?** You can get that list by running `apm list --installed`. * **Are you using [local configuration files](https://flight-manual.atom.io/using-atom/sections/basic-customization/)** `config.cson`, `keymap.cson`, `snippets.cson`, `styles.less` and `init.coffee` to customize Atom? If so, provide the contents of those files, preferably in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines) or with a link to a [gist](https://gist.github.com/). * **Are you using Atom with multiple monitors?** If so, can you reproduce the problem when you use a single monitor? * **Which keyboard layout are you using?** Are you using a US layout or some other layout? ### Suggesting Enhancements This section guides you through submitting an enhancement suggestion for Atom, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](https://github.com/atom/.github/blob/master/.github/ISSUE_TEMPLATE/feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed. #### Before Submitting An Enhancement Suggestion * **Check the [debugging guide](https://flight-manual.atom.io/hacking-atom/sections/debugging/)** for tips — you might discover that the enhancement is already available. Most importantly, check if you're using [the latest version of Atom](https://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version) and if you can get the desired behavior by changing [Atom's or packages' config settings](https://flight-manual.atom.io/hacking-atom/sections/debugging/#check-atom-and-package-settings). * **Check if there's already [a package](https://atom.io/packages) which provides that enhancement.** * **Determine [which repository the enhancement should be suggested in](#atom-and-packages).** * **Perform a [cursory search](https://github.com/search?q=+is%3Aissue+user%3Aatom)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. #### How Do I Submit A (Good) Enhancement Suggestion? Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#atom-and-packages) your enhancement suggestion is related to, create an issue on that repository and provide the following information: * **Use a clear and descriptive title** for the issue to identify the suggestion. * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. * **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). * **Describe the current behavior** and **explain which behavior you expected to see instead** and why. * **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Atom which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. * **Explain why this enhancement would be useful** to most Atom users and isn't something that can or should be implemented as a [community package](#atom-and-packages). * **List some other text editors or applications where this enhancement exists.** * **Specify which version of Atom you're using.** You can get the exact version by running `atom -v` in your terminal, or by starting Atom and running the `Application: About` command from the [Command Palette](https://github.com/atom/command-palette). * **Specify the name and version of the OS you're using.** ### Your First Code Contribution Unsure where to begin contributing to Atom? You can start by looking through these `beginner` and `help-wanted` issues: * [Beginner issues][beginner] - issues which should only require a few lines of code, and a test or two. * [Help wanted issues][help-wanted] - issues which should be a bit more involved than `beginner` issues. Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have. If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](https://flight-manual.atom.io) is free and available online. You can find the source to the manual in [atom/flight-manual.atom.io](https://github.com/atom/flight-manual.atom.io). #### Local development Atom Core and all packages can be developed locally. For instructions on how to do this, see the following sections in the [Atom Flight Manual](https://flight-manual.atom.io): * [Hacking on Atom Core][hacking-on-atom-core] * [Contributing to Official Atom Packages][contributing-to-official-atom-packages] ### Pull Requests The process described here has several goals: - Maintain Atom's quality - Fix problems that are important to users - Engage the community in working toward the best possible Atom - Enable a sustainable system for Atom's maintainers to review contributions Please follow these steps to have your contribution considered by the maintainers: 1. Follow all instructions in [the template](PULL_REQUEST_TEMPLATE.md) 2. Follow the [styleguides](#styleguides) 3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing
What if the status checks are failing?If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. ## Styleguides ### Git Commit Messages * Use the present tense ("Add feature" not "Added feature") * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") * Limit the first line to 72 characters or less * Reference issues and pull requests liberally after the first line * When only changing documentation, include `[ci skip]` in the commit title * Consider starting the commit message with an applicable emoji: * :art: `:art:` when improving the format/structure of the code * :racehorse: `:racehorse:` when improving performance * :non-potable_water: `:non-potable_water:` when plugging memory leaks * :memo: `:memo:` when writing docs * :penguin: `:penguin:` when fixing something on Linux * :apple: `:apple:` when fixing something on macOS * :checkered_flag: `:checkered_flag:` when fixing something on Windows * :bug: `:bug:` when fixing a bug * :fire: `:fire:` when removing code or files * :green_heart: `:green_heart:` when fixing the CI build * :white_check_mark: `:white_check_mark:` when adding tests * :lock: `:lock:` when dealing with security * :arrow_up: `:arrow_up:` when upgrading dependencies * :arrow_down: `:arrow_down:` when downgrading dependencies * :shirt: `:shirt:` when removing linter warnings ### JavaScript Styleguide All JavaScript code is linted with [Prettier](https://prettier.io/). * Prefer the object spread operator (`{...anotherObj}`) to `Object.assign()` * Inline `export`s with expressions whenever possible ```js // Use this: export default class ClassName { } // Instead of: class ClassName { } export default ClassName ``` * Place requires in the following order: * Built in Node Modules (such as `path`) * Built in Atom and Electron Modules (such as `atom`, `remote`) * Local Modules (using relative paths) * Place class properties in the following order: * Class methods and properties (methods starting with `static`) * Instance methods and properties * [Avoid platform-dependent code](https://flight-manual.atom.io/hacking-atom/sections/cross-platform-compatibility/) ### CoffeeScript Styleguide * Set parameter defaults without spaces around the equal sign * `clear = (count=1) ->` instead of `clear = (count = 1) ->` * Use spaces around operators * `count + 1` instead of `count+1` * Use spaces after commas (unless separated by newlines) * Use parentheses if it improves code clarity. * Prefer alphabetic keywords to symbolic keywords: * `a is b` instead of `a == b` * Avoid spaces inside the curly-braces of hash literals: * `{a: 1, b: 2}` instead of `{ a: 1, b: 2 }` * Include a single line of whitespace between methods. * Capitalize initialisms and acronyms in names, except for the first word, which should be lower-case: * `getURI` instead of `getUri` * `uriToOpen` instead of `URIToOpen` * Use `slice()` to copy an array * Add an explicit `return` when your function ends with a `for`/`while` loop and you don't want it to return a collected array. * Use `this` instead of a standalone `@` * `return this` instead of `return @` * Place requires in the following order: * Built in Node Modules (such as `path`) * Built in Atom and Electron Modules (such as `atom`, `remote`) * Local Modules (using relative paths) * Place class properties in the following order: * Class methods and properties (methods starting with a `@`) * Instance methods and properties * [Avoid platform-dependent code](https://flight-manual.atom.io/hacking-atom/sections/cross-platform-compatibility/) ### Specs Styleguide - Include thoughtfully-worded, well-structured [Jasmine](https://jasmine.github.io/) specs in the `./spec` folder. - Treat `describe` as a noun or situation. - Treat `it` as a statement about state or how an operation changes state. #### Example ```coffee describe 'a dog', -> it 'barks', -> # spec here describe 'when the dog is happy', -> it 'wags its tail', -> # spec here ``` ### Documentation Styleguide * Use [AtomDoc](https://github.com/atom/atomdoc). * Use [Markdown](https://daringfireball.net/projects/markdown). * Reference methods and classes in markdown with the custom `{}` notation: * Reference classes with `{ClassName}` * Reference instance methods with `{ClassName::methodName}` * Reference class methods with `{ClassName.methodName}` #### Example ```coffee # Public: Disable the package with the given name. # # * `name` The {String} name of the package to disable. # * `options` (optional) The {Object} with disable options (default: {}): # * `trackTime` A {Boolean}, `true` to track the amount of time taken. # * `ignoreErrors` A {Boolean}, `true` to catch and ignore errors thrown. # * `callback` The {Function} to call after the package has been disabled. # # Returns `undefined`. disablePackage: (name, options, callback) -> ``` ## Additional Notes ### Issue and Pull Request Labels This section lists the labels we use to help us track and manage issues and pull requests. Most labels are used across all Atom repositories, but some are specific to `atom/atom`. [GitHub search](https://help.github.com/articles/searching-issues/) makes it easy to use labels for finding groups of issues or pull requests you're interested in. For example, you might be interested in [open issues across `atom/atom` and all Atom-owned packages which are labeled as bugs, but still need to be reliably reproduced](https://github.com/search?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abug+label%3Aneeds-reproduction) or perhaps [open pull requests in `atom/atom` which haven't been reviewed yet](https://github.com/search?utf8=%E2%9C%93&q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+comments%3A0). To help you find issues and pull requests, each label is listed with search links for finding open items with that label in `atom/atom` only and also across all Atom repositories. We encourage you to read about [other search filters](https://help.github.com/articles/searching-issues/) which will help you write more focused queries. The labels are loosely grouped by their purpose, but it's not required that every issue has a label from every group or that an issue can't have more than one label from the same group. Please open an issue on `atom/atom` if you have suggestions for new labels, and if you notice some labels are missing on some repositories, then please open an issue on that repository. #### Type of Issue and Issue State | Label name | `atom/atom` :mag_right: | `atom`‑org :mag_right: | Description | | --- | --- | --- | --- | | `enhancement` | [search][search-atom-repo-label-enhancement] | [search][search-atom-org-label-enhancement] | Feature requests. | | `bug` | [search][search-atom-repo-label-bug] | [search][search-atom-org-label-bug] | Confirmed bugs or reports that are very likely to be bugs. | | `question` | [search][search-atom-repo-label-question] | [search][search-atom-org-label-question] | Questions more than bug reports or feature requests (e.g. how do I do X). | | `feedback` | [search][search-atom-repo-label-feedback] | [search][search-atom-org-label-feedback] | General feedback more than bug reports or feature requests. | | `help-wanted` | [search][search-atom-repo-label-help-wanted] | [search][search-atom-org-label-help-wanted] | The Atom core team would appreciate help from the community in resolving these issues. | | `beginner` | [search][search-atom-repo-label-beginner] | [search][search-atom-org-label-beginner] | Less complex issues which would be good first issues to work on for users who want to contribute to Atom. | | `more-information-needed` | [search][search-atom-repo-label-more-information-needed] | [search][search-atom-org-label-more-information-needed] | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). | | `needs-reproduction` | [search][search-atom-repo-label-needs-reproduction] | [search][search-atom-org-label-needs-reproduction] | Likely bugs, but haven't been reliably reproduced. | | `blocked` | [search][search-atom-repo-label-blocked] | [search][search-atom-org-label-blocked] | Issues blocked on other issues. | | `duplicate` | [search][search-atom-repo-label-duplicate] | [search][search-atom-org-label-duplicate] | Issues which are duplicates of other issues, i.e. they have been reported before. | | `wontfix` | [search][search-atom-repo-label-wontfix] | [search][search-atom-org-label-wontfix] | The Atom core team has decided not to fix these issues for now, either because they're working as intended or for some other reason. | | `invalid` | [search][search-atom-repo-label-invalid] | [search][search-atom-org-label-invalid] | Issues which aren't valid (e.g. user errors). | | `package-idea` | [search][search-atom-repo-label-package-idea] | [search][search-atom-org-label-package-idea] | Feature request which might be good candidates for new packages, instead of extending Atom or core Atom packages. | | `wrong-repo` | [search][search-atom-repo-label-wrong-repo] | [search][search-atom-org-label-wrong-repo] | Issues reported on the wrong repository (e.g. a bug related to the [Settings View package](https://github.com/atom/settings-view) was reported on [Atom core](https://github.com/atom/atom)). | #### Topic Categories | Label name | `atom/atom` :mag_right: | `atom`‑org :mag_right: | Description | | --- | --- | --- | --- | | `windows` | [search][search-atom-repo-label-windows] | [search][search-atom-org-label-windows] | Related to Atom running on Windows. | | `linux` | [search][search-atom-repo-label-linux] | [search][search-atom-org-label-linux] | Related to Atom running on Linux. | | `mac` | [search][search-atom-repo-label-mac] | [search][search-atom-org-label-mac] | Related to Atom running on macOS. | | `documentation` | [search][search-atom-repo-label-documentation] | [search][search-atom-org-label-documentation] | Related to any type of documentation (e.g. [API documentation](https://atom.io/docs/api/latest/) and the [flight manual](https://flight-manual.atom.io/)). | | `performance` | [search][search-atom-repo-label-performance] | [search][search-atom-org-label-performance] | Related to performance. | | `security` | [search][search-atom-repo-label-security] | [search][search-atom-org-label-security] | Related to security. | | `ui` | [search][search-atom-repo-label-ui] | [search][search-atom-org-label-ui] | Related to visual design. | | `api` | [search][search-atom-repo-label-api] | [search][search-atom-org-label-api] | Related to Atom's public APIs. | | `uncaught-exception` | [search][search-atom-repo-label-uncaught-exception] | [search][search-atom-org-label-uncaught-exception] | Issues about uncaught exceptions, normally created from the [Notifications package](https://github.com/atom/notifications). | | `crash` | [search][search-atom-repo-label-crash] | [search][search-atom-org-label-crash] | Reports of Atom completely crashing. | | `auto-indent` | [search][search-atom-repo-label-auto-indent] | [search][search-atom-org-label-auto-indent] | Related to auto-indenting text. | | `encoding` | [search][search-atom-repo-label-encoding] | [search][search-atom-org-label-encoding] | Related to character encoding. | | `network` | [search][search-atom-repo-label-network] | [search][search-atom-org-label-network] | Related to network problems or working with remote files (e.g. on network drives). | | `git` | [search][search-atom-repo-label-git] | [search][search-atom-org-label-git] | Related to Git functionality (e.g. problems with gitignore files or with showing the correct file status). | #### `atom/atom` Topic Categories | Label name | `atom/atom` :mag_right: | `atom`‑org :mag_right: | Description | | --- | --- | --- | --- | | `editor-rendering` | [search][search-atom-repo-label-editor-rendering] | [search][search-atom-org-label-editor-rendering] | Related to language-independent aspects of rendering text (e.g. scrolling, soft wrap, and font rendering). | | `build-error` | [search][search-atom-repo-label-build-error] | [search][search-atom-org-label-build-error] | Related to problems with building Atom from source. | | `error-from-pathwatcher` | [search][search-atom-repo-label-error-from-pathwatcher] | [search][search-atom-org-label-error-from-pathwatcher] | Related to errors thrown by the [pathwatcher library](https://github.com/atom/node-pathwatcher). | | `error-from-save` | [search][search-atom-repo-label-error-from-save] | [search][search-atom-org-label-error-from-save] | Related to errors thrown when saving files. | | `error-from-open` | [search][search-atom-repo-label-error-from-open] | [search][search-atom-org-label-error-from-open] | Related to errors thrown when opening files. | | `installer` | [search][search-atom-repo-label-installer] | [search][search-atom-org-label-installer] | Related to the Atom installers for different OSes. | | `auto-updater` | [search][search-atom-repo-label-auto-updater] | [search][search-atom-org-label-auto-updater] | Related to the auto-updater for different OSes. | | `deprecation-help` | [search][search-atom-repo-label-deprecation-help] | [search][search-atom-org-label-deprecation-help] | Issues for helping package authors remove usage of deprecated APIs in packages. | | `electron` | [search][search-atom-repo-label-electron] | [search][search-atom-org-label-electron] | Issues that require changes to [Electron](https://electron.atom.io) to fix or implement. | #### Pull Request Labels | Label name | `atom/atom` :mag_right: | `atom`‑org :mag_right: | Description | --- | --- | --- | --- | | `work-in-progress` | [search][search-atom-repo-label-work-in-progress] | [search][search-atom-org-label-work-in-progress] | Pull requests which are still being worked on, more changes will follow. | | `needs-review` | [search][search-atom-repo-label-needs-review] | [search][search-atom-org-label-needs-review] | Pull requests which need code review, and approval from maintainers or Atom core team. | | `under-review` | [search][search-atom-repo-label-under-review] | [search][search-atom-org-label-under-review] | Pull requests being reviewed by maintainers or Atom core team. | | `requires-changes` | [search][search-atom-repo-label-requires-changes] | [search][search-atom-org-label-requires-changes] | Pull requests which need to be updated based on review comments and then reviewed again. | | `needs-testing` | [search][search-atom-repo-label-needs-testing] | [search][search-atom-org-label-needs-testing] | Pull requests which need manual testing. | [search-atom-repo-label-enhancement]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aenhancement [search-atom-org-label-enhancement]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aenhancement [search-atom-repo-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abug [search-atom-org-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abug [search-atom-repo-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aquestion [search-atom-org-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aquestion [search-atom-repo-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Afeedback [search-atom-org-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Afeedback [search-atom-repo-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ahelp-wanted [search-atom-org-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ahelp-wanted [search-atom-repo-label-beginner]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abeginner [search-atom-org-label-beginner]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abeginner [search-atom-repo-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Amore-information-needed [search-atom-org-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Amore-information-needed [search-atom-repo-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aneeds-reproduction [search-atom-org-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aneeds-reproduction [search-atom-repo-label-triage-help-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Atriage-help-needed [search-atom-org-label-triage-help-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Atriage-help-needed [search-atom-repo-label-windows]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awindows [search-atom-org-label-windows]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awindows [search-atom-repo-label-linux]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Alinux [search-atom-org-label-linux]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Alinux [search-atom-repo-label-mac]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Amac [search-atom-org-label-mac]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Amac [search-atom-repo-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Adocumentation [search-atom-org-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Adocumentation [search-atom-repo-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aperformance [search-atom-org-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aperformance [search-atom-repo-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Asecurity [search-atom-org-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Asecurity [search-atom-repo-label-ui]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aui [search-atom-org-label-ui]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aui [search-atom-repo-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aapi [search-atom-org-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aapi [search-atom-repo-label-crash]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Acrash [search-atom-org-label-crash]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Acrash [search-atom-repo-label-auto-indent]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aauto-indent [search-atom-org-label-auto-indent]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aauto-indent [search-atom-repo-label-encoding]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aencoding [search-atom-org-label-encoding]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aencoding [search-atom-repo-label-network]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Anetwork [search-atom-org-label-network]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Anetwork [search-atom-repo-label-uncaught-exception]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Auncaught-exception [search-atom-org-label-uncaught-exception]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Auncaught-exception [search-atom-repo-label-git]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Agit [search-atom-org-label-git]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Agit [search-atom-repo-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ablocked [search-atom-org-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ablocked [search-atom-repo-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aduplicate [search-atom-org-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aduplicate [search-atom-repo-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awontfix [search-atom-org-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awontfix [search-atom-repo-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ainvalid [search-atom-org-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ainvalid [search-atom-repo-label-package-idea]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Apackage-idea [search-atom-org-label-package-idea]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Apackage-idea [search-atom-repo-label-wrong-repo]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awrong-repo [search-atom-org-label-wrong-repo]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awrong-repo [search-atom-repo-label-editor-rendering]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aeditor-rendering [search-atom-org-label-editor-rendering]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aeditor-rendering [search-atom-repo-label-build-error]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abuild-error [search-atom-org-label-build-error]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abuild-error [search-atom-repo-label-error-from-pathwatcher]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-pathwatcher [search-atom-org-label-error-from-pathwatcher]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-pathwatcher [search-atom-repo-label-error-from-save]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-save [search-atom-org-label-error-from-save]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-save [search-atom-repo-label-error-from-open]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-open [search-atom-org-label-error-from-open]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-open [search-atom-repo-label-installer]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ainstaller [search-atom-org-label-installer]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ainstaller [search-atom-repo-label-auto-updater]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aauto-updater [search-atom-org-label-auto-updater]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aauto-updater [search-atom-repo-label-deprecation-help]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Adeprecation-help [search-atom-org-label-deprecation-help]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Adeprecation-help [search-atom-repo-label-electron]: https://github.com/search?q=is%3Aissue+repo%3Aatom%2Fatom+is%3Aopen+label%3Aelectron [search-atom-org-label-electron]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aelectron [search-atom-repo-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Awork-in-progress [search-atom-org-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Awork-in-progress [search-atom-repo-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aneeds-review [search-atom-org-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aneeds-review [search-atom-repo-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aunder-review [search-atom-org-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aunder-review [search-atom-repo-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Arequires-changes [search-atom-org-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Arequires-changes [search-atom-repo-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aneeds-testing [search-atom-org-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aneeds-testing [beginner]:https://github.com/search?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc [help-wanted]:https://github.com/search?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc+-label%3Abeginner [contributing-to-official-atom-packages]:https://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/ [hacking-on-atom-core]: https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/ ================================================ FILE: Dockerfile ================================================ # VERSION: 0.2 # DESCRIPTION: Image to build Atom FROM ubuntu:20.04 # Install dependencies RUN apt-get update && \ DEBIAN_FRONTEND="noninteractive" \ apt-get install -y \ build-essential \ git \ libsecret-1-dev \ fakeroot \ rpm \ libx11-dev \ libxkbfile-dev \ libgdk-pixbuf2.0-dev \ libgtk-3-dev \ libxss-dev \ libasound2-dev \ npm && \ rm -rf /var/lib/apt/lists/* # Update npm and dependencies RUN npm install -g npm --loglevel error # Use python2 by default RUN npm config set python /usr/bin/python2 -g ENTRYPOINT ["/usr/bin/env", "sh", "-c"] CMD ["bash"] ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2011-2021 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: PULL_REQUEST_TEMPLATE.md ================================================ ⚛👋 Hello there! Welcome. Please follow the steps below to tell us about your contribution. 1. Copy the correct template for your contribution - 🐛 Are you fixing a bug? Copy the template from - 📈 Are you improving performance? Copy the template from - 📝 Are you updating documentation? Copy the template from - 💻 Are you changing functionality? Copy the template from 2. Replace this text with the contents of the template 3. Fill in all sections of the template 4. Click "Create pull request" ================================================ FILE: README.md ================================================ # Atom [![Build status](https://dev.azure.com/github/Atom/_apis/build/status/Atom%20Production%20Branches?branchName=master)](https://dev.azure.com/github/Atom/_build/latest?definitionId=32&branchName=master) Atom is a hackable text editor for the 21st century, built on [Electron](https://github.com/electron/electron), and based on everything we love about our favorite editors. We designed it to be deeply customizable, but still approachable using the default configuration. ![Atom](https://user-images.githubusercontent.com/378023/49132477-f4b77680-f31f-11e8-8357-ac6491761c6c.png) ![Atom Screenshot](https://user-images.githubusercontent.com/378023/49132478-f4b77680-f31f-11e8-9e10-e8454d8d9b7e.png) Visit [atom.io](https://atom.io) to learn more or visit the [Atom forum](https://github.com/atom/atom/discussions). Follow [@AtomEditor](https://twitter.com/atomeditor) on Twitter for important announcements. This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to atom@github.com. ## Documentation If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](https://flight-manual.atom.io) is free and available online. You can find the source to the manual in [atom/flight-manual.atom.io](https://github.com/atom/flight-manual.atom.io). The [API reference](https://atom.io/docs/api) for developing packages is also documented on Atom.io. ## Installing ### Prerequisites - [Git](https://git-scm.com) ### macOS Download the latest [Atom release](https://github.com/atom/atom/releases/latest). Atom will automatically update when a new release is available. ### Windows Download the latest [Atom installer](https://github.com/atom/atom/releases/latest). `AtomSetup.exe` is 32-bit. For 64-bit systems, download `AtomSetup-x64.exe`. Atom will automatically update when a new release is available. You can also download `atom-windows.zip` (32-bit) or `atom-x64-windows.zip` (64-bit) from the [releases page](https://github.com/atom/atom/releases/latest). The `.zip` version will not automatically update. Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the latest version of Atom. ### Linux Atom is only available for 64-bit Linux systems. Configure your distribution's package manager to install and update Atom by following the [Linux installation instructions](https://flight-manual.atom.io/getting-started/sections/installing-atom/#platform-linux) in the Flight Manual. You will also find instructions on how to install Atom's official Linux packages without using a package repository, though you will not get automatic updates after installing Atom this way. #### Archive extraction An archive is available for people who don't want to install `atom` as root. This version enables you to install multiple Atom versions in parallel. It has been built on Ubuntu 64-bit, but should be compatible with other Linux distributions. 1. Install dependencies (on Ubuntu): ```sh sudo apt install git libasound2 libcurl4 libgbm1 libgcrypt20 libgtk-3-0 libnotify4 libnss3 libglib2.0-bin xdg-utils libx11-xcb1 libxcb-dri3-0 libxss1 libxtst6 libxkbfile1 ``` 2. Download `atom-amd64.tar.gz` from the [Atom releases page](https://github.com/atom/atom/releases/latest). 3. Run `tar xf atom-amd64.tar.gz` in the directory where you want to extract the Atom folder. 4. Launch Atom using the installed `atom` command from the newly extracted directory. The Linux version does not currently automatically update so you will need to repeat these steps to upgrade to future releases. ## Building * [Linux](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-linux) * [macOS](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-mac) * [Windows](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-windows) ## Discussion * Discuss Atom on [GitHub Discussions](https://github.com/atom/atom/discussions) ## License [MIT](https://github.com/atom/atom/blob/master/LICENSE.md) When using the Atom or other GitHub logos, be sure to follow the [GitHub logo guidelines](https://github.com/logos). ================================================ FILE: SUPPORT.md ================================================ # Atom Support If you're looking for support for Atom there are a lot of options, check out: * User Documentation — [The Atom Flight Manual](https://flight-manual.atom.io) * Developer Documentation — [Atom API Documentation](https://atom.io/docs/api/latest) * Message Board — [Github Discussions, the official Atom message board](https://github.com/atom/atom/discussions) On Atoms Github Discussions board, there are a bunch of helpful community members that should be willing to point you in the right direction. ================================================ FILE: apm/README.md ================================================ This folder is where [apm](https://github.com/atom/apm) is installed to so that it is bundled with Atom. ================================================ FILE: apm/package.json ================================================ { "name": "atom-bundled-apm", "description": "Atom's bundled apm", "repository": { "type": "git", "url": "https://github.com/atom/atom.git" }, "dependencies": { "atom-package-manager": "2.6.2" } } ================================================ FILE: atom.sh ================================================ #!/bin/bash if [ "$(uname)" == 'Darwin' ]; then OS='Mac' elif [ "$(expr substr $(uname -s) 1 5)" == 'Linux' ]; then OS='Linux' else echo "Your platform ($(uname -a)) is not supported." exit 1 fi case $(basename $0) in atom-beta) CHANNEL=beta ;; atom-nightly) CHANNEL=nightly ;; atom-dev) CHANNEL=dev ;; *) CHANNEL=stable ;; esac # Only set the ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT env var if it hasn't been set. if [ -z "$ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT" ] then export ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT=true fi ATOM_ADD=false ATOM_NEW_WINDOW=false EXIT_CODE_OVERRIDE= while getopts ":anwtfvh-:" opt; do case "$opt" in -) case "${OPTARG}" in add) ATOM_ADD=true ;; new-window) ATOM_NEW_WINDOW=true ;; wait) WAIT=1 ;; help|version) REDIRECT_STDERR=1 EXPECT_OUTPUT=1 ;; foreground|benchmark|benchmark-test|test) EXPECT_OUTPUT=1 ;; enable-electron-logging) export ELECTRON_ENABLE_LOGGING=1 ;; esac ;; a) ATOM_ADD=true ;; n) ATOM_NEW_WINDOW=true ;; w) WAIT=1 ;; h|v) REDIRECT_STDERR=1 EXPECT_OUTPUT=1 ;; f|t) EXPECT_OUTPUT=1 ;; esac done if [ "${ATOM_ADD}" = "true" ] && [ "${ATOM_NEW_WINDOW}" = "true" ]; then EXPECT_OUTPUT=1 EXIT_CODE_OVERRIDE=1 fi if [ $REDIRECT_STDERR ]; then exec 2> /dev/null fi ATOM_HOME="${ATOM_HOME:-$HOME/.atom}" mkdir -p "$ATOM_HOME" if [ $OS == 'Mac' ]; then if [ -L "$0" ]; then SCRIPT="$(readlink "$0")" else SCRIPT="$0" fi ATOM_APP="$(dirname "$(dirname "$(dirname "$(dirname "$SCRIPT")")")")" if [ "$ATOM_APP" == . ]; then unset ATOM_APP else ATOM_PATH="$(dirname "$ATOM_APP")" ATOM_APP_NAME="$(basename "$ATOM_APP")" fi if [ ! -z "${ATOM_APP_NAME}" ]; then # If ATOM_APP_NAME is known, use it as the executable name ATOM_EXECUTABLE_NAME="${ATOM_APP_NAME%.*}" else # Else choose it from the inferred channel name if [ "$CHANNEL" == 'beta' ]; then ATOM_EXECUTABLE_NAME="Atom Beta" elif [ "$CHANNEL" == 'nightly' ]; then ATOM_EXECUTABLE_NAME="Atom Nightly" elif [ "$CHANNEL" == 'dev' ]; then ATOM_EXECUTABLE_NAME="Atom Dev" else ATOM_EXECUTABLE_NAME="Atom" fi fi if [ -z "${ATOM_PATH}" ]; then # If ATOM_PATH isn't set, check /Applications and then ~/Applications for Atom.app if [ -x "/Applications/$ATOM_APP_NAME" ]; then ATOM_PATH="/Applications" elif [ -x "$HOME/Applications/$ATOM_APP_NAME" ]; then ATOM_PATH="$HOME/Applications" else # We haven't found an Atom.app, use spotlight to search for Atom ATOM_PATH="$(mdfind "kMDItemCFBundleIdentifier == 'com.github.atom'" | grep -v ShipIt | head -1 | xargs -0 dirname)" # Exit if Atom can't be found if [ ! -x "$ATOM_PATH/$ATOM_APP_NAME" ]; then echo "Cannot locate ${ATOM_APP_NAME}, it is usually located in /Applications. Set the ATOM_PATH environment variable to the directory containing ${ATOM_APP_NAME}." exit 1 fi fi fi if [ $EXPECT_OUTPUT ]; then "$ATOM_PATH/$ATOM_APP_NAME/Contents/MacOS/$ATOM_EXECUTABLE_NAME" --executed-from="$(pwd)" --pid=$$ "$@" ATOM_EXIT=$? if [ ${ATOM_EXIT} -eq 0 ] && [ -n "${EXIT_CODE_OVERRIDE}" ]; then exit "${EXIT_CODE_OVERRIDE}" else exit ${ATOM_EXIT} fi else open -a "$ATOM_PATH/$ATOM_APP_NAME" -n --args --executed-from="$(pwd)" --pid=$$ --path-environment="$PATH" "$@" fi elif [ $OS == 'Linux' ]; then SCRIPT=$(readlink -f "$0") USR_DIRECTORY=$(readlink -f $(dirname $SCRIPT)/..) case $CHANNEL in beta) ATOM_PATH="$USR_DIRECTORY/share/atom-beta/atom" ;; nightly) ATOM_PATH="$USR_DIRECTORY/share/atom-nightly/atom" ;; dev) ATOM_PATH="$USR_DIRECTORY/share/atom-dev/atom" ;; *) ATOM_PATH="$USR_DIRECTORY/share/atom/atom" ;; esac #Will allow user to get context menu on cinnamon desktop enviroment if [[ "$(expr substr $(printenv | grep "DESKTOP_SESSION=") 17 8)" == "cinnamon" ]]; then cp "resources/linux/desktopenviroment/cinnamon/atom.nemo_action" "/usr/share/nemo/actions/atom.nemo_action" fi : ${TMPDIR:=/tmp} [ -x "$ATOM_PATH" ] || ATOM_PATH="$TMPDIR/atom-build/Atom/atom" if [ $EXPECT_OUTPUT ]; then "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" ATOM_EXIT=$? if [ ${ATOM_EXIT} -eq 0 ] && [ -n "${EXIT_CODE_OVERRIDE}" ]; then exit "${EXIT_CODE_OVERRIDE}" else exit ${ATOM_EXIT} fi else ( nohup "$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@" > "$ATOM_HOME/nohup.out" 2>&1 if [ $? -ne 0 ]; then cat "$ATOM_HOME/nohup.out" exit $? fi ) & fi fi # Exits this process when Atom is used as $EDITOR on_die() { exit 0 } trap 'on_die' SIGQUIT SIGTERM # If the wait flag is set, don't exit this process until Atom kills it. if [ $WAIT ]; then WAIT_FIFO="$ATOM_HOME/.wait_fifo" if [ ! -p "$WAIT_FIFO" ]; then rm -f "$WAIT_FIFO" mkfifo "$WAIT_FIFO" fi # Block endlessly by reading from a named pipe. exec 2>/dev/null read < "$WAIT_FIFO" # If the read completes for some reason, fall back to sleeping in a loop. while true; do sleep 1 done fi ================================================ FILE: benchmarks/benchmark-runner.js ================================================ const Chart = require('chart.js'); const glob = require('glob'); const fs = require('fs-plus'); const path = require('path'); module.exports = async ({ test, benchmarkPaths }) => { document.body.style.backgroundColor = '#ffffff'; document.body.style.overflow = 'auto'; let paths = []; for (const benchmarkPath of benchmarkPaths) { if (fs.isDirectorySync(benchmarkPath)) { paths = paths.concat( glob.sync(path.join(benchmarkPath, '**', '*.bench.js')) ); } else { paths.push(benchmarkPath); } } while (paths.length > 0) { const benchmark = require(paths.shift())({ test }); let results; if (benchmark instanceof Promise) { results = await benchmark; } else { results = benchmark; } const dataByBenchmarkName = {}; for (const { name, duration, x } of results) { dataByBenchmarkName[name] = dataByBenchmarkName[name] || { points: [] }; dataByBenchmarkName[name].points.push({ x, y: duration }); } const benchmarkContainer = document.createElement('div'); document.body.appendChild(benchmarkContainer); for (const key in dataByBenchmarkName) { const data = dataByBenchmarkName[key]; if (data.points.length > 1) { const canvas = document.createElement('canvas'); benchmarkContainer.appendChild(canvas); // eslint-disable-next-line no-new new Chart(canvas, { type: 'line', data: { datasets: [{ label: key, fill: false, data: data.points }] }, options: { showLines: false, scales: { xAxes: [{ type: 'linear', position: 'bottom' }] } } }); const textualOutput = `${key}:\n\n` + data.points.map(p => `${p.x}\t${p.y}`).join('\n'); console.log(textualOutput); } else { const title = document.createElement('h2'); title.textContent = key; benchmarkContainer.appendChild(title); const duration = document.createElement('p'); duration.textContent = `${data.points[0].y}ms`; benchmarkContainer.appendChild(duration); const textualOutput = `${key}: ${data.points[0].y}`; console.log(textualOutput); } await global.atom.reset(); } } return 0; }; ================================================ FILE: benchmarks/text-editor-large-file-construction.bench.js ================================================ const { TextEditor, TextBuffer } = require('atom'); const MIN_SIZE_IN_KB = 0 * 1024; const MAX_SIZE_IN_KB = 10 * 1024; const SIZE_STEP_IN_KB = 1024; const LINE_TEXT = 'Lorem ipsum dolor sit amet\n'; const TEXT = LINE_TEXT.repeat( Math.ceil((MAX_SIZE_IN_KB * 1024) / LINE_TEXT.length) ); module.exports = async ({ test }) => { const data = []; document.body.appendChild(atom.workspace.getElement()); atom.packages.loadPackages(); await atom.packages.activate(); for (let pane of atom.workspace.getPanes()) { pane.destroy(); } for ( let sizeInKB = MIN_SIZE_IN_KB; sizeInKB < MAX_SIZE_IN_KB; sizeInKB += SIZE_STEP_IN_KB ) { const text = TEXT.slice(0, sizeInKB * 1024); console.log(text.length / 1024); let t0 = window.performance.now(); const buffer = new TextBuffer({ text }); const editor = new TextEditor({ buffer, autoHeight: false, largeFileMode: true }); atom.grammars.autoAssignLanguageMode(buffer); atom.workspace.getActivePane().activateItem(editor); let t1 = window.performance.now(); data.push({ name: 'Opening a large file', x: sizeInKB, duration: t1 - t0 }); const tickDurations = []; for (let i = 0; i < 20; i++) { await timeout(50); t0 = window.performance.now(); await timeout(0); t1 = window.performance.now(); tickDurations[i] = t1 - t0; } data.push({ name: 'Max time event loop was blocked after opening a large file', x: sizeInKB, duration: Math.max(...tickDurations) }); t0 = window.performance.now(); editor.setCursorScreenPosition( editor.element.screenPositionForPixelPosition({ top: 100, left: 30 }) ); t1 = window.performance.now(); data.push({ name: 'Clicking the editor after opening a large file', x: sizeInKB, duration: t1 - t0 }); t0 = window.performance.now(); editor.element.setScrollTop(editor.element.getScrollTop() + 100); t1 = window.performance.now(); data.push({ name: 'Scrolling down after opening a large file', x: sizeInKB, duration: t1 - t0 }); editor.destroy(); buffer.destroy(); await timeout(10000); } atom.workspace.getElement().remove(); return data; }; function timeout(duration) { return new Promise(resolve => setTimeout(resolve, duration)); } ================================================ FILE: benchmarks/text-editor-long-lines.bench.js ================================================ const path = require('path'); const fs = require('fs'); const { TextEditor, TextBuffer } = require('atom'); const SIZES_IN_KB = [512, 1024, 2048]; const REPEATED_TEXT = fs .readFileSync( path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'), 'utf8' ) .replace(/\n/g, ''); const TEXT = REPEATED_TEXT.repeat( Math.ceil((SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024) / REPEATED_TEXT.length) ); module.exports = async ({ test }) => { const data = []; const workspaceElement = atom.workspace.getElement(); document.body.appendChild(workspaceElement); atom.packages.loadPackages(); await atom.packages.activate(); console.log(atom.getLoadSettings().resourcePath); for (let pane of atom.workspace.getPanes()) { pane.destroy(); } for (const sizeInKB of SIZES_IN_KB) { const text = TEXT.slice(0, sizeInKB * 1024); console.log(text.length / 1024); let t0 = window.performance.now(); const buffer = new TextBuffer({ text }); const editor = new TextEditor({ buffer, autoHeight: false, largeFileMode: true }); atom.grammars.assignLanguageMode(buffer, 'source.js'); atom.workspace.getActivePane().activateItem(editor); let t1 = window.performance.now(); data.push({ name: 'Opening a large single-line file', x: sizeInKB, duration: t1 - t0 }); const tickDurations = []; for (let i = 0; i < 20; i++) { await timeout(50); t0 = window.performance.now(); await timeout(0); t1 = window.performance.now(); tickDurations[i] = t1 - t0; } data.push({ name: 'Max time event loop was blocked after opening a large single-line file', x: sizeInKB, duration: Math.max(...tickDurations) }); t0 = window.performance.now(); editor.setCursorScreenPosition( editor.element.screenPositionForPixelPosition({ top: 100, left: 30 }) ); t1 = window.performance.now(); data.push({ name: 'Clicking the editor after opening a large single-line file', x: sizeInKB, duration: t1 - t0 }); t0 = window.performance.now(); editor.element.setScrollTop(editor.element.getScrollTop() + 100); t1 = window.performance.now(); data.push({ name: 'Scrolling down after opening a large single-line file', x: sizeInKB, duration: t1 - t0 }); editor.destroy(); buffer.destroy(); await timeout(10000); } workspaceElement.remove(); return data; }; function timeout(duration) { return new Promise(resolve => setTimeout(resolve, duration)); } ================================================ FILE: coffeelint.json ================================================ { "max_line_length": { "level": "ignore" }, "no_empty_param_list": { "level": "error" }, "arrow_spacing": { "level": "error" }, "no_interpolation_in_single_quotes": { "level": "error" }, "no_debugger": { "level": "error" }, "prefer_english_operator": { "level": "error" }, "colon_assignment_spacing": { "spacing": { "left": 0, "right": 1 }, "level": "error" }, "braces_spacing": { "spaces": 0, "level": "error" }, "spacing_after_comma": { "level": "error" }, "no_stand_alone_at": { "level": "error" } } ================================================ FILE: docs/README.md ================================================ # Atom Docs ![Atom](https://cloud.githubusercontent.com/assets/72919/2874231/3af1db48-d3dd-11e3-98dc-6066f8bc766f.png) Most of the Atom user and developer documentation is contained in the [Atom Flight Manual](https://github.com/atom/flight-manual.atom.io). ## Build documentation Instructions for building Atom on various platforms from source. * Moved to [the Flight Manual](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/) * Linux * macOS * Windows ## Other documentation [Native Profiling on macOS](./native-profiling.md) The other documentation that was listed here previously has been moved to [the Flight Manual](https://flight-manual.atom.io). ================================================ FILE: docs/apm-rest-api.md ================================================ # Atom.io package and update API The information that was here has been moved to [a permanent home inside Atom's Flight Manual.](https://flight-manual.atom.io/atom-server-side-apis/) ================================================ FILE: docs/build-instructions/build-status.md ================================================ # Atom build status > **Note**: Since Atom's electron version is outdated, the electron badges are from old versions. | System | Azure Pipelines | CircleCI | AppVeyor/Win | Dependencies | |--------|--------|--------------|------------|--------------| | [Atom](https://github.com/atom/atom) | [![Build status](https://github.visualstudio.com/Atom/_apis/build/status/Atom%20Production%20Branches?branch=master)](https://github.visualstudio.com/Atom/_build/latest?definitionId=32&branch=master) | | | [![Dependency Status](https://david-dm.org/atom/atom.svg)](https://david-dm.org/atom/atom) | | [APM](https://github.com/atom/apm) | [![Build status](https://dev.azure.com/github/Atom/_apis/build/status/Atom%20Production%20Branches?branchName=master)](https://dev.azure.com/github/Atom/_build/latest?definitionId=32&branchName=master) | | | [Electron](https://github.com/electron/electron) | | [![CircleCI Build Status](https://circleci.com/gh/electron/electron/tree/master.svg?style=shield)](https://circleci.com/gh/electron/electron/tree/master) | [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/4lggi9dpjc1qob7k/branch/master?svg=true)](https://ci.appveyor.com/project/electron-bot/electron-ljo26/branch/master) | [![Dependency Status](https://david-dm.org/electron/electron/dev-status.svg)](https://david-dm.org/electron/electron) > **Note**: Some repositories have been merged with Atom. > See for details. > > Here are the packages, libraries, tools, and languages tested along with Atom, and so have no testing badges: > > - [about](https://github.com/atom/atom/tree/master/packages/about) > - [autoflow](https://github.com/atom/atom/tree/master/packages/autoflow) > - [deprecation-cop](https://github.com/atom/atom/tree/master/packages/deprecation-cop) > - [dev-live-reload](https://github.com/atom/atom/tree/master/packages/dev-live-reload) > - [exception-reporting](https://github.com/atom/atom/tree/master/packages/exception-reporting) > - [git-diff](https://github.com/atom/atom/tree/master/packages/git-diff) > - [go-to-line](https://github.com/atom/atom/tree/master/packages/go-to-line) > - [grammar-selector](https://github.com/atom/atom/tree/master/packages/grammar-selector) > - [line-ending-selector](https://github.com/atom/atom/tree/master/packages/line-ending-selector) > - [link](https://github.com/atom/atom/tree/master/packages/link) > - [ruby-on-rails](https://github.com/atom/atom/tree/master/packages/ruby-on-rails) > - [update-package-dependencies](https://github.com/atom/atom/tree/master/packages/update-package-dependencies) > - [welcome](https://github.com/atom/atom/tree/master/packages/welcome) > > The dependency badges might be irrelevant, so take them with a grain of salt (e.g. not very seriously). ## Packages | Package | Github Actions | Dependencies | |---|---|---| | [About](https://github.com/atom/atom/tree/master/packages/about) | | [![Dependency Status](https://david-dm.org/atom/about.svg)](https://david-dm.org/atom/about) | | [Archive View](https://github.com/atom/archive-view) | [![build](https://github.com/atom/archive-view/workflows/CI/badge.svg)](https://github.com/atom/archive-view/actions) | [![Dependency Status](https://david-dm.org/atom/archive-view.svg)](https://david-dm.org/atom/archive-view) | | [AutoComplete Atom API](https://github.com/atom/autocomplete-atom-api) | [![build](https://github.com/atom/autocomplete-atom-api/workflows/CI/badge.svg)](https://github.com/atom/autocomplete-atom-api/actions) | [![Dependency Status](https://david-dm.org/atom/autocomplete-atom-api.svg)](https://david-dm.org/atom/autocomplete-atom-api) | | [AutoComplete CSS](https://github.com/atom/autocomplete-css) | [![build](https://github.com/atom/autocomplete-css/workflows/CI/badge.svg)](https://github.com/atom/autocomplete-css/actions) | [![Dependency Status](https://david-dm.org/atom/autocomplete-css.svg)](https://david-dm.org/atom/autocomplete-css) | | [AutoComplete HTML](https://github.com/atom/autocomplete-html) | [![build](https://github.com/atom/autocomplete-html/workflows/CI/badge.svg)](https://github.com/atom/autocomplete-html/actions) | [![Dependency Status](https://david-dm.org/atom/autocomplete-html.svg)](https://david-dm.org/atom/autocomplete-html) | | [AutoComplete+](https://github.com/atom/autocomplete-plus) | [![build](https://github.com/atom/autocomplete-plus/workflows/CI/badge.svg)](https://github.com/atom/autocomplete-plus/actions) | [![Dependency Status](https://david-dm.org/atom/autocomplete-plus.svg)](https://david-dm.org/atom/autocomplete-plus) | | [AutoComplete Snippets](https://github.com/atom/autocomplete-snippets) | [![build](https://github.com/atom/autocomplete-snippets/workflows/CI/badge.svg)](https://github.com/atom/autocomplete-snippets/actions) | [![Dependency Status](https://david-dm.org/atom/autocomplete-snippets.svg)](https://david-dm.org/atom/autocomplete-snippets) | | [AutoFlow](https://github.com/atom/atom/tree/master/packages/autoflow) | | [![Dependency Status](https://david-dm.org/atom/autoflow.svg)](https://david-dm.org/atom/autoflow) | | [AutoSave](https://github.com/atom/autosave) | [![build](https://github.com/atom/autosave/workflows/CI/badge.svg)](https://github.com/atom/autosave/actions) | [![Dependency Status](https://david-dm.org/atom/autosave.svg)](https://david-dm.org/atom/autosave) | | [Background Tips](https://github.com/atom/background-tips) | [![build](https://github.com/atom/background-tips/workflows/CI/badge.svg)](https://github.com/atom/background-tips/actions) | [![Dependency Status](https://david-dm.org/atom/background-tips.svg)](https://david-dm.org/atom/background-tips) | | [Bookmarks](https://github.com/atom/bookmarks) | [![build](https://github.com/atom/bookmarks/workflows/CI/badge.svg)](https://github.com/atom/bookmarks/actions) | [![Dependency Status](https://david-dm.org/atom/bookmarks.svg)](https://david-dm.org/atom/bookmarks) | | [Bracket Matcher](https://github.com/atom/bracket-matcher) | [![build](https://github.com/atom/bracket-matcher/workflows/CI/badge.svg)](https://github.com/atom/bracket-matcher/actions) | [![Dependency Status](https://david-dm.org/atom/bracket-matcher.svg)](https://david-dm.org/atom/bracket-matcher) | | [Command Palette](https://github.com/atom/command-palette) | [![build](https://github.com/atom/command-palette/workflows/CI/badge.svg)](https://github.com/atom/command-palette/actions) | [![Dependency Status](https://david-dm.org/atom/command-palette.svg)](https://david-dm.org/atom/command-palette) | | [Deprecation Cop](https://github.com/atom/atom/tree/master/packages/deprecation-cop) | | [![Dependency Status](https://david-dm.org/atom/deprecation-cop.svg)](https://david-dm.org/atom/deprecation-cop) | | [Dev Live Reload](https://github.com/atom/atom/tree/master/packages/dev-live-reload) | | [![Dependency Status](https://david-dm.org/atom/dev-live-reload.svg)](https://david-dm.org/atom/dev-live-reload) | | [Encoding Selector](https://github.com/atom/encoding-selector) | [![build](https://github.com/atom/encoding-selector/workflows/CI/badge.svg)](https://github.com/atom/encoding-selector/actions) | [![Dependency Status](https://david-dm.org/atom/encoding-selector.svg)](https://david-dm.org/atom/encoding-selector) | | [Exception Reporting](https://github.com/atom/atom/tree/master/packages/exception-reporting) | | [![Dependency Status](https://david-dm.org/atom/exception-reporting.svg)](https://david-dm.org/atom/exception-reporting) | | [Find and Replace](https://github.com/atom/find-and-replace) | [![build](https://github.com/atom/find-and-replace/workflows/CI/badge.svg)](https://github.com/atom/find-and-replace/actions) | [![Dependency Status](https://david-dm.org/atom/find-and-replace.svg)](https://david-dm.org/atom/find-and-replace) | | [Fuzzy Finder](https://github.com/atom/fuzzy-finder) | [![build](https://github.com/atom/fuzzy-finder/workflows/CI/badge.svg)](https://github.com/atom/fuzzy-finder/actions) | [![Dependency Status](https://david-dm.org/atom/fuzzy-finder.svg)](https://david-dm.org/atom/fuzzy-finder) | | [GitHub](https://github.com/atom/github) | [![Build Status](https://github.com/atom/github/workflows/ci/badge.svg)](https://github.com/atom/github/actions?query=workflow%3Aci+branch%3Amaster) | [![Dependency Status](https://david-dm.org/atom/github.svg)](https://david-dm.org/atom/github) | | [Git Diff](https://github.com/atom/atom/tree/master/packages/) | | [![Dependency Status](https://david-dm.org/atom/git-diff.svg)](https://david-dm.org/atom/git-diff) | | [Go to Line](https://github.com/atom/atom/tree/master/packages/) | | [![Dependency Status](https://david-dm.org/atom/go-to-line.svg)](https://david-dm.org/atom/go-to-line) | | [Grammar Selector](https://github.com/atom/atom/tree/master/packages/grammar-selector) | | [![Dependency Status](https://david-dm.org/atom/grammar-selector.svg)](https://david-dm.org/atom/grammar-selector) | | [Image View](https://github.com/atom/image-view) | [![build](https://github.com/atom/image-view/workflows/CI/badge.svg)](https://github.com/atom/image-view/actions) | [![Dependency Status](https://david-dm.org/atom/image-view.svg)](https://david-dm.org/atom/image-view) | | [Incompatible Packages](https://github.com/atom/incompatible-packages) | | [![Dependency Status](https://david-dm.org/atom/incompatible-packages.svg)](https://david-dm.org/atom/incompatible-packages) | | [Keybinding Resolver](https://github.com/atom/keybinding-resolver) | [![build](https://github.com/atom/keybinding-resolver/workflows/CI/badge.svg)](https://github.com/atom/keybinding-resolver/actions) | [![Dependency Status](https://david-dm.org/atom/keybinding-resolver.svg)](https://david-dm.org/atom/keybinding-resolver) | | [Line Ending Selector](https://github.com/atom/atom/tree/master/packages/line-ending-selector) | | [![Dependency Status](https://david-dm.org/atom/line-ending-selector.svg)](https://david-dm.org/atom/line-ending-selector) | | [Link](https://github.com/atom/atom/tree/master/packages/link) | | [![Dependency Status](https://david-dm.org/atom/link.svg)](https://david-dm.org/atom/link) | | [Markdown Preview](https://github.com/atom/markdown-preview) | [![build](https://github.com/atom/markdown-preview/workflows/CI/badge.svg)](https://github.com/atom/markdown-preview/actions) | [![Dependency Status](https://david-dm.org/atom/markdown-preview.svg)](https://david-dm.org/atom/markdown-preview) | | [Metrics](https://github.com/atom/metrics) | [![build](https://github.com/atom/metrics/workflows/CI/badge.svg)](https://github.com/atom/metrics/actions) | [![Dependency Status](https://david-dm.org/atom/metrics.svg)](https://david-dm.org/atom/metrics) | | [Notifications](https://github.com/atom/notifications) | [![build](https://github.com/atom/notifications/workflows/CI/badge.svg)](https://github.com/atom/notifications/actions) | [![Dependency Status](https://david-dm.org/atom/notifications.svg)](https://david-dm.org/atom/notifications) | | [Open on GitHub](https://github.com/atom/open-on-github) | [![build](https://github.com/atom/open-on-github/workflows/CI/badge.svg)](https://github.com/atom/open-on-github/actions) | [![Dependency Status](https://david-dm.org/atom/open-on-github.svg)](https://david-dm.org/atom/open-on-github) | | [Package Generator](https://github.com/atom/package-generator) | [![build](https://github.com/atom/package-generator/workflows/CI/badge.svg)](https://github.com/atom/package-generator/actions) | [![Dependency Status](https://david-dm.org/atom/package-generator.svg)](https://david-dm.org/atom/package-generator) | | [Settings View](https://github.com/atom/settings-view) | [![build](https://github.com/atom/settings-view/workflows/CI/badge.svg)](https://github.com/atom/settings-view/actions) | [![Dependency Status](https://david-dm.org/atom/settings-view.svg)](https://david-dm.org/atom/settings-view) | | [Snippets](https://github.com/atom/snippets) | [![build](https://github.com/atom/snippets/workflows/CI/badge.svg)](https://github.com/atom/snippets/actions) | [![Dependency Status](https://david-dm.org/atom/snippets.svg)](https://david-dm.org/atom/snippets) | | [Spell Check](https://github.com/atom/spell-check) | [![build](https://github.com/atom/spell-check/workflows/CI/badge.svg)](https://github.com/atom/spell-check/actions) | [![Dependency Status](https://david-dm.org/atom/spell-check.svg)](https://david-dm.org/atom/spell-check) | | [Status Bar](https://github.com/atom/status-bar) | [![build](https://github.com/atom/status-bar/workflows/CI/badge.svg)](https://github.com/atom/status-bar/actions) | [![Dependency Status](https://david-dm.org/atom/status-bar.svg)](https://david-dm.org/atom/status-bar) | | [Styleguide](https://github.com/atom/styleguide) | [![build](https://github.com/atom/styleguide/workflows/CI/badge.svg)](https://github.com/atom/styleguide/actions) | [![Dependency Status](https://david-dm.org/atom/styleguide.svg)](https://david-dm.org/atom/styleguide) | | [Symbols View](https://github.com/atom/symbols-view) | [![build](https://github.com/atom/symbols-view/workflows/CI/badge.svg)](https://github.com/atom/symbols-view/actions) | [![Dependency Status](https://david-dm.org/atom/symbols-view.svg)](https://david-dm.org/atom/symbols-view) | | [Tabs](https://github.com/atom/tabs) | [![build](https://github.com/atom/tabs/workflows/CI/badge.svg)](https://github.com/atom/tabs/actions) | [![Dependency Status](https://david-dm.org/atom/tabs.svg)](https://david-dm.org/atom/tabs) | | [Timecop](https://github.com/atom/timecop) | [![build](https://github.com/atom/timecop/workflows/CI/badge.svg)](https://github.com/atom/timecop/actions) | [![Dependency Status](https://david-dm.org/atom/timecop.svg)](https://david-dm.org/atom/timecop) | | [Tree View](https://github.com/atom/tree-view) | [![build](https://github.com/atom/tree-view/workflows/CI/badge.svg)](https://github.com/atom/tree-view/actions) | [![Dependency Status](https://david-dm.org/atom/tree-view.svg)](https://david-dm.org/atom/tree-view) | | [Update Package Dependencies](https://github.com/atom/atom/tree/master/packages/update-package-dependencies) | | [![Dependency Status](https://david-dm.org/atom/update-package-dependencies.svg)](https://david-dm.org/atom/update-package-dependencies) | | [Welcome](https://github.com/atom/atom/tree/master/packages/welcome) | | [![Dependency Status](https://david-dm.org/atom/welcome.svg)](https://david-dm.org/atom/welcome) | | [Whitespace](https://github.com/atom/whitespace) | [![build](https://github.com/atom/whitespace/workflows/CI/badge.svg)](https://github.com/atom/whitespace/actions) | [![Dependency Status](https://david-dm.org/atom/whitespace.svg)](https://david-dm.org/atom/whitespace) | | [Wrap Guide](https://github.com/atom/wrap-guide) | [![build](https://github.com/atom/wrap-guide/workflows/CI/badge.svg)](https://github.com/atom/wrap-guide/actions) | [![Dependency Status](https://david-dm.org/atom/wrap-guide.svg)](https://david-dm.org/atom/wrap-guide) | ## Libraries | Library | Github Actions | Dependencies | |---------|----------------|--------------| | [Clear Cut](https://github.com/atom/clear-cut) | | [![Dependency Status](https://david-dm.org/atom/clear-cut.svg)](https://david-dm.org/atom/clear-cut) | | [Event Kit](https://github.com/atom/event-kit) | [![build](https://github.com/atom/event-kit/workflows/CI/badge.svg)](https://github.com/atom/event-kit/actions) | [![Dependency Status](https://david-dm.org/atom/event-kit.svg)](https://david-dm.org/atom/event-kit) | | [First Mate](https://github.com/atom/first-mate) | [![build](https://github.com/atom/first-mate/workflows/CI/badge.svg)](https://github.com/atom/first-mate/actions) | [![Dependency Status](https://david-dm.org/atom/first-mate/status.svg)](https://david-dm.org/atom/first-mate) | | [Fs Plus](https://github.com/atom/fs-plus) | [![build](https://github.com/atom/fs-plus/workflows/CI/badge.svg)](https://github.com/atom/fs-plus/actions) | [![Dependency Status](https://david-dm.org/atom/fs-plus.svg)](https://david-dm.org/atom/fs-plus) | | [Grim](https://github.com/atom/grim) | [![build](https://github.com/atom/grim/workflows/CI/badge.svg)](https://github.com/atom/grim/actions) | [![Dependency Status](https://david-dm.org/atom/grim.svg)](https://david-dm.org/atom/grim) | | [Jasmine Focused](https://github.com/atom/jasmine-focused) | | [![Dependency Status](https://david-dm.org/atom/jasmine-focused.svg)](https://david-dm.org/atom/jasmine-focused) | | [Keyboard Layout](https://github.com/atom/keyboard-layout) | [![build](https://github.com/atom/keyboard-layout/workflows/CI/badge.svg)](https://github.com/atom/keyboard-layout/actions) | [![Dependency Status](https://david-dm.org/atom/keyboard-layout/status.svg)](https://david-dm.org/atom/keyboard-layout) | | [Oniguruma](https://github.com/atom/node-oniguruma) | [![build](https://github.com/atom/node-oniguruma/workflows/CI/badge.svg)](https://github.com/atom/node-oniguruma/actions) | [![Dependency Status](https://david-dm.org/atom/node-oniguruma.svg)](https://david-dm.org/atom/node-oniguruma) | | [PathWatcher](https://github.com/atom/node-pathwatcher) | [![build](https://github.com/atom/node-pathwatcher/workflows/ci/badge.svg)](https://github.com/atom/node-pathwatcher/actions) | [![Dependency Status](https://david-dm.org/atom/node-pathwatcher/status.svg)](https://david-dm.org/atom/node-pathwatcher) | | [Property Accessors](https://github.com/atom/property-accessors) | | [![Dependency Status](https://david-dm.org/atom/property-accessors.svg)](https://david-dm.org/atom/property-accessors) | | [Season](https://github.com/atom/season) | | [![Dependency Status](https://david-dm.org/atom/season.svg)](https://david-dm.org/atom/season) | | [Superstring](https://github.com/atom/superstring) | [![build](https://github.com/atom/superstring/workflows/ci/badge.svg)](https://github.com/atom/superstring/actions) | [![Dependency Status](https://david-dm.org/atom/superstring.svg)](https://david-dm.org/atom/superstring) | | [TextBuffer](https://github.com/atom/text-buffer) | [![build](https://github.com/atom/text-buffer/workflows/CI/badge.svg)](https://github.com/atom/text-buffer/actions) | [![Dependency Status](https://david-dm.org/atom/text-buffer.svg)](https://david-dm.org/atom/text-buffer) | | [Underscore-Plus](https://github.com/atom/underscore-plus) | [![build](https://github.com/atom/underscore-plus/workflows/CI/badge.svg)](https://github.com/atom/underscore-plus/actions) | [![Dependency Status](https://david-dm.org/atom/underscore-plus.svg)](https://david-dm.org/atom/underscore-plus) | ## Tools | Language | Github Actions | Dependencies | |----------|----------------|--------------| | [AtomDoc](https://github.com/atom/atomdoc) | [![build](https://github.com/atom/atomdoc/workflows/CI/badge.svg)](https://github.com/atom/atomdoc/actions) | [![Dependency Status](https://david-dm.org/atom/atomdoc.svg)](https://david-dm.org/atom/atomdoc) ## Languages | Language | Github Actions | |----------|----------------| | [C/C++](https://github.com/atom/language-c) | [![build](https://github.com/atom/language-c/workflows/CI/badge.svg)](https://github.com/atom/language-c/actions) | | [C#](https://github.com/atom/language-csharp) | [![build](https://github.com/atom/language-csharp/workflows/CI/badge.svg)](https://github.com/atom/language-csharp/actions) | | [Clojure](https://github.com/atom/language-clojure) | [![build](https://github.com/atom/language-clojure/workflows/CI/badge.svg)](https://github.com/atom/language-clojure/actions) | | [CoffeeScript](https://github.com/atom/language-coffee-script) | [![build](https://github.com/atom/language-coffee-script/workflows/CI/badge.svg)](https://github.com/atom/language-coffee-script/actions) | | [CSS](https://github.com/atom/language-css) | [![build](https://github.com/atom/language-css/workflows/CI/badge.svg)](https://github.com/atom/language-css/actions) | | [Git](https://github.com/atom/language-git) | [![build](https://github.com/atom/language-git/workflows/CI/badge.svg)](https://github.com/atom/language-git/actions) | | [GitHub Flavored Markdown](https://github.com/atom/language-gfm) | [![build](https://github.com/atom/language-gfm/workflows/CI/badge.svg)](https://github.com/atom/language-gfm/actions) | | [Go](https://github.com/atom/language-go) | [![build](https://github.com/atom/language-go/workflows/CI/badge.svg)](https://github.com/atom/language-go/actions) | | [HTML](https://github.com/atom/language-html) | [![build](https://github.com/atom/language-html/workflows/CI/badge.svg)](https://github.com/atom/language-html/actions) | | [Hyperlink](https://github.com/atom/language-hyperlink) | [![build](https://github.com/atom/language-hyperlink/workflows/CI/badge.svg)](https://github.com/atom/language-hyperlink/actions) | | [Java](https://github.com/atom/language-java) | [![build](https://github.com/atom/language-java/workflows/build/badge.svg)](https://github.com/atom/language-java/actions) | | [JavaScript](https://github.com/atom/language-javascript) | [![build](https://github.com/atom/language-javascript/workflows/ci/badge.svg)](https://github.com/atom/language-javascript/actions) | | [JSON](https://github.com/atom/language-json) | [![build](https://github.com/atom/language-json/workflows/CI/badge.svg)](https://github.com/atom/language-json/actions) | | [Less](https://github.com/atom/language-less) | [![build](https://github.com/atom/language-less/workflows/CI/badge.svg)](https://github.com/atom/language-less/actions) | | [Make](https://github.com/atom/language-make) | [![build](https://github.com/atom/language-make/workflows/CI/badge.svg)](https://github.com/atom/language-make/actions) | | [Mustache](https://github.com/atom/language-mustache) | [![build](https://github.com/atom/language-mustache/workflows/CI/badge.svg)](https://github.com/atom/language-mustache/actions) | | [Objective-C](https://github.com/atom/language-objective-c) | [![build](https://github.com/atom/language-objective-c/workflows/CI/badge.svg)](https://github.com/atom/language-objective-c/actions) | | [Pegjs](https://github.com/atom/language-pegjs) | | | [Perl](https://github.com/atom/language-perl) | [![build](https://github.com/atom/language-perl/workflows/CI/badge.svg)](https://github.com/atom/language-perl/actions) | | [PHP](https://github.com/atom/language-php) | [![build](https://github.com/atom/language-php/workflows/CI/badge.svg)](https://github.com/atom/language-php/actions) | | [Property-List](https://github.com/atom/language-property-list) | [![build](https://github.com/atom/language-property-list/workflows/CI/badge.svg)](https://github.com/atom/language-property-list/actions) | | [Puppet](https://github.com/atom/language-puppet) | [![build](https://github.com/atom/language-puppet/workflows/CI/badge.svg)](https://github.com/atom/language-puppet/actions) | | [Python](https://github.com/atom/language-python) | [![build](https://github.com/atom/language-python/workflows/ci/badge.svg)](https://github.com/atom/language-python/actions) | | [Ruby](https://github.com/atom/language-ruby) | [![build](https://github.com/atom/language-ruby/workflows/ci/badge.svg)](https://github.com/atom/language-ruby/actions) | | [Ruby on Rails](https://github.com/atom/atom/tree/master/packages/ruby-on-rails) | | | [Sass](https://github.com/atom/language-sass) | [![build](https://github.com/atom/language-sass/workflows/CI/badge.svg)](https://github.com/atom/language-sass/actions) | | [Shellscript](https://github.com/atom/language-shellscript) | [![build](https://github.com/atom/language-shellscript/workflows/CI/badge.svg)](https://github.com/atom/language-shellscript/actions) | | [Source](https://github.com/atom/language-source) | [![build](https://github.com/atom/language-source/workflows/CI/badge.svg)](https://github.com/atom/language-source/actions) | | [SQL](https://github.com/atom/language-sql) | [![build](https://github.com/atom/language-sql/workflows/CI/badge.svg)](https://github.com/atom/language-sql/actions) | | [Text](https://github.com/atom/language-text) | [![build](https://github.com/atom/language-text/workflows/CI/badge.svg)](https://github.com/atom/language-text/actions) | | [TODO](https://github.com/atom/language-todo) | [![build](https://github.com/atom/language-todo/workflows/CI/badge.svg)](https://github.com/atom/language-todo/actions) | | [TOML](https://github.com/atom/language-toml) | [![build](https://github.com/atom/language-toml/workflows/CI/badge.svg)](https://github.com/atom/language-toml/actions) | | [TypeScript](https://github.com/atom/language-typescript) | [![build](https://github.com/atom/language-typescript/workflows/CI/badge.svg)](https://github.com/atom/language-typescript/actions) | | [XML](https://github.com/atom/language-xml) | [![build](https://github.com/atom/language-xml/workflows/CI/badge.svg)](https://github.com/atom/language-xml/actions) | | [YAML](https://github.com/atom/language-yaml) | [![build](https://github.com/atom/language-yaml/workflows/CI/badge.svg)](https://github.com/atom/language-yaml/actions) | ================================================ FILE: docs/build-instructions/linux.md ================================================ See the [Hacking on Atom Core](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-linux) section in the [Atom Flight Manual](https://flight-manual.atom.io). ================================================ FILE: docs/build-instructions/macOS.md ================================================ See the [Hacking on Atom Core](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-mac) section in the [Atom Flight Manual](https://flight-manual.atom.io). ================================================ FILE: docs/build-instructions/windows.md ================================================ See the [Hacking on Atom Core](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-windows) section in the [Atom Flight Manual](https://flight-manual.atom.io). ================================================ FILE: docs/contributing-to-packages.md ================================================ See https://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/ ================================================ FILE: docs/native-profiling.md ================================================ # Profiling the Atom Render Process on macOS with Instruments ![Instruments](https://cloud.githubusercontent.com/assets/1789/14193295/d503db7a-f760-11e5-88bf-fe417c0cd913.png) * Determine the version of Electron for your version of Atom. * Open the dev tools with `alt-cmd-i` * Evaluate `process.versions.electron` in the console. * Based on this version, download the appropriate Electron symbols from the [releases](https://github.com/atom/electron/releases) page. * The file name should look like `electron-v1.X.Y-darwin-x64-dsym.zip`. * Decompress these symbols in your `~/Downloads` directory. * Now create a time profile in Instruments. * Open `Instruments.app`. * Select `Time Profiler` * In Atom, determine the pid to attach to by evaluating `process.pid` in the dev tools console. * Attach to this pid via the menu at the upper left corner of the Instruments profiler. * Click record, do your thing. * Click stop. * The symbols should have been automatically located by Instruments (via Spotlight or something?), giving you a readable profile. ================================================ FILE: docs/rfcs/000-template.md ================================================ # Feature title ## Status Proposed ## Summary One paragraph explanation of the feature. ## Motivation Why are we doing this? What use cases does it support? What is the expected outcome? ## Explanation Explain the proposal as if it was already implemented and you were describing it to an Atom user. That generally means: - Introducing new named concepts. - Explaining the feature largely in terms of examples. - Explaining any changes to existing workflows. ## Drawbacks Why should we *not* do this? ## Rationale and alternatives - Why is this approach the best in the space of possible approaches? - What other approaches have been considered and what is the rationale for not choosing them? - What is the impact of not doing this? ## Unresolved questions - What unresolved questions do you expect to resolve through the RFC process before this gets merged? - What unresolved questions do you expect to resolve through the implementation of this feature before it is released in a new version of Atom? - What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? ================================================ FILE: docs/rfcs/001-updatable-bundled-packages.md ================================================ # Updatable Bundled Packages ## Status Proposed ## Summary This feature will enable an opt-in subset of bundled Atom packages to be updated with `apm` outside of the Atom release cycle. This will enable users to receive new functionality and bug fixes for some bundled packages as regularly as needed without waiting for them to be included in a new Atom release. This is especially important for packages like [GitHub](https://github.com/atom/github/) and [Teletype](https://github.com/atom/teletype/) which provide essential Atom functionality and could be improved independently of Atom. ## Motivation Atom currently uses a monthly release cycle with staged Stable and Beta releases so that major issues get caught early in Beta before reaching the Stable release. Because Atom releases updates monthly, this means that a new feature merged into `master` right after a new Atom release could take one month to reach the next Beta and then another month to reach Stable. Since a large part of Atom's built-in functionality is provided by bundled packages, it makes sense to allow some of those packages to be updated independently of Atom's monthly release cycle so that users can receive new features and fixes whenever they become available. Bundled packages are treated differently than community packages that you can install using `apm`: - You are not prompted to update them when new versions are released on `apm` - `apm` will warn you at the command line when you try to install or update a bundled package - If a user intentionally installs a bundled package from `apm` the [dalek package](https://github.com/atom/dalek/) will show a warning in the "deprecations" view asking the user to remove the offending package Despite all this, if the user *does* manually install an update to a bundled package using `apm`, it will be loaded into the editor and updated dutifully as new releases occur. The only new functionality needed is to enable `apm` to check bundled packages for updates when those packages haven't yet been installed in the user's `~/.atom/packages` folder. The primary use case for this improvement is enabling the GitHub package to ship improvements more frequently than Atom's release cycle since many of its improvements can be done without changes to Atom itself. If this approach is proven to work well for the GitHub package, we might also consider using it to ship Teletype as a bundled Atom package. ## Explanation Any bundled Atom package can opt in to new updates released via `apm` by adding `"coreUpdatable": true` to its `package.json` file. This causes `apm` to consider it as part of the list of packages it checks for updates. If a community (non-bundled) package sets this field to `true` or `false` it will be ignored as it's only relevant to bundled packages. Atom shows update notifications for Updatable bundled packages whenever they are available so long as those updates support the engine version of the current Atom build. Bundled package updates can also be found and installed in the Settings view's *Updates* tab. The `dalek` package is aware of the new "Updatable" metadata and excludes updated bundled packages from its deprecation warnings. ### User Experience Examples 1. The user downloads and installs Atom 1.28.0 which includes GitHub package version 0.15.0. Two weeks later, GitHub package 0.16.0 is released with a few new features. The user is prompted to update to the new version and gets the new features even though Atom 1.29.0 hasn't been released yet. 2. The user downloads and installs Atom 1.28.0, including GitHub package 0.15.0, which was released two weeks prior. Since that release the GitHub package has been updated to version 0.15.1 on `apm`. When the user starts Atom for the first time they are prompted to update the GitHub package. 3. In the future, a user has an old install of Atom 1.28.0 and waits a long time between installing Atom updates. The GitHub package releases version 0.25.0 but the user is not prompted to install it because the GitHub package has set `engines` in `package.json` to restrict to Atom 1.32.0 and above. ### Rules for Updatable Bundled Packages Any package that opts into this behavior must adhere to these rules: 1. **Each release must ensure that its `engines` field in `package.json` reflects the necessary Atom version for the Atom, Electron, and Node.js APIs used in the package**. This field defines the range of Atom versions in which the package is expected to work. The field should always be set to the lowest possible Atom version that the package supports. 2. **Any new update to a bundled package *must* support current Stable *and* Beta releases**. This enables the user to upgrade the package and continue to use it in side-by-side Stable and Beta installs on their machine. If a package wants to use API features of a newer version of Atom while still supporting older Atom versions, it must do so in a way that is aware of the user's version and adjust itself accordingly. 3. **Atom's `package.json` *must* stay up to date with the latest supported version of the package** in the `master` and Beta release branches. This ensures that the user always gets the latest version of the package in a new release and also benefits from its inclusion in Atom's snapshot. For rule #3, it will be important to have automation to ensure that current Beta release and `master` are kept up to date with the latest compatible version of any updatable bundled package as it will be difficult for maintainers to do that manually. This could be accomplished by a nightly CI run which is focused explicitly on bumping package dependencies in this manner. ## Drawbacks ### Possible API incompatibility The primary drawback of this approach is that Updatable bundled packages might exhibit problems on older Atom versions due to missing or changed APIs in Atom, Electron, or Node.js. The solution for these packages is to keep their `engines` field updated appropriately, but there's still a chance that some updates will slip through without the necessary engine version changes. If this does occur and users are affected by it, the solution is to publish a new update which rolls back the package to the functionality of its previous release and then publish another new update with the new functionality restored and the proper `engines` version in place. ### Increased Atom startup time Another major drawback is that the snapshotted code for the bundled package will no longer be used since a newer version has been installed. This updated version of the package cannot be easily added back into Atom's snapshot so it could cause a noticeable drag on Atom's startup time. Some quick measurements with Timecop show a 10x increase in GitHub package load time for bundled (snapshot) vs updated (non-snapshot) package code: | GitHub Package Code | Load Time | |----------------------------------|-----------| | **Bundled** | 52 ms | | **Updated (first load)** | 5026 ms | | **Updated (subsequent loads)** | 591 ms | There was no measurable effect on shell or window startup time, only package load time. It seems that the transpilation phase of the first load of the package incurs a 100x increase in load time. Pre-transpilation of the package code (either when shipped or when installed using `apm`) will be useful in mitigating this cost. Further investigation into snapshotting package code will be needed to understand if the load time increase can be mitigated. There is a possibility that the GitHub package could load parts of its codebase on demand to mitigate the increased startup time when not loaded as part of Atom's snapshot. This approach is discussed in more detail at [atom/github#1522](https://github.com/atom/github/issues/1522). ### Incompatibility across Atom release channels One other possible drawback is that an updated version of a bundled package might not be compatible across two different Atom channels. For example, if the user installs a new update to a bundled package that only supports the current Atom Beta release or higher, the user will no longer have access to that package if they open Atom Stable. However, this drawback is no different than what the user would face today installing a community package under the same circumstances, so this could be considered a general problem in the Atom package ecosystem. Finally, one risk of this approach is that the Atom team forgets to update a bundled package to its latest appropriate version on `apm` just before a new release. If this happens, the user will install a new Atom update and then be prompted to update a package that should have been snapshotted and shipped in-box. To avoid this problem we could add some build automation that checks for the latest version of a bundled package to see if the current Atom build would be supported by it. ## Rationale and alternatives This is the best approach for updating bundled packages because it allows those packages to take control of their own release cycle so long as they manage their Atom engine version correctly. It also does so in a way that allows us to decide which packages can be updated independently, reducing the likelihood of problems for users. The primary alternative to this approach is to speed up the Atom release cycle so that bundled Atom package updates will reach users more frequently. This approach will be investigated independently of this RFC as it may still be valuable even with Updatable bundled packages. ## Unresolved questions > - What unresolved questions do you expect to resolve through the RFC process before this gets merged? Is it enough to just depend on the `engines` field of `package.json` to protect users from installing a package update that doesn't work with their version of Atom? > - What unresolved questions do you expect to resolve through the implementation of this feature before it is released in a new version of Atom? Is there any optimization we can use to reduce the performance hit of loading updated bundled packages? > - What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? One issue that's out of scope for this RFC is how we ship new features and fixes to the core components of Atom (not its bundled packages) more frequently. There are two options we can investigate to accomplish this: - **Ship Atom updates more frequently, possibly every two weeks** - **Introduce a channel for nightly builds which surface the latest changes every day** Both of these possibilities will be covered in future RFCs as they could be implemented independently of the feature described in this RFC. ================================================ FILE: docs/rfcs/002-atom-nightly-releases.md ================================================ # Atom Nightly Releases ## Status Implemented in PR [#17538](https://github.com/atom/atom/pull/17538) ## Summary This RFC proposes that Atom add a third official release channel which delivers new builds of Atom nightly from the `master` branch. Nightly releases will allow new improvements to reach users long before a new Stable or Beta release is shipped. This effort will also give us the opportunity to experiment with new release automation strategies that could eventually be used to speed up the Stable and Beta release cadence. ## Motivation Atom currently uses a monthly release cycle with staged Stable and Beta releases so that major issues get caught early in Beta before reaching the Stable release. Because Atom releases updates monthly, this means that a new feature merged into `master` right after a new Atom release could take one month to reach the next Beta and then another month to reach Stable. This release process works well for delivering stable improvements to users on a regular basis but it results in friction for users who want to try out the latest Atom improvements and provide feedback. If we deliver a nightly release channel, it will be possible to deliver new features and bug fixes on a regular basis and get valuable feedback to guide our work. Today, a bleeding-edge user must manually pull Atom's `master` branch and compile their own build. There is a source of `dev` builds from `master` across our CI services but those aren't made available to users as an official distribution. ## Explanation A user who wants to use the latest improvements to Atom each day can go to atom.io, download the Atom Nightly release, and install it on their machine. This release can be installed alongside Atom Stable and Atom Beta. Each night when there are new commits to Atom's `master` branch, a scheduled CI build creates a new Atom Nightly release with packages for Windows, macOS, and Linux. These packages are automatically uploaded to a new GitHub release on the `atom/atom-nightly-releases` repository using a monotonically-increasing nightly version based off of the version in `master` (e.g. `v1.29.0-nightly1`). Every 4 hours, an Atom Nightly release installed on Windows or macOS checks for a new update by consulting Electron's [update.electronjs.org](update-electron) service. If a new update is available, it is downloaded in the background and the user is notified to restart Atom once it's complete. This update flow is the same as what users experience in Atom Stable or Beta releases but updates occur more frequently. Linux users must manually download nightly releases for now as there isn't an easy way to automatically install new updates across the various Linux distributions. We may consider providing updatable [AppImage](http://appimage.org/) packages in the future; this will be proposed in a separate RFC. ## Drawbacks There isn't a major downside to this effort since it would run in parallel to the existing Atom release process without affecting it. ## Rationale and alternatives This is a useful approach because it allows us to achieve a much more rapid feedback loop with highly engaged users to ensure that Atom is improving regularly. It's the best approach because it allows us to get rapid feedback without sacrificing the stability of the Stable and Beta releases. Another option is to speed up Atom's release cadence to ship Stable and Beta every two weeks (or more regularly). This approach could shorten our feedback loop but at the expense of greater instability since new improvements would not have as much time to be polished before release. The impact of not taking this approach is that we continue to have to wait 1-2 months to get feedback from users about new features or bugs in Stable and Beta releases. ## Unresolved questions - **What should we call this release channel?** Some ideas: - Atom Nightly - Atom Reactor - Atom Dev - Currently the name of dev builds but it might make sense to leave that for "normal" builds from `master` According to a [Twitter poll](https://twitter.com/daviwil/status/1006545552987701248) with about 1,600 responses, 50% of the voters chose "Atom Nightly". The final name will be determined before launch. - **Will Electron's new autoUpdate service work for all Atom releases?** One outcome of this effort is to use the new [update.electronjs.org](update-electron) service for Atom's update checks so that we can deprecate on our own custom update service. Building the Nightly channel on this service will allow us to evaluate it to see if it meets the needs of the Stable and Beta channels. [update-electron]: https://github.com/electron/update.electronjs.org ================================================ FILE: docs/rfcs/003-consolidate-core-packages.md ================================================ # Consolidate Core Atom Packages ## Status Accepted ## Summary Atom's official distribution is comprised of 92 core packages which provide its built-in functionality. These packages currently live in their own independent repositories in the Atom organization, all with their own separate issues, PRs, releases, and CI configurations. This RFC proposes that by consolidating most, if not all, of these core packages back into the `atom/atom` repo, we will see the following benefits: - Less confusion for new contributors - Simpler core package contribution experience - Greatly reduced burden for maintainers ## Motivation Let's cover each of the bullet points mentioned above: ### Less confusion for contributors Imagine that a new contributor wants to add a small new feature to the `tree-view` package. The first place they are likely to look is the `atom/atom` repository. Scanning through the folders will lead to a dead end as nothing that looks like `tree-view` code can be found. They might take one of the following steps next: - By reading README.md, maybe they will decide to click the link to the Atom Flight Manual and _maybe_ find the [Contributing to Official Atom Packages](https://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/) page there - They could read the CONTRIBUTING.md file which [has a section](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#atom-and-packages) that explains where to find the repos for core packages and how to contribute, but we don't really have a clear pointer to that in our README.md - If they don't happen to find that page, they might use Google to search for "atom tree view" and find the atom/tree-view repo and _maybe_ read the CONTRIBUTING.md file which sends them to Atom's overall contribution documentation - They might go to the Atom Forum or Slack community to ask how to contribute to a particular part of Atom and *hopefully* get a helpful response that points them in the right direction Having all of the core Atom packages represented in a top-level `packages` folder, even if they all don't actually live in the repo, will go a long way to making the core package code more discoverable. ### Simpler core package contribution experience Separating core Atom features out into individual repositories and delivering them to Atom builds via `apm` is a great idea in theory because it validates the Atom package ecosystem and gives developers many examples of how to develop an Atom package. It also gives Atom developers real-world experience working with Atom's APIs so that we ensure community package authors have the same hackability that Atom developers enjoy. On the other hand, having these packages live in separate repositories and released "independently" introduces a great deal of overhead when adding new features. Here is a comparison of the current package development workflow contrasted to what we could achieve with consolidated packages: #### Current Package Development Workflow For example, to add a single feature to the `tree-view` package, one must: 1. Fork and clone the `tree-view` repository to their computer (making sure to pull the commit relevant to the version of Atom they are working with) 1. Run `apm install` and `apm link` inside of the repo folder 1. Make their desired changes to the code 1. Open a PR to the `tree-view` repo and wait for CI to pass and a maintainer to review it 1. Work with maintainers to get the PR approved and merged After this is finished, an Atom maintainer must take the following steps: 1. Clone the `tree-view` repo 2. Run `apm publish` to publish a new release of the package 3. Edit `package.json` in the Atom repo to reflect the new version of `tree-view` 4. Commit and push the changes to the relevant branch where the change belongs (`master` or `1.nn-releases`) #### Simplified Package Development If we were to move `tree-view` (or any other core Atom package) back into `atom/atom`, the development workflow would look more like this: 1. Fork and clone `atom/atom` and switch to a release branch if necessary 1. Build Atom and launch it in dev mode 1. Make desired changes to the code in `packages/tree-view` 1. Open a PR on `atom/atom` and wait for CI to pass and a maintainer to review it 1. Work with maintainers to get the PR approved and merged At this point, the change is merged into Atom and ready for inclusion in the next release. ### Greatly reduced burden for maintainers Since packages all have their own repositories, this means that we have to watch 91 different repos for issues and pull requests. This also means that we have to redirect issues filed on `atom/atom` to the appropriate repository when a user doesn't know where it belongs. Even more importantly, there's not an easy way to prioritize and track issues across the Atom organization without using GitHub Projects. Also, as mentioned above, there's the added duty of doing the package "version dance" when we merge any new PRs to a package repository: publish the package update, update `package.json` in Atom. It's very easy to forget to do this and not have community contributions included in the next Atom release! The more core packages live in `atom/atom`, the less work Atom maintainers have to do overall. ## Explanation Many of Atom's core packages now live in the core `atom/atom` repository. To the Atom user, this change will be imperceptible as these packages still show up in the list of Core Packages in the Settings View. Users can still optionally disable these packages. For maintainers and contributors, there will be less juggling of repositories and no more publishing of updates to these packages with `apm`: Contributors now clone and build `atom/atom` to work on improvements to core packages. They will no longer have to use `apm link` in dev mode to test changes they make to packages in the repo's `packages` folder. Core packages that aren't consolidated still have folders under `packages` with README.md files that point to the home repository for that package. When a contributor sends a PR to `atom/atom` that only affects files in a folder under `packages`, only the specs for the relevant package folders will be executed using Atom's CI scripts. This means that a full Atom build will not be required when no Atom Core code is changed in a PR. Package specs are also now run against all 3 OSes on Atom `master` and release builds. Atom maintainers no longer have to publish new versions to consolidated core packages and then edit `package.json` to bump the package version in a particular Atom release branch (Stable, Beta, or `master`). When a PR against a consolidated core package in `atom/atom` is merged, no version number change is required and the changes will immediately be a part of the next release from that branch. ## Drawbacks One possible drawback of this approach is that there might be some initial confusion where core Atom packages live, especially if some are consolidated into `atom/atom` and others still live in their own repositories. We will manage this confusion by doing the following: - Include a `README.md` file in the `packages` folder which lists core packages that are not consolidated in the Atom repo. This will enable users to find the home repositories of those packages. - Archive the repositories for consolidated core packages, but only after migrating existing issues, merging or closing existing PRs, and updating the README.md to point to the new home of the package code. Also, contributors will now have to fork, clone, and build `atom/atom` to contribute to core packages where they would previously just need to clone the package repository. This might put added burden on them such as installing necessary build dependencies on their machine that they wouldn't otherwise need. It is very likely we could simplify this process for them, though. One final drawback is that it will now be harder to have single-package maintainers. We currently have 7 core packages where there is a maintainer who isn't a part of the core Atom maintainers team. These maintainers generally are able to merge community PRs and make commits to those packages with their own judgement. If we get rid of individual package repositories, do we now make those maintainers full Atom maintainers? ## Rationale and alternatives The Motivation section explains most of the rationale, so this section will focus on the process of consolidating packages back into `atom/atom`. The set of packages we've chosen to consolidate were evaluated based on a few factors: - Number of open issues and PRs (exclude any with > 10 open PRs) - Time since last update (longer duration since last update is prioritized) - Number of package-only maintainers on the repo (exclude any with package maintainers for now) Using this criteria, all 91 packages have been evaluated and categorized to determine whether they are good candidates for consolidation: #### Initial Consolidation Candidates | Package | Open Issues | Open PRs | Outside Maintainers | Last Updated | |---------|-------------|----------|---------------------| -------------| | **[about]** | 2 | 0 | 0 | 7/11/18 | | **[archive-view]** | 10 | 0 | 0 | 6/3/18 | | **[atom-dark-syntax]** | 5 | 0 | 0 | 12/6/17 | | **[atom-dark-ui]** | 1 | 2 | 0 | 2/13/18 | | **[atom-light-syntax]** | 1 | 0 | 0 | 10/17/16 | | **[atom-light-ui]** | 1 | 0 | 0 | 2/13/18 | | **[autoflow]** | 17 | 4 | 0 | 4/17/18 | | **[autosave]** | 13 | 0 | 0 | 9/16/17 | | **[background-tips]** | 3 | 2 | 0 | 2/17/18 | | **[base16-tomorrow-dark-theme]** | 5 | 0 | 0 | 1/10/17 | | **[base16-tomorrow-light-theme]** | 1 | 0 | 0 | 1/10/17 | | **[bookmarks]** | 19 | 4 | 0 | 12/10/17 | | **[bracket-matcher]** | 74 | 8 | 0 | 3/20/18 | | **[command-palette]** | 18 | 6 | 0 | 2/27/18 | | **[dalek]** | 2 | 0 | 0 | 2/28/18 | | **[deprecation-cop]** | 5 | 0 | 0 | 9/7/17 | | **[dev-live-reload]** | 4 | 0 | 0 | 11/14/17 | | **[encoding-selector]** | 11 | 2 | 0 | 4/19/18 | | **[exception-reporting]** | 5 | 0 | 0 | 2/6/18 | | **[git-diff]** | 38 | 1 | 0 | 1/18/18 | | **[go-to-line]** | 5 | 2 | 0 | 1/25/18 | | **[grammar-selector]** | 3 | 1 | 0 | 4/12/18 | | **[image-view]** | 4 | 4 | 0 | 7/9/18 | | **[incompatible-packages]** | 1 | 0 | 0 | 4/25/17 | | **[keybinding-resolver]** | 11 | 3 | 0 | 7/6/18 | | **[language-clojure]** | 13 | 3 | 0 | 1/26/18 | | **[language-coffee-script]** | 9 | 2 | 0 | 11/1/17 | | **[language-csharp]** | 1 | 1 | 0 | 4/27/18 | | **[language-css]** | 6 | 7 | 0 | 6/11/18 | | **[language-gfm]** | 52 | 9 | 0 | 6/15/18 | | **[language-git]** | 4 | 2 | 0 | 4/18/17 | | **[language-html]** | 11 | 4 | 0 | 7/5/18 | | **[language-hyperlink]** | 2 | 3 | 0 | 10/25/17 | | **[language-json]** | 1 | 0 | 0 | 5/11/18 | | **[language-less]** | 5 | 1 | 0 | 6/11/18 | | **[language-make]** | 7 | 3 | 0 | 11/26/16 | | **[language-mustache]** | 0 | 0 | 0 | 2/5/18 | | **[language-objective-c]** | 2 | 0 | 0 | 12/1/15 | | **[language-php]** | 25 | 7 | 0 | 6/11/18 | | **[language-property-list]** | 1 | 0 | 0 | 3/11/17 | | **[language-python]** | 33 | 4 | 0 | 6/18/18 | | **[language-ruby]** | 38 | 10 | 0 | 10/25/17 | | **[language-ruby-on-rails]** | 9 | 6 | 0 | 12/7/17 | | **[language-sass]** | 12 | 5 | 0 | 5/2/18 | | **[language-shellscript]** | 12 | 3 | 0 | 6/18/18 | | **[language-source]** | 0 | 0 | 0 | 1/6/15 | | **[language-sql]** | 6 | 4 | 0 | 1/26/18 | | **[language-text]** | 1 | 0 | 0 | 3/9/18 | | **[language-todo]** | 10 | 6 | 0 | 1/26/18 | | **[language-toml]** | 1 | 0 | 0 | 1/6/18 | | **[language-typescript]** | 6 | 0 | 0 | 6/18/18 | | **[language-xml]** | 2 | 1 | 0 | 6/12/17 | | **[language-yaml]** | 8 | 2 | 0 | 3/9/18 | | **[line-ending-selector]** | 10 | 0 | 0 | 5/18/18 | | **[link]** | 0 | 1 | 0 | 11/14/17 | | **[metrics]** | 1 | 2 | 0 | 7/5/18 | | **[notifications]** | 29 | 8 | 0 | 3/22/18 | | **[one-dark-syntax]** | 4 | 0 | 0 | 5/27/18 | | **[one-dark-ui]** | 13 | 1 | 0 | 5/1/18 | | **[one-light-syntax]** | 2 | 1 | 0 | 5/27/18 | | **[one-light-ui]** | 2 | 0 | 0 | 5/1/18 | | **[open-on-github]** | 8 | 3 | 0 | 11/21/17 | | **[package-generator]** | 10 | 2 | 0 | 11/16/17 | | **[status-bar]** | 25 | 3 | 0 | 11/6/17 | | **[styleguide]** | 12 | 2 | 0 | 4/12/18 | | **[tabs]** | 66 | 7 | 0 | 5/13/18 | | **[timecop]** | 5 | 0 | 0 | 11/4/17 | | **[update-package-dependencies]** | 0 | 0 | 0 | 12/10/17 | | **[welcome]** | 0 | 0 | 0 | 11/21/17 | | **[whitespace]** | 31 | 6 | 0 | 5/30/18 | | **[wrap-guide]** | 3 | 4 | 0 | 11/27/17 | #### Packages to be Consolidated Later The following packages will not be consolidated until the stated reasons can be resolved or we decide on a consolidation strategy for them: | Package | Open Issues | Open PRs | Outside Maintainers | Last Updated | Reason | |---------|-------------|----------|---------------------|--------------|-------| | **[find-and-replace]** | 219 | 17 | 0 | 6/4/18 | Too many open PRs | | **[fuzzy-finder]** | 89 | 22 | 0 | 5/17/18 | Too many open PRs | | **[github]** | | | | | Independent project | | **[language-c]** | 53 | 15 | 0 | 7/10/18 | Too many open PRs | | **[language-go]** | 12 | 2 | **1** | 6/18/18 | Package maintainer, possibly inactive? | | **[language-java]** | 8 | 2 | **1** | 6/11/18 | Package maintainer | | **[language-javascript]** | 66 | 12 | 0 | 7/6/18 | Too many open PRs | | **[language-perl]** | 17 | 1 | **1** | 10/30/17 | Package maintainer, possibly inactive? | | **[markdown-preview]** | 139 | 12 | 0 | 1/8/18 | Too many open PRs | | **[settings-view]** | 137 | 18 | 0 | 5/17/18 | Too many open PRs | | **[snippets]** | 57 | 4 | **1** | 4/17/18 | Package maintainer | | **[solarized-dark-syntax]** | 8 | 3 | **1** | 5/27/18 | Package maintainer | | **[solarized-light-syntax]** | 2 | 3 | **1** | 5/27/18 | Package maintainer | | **[spell-check]** | 68 | 14 | **1** | 5/25/18 | Too many open PRs, package maintainer | | **[symbols-view]** | 86 | 13 | 0 | 12/10/17 | Too many open PRs | | **[tree-view]** | 210 | 36 | 0 | 3/21/18 | Too many open PRs | #### Packages to Never Consolidate These packages will not be consolidated because they would inhibit contributions from our friends in the Nuclide team at Facebook: - **[autocomplete-atom-api]** - **[autocomplete-css]** - **[autocomplete-html]** - **[autocomplete-plus]** - **[autocomplete-snippets]** ### Consolidation Process To consolidate a single core package repository back into `atom/atom`, the following steps will be taken: 1. All open pull requests on the package's repository must either be closed or merged before consolidation can proceed 1. The package repository's code in `master` will be copied over to Atom's `packages` folder in a subfolder bearing that package's name. 1. Atom's `package.json` file will be updated to change the package's `packageDependencies` entry to reference its local path with the following syntax: `"tree-view": "file:./packages/tree-view"` 1. A test build will be created locally to manually verify that the package loads and works correctly at first glance 1. The package specs for the newly-consolidated package will be run against the local Atom build 1. A PR will be sent to `atom/atom` to verify that CI passes with the introduction of the consolidated package 1. Once CI is clean and the PR is approved, the PR will be merged 1. The package's original repository will have all of its existing issues moved over to `atom/atom` using a bulk issue mover tool, assigning a label to those issues relative to the package name, like `packages/tree-view` 1. The package's original repository will have its README.md updated to point contributors to the code's new home in `atom/atom` 1. The package's original repository will now be archived on GitHub ### Alternative Approaches One alternative approach would be to break this core Atom functionality out of packages and put it directly in the Atom codebase without treating them as packages. This would simplify the development process even further but with the following drawbacks: - The Atom team would have less regular exposure to Atom package development - Users would no longer be able to disable core packages to replace their behavior with other packages (different tree views, etc) ## Unresolved questions - Is there a good reason to not move the `language-*` packages into `atom/atom`? One concern here is that there exist projects which depend directly on these repositories for the TextMate syntax grammars they contain. Moving the code into `atom/atom` would require that we notify the consumers of the grammars so that they can redirect their requests to the `atom/atom` repo. - Should we use `git subtree` to migrate the entire commit history of these packages over or just depend on the history from a package's original repository? For now, we won't use `git subtree` due to the possibility that bringing over thousands of commits could cause unknown problems in the Atom repo. We may try this for newly consolidated packages in the future if we decide that not having the package commit history is a sufficient impediment to problem investigations. - What are the criteria we might use to eventually decide to move larger packages like `tree-view`, `settings-view`, and `find-and-replace` back into `atom/atom`? - Will we be losing any useful data about these packages if we don't have standalone repositories anymore? - Should we use this as an opportunity to remove any unnecessary packages from the core Atom distribution? [about]: https://github.com/atom/about [archive-view]: https://github.com/atom/archive-view [atom-dark-syntax]: https://github.com/atom/atom-dark-syntax [atom-dark-ui]: https://github.com/atom/atom-dark-ui [atom-light-syntax]: https://github.com/atom/atom-light-syntax [atom-light-ui]: https://github.com/atom/atom-light-ui [autocomplete-atom-api]: https://github.com/atom/autocomplete-atom-api [autocomplete-css]: https://github.com/atom/autocomplete-css [autocomplete-html]: https://github.com/atom/autocomplete-html [autocomplete-plus]: https://github.com/atom/autocomplete-plus [autocomplete-snippets]: https://github.com/atom/autocomplete-snippets [autoflow]: https://github.com/atom/autoflow [autosave]: https://github.com/atom/autosave [background-tips]: https://github.com/atom/background-tips [base16-tomorrow-dark-theme]: https://github.com/atom/base16-tomorrow-dark-theme [base16-tomorrow-light-theme]: https://github.com/atom/base16-tomorrow-light-theme [bookmarks]: https://github.com/atom/bookmarks [bracket-matcher]: https://github.com/atom/bracket-matcher [command-palette]: https://github.com/atom/command-palette [dalek]: https://github.com/atom/dalek [deprecation-cop]: https://github.com/atom/deprecation-cop [dev-live-reload]: https://github.com/atom/dev-live-reload [encoding-selector]: https://github.com/atom/encoding-selector [exception-reporting]: https://github.com/atom/exception-reporting [find-and-replace]: https://github.com/atom/find-and-replace [fuzzy-finder]: https://github.com/atom/fuzzy-finder [git-diff]: https://github.com/atom/git-diff [github]: https://github.com/atom/github [go-to-line]: https://github.com/atom/go-to-line [grammar-selector]: https://github.com/atom/grammar-selector [image-view]: https://github.com/atom/image-view [incompatible-packages]: https://github.com/atom/incompatible-packages [keybinding-resolver]: https://github.com/atom/keybinding-resolver [language-c]: https://github.com/atom/language-c [language-clojure]: https://github.com/atom/language-clojure [language-coffee-script]: https://github.com/atom/language-coffee-script [language-csharp]: https://github.com/atom/language-csharp [language-css]: https://github.com/atom/language-css [language-gfm]: https://github.com/atom/language-gfm [language-git]: https://github.com/atom/language-git [language-go]: https://github.com/atom/language-go [language-html]: https://github.com/atom/language-html [language-hyperlink]: https://github.com/atom/language-hyperlink [language-java]: https://github.com/atom/language-java [language-javascript]: https://github.com/atom/language-javascript [language-json]: https://github.com/atom/language-json [language-less]: https://github.com/atom/language-less [language-make]: https://github.com/atom/language-make [language-mustache]: https://github.com/atom/language-mustache [language-objective-c]: https://github.com/atom/language-objective-c [language-perl]: https://github.com/atom/language-perl [language-php]: https://github.com/atom/language-php [language-property-list]: https://github.com/atom/language-property-list [language-python]: https://github.com/atom/language-python [language-ruby]: https://github.com/atom/language-ruby [language-ruby-on-rails]: https://github.com/atom/language-ruby-on-rails [language-sass]: https://github.com/atom/language-sass [language-shellscript]: https://github.com/atom/language-shellscript [language-source]: https://github.com/atom/language-source [language-sql]: https://github.com/atom/language-sql [language-text]: https://github.com/atom/language-text [language-todo]: https://github.com/atom/language-todo [language-toml]: https://github.com/atom/language-toml [language-typescript]: https://github.com/atom/language-typescript [language-xml]: https://github.com/atom/language-xml [language-yaml]: https://github.com/atom/language-yaml [line-ending-selector]: https://github.com/atom/line-ending-selector [link]: https://github.com/atom/link [markdown-preview]: https://github.com/atom/markdown-preview [metrics]: https://github.com/atom/metrics [notifications]: https://github.com/atom/notifications [one-dark-syntax]: https://github.com/atom/one-dark-syntax [one-dark-ui]: https://github.com/atom/one-dark-ui [one-light-syntax]: https://github.com/atom/one-light-syntax [one-light-ui]: https://github.com/atom/one-light-ui [open-on-github]: https://github.com/atom/open-on-github [package-generator]: https://github.com/atom/package-generator [settings-view]: https://github.com/atom/settings-view [snippets]: https://github.com/atom/snippets [solarized-dark-syntax]: https://github.com/atom/solarized-dark-syntax [solarized-light-syntax]: https://github.com/atom/solarized-light-syntax [spell-check]: https://github.com/atom/spell-check [status-bar]: https://github.com/atom/status-bar [styleguide]: https://github.com/atom/styleguide [symbols-view]: https://github.com/atom/symbols-view [tabs]: https://github.com/atom/tabs [timecop]: https://github.com/atom/timecop [tree-view]: https://github.com/atom/tree-view [update-package-dependencies]: https://github.com/atom/update-package-dependencies [welcome]: https://github.com/atom/welcome [whitespace]: https://github.com/atom/whitespace [wrap-guide]: https://github.com/atom/wrap-guide ================================================ FILE: docs/rfcs/004-decoration-ordering.md ================================================ # Decoration ordering ## Status Accepted ## Summary Order block decoration items in the DOM in a deterministic and controllable way. ## Motivation When multiple block decorations are created at the same screen line, they are inserted into the DOM in an order determined by the sequence of their creation; from oldest to newest when `position` is set to `"before"`, from newest to oldest when `position` is set to `"after"`. While this is deterministic, it is limited: it isn't possible to insert decorations within a sequence of existing ones, and it's difficult to control the order of decorations when creating and destroying and moving markers around an editor. We hit the need for this in [atom/github#1913](https://github.com/atom/github/pull/1913) when we have a block decoration for multiple consecutive collapsed file patches. ## Explanation [TextEditor::decorateMarker()](https://atom.io/docs/api/v1.34.0/TextEditor#instance-decorateMarker) accepts an additional `order` parameter in its `decorationParams` argument when `type` is "block". When multiple block or overlay decorations occur at the same screen line, they are ordered within the DOM in increasing "order" value. Block decorations with the same `order` property are rendered in the order they were created, oldest to newest. Block decorations with no `order` property are rendered after those with one, in the order in which they were created, oldest to newest. ## Drawbacks This is a breaking change for co-located block decorations created with an "after" position - they'll now appear in the reverse order. When multiple packages create block decorations at the same screen line, they'll need to coordinate their `order` values to have expected behavior. There may not even be a clear, universal answer about how block decorations from distinct packages _should_ be ordered. This adds another situational parameter to `TextEditor::decorationMarker()`, which already has complicated arguments. ## Rationale and alternatives Originally I wanted to address the package coordination problem with a similar approach to [the way context menu items are ordered](https://github.com/atom/atom/pull/16661), by allowing individual decorations to specify constraints: "before this block," "after this block," "next to this block" and so forth. I ultimately chose to write up the simpler proposal because: * Block decoration collisions among packages seem much less likely than context menu collisions. * Constraint satisfaction problems are complex. There would be a relatively high chance of introducing bugs and performance regressions. * The order number approach is similar to the APIs already offered to order status bar tiles and custom gutters. The alternative to having an explicit API for this is at all is to create and destroy decorations to achieve the desired order. That's possible, but requires a great deal of bookkeeping on the package's side to accomplish, especially as decorations are added and removed and text is edited. ## Unresolved questions - Should overlay decorations respect an `order` parameter in a similar fashion? - Should screen column effect decoration ordering at all? ================================================ FILE: docs/rfcs/005-grammar-comment-delims.md ================================================ # Add comment delims to grammar declaration ## Status Proposed ## Summary Grammars currently only sort of declare their comment delims. E.g., TextMate JavaScript will say `'commentStart': '// '` (and not even in the same file as the grammar), and Tree-sitter says `comments: start: '// '` (in the same file; better). However, it is impossible to tell that `/* */` delimits JS block comments. This RFC is to request a grammar property to declare all comment delims a language supports. ## Motivation It can be useful for a package to be able to treat comment characters abstractly. My specific need is for the resolution of `BLOCK_COMMENT_START` and similar variables for LSP snippets. There is currently no way I know of to resolve this without hardcoding it for each language. ## Explanation This RFC is to have both line and block delims a property of the grammar. E.g., ```cson # javascript.cson comments: line: '//' start: '/*' end: '*/' ``` The lack of a property would indicate the grammar does not have it (e.g., HTML would omit the `line` property), or that a grammar has not been updated to comply with this spec. If the latter, it may be possible to partially extract this information from the existing implementations (e.g., even now we can tell that JS has `//` for line comment and HTML has `` for block comments). This is similar to the current Tree-sitter grammars, but they currently mix line and block in the `start` key depending on if the language has line comments (so HTML has `start: ''`, but JS has `start: '//'`). ## Drawbacks Many community grammars would not get this property added to them. However, this feature can be considered a strict enhancement of the current status, and non compliant grammars can be accounted for. E.g., if a grammar still declares `start: '//'` but doesn't have an `end` property, then it can be reinterpreted as a line delim. Additionally, users would be quick to raise an issue here (:frowning_face:) or on the language repo (:slightly_smiling_face:) if they are trying to use a feature that relies on this and it doesn't work. ## Rationale and alternatives #### Why is this approach the best in the space of possible approaches? Tying all language specific data to the language file makes intuitive sense. This is stuff that will not change based on what the user wants (and already is tied directly to Tree-sitter language files). #### What other approaches have been considered and what is the rationale for not choosing them? It's possible to use the settings approach like for TextMate grammars. I find this unnecessarily separated though, especially for something like comment delims which shouldn't rely on what the user fancies. However, I'm not set on requiring the TextMate grammars to have it in the file (doing so would require an update on the First mate side too*). It can still work in the settings file. This would also support the possible language that has multiple delim characters (if it exists), letting the user set their choice. \* Maybe First mate should just add all properties from the file to the grammar it constructs, instead of a whitelist? It would save headaches around enhancing future grammar features. #### What is the impact of not doing this? Getting the snippet variables working would require hard coding them for each language, which is impossible to do completely. ## Unresolved questions #### What unresolved questions do you expect to resolve through the RFC process before this gets merged? #### What unresolved questions do you expect to resolve through the implementation of this feature before it is released in a new version of Atom? #### What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? What I would like is then for public TextEditor methods `getCommentDelims` and `getCommentDelimsForPoint`, which returns all the correct delims for the root grammar, or the one at the given point (accounting for embedded grammars ... though could be weird when the embedded grammar is only something like TODO or SQL syntax). However, this future enhancement is not necessary for the current RFC. This RFC is about getting comment delim information tied to the Grammar object and is independant of any attempt to handle this information. ================================================ FILE: docs/rfcs/005-pretranspile.md ================================================ # Pre-transpiled Atom packages ## Status Proposed ## Summary This feature will enable package authors to use conventional npm tooling and package.json conventions to take advantage of JavaScript transpilers like Babel or TypeScript. ## Motivation Transpiling packages on _publish_ rather than _load_ will have great benefits for package authors: * Standard `npm` tooling like `prepare` scripts will work for apm packages exactly as they work for npm packages. This will remove the need for custom transpiler pipeline modules like [atom-babel6-transpiler](https://github.com/atom/atom-babel6-transpiler) or [atom-typescript-transpiler](https://github.com/smhxx/atom-ts-transpiler) with their own, independent documentation, configuration and setup. * Packages can move transpiler-related dependencies to `devDependencies` and trim installation bloat substantially. (as a data point, the TypeScript compiler is 30MB.) * First-time package load will no longer take a hit from transpiling all of the source into the cache. ## Explanation ### Package publishing During the `apm publish` call, apm will invoke [`npm pack`](https://docs.npmjs.com/cli/pack) to run all standard npm lifecycle hooks and prepare a `.tar.gz` file. apm then uploads the `.tar.gz` file to atom.io, which uploads it to an S3 bucket. The `npm version` call will still be skipped if the `--tag` is provided, so manual publishing with `apm publish --tag` will still work as it does today. ### Package installation When a user installs a package from atom.io, atom.io first checks to see if it has a precompiled tarball in its S3 bucket. If one is found, the artifact's public URL is returned as the `dist` field in the [API response](https://flight-manual.atom.io/atom-server-side-apis/sections/atom-package-server-api/#get-apipackagespackage_nameversionsversion_name). Otherwise, the existing logic is used to return the GitHub tag tarball URL that's returned now. ## Drawbacks Doing this makes installing a package in production more different than loading it during development. This increases the number of variables that can cause issues between local development and the production of an `apm publish` artifact, like tweaking your `.npmignore` file properly. ## Rationale and alternatives _Alternative: publish packages to Actual Npm.org._ We could identify Atom packages in the npm registry by the `engine` field we already use, which should keep regular npm from installing it by mistake. The downsides here are: * It becomes harder to search for _just_ Atom packages; we'd have to hack npm search a bit. * "Starring" would likely break. * The transition path for existing users of apm and atom.io is not as smooth. * Easier to typo `apm` and `npm` commands and have an undesirable outcome. ## Unresolved questions Do we want to deprecate transpilation-on-demand for local development, as well? It may add a bit of friction for package development, but transpilers like TypeScript tend to offer a `--watch` option to transpile live, and it would let us eliminate a lot of complexity in the way Atom loads JavaScript. ================================================ FILE: docs/rfcs/005-scope-naming.md ================================================ # Semantic scope naming ## Status Proposed ## Summary When deciding which scopes to apply, built-in grammars should be guided only by what would be useful to annotate. A suggested scope addition should be assessed by the semantic value it adds. It not be rejected if its _only_ drawback is that it would result in undesirable syntax highlighting for one of Atom’s built-in syntax themes. ## Motivation Tree-sitter grammars are a unique opportunity to deliver more accurate scoping. They can identify constructs that would’ve been too ambiguous for a TM-style grammar. Scopes themselves are immensely powerful; they’re hooks that allow weirdos like myself to customize Atom to my exact specifications. Not only for syntax themes, either; I’ve got lots of commands that behave in different ways based on the surrounding scope. The richer the scope descriptor, the better. (Within reasonable bounds, of course.) ## Explanation I think this is best illustrated by example. [Here’s a ticket](https://github.com/atom/language-javascript/issues/615) from `language-javascript` about syntax highlighting of imports. For example: ```js import { foo } from "thing"; ``` For reference, the `language-babel` grammar scopes `foo` as `variable.other.readwrite.js`. I’d probably opt for something like `variable.import`; others may want to put it into the `support` namespace. There’s actually little cross-language consensus here. But right now, that `foo` doesn’t have _any_ scope name applied in the tree-sitter JavaScript grammar, and this is by design. The explanation, as stated in the ticket, is that Atom wants to avoid marking a variable with a certain color if it doesn’t have the capability of making it the same color _throughout_ the document, across its various usages. This is a fine design goal; I don’t think @maxbrunsfeld is wrong to argue for it. But I’d suggest that it isn’t a design goal for `language-javascript` or any other grammar; it’s a design goal for a _syntax theme_, and a grammar should not refrain from applying scopes in order to satisfy the design goals of a specific syntax theme. This isn’t just a beard-stroking nitpick on my part. I can think of a handful of reasons why someone might want to be able to spot import names at a glance. It’s reasonable for someone to want `foo` in the example above to remain the same color throughout the file. It’s also reasonable, I think, to want `foo` to have a special color on the line where it’s introduced. Or to communicate that this token has special behavior — as it would if you have the [js-hyperclick](https://atom.io/packages/js-hyperclick) package installed and are in the habit of cmd-clicking package names to jump to the files where they’re defined. I don’t mind that the One Dark syntax theme doesn’t want to give `foo` a special color; I mind that its decision is also binding on _all possible_ syntax themes. If that scope name is present, and it’s undesirable to a syntax theme, that theme can apply the overrides necessary to ignore it. But if that scope name is missing altogether, that constrains _all_ syntax themes, and I’m unable to write a syntax theme that behaves differently. Thus, here’s what I propose: If an issue or PR proposes adding a scope and can justify its presence somehow — including the goal of parity with its non-tree-sitter predecessor — an answer of “no, because the built-in syntax themes don’t want to highlight it that way” should become “OK, but only if someone does the associated work to ensure no visual regressions in the built-in syntax themes.” That someone could be the PR’s author or anyone else who has an interest in getting it landed. This is tricky, of course — not only the coordination of PRs across packages, but also the need to apply overrides to all six (is it six? I think it’s six) of the built-in syntax themes. If all built-in themes are going to share an austere philosophy ([and it seems like that’s the plan](https://github.com/atom/atom/pull/18383#issuecomment-435460854)), then perhaps it makes sense for them to start sharing a core set of contextual rules. The only difference between them would be the specific color choices that they make. ## Drawbacks The drawback is that what I’m suggesting is a lot of work. I don’t propose it lightly; I propose it because it strikes me as the least bad of all available choices. ## Rationale and alternatives And what are those other choices? 1. The status quo, in which built-in grammars (like `language-javascript`’s tree-sitter grammar) are developed with goals that are tightly coupled to the goals of syntax themes. I think this would be a tragic lost opportunity. The fact that the `tree-sitter-javascript` parser groks ES6 and JSX means that lots of people no longer have to rely on a third-party grammar like `language-babel`. If I can’t get my syntax highlighting the way I want it because the built-in grammar applies scopes too sparsely, then my only recourse is to write my own tree-sitter grammar that adds in the mappings I want. That’s easier than writing a TM-style grammar, but it still involves some portion of the community dedicating their efforts to an effective fork of `language-javascript`, and for far more mundane reasons than the fork that produced `language-babel` in the first place. 2. A suggestion made by @Ben3eeE in [the issue that inspired this RFC](https://github.com/atom/language-javascript/issues/649): intentionally picking ornery scope names that don’t have implicit highlighting in the built-in syntax themes. That’s at least a way forward, but I think it abandons a hard-won lesson. TextMate’s attempt to devise a system of semantic scope names has borne quite a bit of fruit. It’s the reason why three major successor editors have signed on to the same conventions. Semantic naming acts as a kind of “middleware” that allows syntax themes and grammars to be unaware of each others’ implementation details. I _could_ write a PR that scopes our `foo` import from above with something like `import-specifier.identifier`, and I still might, but in choosing an arbitrary name I’m once again obligating syntax themes to care about a grammar’s implementation details. 3. Some sort of grand compromise that I don’t have the breadth of experience to envision on my own. I’m hoping for this one, actually. For instance, it occurs to me that the “variables shouldn’t ever be highlighted with different colors across different usages” problem is only a problem in languages where there’s no sigil to mark variables. PHP, Perl, and Less don’t have this problem because all variables begin with a symbol. Maybe the solution is to include some token like `without-sigil` in the scope name, and then the built-in themes can write a rule like `.syntax--without-sigil { color: @mono-1 !important }`. ## Unresolved questions - Ideally, I’d love to have some sort of canonical scope document like [TextMate](https://macromates.com/manual/en/language_grammars#naming_conventions) and [Sublime Text](https://www.sublimetext.com/docs/3/scope_naming.html) have. But the future of TM-style scope naming seems to be up in the air. I think that they’re no less relevant in the era of tree-sitter grammars, but I bet others disagree. - To what extent should tree-sitter grammars be expected to scope documents identically to their TM-style predecessors? Obviously not 100%, or else there’d be no gains. What’s the right balancing test? - Atom has made some infrastructural choices that can complicate how scopes get applied and consumed. Are these permanent? For instance, if I wanted to implement Alternative 2 (as described above), I could choose a scope name like `meta.variable.import`, on the assumption that `meta.`-prefixed scope names won’t have syntax highlighting. But `meta.variable` gets caught by a `.syntax--variable` CSS selector just as much as it would if the scope name began with `variable`. The order and hierarchy implied in the scope name is not actually present. Syntax themes could write selectors more creatively to get around this — e.g., `*[class^="syntax--variable "]` instead of `.syntax--variable` — but I don’t think many do, and I can hardly blame them. Is this a limitation that Atom can evolve its way out of without breaking anything? Or are we stuck with it? ================================================ FILE: dot-atom/.gitignore ================================================ blob-store compile-cache dev storage .apm .node-gyp .npm .atom-socket-secret-* ================================================ FILE: dot-atom/init.coffee ================================================ # Your init script # # Atom will evaluate this file each time a new window is opened. It is run # after packages are loaded/activated and after the previous editor state # has been restored. # # An example hack to log to the console when each text editor is saved. # # atom.workspace.observeTextEditors (editor) -> # editor.onDidSave -> # console.log "Saved! #{editor.getPath()}" ================================================ FILE: dot-atom/keymap.cson ================================================ # Your keymap # # Atom keymaps work similarly to style sheets. Just as style sheets use # selectors to apply styles to elements, Atom keymaps use selectors to associate # keystrokes with events in specific contexts. Unlike style sheets however, # each selector can only be declared once. # # You can create a new keybinding in this file by typing "key" and then hitting # tab. # # Here's an example taken from Atom's built-in keymap: # # 'atom-text-editor': # 'enter': 'editor:newline' # # 'atom-workspace': # 'ctrl-shift-p': 'core:move-up' # 'ctrl-p': 'core:move-down' # # You can find more information about keymaps in these guides: # * http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings # * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/ # # If you're having trouble with your keybindings not working, try the # Keybinding Resolver: `Cmd+.` on macOS and `Ctrl+.` on other platforms. See the # Debugging Guide for more information: # * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings # # This file uses CoffeeScript Object Notation (CSON). # If you are unfamiliar with CSON, you can read more about it in the # Atom Flight Manual: # http://flight-manual.atom.io/using-atom/sections/basic-customization/#configuring-with-cson ================================================ FILE: dot-atom/packages/README.md ================================================ All packages in this directory will be automatically loaded ================================================ FILE: dot-atom/snippets.cson ================================================ # Your snippets # # Atom snippets allow you to enter a simple prefix in the editor and hit tab to # expand the prefix into a larger code block with templated values. # # You can create a new snippet in this file by typing "snip" and then hitting # tab. # # An example CoffeeScript snippet to expand log to console.log: # # '.source.coffee': # 'Console log': # 'prefix': 'log' # 'body': 'console.log $1' # # Each scope (e.g. '.source.coffee' above) can only be declared once. # # This file uses CoffeeScript Object Notation (CSON). # If you are unfamiliar with CSON, you can read more about it in the # Atom Flight Manual: # http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson ================================================ FILE: dot-atom/styles.less ================================================ /* * Your Stylesheet * * This stylesheet is loaded when Atom starts up and is reloaded automatically * when it is changed and saved. * * Add your own CSS or Less to fully customize Atom. * If you are unfamiliar with Less, you can read more about it here: * http://lesscss.org */ /* * Examples * (To see them, uncomment and save) */ // style the background color of the tree view .tree-view { // background-color: whitesmoke; } // style the background and foreground colors on the atom-text-editor-element itself atom-text-editor { // color: white; // background-color: hsl(180, 24%, 12%); } // style UI elements inside atom-text-editor atom-text-editor .cursor { // border-color: red; } ================================================ FILE: exports/atom.js ================================================ const TextBuffer = require('text-buffer'); const { Point, Range } = TextBuffer; const { File, Directory } = require('pathwatcher'); const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); const BufferedNodeProcess = require('../src/buffered-node-process'); const BufferedProcess = require('../src/buffered-process'); const GitRepository = require('../src/git-repository'); const Notification = require('../src/notification'); const { watchPath } = require('../src/path-watcher'); const atomExport = { BufferedNodeProcess, BufferedProcess, GitRepository, Notification, TextBuffer, Point, Range, File, Directory, Emitter, Disposable, CompositeDisposable, watchPath }; // Shell integration is required by both Squirrel and Settings-View if (process.platform === 'win32') { Object.defineProperty(atomExport, 'WinShell', { enumerable: true, get() { return require('../src/main-process/win-shell'); } }); } // The following classes can't be used from a Task handler and should therefore // only be exported when not running as a child node process if (process.type === 'renderer') { atomExport.Task = require('../src/task'); atomExport.TextEditor = require('../src/text-editor'); } module.exports = atomExport; ================================================ FILE: exports/clipboard.js ================================================ module.exports = require('electron').clipboard; const Grim = require('grim'); Grim.deprecate( 'Use `require("electron").clipboard` instead of `require("clipboard")`' ); // Ensure each package that requires this shim causes a deprecation warning delete require.cache[__filename]; ================================================ FILE: exports/ipc.js ================================================ module.exports = require('electron').ipcRenderer; const Grim = require('grim'); Grim.deprecate( 'Use `require("electron").ipcRenderer` instead of `require("ipc")`' ); // Ensure each package that requires this shim causes a deprecation warning delete require.cache[__filename]; ================================================ FILE: exports/remote.js ================================================ module.exports = require('electron').remote; const Grim = require('grim'); Grim.deprecate( 'Use `require("electron").remote` instead of `require("remote")`' ); // Ensure each package that requires this shim causes a deprecation warning delete require.cache[__filename]; ================================================ FILE: exports/shell.js ================================================ module.exports = require('electron').shell; const Grim = require('grim'); Grim.deprecate('Use `require("electron").shell` instead of `require("shell")`'); // Ensure each package that requires this shim causes a deprecation warning delete require.cache[__filename]; ================================================ FILE: exports/web-frame.js ================================================ module.exports = require('electron').webFrame; const Grim = require('grim'); Grim.deprecate( 'Use `require("electron").webFrame` instead of `require("web-frame")`' ); // Ensure each package that requires this shim causes a deprecation warning delete require.cache[__filename]; ================================================ FILE: keymaps/base.cson ================================================ 'atom-text-editor': # Platform Bindings 'home': 'editor:move-to-first-character-of-line' 'end': 'editor:move-to-end-of-screen-line' 'shift-home': 'editor:select-to-first-character-of-line' 'shift-end': 'editor:select-to-end-of-line' 'atom-text-editor:not([mini])': # Atom Specific 'ctrl-shift-c': 'editor:copy-path' 'alt-up': 'editor:select-larger-syntax-node' 'alt-down': 'editor:select-smaller-syntax-node' # Sublime Parity 'tab': 'editor:indent' 'enter': 'editor:newline' 'shift-tab': 'editor:outdent-selected-rows' 'ctrl-shift-k': 'editor:delete-line' '.select-list atom-text-editor[mini]': 'enter': 'core:confirm' '.tool-panel.panel-left, .tool-panel.panel-right': 'escape': 'tool-panel:unfocus' 'atom-text-editor !important, atom-text-editor[mini] !important': 'escape': 'editor:consolidate-selections' # Allow standard input fields to work correctly 'body .native-key-bindings': 'tab': 'core:focus-next' 'shift-tab': 'core:focus-previous' 'enter': 'native!' 'backspace': 'native!' 'shift-backspace': 'native!' 'delete': 'native!' 'up': 'native!' 'down': 'native!' 'shift-up': 'native!' 'shift-down': 'native!' 'alt-up': 'native!' 'alt-down': 'native!' 'alt-shift-up': 'native!' 'alt-shift-down': 'native!' 'cmd-up': 'native!' 'cmd-down': 'native!' 'cmd-shift-up': 'native!' 'cmd-shift-down': 'native!' 'ctrl-up': 'native!' 'ctrl-down': 'native!' 'ctrl-shift-up': 'native!' 'ctrl-shift-down': 'native!' 'left': 'native!' 'right': 'native!' 'shift-left': 'native!' 'shift-right': 'native!' 'alt-left': 'native!' 'alt-right': 'native!' 'alt-shift-left': 'native!' 'alt-shift-right': 'native!' 'cmd-left': 'native!' 'cmd-right': 'native!' 'cmd-shift-left': 'native!' 'cmd-shift-right': 'native!' 'ctrl-left': 'native!' 'ctrl-right': 'native!' 'ctrl-shift-left': 'native!' 'ctrl-shift-right': 'native!' 'ctrl-b': 'native!' 'ctrl-f': 'native!' 'ctrl-shift-f': 'native!' 'ctrl-shift-b': 'native!' 'ctrl-h': 'native!' 'ctrl-d': 'native!' ================================================ FILE: keymaps/darwin.cson ================================================ 'body': # Apple specific 'cmd-q': 'application:quit' 'cmd-h': 'application:hide' 'cmd-alt-h': 'application:hide-other-applications' 'cmd-m': 'application:minimize' 'alt-cmd-ctrl-m': 'application:zoom' 'ctrl-p': 'core:move-up' 'ctrl-n': 'core:move-down' 'ctrl-b': 'core:move-left' 'ctrl-f': 'core:move-right' 'ctrl-shift-p': 'core:select-up' 'ctrl-shift-n': 'core:select-down' 'ctrl-shift-f': 'core:select-right' 'ctrl-shift-b': 'core:select-left' 'ctrl-h': 'core:backspace' 'ctrl-d': 'core:delete' # Atom Specific 'enter': 'core:confirm' 'escape': 'core:cancel' 'up': 'core:move-up' 'down': 'core:move-down' 'ctrl-up': 'core:move-up' 'ctrl-down': 'core:move-down' 'left': 'core:move-left' 'right': 'core:move-right' 'ctrl-alt-cmd-l': 'window:reload' 'alt-cmd-i': 'window:toggle-dev-tools' 'cmd-alt-ctrl-p': 'window:run-package-specs' 'ctrl-shift-left': 'pane:move-item-left' 'ctrl-shift-right': 'pane:move-item-right' # Sublime Parity 'cmd-,': 'application:show-settings' 'cmd-shift-n': 'application:new-window' 'cmd-shift-w': 'window:close' 'cmd-o': 'application:open' 'cmd-shift-o': 'application:add-project-folder' 'cmd-shift-t': 'pane:reopen-closed-item' 'cmd-n': 'application:new-file' 'cmd-s': 'core:save' 'cmd-shift-s': 'core:save-as' 'cmd-alt-s': 'window:save-all' 'cmd-w': 'core:close' 'cmd-ctrl-f': 'window:toggle-full-screen' 'cmd-z': 'core:undo' 'cmd-shift-z': 'core:redo' 'cmd-y': 'core:redo' 'cmd-x': 'core:cut' 'cmd-c': 'core:copy' 'cmd-v': 'core:paste' 'shift-up': 'core:select-up' 'shift-down': 'core:select-down' 'shift-left': 'core:select-left' 'shift-right': 'core:select-right' 'shift-pageup': 'core:select-page-up' 'shift-pagedown': 'core:select-page-down' 'delete': 'core:delete' 'shift-delete': 'core:delete' 'pageup': 'core:page-up' 'pagedown': 'core:page-down' 'backspace': 'core:backspace' 'shift-backspace': 'core:backspace' 'cmd-up': 'core:move-to-top' 'cmd-down': 'core:move-to-bottom' 'cmd-shift-up': 'core:select-to-top' 'cmd-shift-down': 'core:select-to-bottom' 'cmd-{': 'pane:show-previous-item' 'cmd-}': 'pane:show-next-item' 'cmd-alt-left': 'pane:show-previous-item' 'cmd-alt-right': 'pane:show-next-item' 'ctrl-pageup': 'pane:show-previous-item' 'ctrl-pagedown': 'pane:show-next-item' 'ctrl-tab': 'pane:show-next-recently-used-item' 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'ctrl-shift-tab': 'pane:show-previous-recently-used-item' 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'cmd-=': 'window:increase-font-size' 'cmd-+': 'window:increase-font-size' 'cmd-_': 'window:decrease-font-size' 'cmd--': 'window:decrease-font-size' 'cmd-0': 'window:reset-font-size' 'cmd-k up': 'pane:split-up-and-copy-active-item' # Atom Specific 'cmd-k down': 'pane:split-down-and-copy-active-item' # Atom Specific 'cmd-k left': 'pane:split-left-and-copy-active-item' # Atom Specific 'cmd-k right': 'pane:split-right-and-copy-active-item' # Atom Specific 'cmd-k cmd-w': 'pane:close' # Atom Specific 'cmd-k alt-cmd-w': 'pane:close-other-items' # Atom Specific 'cmd-k cmd-p': 'window:focus-previous-pane' 'cmd-k cmd-n': 'window:focus-next-pane' 'cmd-k cmd-up': 'window:focus-pane-above' 'cmd-k cmd-down': 'window:focus-pane-below' 'cmd-k cmd-left': 'window:focus-pane-on-left' 'cmd-k cmd-right': 'window:focus-pane-on-right' 'cmd-1': 'pane:show-item-1' 'cmd-2': 'pane:show-item-2' 'cmd-3': 'pane:show-item-3' 'cmd-4': 'pane:show-item-4' 'cmd-5': 'pane:show-item-5' 'cmd-6': 'pane:show-item-6' 'cmd-7': 'pane:show-item-7' 'cmd-8': 'pane:show-item-8' 'cmd-9': 'pane:show-item-9' 'atom-text-editor': # Platform Bindings 'alt-left': 'editor:move-to-beginning-of-word' 'alt-right': 'editor:move-to-end-of-word' 'alt-shift-left': 'editor:select-to-beginning-of-word' 'alt-shift-right': 'editor:select-to-end-of-word' # Apple Specific 'cmd-backspace': 'editor:delete-to-beginning-of-line' 'cmd-shift-backspace': 'editor:delete-to-beginning-of-line' 'cmd-delete': 'editor:delete-to-end-of-line' 'ctrl-shift-a': 'editor:select-to-first-character-of-line' 'ctrl-shift-e': 'editor:select-to-end-of-line' 'cmd-left': 'editor:move-to-first-character-of-line' 'cmd-right': 'editor:move-to-end-of-screen-line' 'cmd-shift-left': 'editor:select-to-first-character-of-line' 'cmd-shift-right': 'editor:select-to-end-of-line' 'alt-backspace': 'editor:delete-to-beginning-of-word' 'alt-delete': 'editor:delete-to-end-of-word' 'ctrl-a': 'editor:move-to-first-character-of-line' 'ctrl-e': 'editor:move-to-end-of-line' 'ctrl-k': 'editor:cut-to-end-of-line' # Atom Specific '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' 'alt-ctrl-f': 'editor:move-to-next-subword-boundary' 'alt-shift-f': 'editor:select-to-end-of-word' 'alt-ctrl-shift-f': 'editor:select-to-next-subword-boundary' 'alt-b': 'editor:move-to-beginning-of-word' 'alt-ctrl-b': 'editor:move-to-previous-subword-boundary' 'alt-shift-b': 'editor:select-to-beginning-of-word' 'alt-ctrl-shift-b': 'editor:select-to-previous-subword-boundary' 'alt-h': 'editor:delete-to-beginning-of-word' 'alt-ctrl-h': 'editor:delete-to-beginning-of-subword' 'alt-d': 'editor:delete-to-end-of-word' 'alt-ctrl-d': 'editor:delete-to-end-of-subword' # Sublime Parity 'cmd-a': 'core:select-all' 'cmd-alt-p': 'editor:log-cursor-scope' 'cmd-k cmd-u': 'editor:upper-case' 'cmd-k cmd-l': 'editor:lower-case' 'cmd-l': 'editor:select-line' 'ctrl-t': 'editor:transpose' 'ctrl-alt-left': 'editor:move-to-previous-subword-boundary' 'ctrl-alt-right': 'editor:move-to-next-subword-boundary' 'ctrl-alt-shift-left': 'editor:select-to-previous-subword-boundary' 'ctrl-alt-shift-right': 'editor:select-to-next-subword-boundary' 'ctrl-alt-backspace': 'editor:delete-to-beginning-of-subword' 'ctrl-alt-delete': 'editor:delete-to-end-of-subword' 'atom-workspace atom-text-editor:not([mini])': # Atom specific 'alt-cmd-z': 'editor:checkout-head-revision' 'cmd-<': 'editor:scroll-to-cursor' 'alt-cmd-ctrl-f': 'editor:fold-selection' # Sublime Parity 'cmd-enter': 'editor:newline-below' 'cmd-shift-enter': 'editor:newline-above' 'alt-enter': 'editor:newline' 'shift-enter': 'editor:newline' 'cmd-]': 'editor:indent-selected-rows' 'cmd-[': 'editor:outdent-selected-rows' 'ctrl-cmd-up': 'editor:move-line-up' 'ctrl-cmd-down': 'editor:move-line-down' 'cmd-/': 'editor:toggle-line-comments' 'cmd-j': 'editor:join-lines' 'cmd-shift-d': 'editor:duplicate-lines' 'cmd-shift-l': 'editor:split-selections-into-lines' 'ctrl-shift-up': 'editor:add-selection-above' 'ctrl-shift-down': 'editor:add-selection-below' 'cmd-alt-[': 'editor:fold-current-row' 'cmd-alt-]': 'editor:unfold-current-row' 'cmd-alt-{': 'editor:fold-all' # Atom Specific 'cmd-alt-}': 'editor:unfold-all' # Atom Specific 'cmd-k cmd-0': 'editor:unfold-all' 'cmd-k cmd-1': 'editor:fold-at-indent-level-1' 'cmd-k cmd-2': 'editor:fold-at-indent-level-2' 'cmd-k cmd-3': 'editor:fold-at-indent-level-3' 'cmd-k cmd-4': 'editor:fold-at-indent-level-4' 'cmd-k cmd-5': 'editor:fold-at-indent-level-5' 'cmd-k cmd-6': 'editor:fold-at-indent-level-6' 'cmd-k cmd-7': 'editor:fold-at-indent-level-7' 'cmd-k cmd-8': 'editor:fold-at-indent-level-8' 'cmd-k cmd-9': 'editor:fold-at-indent-level-9' 'atom-workspace atom-pane': 'cmd-alt-=': 'pane:increase-size' 'cmd-alt--': 'pane:decrease-size' # Allow standard input fields to work correctly 'body .native-key-bindings': 'cmd-z': 'native!' 'cmd-shift-z': 'native!' 'cmd-x': 'native!' 'cmd-c': 'native!' 'cmd-v': 'native!' ================================================ FILE: keymaps/linux.cson ================================================ 'body': # Atom Specific 'enter': 'core:confirm' 'escape': 'core:cancel' 'up': 'core:move-up' 'down': 'core:move-down' 'left': 'core:move-left' 'right': 'core:move-right' 'ctrl-shift-f5': 'window:reload' 'ctrl-shift-i': 'window:toggle-dev-tools' 'ctrl-shift-y': 'window:run-package-specs' 'ctrl-shift-o': 'application:open-folder' 'ctrl-shift-a': 'application:add-project-folder' 'ctrl-shift-pageup': 'pane:move-item-left' 'ctrl-shift-pagedown': 'pane:move-item-right' 'f11': 'window:toggle-full-screen' 'alt-shift-left': 'editor:move-selection-left' 'alt-shift-right': 'editor:move-selection-right' # Sublime Parity 'ctrl-,': 'application:show-settings' 'ctrl-shift-n': 'application:new-window' 'ctrl-shift-w': 'window:close' 'ctrl-o': 'application:open-file' 'ctrl-q': 'application:quit' 'ctrl-shift-t': 'pane:reopen-closed-item' 'ctrl-n': 'application:new-file' 'ctrl-s': 'core:save' 'ctrl-shift-s': 'core:save-as' 'ctrl-f4': 'core:close' 'ctrl-w': 'core:close' 'ctrl-z': 'core:undo' 'ctrl-y': 'core:redo' 'ctrl-shift-z': 'core:redo' 'ctrl-x': 'core:cut' 'ctrl-c': 'core:copy' 'ctrl-v': 'core:paste' 'ctrl-insert': 'core:copy' 'shift-insert': 'core:paste' 'shift-up': 'core:select-up' 'shift-down': 'core:select-down' 'shift-left': 'core:select-left' 'shift-right': 'core:select-right' 'shift-pageup': 'core:select-page-up' 'shift-pagedown': 'core:select-page-down' 'delete': 'core:delete' 'shift-delete': 'core:cut' 'pageup': 'core:page-up' 'pagedown': 'core:page-down' 'backspace': 'core:backspace' 'shift-backspace': 'core:backspace' 'ctrl-tab': 'pane:show-next-recently-used-item' 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'ctrl-shift-tab': 'pane:show-previous-recently-used-item' 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'ctrl-pageup': 'pane:show-previous-item' 'ctrl-pagedown': 'pane:show-next-item' 'ctrl-up': 'core:move-up' 'ctrl-down': 'core:move-down' 'ctrl-shift-up': 'core:move-up' 'ctrl-shift-down': 'core:move-down' 'ctrl-=': 'window:increase-font-size' 'ctrl-+': 'window:increase-font-size' 'ctrl--': 'window:decrease-font-size' 'ctrl-_': 'window:decrease-font-size' 'ctrl-0': 'window:reset-font-size' 'ctrl-k up': 'pane:split-up-and-copy-active-item' # Atom Specific 'ctrl-k down': 'pane:split-down-and-copy-active-item' # Atom Specific 'ctrl-k left': 'pane:split-left-and-copy-active-item' # Atom Specific 'ctrl-k right': 'pane:split-right-and-copy-active-item' # Atom Specific 'ctrl-k ctrl-w': 'pane:close' # Atom Specific 'ctrl-k ctrl-alt-w': 'pane:close-other-items' # Atom Specific 'ctrl-k ctrl-p': 'window:focus-previous-pane' 'ctrl-k ctrl-n': 'window:focus-next-pane' 'ctrl-k ctrl-up': 'window:focus-pane-above' 'ctrl-k ctrl-down': 'window:focus-pane-below' 'ctrl-k ctrl-left': 'window:focus-pane-on-left' 'ctrl-k ctrl-right': 'window:focus-pane-on-right' 'alt-1': 'pane:show-item-1' 'alt-2': 'pane:show-item-2' 'alt-3': 'pane:show-item-3' 'alt-4': 'pane:show-item-4' 'alt-5': 'pane:show-item-5' 'alt-6': 'pane:show-item-6' 'alt-7': 'pane:show-item-7' 'alt-8': 'pane:show-item-8' 'alt-9': 'pane:show-item-9' 'atom-text-editor': # Platform Bindings 'ctrl-left': 'editor:move-to-beginning-of-word' 'ctrl-right': 'editor:move-to-end-of-word' 'ctrl-shift-left': 'editor:select-to-beginning-of-word' 'ctrl-shift-right': 'editor:select-to-end-of-word' 'ctrl-backspace': 'editor:delete-to-beginning-of-word' 'ctrl-delete': 'editor:delete-to-end-of-word' 'ctrl-home': 'core:move-to-top' 'ctrl-end': 'core:move-to-bottom' 'ctrl-shift-home': 'core:select-to-top' 'ctrl-shift-end': 'core:select-to-bottom' 'alt-left': 'editor:move-to-previous-subword-boundary' 'alt-right': 'editor:move-to-next-subword-boundary' 'alt-shift-left': 'editor:select-to-previous-subword-boundary' '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' 'ctrl-k ctrl-u': 'editor:upper-case' 'ctrl-k ctrl-l': 'editor:lower-case' 'ctrl-l': 'editor:select-line' 'atom-workspace atom-text-editor:not([mini])': # Atom specific 'ctrl-<': 'editor:scroll-to-cursor' 'ctrl-alt-shift-[': 'editor:fold-selection' # Sublime Parity 'ctrl-enter': 'editor:newline-below' 'ctrl-shift-enter': 'editor:newline-above' 'ctrl-]': 'editor:indent-selected-rows' 'ctrl-[': 'editor:outdent-selected-rows' 'ctrl-up': 'editor:move-line-up' 'ctrl-down': 'editor:move-line-down' 'ctrl-/': 'editor:toggle-line-comments' 'ctrl-j': 'editor:join-lines' 'ctrl-shift-d': 'editor:duplicate-lines' 'alt-shift-up': 'editor:add-selection-above' 'alt-shift-down': 'editor:add-selection-below' 'ctrl-alt-[': 'editor:fold-current-row' 'ctrl-alt-]': 'editor:unfold-current-row' 'ctrl-alt-{': 'editor:fold-all' # Atom Specific 'ctrl-alt-}': 'editor:unfold-all' # Atom Specific 'ctrl-k ctrl-0': 'editor:unfold-all' 'ctrl-k ctrl-1': 'editor:fold-at-indent-level-1' 'ctrl-k ctrl-2': 'editor:fold-at-indent-level-2' 'ctrl-k ctrl-3': 'editor:fold-at-indent-level-3' 'ctrl-k ctrl-4': 'editor:fold-at-indent-level-4' 'ctrl-k ctrl-5': 'editor:fold-at-indent-level-5' 'ctrl-k ctrl-6': 'editor:fold-at-indent-level-6' 'ctrl-k ctrl-7': 'editor:fold-at-indent-level-7' 'ctrl-k ctrl-8': 'editor:fold-at-indent-level-8' 'ctrl-k ctrl-9': 'editor:fold-at-indent-level-9' 'atom-workspace atom-pane': 'ctrl-alt-=': 'pane:increase-size' 'ctrl-alt--': 'pane:decrease-size' # Allow standard input fields to work correctly 'body .native-key-bindings': 'ctrl-z': 'native!' 'ctrl-shift-z': 'native!' 'ctrl-x': 'native!' 'ctrl-c': 'native!' 'ctrl-v': 'native!' ================================================ FILE: keymaps/win32.cson ================================================ 'body': # Platform Bindings 'ctrl-pageup': 'pane:show-previous-item' 'ctrl-pagedown': 'pane:show-next-item' # Atom Specific 'enter': 'core:confirm' 'escape': 'core:cancel' 'up': 'core:move-up' 'down': 'core:move-down' 'ctrl-up': 'core:move-up' 'ctrl-down': 'core:move-down' 'left': 'core:move-left' 'right': 'core:move-right' 'ctrl-shift-f5': 'window:reload' 'ctrl-shift-i': 'window:toggle-dev-tools' 'ctrl-shift-y': 'window:run-package-specs' 'ctrl-shift-o': 'application:open-folder' 'ctrl-shift-a': 'application:add-project-folder' 'ctrl-shift-left': 'pane:move-item-left' 'ctrl-shift-right': 'pane:move-item-right' 'f11': 'window:toggle-full-screen' 'alt-shift-left': 'editor:move-selection-left' 'alt-shift-right': 'editor:move-selection-right' # Sublime Parity 'ctrl-,': 'application:show-settings' 'ctrl-shift-n': 'application:new-window' 'ctrl-shift-w': 'window:close' 'ctrl-o': 'application:open-file' 'ctrl-shift-t': 'pane:reopen-closed-item' 'ctrl-n': 'application:new-file' 'ctrl-s': 'core:save' 'ctrl-shift-s': 'core:save-as' 'ctrl-f4': 'core:close' 'ctrl-w': 'core:close' 'ctrl-z': 'core:undo' 'ctrl-shift-z': 'core:redo' 'ctrl-y': 'core:redo' 'shift-delete': 'core:cut' 'ctrl-insert': 'core:copy' 'shift-insert': 'core:paste' 'ctrl-x': 'core:cut' 'ctrl-c': 'core:copy' 'ctrl-v': 'core:paste' 'shift-up': 'core:select-up' 'shift-down': 'core:select-down' 'shift-left': 'core:select-left' 'shift-right': 'core:select-right' 'shift-pageup': 'core:select-page-up' 'shift-pagedown': 'core:select-page-down' 'delete': 'core:delete' 'pageup': 'core:page-up' 'pagedown': 'core:page-down' 'backspace': 'core:backspace' 'shift-backspace': 'core:backspace' 'ctrl-tab': 'pane:show-next-recently-used-item' 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'ctrl-shift-tab': 'pane:show-previous-recently-used-item' 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'ctrl-pageup': 'pane:show-previous-item' 'ctrl-pagedown': 'pane:show-next-item' 'ctrl-shift-up': 'core:move-up' 'ctrl-shift-down': 'core:move-down' 'ctrl-alt-up': 'editor:add-selection-above' 'ctrl-alt-down': 'editor:add-selection-below' 'ctrl-=': 'window:increase-font-size' 'ctrl-+': 'window:increase-font-size' 'ctrl--': 'window:decrease-font-size' 'ctrl-_': 'window:decrease-font-size' 'ctrl-0': 'window:reset-font-size' 'ctrl-k up': 'pane:split-up-and-copy-active-item' # Atom Specific 'ctrl-k down': 'pane:split-down-and-copy-active-item' # Atom Specific 'ctrl-k left': 'pane:split-left-and-copy-active-item' # Atom Specific 'ctrl-k right': 'pane:split-right-and-copy-active-item' # Atom Specific 'ctrl-k ctrl-w': 'pane:close' # Atom Specific 'ctrl-k ctrl-alt-w': 'pane:close-other-items' # Atom Specific 'ctrl-k ctrl-p': 'window:focus-previous-pane' 'ctrl-k ctrl-n': 'window:focus-next-pane' 'ctrl-k ctrl-up': 'window:focus-pane-above' 'ctrl-k ctrl-down': 'window:focus-pane-below' 'ctrl-k ctrl-left': 'window:focus-pane-on-left' 'ctrl-k ctrl-right': 'window:focus-pane-on-right' 'alt-1': 'pane:show-item-1' 'alt-2': 'pane:show-item-2' 'alt-3': 'pane:show-item-3' 'alt-4': 'pane:show-item-4' 'alt-5': 'pane:show-item-5' 'alt-6': 'pane:show-item-6' 'alt-7': 'pane:show-item-7' 'alt-8': 'pane:show-item-8' 'alt-9': 'pane:show-item-9' 'atom-text-editor': # Platform Bindings 'ctrl-left': 'editor:move-to-beginning-of-word' 'ctrl-right': 'editor:move-to-end-of-word' 'ctrl-shift-left': 'editor:select-to-beginning-of-word' 'ctrl-shift-right': 'editor:select-to-end-of-word' 'ctrl-backspace': 'editor:delete-to-beginning-of-word' 'ctrl-delete': 'editor:delete-to-end-of-word' 'ctrl-home': 'core:move-to-top' 'ctrl-end': 'core:move-to-bottom' 'ctrl-shift-home': 'core:select-to-top' 'ctrl-shift-end': 'core:select-to-bottom' 'alt-left': 'editor:move-to-previous-subword-boundary' 'alt-right': 'editor:move-to-next-subword-boundary' 'alt-shift-left': 'editor:select-to-previous-subword-boundary' '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' 'ctrl-k ctrl-u': 'editor:upper-case' 'ctrl-k ctrl-l': 'editor:lower-case' 'ctrl-l': 'editor:select-line' 'atom-workspace atom-text-editor:not([mini])': # Atom specific 'ctrl-<': 'editor:scroll-to-cursor' 'ctrl-alt-shift-[': 'editor:fold-selection' # Sublime Parity 'ctrl-enter': 'editor:newline-below' 'ctrl-shift-enter': 'editor:newline-above' 'ctrl-]': 'editor:indent-selected-rows' 'ctrl-[': 'editor:outdent-selected-rows' 'ctrl-up': 'editor:move-line-up' 'ctrl-down': 'editor:move-line-down' 'ctrl-/': 'editor:toggle-line-comments' 'ctrl-j': 'editor:join-lines' 'ctrl-shift-d': 'editor:duplicate-lines' 'ctrl-alt-[': 'editor:fold-current-row' 'ctrl-alt-]': 'editor:unfold-current-row' 'ctrl-alt-{': 'editor:fold-all' # Atom Specific 'ctrl-alt-}': 'editor:unfold-all' # Atom Specific 'ctrl-k ctrl-0': 'editor:unfold-all' 'ctrl-k ctrl-1': 'editor:fold-at-indent-level-1' 'ctrl-k ctrl-2': 'editor:fold-at-indent-level-2' 'ctrl-k ctrl-3': 'editor:fold-at-indent-level-3' 'ctrl-k ctrl-4': 'editor:fold-at-indent-level-4' 'ctrl-k ctrl-5': 'editor:fold-at-indent-level-5' 'ctrl-k ctrl-6': 'editor:fold-at-indent-level-6' 'ctrl-k ctrl-7': 'editor:fold-at-indent-level-7' 'ctrl-k ctrl-8': 'editor:fold-at-indent-level-8' 'ctrl-k ctrl-9': 'editor:fold-at-indent-level-9' 'atom-workspace atom-pane': 'ctrl-alt-=': 'pane:increase-size' 'ctrl-alt--': 'pane:decrease-size' # Allow standard input fields to work correctly 'body .native-key-bindings': 'ctrl-z': 'native!' 'ctrl-shift-z': 'native!' 'ctrl-x': 'native!' 'ctrl-c': 'native!' 'ctrl-v': 'native!' ================================================ FILE: menus/darwin.cson ================================================ 'menu': [ { label: 'Atom' submenu: [ { label: 'About Atom', command: 'application:about' } { label: 'View License', command: 'application:open-license' } { label: 'VERSION', enabled: false } { label: 'Restart and Install Update', command: 'application:install-update', visible: false} { label: 'Check for Update', command: 'application:check-for-update', visible: false} { label: 'Checking for Update', enabled: false, visible: false} { label: 'Downloading Update', enabled: false, visible: false} { type: 'separator' } { label: 'Preferences…', command: 'application:show-settings' } { type: 'separator' } { label: 'Config…', command: 'application:open-your-config' } { label: 'Init Script…', command: 'application:open-your-init-script' } { label: 'Keymap…', command: 'application:open-your-keymap' } { label: 'Snippets…', command: 'application:open-your-snippets' } { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } { label: 'Install Shell Commands', command: 'window:install-shell-commands' } { type: 'separator' } { label: 'Services', role: 'services', submenu: [] } { type: 'separator' } { label: 'Hide Atom', command: 'application:hide' } { label: 'Hide Others', command: 'application:hide-other-applications' } { label: 'Show All', command: 'application:unhide-all-applications' } { type: 'separator' } { label: 'Quit Atom', command: 'application:quit' } ] } { label: 'File' submenu: [ { label: 'New Window', command: 'application:new-window' } { label: 'New File', command: 'application:new-file' } { label: 'Open…', command: 'application:open' } { label: 'Add Project Folder…', command: 'application:add-project-folder' } { label: 'Reopen Project', submenu: [ { label: 'Clear Project History', command: 'application:clear-project-history' } { type: 'separator' } ] } { label: 'Reopen Last Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Save', command: 'core:save' } { label: 'Save As…', command: 'core:save-as' } { label: 'Save All', command: 'window:save-all' } { type: 'separator' } { label: 'Close Tab', command: 'core:close' } { label: 'Close Pane', command: 'pane:close' } { label: 'Close Window', command: 'window:close' } ] } { label: 'Edit' submenu: [ { label: 'Undo', command: 'core:undo' } { label: 'Redo', command: 'core:redo' } { type: 'separator' } { label: 'Cut', command: 'core:cut' } { 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' } { label: 'Lines', submenu: [ { label: 'Indent', command: 'editor:indent-selected-rows' } { label: 'Outdent', command: 'editor:outdent-selected-rows' } { label: 'Auto Indent', command: 'editor:auto-indent' } { type: 'separator' } { label: 'Move Line Up', command: 'editor:move-line-up' } { label: 'Move Line Down', command: 'editor:move-line-down' } { label: 'Duplicate Lines', command: 'editor:duplicate-lines' } { label: 'Delete Line', command: 'editor:delete-line' } { label: 'Join Lines', command: 'editor:join-lines' } ] } { label: 'Columns', submenu: [ { label: 'Move Selection Left', command: 'editor:move-selection-left' } { label: 'Move Selection Right', command: 'editor:move-selection-right' } ] } { label: 'Text', submenu: [ { label: 'Upper Case', command: 'editor:upper-case' } { label: 'Lower Case', command: 'editor:lower-case' } { type: 'separator' } { label: 'Delete to End of Word', command: 'editor:delete-to-end-of-word' } { label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' } { label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' } { label: 'Delete Line', command: 'editor:delete-line' } { type: 'separator' } { label: 'Transpose', command: 'editor:transpose' } ] } { label: 'Folding', submenu: [ { label: 'Fold', command: 'editor:fold-current-row' } { label: 'Unfold', command: 'editor:unfold-current-row' } { label: 'Fold All', command: 'editor:fold-all' } { label: 'Unfold All', command: 'editor:unfold-all' } { type: 'separator' } { label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' } { label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' } { label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' } { label: 'Fold Level 4', command: 'editor:fold-at-indent-level-4' } { label: 'Fold Level 5', command: 'editor:fold-at-indent-level-5' } { label: 'Fold Level 6', command: 'editor:fold-at-indent-level-6' } { label: 'Fold Level 7', command: 'editor:fold-at-indent-level-7' } { label: 'Fold Level 8', command: 'editor:fold-at-indent-level-8' } { label: 'Fold Level 9', command: 'editor:fold-at-indent-level-9' } ] } ] } { label: 'View' submenu: [ { label: 'Toggle Full Screen', command: 'window:toggle-full-screen' } { label: 'Panes' submenu: [ { label: 'Split Up', command: 'pane:split-up-and-copy-active-item' } { label: 'Split Down', command: 'pane:split-down-and-copy-active-item' } { label: 'Split Left', command: 'pane:split-left-and-copy-active-item' } { label: 'Split Right', command: 'pane:split-right-and-copy-active-item' } { type: 'separator' } { label: 'Focus Next Pane', command: 'window:focus-next-pane' } { label: 'Focus Previous Pane', command: 'window:focus-previous-pane' } { type: 'separator' } { label: 'Focus Pane Above', command: 'window:focus-pane-above' } { label: 'Focus Pane Below', command: 'window:focus-pane-below' } { label: 'Focus Pane On Left', command: 'window:focus-pane-on-left' } { label: 'Focus Pane On Right', command: 'window:focus-pane-on-right' } { type: 'separator' } { label: 'Close Pane', command: 'pane:close' } ] } { label: 'Developer' submenu: [ { label: 'Open In Dev Mode…', command: 'application:open-dev' } { label: 'Reload Window', command: 'window:reload' } { label: 'Run Package Specs', command: 'window:run-package-specs' } { label: 'Run Benchmarks', command: 'window:run-benchmarks' } { label: 'Toggle Developer Tools', command: 'window:toggle-dev-tools' } ] } { type: 'separator' } { label: 'Increase Font Size', command: 'window:increase-font-size' } { label: 'Decrease Font Size', command: 'window:decrease-font-size' } { label: 'Reset Font Size', command: 'window:reset-font-size' } { type: 'separator' } { label: 'Toggle Soft Wrap', command: 'editor:toggle-soft-wrap' } ] } { label: 'Selection' submenu: [ { label: 'Add Selection Above', command: 'editor:add-selection-above' } { label: 'Add Selection Below', command: 'editor:add-selection-below' } { label: 'Single Selection', command: 'editor:consolidate-selections'} { label: 'Split into Lines', command: 'editor:split-selections-into-lines'} { type: 'separator' } { label: 'Select to Top', command: 'core:select-to-top' } { label: 'Select to Bottom', command: 'core:select-to-bottom' } { type: 'separator' } { label: 'Select Line', command: 'editor:select-line' } { label: 'Select Word', command: 'editor:select-word' } { label: 'Select to Beginning of Word', command: 'editor:select-to-beginning-of-word' } { label: 'Select to Beginning of Line', command: 'editor:select-to-beginning-of-line' } { label: 'Select to First Character of Line', command: 'editor:select-to-first-character-of-line' } { label: 'Select to End of Word', command: 'editor:select-to-end-of-word' } { label: 'Select to End of Line', command: 'editor:select-to-end-of-line' } ] } { label: 'Find' submenu: [] } { label: 'Packages' submenu: [] } { label: 'Window' role: 'window' submenu: [ { label: 'Minimize', command: 'application:minimize' } { label: 'Zoom', command: 'application:zoom' } { type: 'separator' } { label: 'Bring All to Front', command: 'application:bring-all-windows-to-front' } ] } { label: 'Help' role: 'help' submenu: [ { label: 'Terms of Use', command: 'application:open-terms-of-use' } { label: 'Documentation', command: 'application:open-documentation' } { label: 'Frequently Asked Questions', command: 'application:open-faq' } { type: 'separator' } { label: 'Community Discussions', command: 'application:open-discussions' } { label: 'Report Issue', command: 'application:report-issue' } { label: 'Search Issues', command: 'application:search-issues' } { type: 'separator' } ] } ] 'context-menu': 'atom-text-editor, .overlayer': [ {label: 'Undo', command: 'core:undo'} {label: 'Redo', command: 'core:redo'} {type: 'separator'} {label: 'Cut', command: 'core:cut'} {label: 'Copy', command: 'core:copy'} {label: 'Paste', command: 'core:paste'} {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] 'atom-pane': [ {type: 'separator'} {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] ================================================ FILE: menus/linux.cson ================================================ 'menu': [ { label: '&File' submenu: [ { label: 'New &Window', command: 'application:new-window' } { label: '&New File', command: 'application:new-file' } { label: '&Open File…', command: 'application:open-file' } { label: 'Open Folder…', command: 'application:open-folder' } { label: 'Add Project Folder…', command: 'application:add-project-folder' } { label: 'Reopen Project', submenu: [ { label: 'Clear Project History', command: 'application:clear-project-history' } { type: 'separator' } ] } { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: '&Save', command: 'core:save' } { label: 'Save &As…', command: 'core:save-as' } { label: 'Save A&ll', command: 'window:save-all' } { type: 'separator' } { label: '&Close Tab', command: 'core:close' } { label: 'Close &Pane', command: 'pane:close' } { label: 'Clos&e Window', command: 'window:close' } { type: 'separator' } { label: 'Quit', command: 'application:quit' } ] } { label: '&Edit' submenu: [ { label: '&Undo', command: 'core:undo' } { label: '&Redo', command: 'core:redo' } { type: 'separator' } { label: '&Cut', command: 'core:cut' } { 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' } { label: 'Lines', submenu: [ { label: '&Indent', command: 'editor:indent-selected-rows' } { label: '&Outdent', command: 'editor:outdent-selected-rows' } { label: '&Auto Indent', command: 'editor:auto-indent' } { type: 'separator' } { label: 'Move Line &Up', command: 'editor:move-line-up' } { label: 'Move Line &Down', command: 'editor:move-line-down' } { label: 'Du&plicate Lines', command: 'editor:duplicate-lines' } { label: 'D&elete Line', command: 'editor:delete-line' } { label: '&Join Lines', command: 'editor:join-lines' } ] } { label: 'Columns', submenu: [ { label: 'Move Selection &Left', command: 'editor:move-selection-left' } { label: 'Move Selection &Right', command: 'editor:move-selection-right' } ] } { label: 'Text', submenu: [ { label: '&Upper Case', command: 'editor:upper-case' } { label: '&Lower Case', command: 'editor:lower-case' } { type: 'separator' } { label: 'Delete to End of &Word', command: 'editor:delete-to-end-of-word' } { label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' } { label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' } { label: '&Delete Line', command: 'editor:delete-line' } { type: 'separator' } { label: '&Transpose', command: 'editor:transpose' } ] } { label: 'Folding', submenu: [ { label: '&Fold', command: 'editor:fold-current-row' } { label: '&Unfold', command: 'editor:unfold-current-row' } { label: 'Fol&d All', command: 'editor:fold-all' } { label: 'Unfold &All', command: 'editor:unfold-all' } { type: 'separator' } { label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' } { label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' } { label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' } { label: 'Fold Level 4', command: 'editor:fold-at-indent-level-4' } { label: 'Fold Level 5', command: 'editor:fold-at-indent-level-5' } { label: 'Fold Level 6', command: 'editor:fold-at-indent-level-6' } { label: 'Fold Level 7', command: 'editor:fold-at-indent-level-7' } { label: 'Fold Level 8', command: 'editor:fold-at-indent-level-8' } { label: 'Fold Level 9', command: 'editor:fold-at-indent-level-9' } ] } { type: 'separator' } { label: '&Preferences', command: 'application:show-settings' } { type: 'separator' } { label: 'Config…', command: 'application:open-your-config' } { label: 'Init Script…', command: 'application:open-your-init-script' } { label: 'Keymap…', command: 'application:open-your-keymap' } { label: 'Snippets…', command: 'application:open-your-snippets' } { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } ] } { label: '&View' submenu: [ { label: 'Toggle &Full Screen', command: 'window:toggle-full-screen' } { label: 'Toggle Menu Bar', command: 'window:toggle-menu-bar' } { label: 'Panes' submenu: [ { label: 'Split Up', command: 'pane:split-up-and-copy-active-item' } { label: 'Split Down', command: 'pane:split-down-and-copy-active-item' } { label: 'Split Left', command: 'pane:split-left-and-copy-active-item' } { label: 'Split Right', command: 'pane:split-right-and-copy-active-item' } { type: 'separator' } { label: 'Focus Next Pane', command: 'window:focus-next-pane' } { label: 'Focus Previous Pane', command: 'window:focus-previous-pane' } { type: 'separator' } { label: 'Focus Pane Above', command: 'window:focus-pane-above' } { label: 'Focus Pane Below', command: 'window:focus-pane-below' } { label: 'Focus Pane On Left', command: 'window:focus-pane-on-left' } { label: 'Focus Pane On Right', command: 'window:focus-pane-on-right' } { type: 'separator' } { label: 'Close Pane', command: 'pane:close' } ] } { label: 'Developer' submenu: [ { label: 'Open In &Dev Mode…', command: 'application:open-dev' } { label: '&Reload Window', command: 'window:reload' } { label: 'Run Package &Specs', command: 'window:run-package-specs' } { label: 'Run &Benchmarks', command: 'window:run-benchmarks' } { label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' } ] } { type: 'separator' } { label: '&Increase Font Size', command: 'window:increase-font-size' } { label: '&Decrease Font Size', command: 'window:decrease-font-size' } { label: 'Re&set Font Size', command: 'window:reset-font-size' } { type: 'separator' } { label: 'Toggle Soft &Wrap', command: 'editor:toggle-soft-wrap' } ] } { label: '&Selection' submenu: [ { label: 'Add Selection &Above', command: 'editor:add-selection-above' } { label: 'Add Selection &Below', command: 'editor:add-selection-below' } { label: 'S&plit into Lines', command: 'editor:split-selections-into-lines'} { label: 'Single Selection', command: 'editor:consolidate-selections'} { type: 'separator' } { label: 'Select to &Top', command: 'core:select-to-top' } { label: 'Select to Botto&m', command: 'core:select-to-bottom' } { type: 'separator' } { label: 'Select &Line', command: 'editor:select-line' } { label: 'Select &Word', command: 'editor:select-word' } { label: 'Select to Beginning of W&ord', command: 'editor:select-to-beginning-of-word' } { label: 'Select to Beginning of L&ine', command: 'editor:select-to-beginning-of-line' } { label: 'Select to First &Character of Line', command: 'editor:select-to-first-character-of-line' } { label: 'Select to End of Wor&d', command: 'editor:select-to-end-of-word' } { label: 'Select to End of Lin&e', command: 'editor:select-to-end-of-line' } ] } { label: 'F&ind' submenu: [] } { label: '&Packages' submenu: [] } { label: '&Help' submenu: [ { label: 'View &Terms of Use', command: 'application:open-terms-of-use' } { label: 'View &License', command: 'application:open-license' } { label: "VERSION", enabled: false } { type: 'separator' } { label: '&Documentation', command: 'application:open-documentation' } { label: 'Frequently Asked Questions', command: 'application:open-faq' } { type: 'separator' } { label: 'Community Discussions', command: 'application:open-discussions' } { label: 'Report Issue', command: 'application:report-issue' } { label: 'Search Issues', command: 'application:search-issues' } { type: 'separator' } { label: 'About Atom', command: 'application:about' } { type: 'separator' } ] } ] 'context-menu': 'atom-text-editor, .overlayer': [ {label: 'Undo', command: 'core:undo'} {label: 'Redo', command: 'core:redo'} {type: 'separator'} {label: 'Cut', command: 'core:cut'} {label: 'Copy', command: 'core:copy'} {label: 'Paste', command: 'core:paste'} {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] 'atom-pane': [ {type: 'separator'} {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] ================================================ FILE: menus/win32.cson ================================================ 'menu': [ { label: '&File' submenu: [ { label: 'New &Window', command: 'application:new-window' } { label: '&New File', command: 'application:new-file' } { label: '&Open File…', command: 'application:open-file' } { label: 'Open Folder…', command: 'application:open-folder' } { label: 'Add Project Folder…', command: 'application:add-project-folder' } { label: 'Reopen Project', submenu: [ { label: 'Clear Project History', command: 'application:clear-project-history' } { type: 'separator' } ] } { label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' } { type: 'separator' } { label: 'Se&ttings', command: 'application:show-settings' } { type: 'separator' } { label: 'Config…', command: 'application:open-your-config' } { label: 'Init Script…', command: 'application:open-your-init-script' } { label: 'Keymap…', command: 'application:open-your-keymap' } { label: 'Snippets…', command: 'application:open-your-snippets' } { label: 'Stylesheet…', command: 'application:open-your-stylesheet' } { type: 'separator' } { label: '&Save', command: 'core:save' } { label: 'Save &As…', command: 'core:save-as' } { label: 'Save A&ll', command: 'window:save-all' } { type: 'separator' } { label: '&Close Tab', command: 'core:close' } { label: 'Close &Pane', command: 'pane:close' } { label: 'Clos&e Window', command: 'window:close' } { type: 'separator' } { label: 'E&xit', command: 'application:quit' } ] } { label: '&Edit' submenu: [ { label: '&Undo', command: 'core:undo' } { label: '&Redo', command: 'core:redo' } { type: 'separator' } { label: 'Cu&t', command: 'core:cut' } { 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' } { label: 'Lines', submenu: [ { label: '&Indent', command: 'editor:indent-selected-rows' } { label: '&Outdent', command: 'editor:outdent-selected-rows' } { label: '&Auto Indent', command: 'editor:auto-indent' } { type: 'separator' } { label: 'Move Line &Up', command: 'editor:move-line-up' } { label: 'Move Line &Down', command: 'editor:move-line-down' } { label: 'Du&plicate Lines', command: 'editor:duplicate-lines' } { label: 'D&elete Line', command: 'editor:delete-line' } { label: '&Join Lines', command: 'editor:join-lines' } ] } { label: 'Columns', submenu: [ { label: 'Move Selection &Left', command: 'editor:move-selection-left' } { label: 'Move Selection &Right', command: 'editor:move-selection-right' } ] } { label: 'Text', submenu: [ { label: '&Upper Case', command: 'editor:upper-case' } { label: '&Lower Case', command: 'editor:lower-case' } { type: 'separator' } { label: 'Delete to End of &Word', command: 'editor:delete-to-end-of-word' } { label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' } { label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' } { label: '&Delete Line', command: 'editor:delete-line' } { type: 'separator' } { label: '&Transpose', command: 'editor:transpose' } ] } { label: 'Folding', submenu: [ { label: '&Fold', command: 'editor:fold-current-row' } { label: '&Unfold', command: 'editor:unfold-current-row' } { label: 'Fol&d All', command: 'editor:fold-all' } { label: 'Unfold &All', command: 'editor:unfold-all' } { type: 'separator' } { label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' } { label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' } { label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' } { label: 'Fold Level 4', command: 'editor:fold-at-indent-level-4' } { label: 'Fold Level 5', command: 'editor:fold-at-indent-level-5' } { label: 'Fold Level 6', command: 'editor:fold-at-indent-level-6' } { label: 'Fold Level 7', command: 'editor:fold-at-indent-level-7' } { label: 'Fold Level 8', command: 'editor:fold-at-indent-level-8' } { label: 'Fold Level 9', command: 'editor:fold-at-indent-level-9' } ] } ] } { label: '&View' submenu: [ { label: 'Toggle &Full Screen', command: 'window:toggle-full-screen' } { label: 'Toggle Menu Bar', command: 'window:toggle-menu-bar' } { label: 'Panes' submenu: [ { label: 'Split Up', command: 'pane:split-up-and-copy-active-item' } { label: 'Split Down', command: 'pane:split-down-and-copy-active-item' } { label: 'Split Left', command: 'pane:split-left-and-copy-active-item' } { label: 'Split Right', command: 'pane:split-right-and-copy-active-item' } { type: 'separator' } { label: 'Focus Next Pane', command: 'window:focus-next-pane' } { label: 'Focus Previous Pane', command: 'window:focus-previous-pane' } { type: 'separator' } { label: 'Focus Pane Above', command: 'window:focus-pane-above' } { label: 'Focus Pane Below', command: 'window:focus-pane-below' } { label: 'Focus Pane On Left', command: 'window:focus-pane-on-left' } { label: 'Focus Pane On Right', command: 'window:focus-pane-on-right' } { type: 'separator' } { label: 'Close Pane', command: 'pane:close' } ] } { label: 'Developer' submenu: [ { label: 'Open In &Dev Mode…', command: 'application:open-dev' } { label: '&Reload Window', command: 'window:reload' } { label: 'Run Package &Specs', command: 'window:run-package-specs' } { label: 'Run &Benchmarks', command: 'window:run-benchmarks' } { label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' } ] } { type: 'separator' } { label: '&Increase Font Size', command: 'window:increase-font-size' } { label: '&Decrease Font Size', command: 'window:decrease-font-size' } { label: 'Re&set Font Size', command: 'window:reset-font-size' } { type: 'separator' } { label: 'Toggle Soft &Wrap', command: 'editor:toggle-soft-wrap' } ] } { label: '&Selection' submenu: [ { label: 'Add Selection &Above', command: 'editor:add-selection-above' } { label: 'Add Selection &Below', command: 'editor:add-selection-below' } { label: 'S&plit into Lines', command: 'editor:split-selections-into-lines'} { label: 'Single Selection', command: 'editor:consolidate-selections'} { type: 'separator' } { label: 'Select to &Top', command: 'core:select-to-top' } { label: 'Select to Botto&m', command: 'core:select-to-bottom' } { type: 'separator' } { label: 'Select &Line', command: 'editor:select-line' } { label: 'Select &Word', command: 'editor:select-word' } { label: 'Select to Beginning of W&ord', command: 'editor:select-to-beginning-of-word' } { label: 'Select to Beginning of L&ine', command: 'editor:select-to-beginning-of-line' } { label: 'Select to First &Character of Line', command: 'editor:select-to-first-character-of-line' } { label: 'Select to End of Wor&d', command: 'editor:select-to-end-of-word' } { label: 'Select to End of Lin&e', command: 'editor:select-to-end-of-line' } ] } { label: 'F&ind' submenu: [] } { label: '&Packages' submenu: [] } { label: '&Help' submenu: [ { label: 'View &Terms of Use', command: 'application:open-terms-of-use' } { label: 'View &License', command: 'application:open-license' } { label: 'VERSION', enabled: false } { label: 'Restart and Install Update', command: 'application:install-update', visible: false} { label: 'Check for Update', command: 'application:check-for-update', visible: false} { label: 'Checking for Update', enabled: false, visible: false} { label: 'Downloading Update', enabled: false, visible: false} { type: 'separator' } { label: '&Documentation', command: 'application:open-documentation' } { label: 'Frequently Asked Questions', command: 'application:open-faq' } { type: 'separator' } { label: 'Community Discussions', command: 'application:open-discussions' } { label: 'Report Issue', command: 'application:report-issue' } { label: 'Search Issues', command: 'application:search-issues' } { type: 'separator' } { label: 'About Atom', command: 'application:about' } { type: 'separator' } ] } ] 'context-menu': 'atom-text-editor, .overlayer': [ {label: 'Undo', command: 'core:undo'} {label: 'Redo', command: 'core:redo'} {type: 'separator'} {label: 'Cut', command: 'core:cut'} {label: 'Copy', command: 'core:copy'} {label: 'Paste', command: 'core:paste'} {label: 'Delete', command: 'core:delete'} {label: 'Select All', command: 'core:select-all'} {type: 'separator'} {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] 'atom-pane': [ {type: 'separator'} {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'} {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'} {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'} {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'} {label: 'Close Pane', command: 'pane:close'} {type: 'separator'} ] ================================================ FILE: package.json ================================================ { "name": "atom", "productName": "Atom", "version": "1.61.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { "type": "git", "url": "https://github.com/atom/atom.git" }, "bugs": { "url": "https://github.com/atom/atom/issues" }, "license": "MIT", "electronVersion": "11.4.12", "dependencies": { "@atom/fuzzy-native": "^1.2.1", "@atom/nsfw": "^1.0.28", "@atom/source-map-support": "^0.3.4", "@atom/watcher": "^1.3.5", "about": "file:packages/about", "archive-view": "https://www.atom.io/api/packages/archive-view/versions/0.66.0/tarball", "async": "3.2.0", "atom-dark-syntax": "file:packages/atom-dark-syntax", "atom-dark-ui": "file:packages/atom-dark-ui", "atom-keymap": "8.2.15", "atom-light-syntax": "file:packages/atom-light-syntax", "atom-light-ui": "file:packages/atom-light-ui", "atom-select-list": "^0.8.1", "autocomplete-atom-api": "https://www.atom.io/api/packages/autocomplete-atom-api/versions/0.10.7/tarball", "autocomplete-css": "https://www.atom.io/api/packages/autocomplete-css/versions/0.17.5/tarball", "autocomplete-html": "https://www.atom.io/api/packages/autocomplete-html/versions/0.8.8/tarball", "autocomplete-plus": "https://www.atom.io/api/packages/autocomplete-plus/versions/2.42.4/tarball", "autocomplete-snippets": "https://www.atom.io/api/packages/autocomplete-snippets/versions/1.12.1/tarball", "autoflow": "file:packages/autoflow", "autosave": "https://www.atom.io/api/packages/autosave/versions/0.24.6/tarball", "babel-core": "5.8.38", "background-tips": "https://www.atom.io/api/packages/background-tips/versions/0.28.0/tarball", "base16-tomorrow-dark-theme": "file:packages/base16-tomorrow-dark-theme", "base16-tomorrow-light-theme": "file:packages/base16-tomorrow-light-theme", "bookmarks": "https://www.atom.io/api/packages/bookmarks/versions/0.46.0/tarball", "bracket-matcher": "https://www.atom.io/api/packages/bracket-matcher/versions/0.92.0/tarball", "chai": "4.3.4", "chart.js": "2.9.4", "clear-cut": "^2.0.2", "coffee-script": "1.12.7", "color": "3.1.3", "command-palette": "https://www.atom.io/api/packages/command-palette/versions/0.43.5/tarball", "dalek": "file:packages/dalek", "dedent": "^0.7.0", "deprecation-cop": "file:packages/deprecation-cop", "dev-live-reload": "file:packages/dev-live-reload", "devtron": "1.4.0", "document-register-element": "^1.14.10", "electron-notarize": "1.0.0", "electron-osx-sign": "0.5.0", "encoding-selector": "https://www.atom.io/api/packages/encoding-selector/versions/0.23.9/tarball", "etch": "0.14.1", "event-kit": "^2.5.3", "exception-reporting": "file:packages/exception-reporting", "find-and-replace": "https://www.atom.io/api/packages/find-and-replace/versions/0.219.8/tarball", "find-parent-dir": "^0.3.0", "first-mate": "7.4.3", "focus-trap": "6.3.0", "fs-admin": "0.15.0", "fs-plus": "^3.1.1", "fstream": "1.0.12", "fuzzaldrin": "^2.1", "fuzzy-finder": "https://www.atom.io/api/packages/fuzzy-finder/versions/1.14.3/tarball", "git-diff": "file:packages/git-diff", "git-utils": "5.7.1", "github": "https://www.atom.io/api/packages/github/versions/0.36.10/tarball", "glob": "^7.1.1", "go-to-line": "file:packages/go-to-line", "grammar-selector": "file:packages/grammar-selector", "grim": "2.0.3", "image-view": "https://www.atom.io/api/packages/image-view/versions/0.64.0/tarball", "incompatible-packages": "file:packages/incompatible-packages", "jasmine-json": "~0.0", "jasmine-reporters": "1.1.0", "jasmine-tagged": "^1.1.4", "key-path-helpers": "^0.4.0", "keybinding-resolver": "https://www.atom.io/api/packages/keybinding-resolver/versions/0.39.1/tarball", "language-c": "https://www.atom.io/api/packages/language-c/versions/0.60.19/tarball", "language-clojure": "https://www.atom.io/api/packages/language-clojure/versions/0.22.8/tarball", "language-coffee-script": "https://www.atom.io/api/packages/language-coffee-script/versions/0.50.0/tarball", "language-csharp": "https://www.atom.io/api/packages/language-csharp/versions/1.1.0/tarball", "language-css": "https://www.atom.io/api/packages/language-css/versions/0.45.1/tarball", "language-gfm": "https://www.atom.io/api/packages/language-gfm/versions/0.90.8/tarball", "language-git": "https://www.atom.io/api/packages/language-git/versions/0.19.1/tarball", "language-go": "https://www.atom.io/api/packages/language-go/versions/0.47.2/tarball", "language-html": "https://www.atom.io/api/packages/language-html/versions/0.53.1/tarball", "language-hyperlink": "https://www.atom.io/api/packages/language-hyperlink/versions/0.17.1/tarball", "language-java": "https://www.atom.io/api/packages/language-java/versions/0.32.1/tarball", "language-javascript": "https://www.atom.io/api/packages/language-javascript/versions/0.134.1/tarball", "language-json": "https://www.atom.io/api/packages/language-json/versions/1.0.5/tarball", "language-less": "https://www.atom.io/api/packages/language-less/versions/0.34.3/tarball", "language-make": "https://www.atom.io/api/packages/language-make/versions/0.23.0/tarball", "language-mustache": "https://www.atom.io/api/packages/language-mustache/versions/0.14.5/tarball", "language-objective-c": "https://www.atom.io/api/packages/language-objective-c/versions/0.16.0/tarball", "language-perl": "https://www.atom.io/api/packages/language-perl/versions/0.38.1/tarball", "language-php": "https://www.atom.io/api/packages/language-php/versions/0.48.1/tarball", "language-property-list": "https://www.atom.io/api/packages/language-property-list/versions/0.9.1/tarball", "language-python": "https://www.atom.io/api/packages/language-python/versions/0.53.6/tarball", "language-ruby": "https://www.atom.io/api/packages/language-ruby/versions/0.72.23/tarball", "language-ruby-on-rails": "https://www.atom.io/api/packages/language-ruby-on-rails/versions/0.25.3/tarball", "language-rust-bundled": "file:packages/language-rust-bundled", "language-sass": "https://www.atom.io/api/packages/language-sass/versions/0.62.1/tarball", "language-shellscript": "https://www.atom.io/api/packages/language-shellscript/versions/0.28.2/tarball", "language-source": "https://www.atom.io/api/packages/language-source/versions/0.9.0/tarball", "language-sql": "https://www.atom.io/api/packages/language-sql/versions/0.25.10/tarball", "language-text": "https://www.atom.io/api/packages/language-text/versions/0.7.4/tarball", "language-todo": "https://www.atom.io/api/packages/language-todo/versions/0.29.4/tarball", "language-toml": "https://www.atom.io/api/packages/language-toml/versions/0.20.0/tarball", "language-typescript": "https://www.atom.io/api/packages/language-typescript/versions/0.6.3/tarball", "language-xml": "https://www.atom.io/api/packages/language-xml/versions/0.35.3/tarball", "language-yaml": "https://www.atom.io/api/packages/language-yaml/versions/0.32.0/tarball", "less-cache": "1.1.0", "line-ending-selector": "file:packages/line-ending-selector", "line-top-index": "0.3.1", "link": "file:packages/link", "markdown-preview": "https://www.atom.io/api/packages/markdown-preview/versions/0.160.2/tarball", "metrics": "https://www.atom.io/api/packages/metrics/versions/1.8.1/tarball", "minimatch": "^3.0.3", "mocha": "6.2.3", "mocha-junit-reporter": "2.0.0", "mocha-multi-reporters": "^1.1.4", "mock-spawn": "^0.2.6", "normalize-package-data": "3.0.2", "notifications": "https://www.atom.io/api/packages/notifications/versions/0.72.1/tarball", "nslog": "^3.0.0", "one-dark-syntax": "file:packages/one-dark-syntax", "one-dark-ui": "file:packages/one-dark-ui", "one-light-syntax": "file:packages/one-light-syntax", "one-light-ui": "file:packages/one-light-ui", "open-on-github": "https://www.atom.io/api/packages/open-on-github/versions/1.3.2/tarball", "package-generator": "https://www.atom.io/api/packages/package-generator/versions/1.3.0/tarball", "pathwatcher": "^8.1.2", "postcss": "8.2.10", "postcss-selector-parser": "6.0.4", "prebuild-install": "6.0.0", "property-accessors": "^1.1.3", "resolve": "1.18.1", "scandal": "^3.2.0", "scoped-property-store": "^0.17.0", "scrollbar-style": "^4.0.1", "season": "^6.0.2", "semver": "7.3.2", "service-hub": "^0.7.4", "settings-view": "https://www.atom.io/api/packages/settings-view/versions/0.261.8/tarball", "sinon": "9.2.1", "snippets": "https://www.atom.io/api/packages/snippets/versions/1.5.1/tarball", "solarized-dark-syntax": "file:packages/solarized-dark-syntax", "solarized-light-syntax": "file:packages/solarized-light-syntax", "spell-check": "https://www.atom.io/api/packages/spell-check/versions/0.77.1/tarball", "status-bar": "https://www.atom.io/api/packages/status-bar/versions/1.8.17/tarball", "styleguide": "https://www.atom.io/api/packages/styleguide/versions/0.49.12/tarball", "superstring": "^2.4.4", "symbols-view": "https://www.atom.io/api/packages/symbols-view/versions/0.118.4/tarball", "tabs": "https://www.atom.io/api/packages/tabs/versions/0.110.2/tarball", "temp": "0.9.2", "text-buffer": "^13.18.5", "timecop": "https://www.atom.io/api/packages/timecop/versions/0.36.2/tarball", "tree-sitter": "git+https://github.com/DeeDeeG/node-tree-sitter.git#bb298eaae66e0c4f11908cb6209f3e141884e88e", "tree-view": "https://www.atom.io/api/packages/tree-view/versions/0.229.1/tarball", "typescript-simple": "8.0.6", "update-package-dependencies": "file:./packages/update-package-dependencies", "vscode-ripgrep": "1.9.0", "welcome": "file:packages/welcome", "whitespace": "https://www.atom.io/api/packages/whitespace/versions/0.37.8/tarball", "winreg": "^1.2.1", "wrap-guide": "https://www.atom.io/api/packages/wrap-guide/versions/0.41.0/tarball", "yargs": "16.1.0" }, "packageDependencies": { "atom-dark-syntax": "file:./packages/atom-dark-syntax", "atom-dark-ui": "file:./packages/atom-dark-ui", "atom-light-syntax": "file:./packages/atom-light-syntax", "atom-light-ui": "file:./packages/atom-light-ui", "base16-tomorrow-dark-theme": "file:./packages/base16-tomorrow-dark-theme", "base16-tomorrow-light-theme": "file:./packages/base16-tomorrow-light-theme", "one-dark-ui": "file:./packages/one-dark-ui", "one-light-ui": "file:./packages/one-light-ui", "one-dark-syntax": "file:./packages/one-dark-syntax", "one-light-syntax": "file:./packages/one-light-syntax", "solarized-dark-syntax": "file:./packages/solarized-dark-syntax", "solarized-light-syntax": "file:./packages/solarized-light-syntax", "about": "file:./packages/about", "archive-view": "0.66.0", "autocomplete-atom-api": "0.10.7", "autocomplete-css": "0.17.5", "autocomplete-html": "0.8.8", "autocomplete-plus": "2.42.4", "autocomplete-snippets": "1.12.1", "autoflow": "file:./packages/autoflow", "autosave": "0.24.6", "background-tips": "0.28.0", "bookmarks": "0.46.0", "bracket-matcher": "0.92.0", "command-palette": "0.43.5", "dalek": "file:./packages/dalek", "deprecation-cop": "file:./packages/deprecation-cop", "dev-live-reload": "file:./packages/dev-live-reload", "encoding-selector": "0.23.9", "exception-reporting": "file:./packages/exception-reporting", "find-and-replace": "0.219.8", "fuzzy-finder": "1.14.3", "github": "0.36.10", "git-diff": "file:./packages/git-diff", "go-to-line": "file:./packages/go-to-line", "grammar-selector": "file:./packages/grammar-selector", "image-view": "0.64.0", "incompatible-packages": "file:./packages/incompatible-packages", "keybinding-resolver": "0.39.1", "line-ending-selector": "file:./packages/line-ending-selector", "link": "file:./packages/link", "markdown-preview": "0.160.2", "metrics": "1.8.1", "notifications": "0.72.1", "open-on-github": "1.3.2", "package-generator": "1.3.0", "settings-view": "0.261.8", "snippets": "1.5.1", "spell-check": "0.77.1", "status-bar": "1.8.17", "styleguide": "0.49.12", "symbols-view": "0.118.4", "tabs": "0.110.2", "timecop": "0.36.2", "tree-view": "0.229.1", "update-package-dependencies": "file:./packages/update-package-dependencies", "welcome": "file:./packages/welcome", "whitespace": "0.37.8", "wrap-guide": "0.41.0", "language-c": "0.60.19", "language-clojure": "0.22.8", "language-coffee-script": "0.50.0", "language-csharp": "1.1.0", "language-css": "0.45.1", "language-gfm": "0.90.8", "language-git": "0.19.1", "language-go": "0.47.2", "language-html": "0.53.1", "language-hyperlink": "0.17.1", "language-java": "0.32.1", "language-javascript": "0.134.1", "language-json": "1.0.5", "language-less": "0.34.3", "language-make": "0.23.0", "language-mustache": "0.14.5", "language-objective-c": "0.16.0", "language-perl": "0.38.1", "language-php": "0.48.1", "language-property-list": "0.9.1", "language-python": "0.53.6", "language-ruby": "0.72.23", "language-ruby-on-rails": "0.25.3", "language-rust-bundled": "file:./packages/language-rust-bundled", "language-sass": "0.62.1", "language-shellscript": "0.28.2", "language-source": "0.9.0", "language-sql": "0.25.10", "language-text": "0.7.4", "language-todo": "0.29.4", "language-toml": "0.20.0", "language-typescript": "0.6.3", "language-xml": "0.35.3", "language-yaml": "0.32.0" }, "private": true, "scripts": { "preinstall": "node -e 'process.exit(0)'", "test": "node script/test" }, "standard-engine": "./script/node_modules/standard", "standard": { "env": { "atomtest": true, "browser": true, "jasmine": true, "node": true }, "globals": [ "atom", "snapshotResult" ] } } ================================================ FILE: packages/README.md ================================================ # Atom Core Packages This folder contains core packages that are bundled with Atom releases. Not all Atom core packages are kept here; please see the table below for the location of every core Atom package. > **NOTE:** There is an ongoing effort to migrate more Atom packages from their individual repositories to this folder. See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate-core-packages.md) for more details. | Package | Where to find it | Migration issue | |---------|------------------|-----------------| | **about** | [`./about`](./about) | [#17832](https://github.com/atom/atom/issues/17832) | | **atom-dark-syntax** | [`./atom-dark-syntax`](./atom-dark-syntax) | [#17849](https://github.com/atom/atom/issues/17849) | | **atom-dark-ui** | [`./atom-dark-ui`](./atom-dark-ui) | [#17850](https://github.com/atom/atom/issues/17850) | | **atom-light-syntax** | [`./atom-light-syntax`](./atom-light-syntax) | [#17851](https://github.com/atom/atom/issues/17851) | | **atom-light-ui** | [`./atom-light-ui`](./atom-light-ui) | [#17852](https://github.com/atom/atom/issues/17852) | | **autocomplete-atom-api** | [`atom/autocomplete-atom-api`][autocomplete-atom-api] | | | **autocomplete-css** | [`atom/autocomplete-css`][autocomplete-css] | | | **autocomplete-html** | [`atom/autocomplete-html`][autocomplete-html] | | | **autocomplete-plus** | [`atom/autocomplete-plus`][autocomplete-plus] | | | **autocomplete-snippets** | [`atom/autocomplete-snippets`][autocomplete-snippets] | | | **autoflow** | [`./autoflow`](./autoflow) | [#17833](https://github.com/atom/atom/issues/17833) | | **autosave** | [`atom/autosave`][autosave] | [#17834](https://github.com/atom/atom/issues/17834) | | **background-tips** | [`atom/background-tips`][background-tips] | [#17835](https://github.com/atom/atom/issues/17835) | | **base16-tomorrow-dark-theme** | [`./base16-tomorrow-dark-theme`](./base16-tomorrow-dark-theme) | [#17836](https://github.com/atom/atom/issues/17836) | | **base16-tomorrow-light-theme** | [`./base16-tomorrow-light-theme`](./base16-tomorrow-light-theme) | [#17837](https://github.com/atom/atom/issues/17837) | | **bookmarks** | [`atom/bookmarks`][bookmarks] | [#18273](https://github.com/atom/atom/issues/18273) | | **bracket-matcher** | [`atom/bracket-matcher`][bracket-matcher] | | | **command-palette** | [`atom/command-palette`][command-palette] | | | **dalek** | [`./dalek`](./dalek) | [#17838](https://github.com/atom/atom/issues/17838) | | **deprecation-cop** | [`./deprecation-cop`](./deprecation-cop) | [#17839](https://github.com/atom/atom/issues/17839) | | **dev-live-reload** | [`./dev-live-reload`](dev-live-reload) | [#17840](https://github.com/atom/atom/issues/17840) | | **encoding-selector** | [`atom/encoding-selector`][encoding-selector] | [#17841](https://github.com/atom/atom/issues/17841) | | **exception-reporting** | [`./exception-reporting`](./exception-reporting) | [#17842](https://github.com/atom/atom/issues/17842) | | **find-and-replace** | [`atom/find-and-replace`][find-and-replace] | | | **fuzzy-finder** | [`atom/fuzzy-finder`][fuzzy-finder] | | | **github** | [`atom/github`][github] | | | **git-diff** | [`./git-diff`](./git-diff) | [#17843](https://github.com/atom/atom/issues/17843) | | **go-to-line** | [`./go-to-line`](./go-to-line) | [#17844](https://github.com/atom/atom/issues/17844) | | **grammar-selector** | [`./grammar-selector`](./grammar-selector) | [#17845](https://github.com/atom/atom/issues/17845) | | **image-view** | [`atom/image-view`][image-view] | [#18274](https://github.com/atom/atom/issues/18274) | | **incompatible-packages** | [`./incompatible-packages`](./incompatible-packages) | [#17846](https://github.com/atom/atom/issues/17846) | | **keybinding-resolver** | [`atom/keybinding-resolver`][keybinding-resolver] | [#18275](https://github.com/atom/atom/issues/18275) | | **language-c** | [`atom/language-c`][language-c] | | | **language-clojure** | [`atom/language-clojure`][language-clojure] | | | **language-coffee-script** | [`atom/language-coffee-script`][language-coffee-script] | | | **language-csharp** | [`atom/language-csharp`][language-csharp] | | | **language-css** | [`atom/language-css`][language-css] | | | **language-gfm** | [`atom/language-gfm`][language-gfm] | | | **language-git** | [`atom/language-git`][language-git] | | | **language-go** | [`atom/language-go`][language-go] | | | **language-html** | [`atom/language-html`][language-html] | | | **language-hyperlink** | [`atom/language-hyperlink`][language-hyperlink] | | | **language-java** | [`atom/language-java`][language-java] | | | **language-javascript** | [`atom/language-javascript`][language-javascript] | | | **language-json** | [`atom/language-json`][language-json] | | | **language-less** | [`atom/language-less`][language-less] | | | **language-make** | [`atom/language-make`][language-make] | | | **language-mustache** | [`atom/language-mustache`][language-mustache] | | | **language-objective-c** | [`atom/language-objective-c`][language-objective-c] | | | **language-perl** | [`atom/language-perl`][language-perl] | | | **language-php** | [`atom/language-php`][language-php] | | | **language-property-list** | [`atom/language-property-list`][language-property-list] | | | **language-python** | [`atom/language-python`][language-python] | | | **language-ruby** | [`atom/language-ruby`][language-ruby] | | | **language-ruby-on-rails** | [`atom/language-ruby-on-rails`][language-ruby-on-rails] | | | **language-rust-bundled** | [`./language-rust-bundled`](./language-rust-bundled) | | | **language-sass** | [`atom/language-sass`][language-sass] | | | **language-shellscript** | [`atom/language-shellscript`][language-shellscript] | | | **language-source** | [`atom/language-source`][language-source] | | | **language-sql** | [`atom/language-sql`][language-sql] | | | **language-text** | [`atom/language-text`][language-text] | | | **language-todo** | [`atom/language-todo`][language-todo] | | | **language-toml** | [`atom/language-toml`][language-toml] | | | **language-typescript** | [`atom/language-typescript`][language-typescript] | | | **language-xml** | [`atom/language-xml`][language-xml] | | | **language-yaml** | [`atom/language-yaml`][language-yaml] | | | **line-ending-selector** | [`./packages/line-ending-selector`](./line-ending-selector) | [#17847](https://github.com/atom/atom/issues/17847) | | **link** | [`./link`](./link) | [#17848](https://github.com/atom/atom/issues/17848) | | **markdown-preview** | [`atom/markdown-preview`][markdown-preview] | | | **metrics** | [`atom/metrics`][metrics] | [#18276](https://github.com/atom/atom/issues/18276) | | **notifications** | [`atom/notifications`][notifications] | [#18277](https://github.com/atom/atom/issues/18277) | | **one-dark-syntax** | [`./one-dark-syntax`](./one-dark-syntax) | [#17853](https://github.com/atom/atom/issues/17853) | | **one-dark-ui** | [`./one-dark-ui`](./one-dark-ui) | [#17854](https://github.com/atom/atom/issues/17854) | | **one-light-syntax** | [`./one-light-syntax`](./one-light-syntax) | [#17855](https://github.com/atom/atom/issues/17855) | | **one-light-ui** | [`./one-light-ui`](./one-light-ui) | [#17856](https://github.com/atom/atom/issues/17856) | | **open-on-github** | [`atom/open-on-github`][open-on-github] | [#18278](https://github.com/atom/atom/issues/18278) | | **package-generator** | [`atom/package-generator`][package-generator] | [#18279](https://github.com/atom/atom/issues/18279) | | **settings-view** | [`atom/settings-view`][settings-view] | | | **snippets** | [`atom/snippets`][snippets] | | | **solarized-dark-syntax** | [`./solarized-dark-syntax`](./solarized-dark-syntax) | [#18280](https://github.com/atom/atom/issues/18280) | | **solarized-light-syntax** | [`./solarized-light-syntax`](./solarized-light-syntax) | [#18281](https://github.com/atom/atom/issues/18281) | | **spell-check** | [`atom/spell-check`][spell-check] | | | **status-bar** | [`atom/status-bar`][status-bar] | [#18282](https://github.com/atom/atom/issues/18282) | | **styleguide** | [`atom/styleguide`][styleguide] | [#18283](https://github.com/atom/atom/issues/18283) | | **symbols-view** | [`atom/symbols-view`][symbols-view] | | | **tabs** | [`atom/tabs`][tabs] | | | **timecop** | [`atom/timecop`][timecop] | [#18272](https://github.com/atom/atom/issues/18272) | | **tree-view** | [`atom/tree-view`][tree-view] | | | **update-package-dependencies** | [`./update-package-dependencies`](./update-package-dependencies) | [#18284](https://github.com/atom/atom/issues/18284) | | **welcome** | [`./welcome`](./welcome) | [#18285](https://github.com/atom/atom/issues/18285) | | **whitespace** | [`atom/whitespace`][whitespace] | | | **wrap-guide** | [`atom/wrap-guide`][wrap-guide] | [#18286](https://github.com/atom/atom/issues/18286) | [archive-view]: https://github.com/atom/archive-view [autocomplete-atom-api]: https://github.com/atom/autocomplete-atom-api [autocomplete-css]: https://github.com/atom/autocomplete-css [autocomplete-html]: https://github.com/atom/autocomplete-html [autocomplete-plus]: https://github.com/atom/autocomplete-plus [autocomplete-snippets]: https://github.com/atom/autocomplete-snippets [autosave]: https://github.com/atom/autosave [background-tips]: https://github.com/atom/background-tips [bookmarks]: https://github.com/atom/bookmarks [bracket-matcher]: https://github.com/atom/bracket-matcher [command-palette]: https://github.com/atom/command-palette [encoding-selector]: https://github.com/atom/encoding-selector [find-and-replace]: https://github.com/atom/find-and-replace [fuzzy-finder]: https://github.com/atom/fuzzy-finder [github]: https://github.com/atom/github [image-view]: https://github.com/atom/image-view [keybinding-resolver]: https://github.com/atom/keybinding-resolver [language-c]: https://github.com/atom/language-c [language-clojure]: https://github.com/atom/language-clojure [language-coffee-script]: https://github.com/atom/language-coffee-script [language-csharp]: https://github.com/atom/language-csharp [language-css]: https://github.com/atom/language-css [language-gfm]: https://github.com/atom/language-gfm [language-git]: https://github.com/atom/language-git [language-go]: https://github.com/atom/language-go [language-html]: https://github.com/atom/language-html [language-hyperlink]: https://github.com/atom/language-hyperlink [language-java]: https://github.com/atom/language-java [language-javascript]: https://github.com/atom/language-javascript [language-json]: https://github.com/atom/language-json [language-less]: https://github.com/atom/language-less [language-make]: https://github.com/atom/language-make [language-mustache]: https://github.com/atom/language-mustache [language-objective-c]: https://github.com/atom/language-objective-c [language-perl]: https://github.com/atom/language-perl [language-php]: https://github.com/atom/language-php [language-property-list]: https://github.com/atom/language-property-list [language-python]: https://github.com/atom/language-python [language-ruby]: https://github.com/atom/language-ruby [language-ruby-on-rails]: https://github.com/atom/language-ruby-on-rails [language-sass]: https://github.com/atom/language-sass [language-shellscript]: https://github.com/atom/language-shellscript [language-source]: https://github.com/atom/language-source [language-sql]: https://github.com/atom/language-sql [language-text]: https://github.com/atom/language-text [language-todo]: https://github.com/atom/language-todo [language-toml]: https://github.com/atom/language-toml [language-typescript]: https://github.com/atom/language-typescript [language-xml]: https://github.com/atom/language-xml [language-yaml]: https://github.com/atom/language-yaml [markdown-preview]: https://github.com/atom/markdown-preview [metrics]: https://github.com/atom/metrics [notifications]: https://github.com/atom/notifications [open-on-github]: https://github.com/atom/open-on-github [package-generator]: https://github.com/atom/package-generator [settings-view]: https://github.com/atom/settings-view [snippets]: https://github.com/atom/snippets [spell-check]: https://github.com/atom/spell-check [status-bar]: https://github.com/atom/status-bar [styleguide]: https://github.com/atom/styleguide [symbols-view]: https://github.com/atom/symbols-view [tabs]: https://github.com/atom/tabs [timecop]: https://github.com/atom/timecop [tree-view]: https://github.com/atom/tree-view [whitespace]: https://github.com/atom/whitespace [wrap-guide]: https://github.com/atom/wrap-guide ================================================ FILE: packages/about/.gitignore ================================================ .DS_Store npm-debug.log node_modules ================================================ FILE: packages/about/LICENSE.md ================================================ Copyright (c) 2015 Machisté N. Quintana Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/about/README.md ================================================ # About package View useful information about your Atom installation. ![About Atom](https://cloud.githubusercontent.com/assets/16760489/19395499/69bbb780-922d-11e6-9779-2b8327027ea5.png) This is a package for [Atom](https://atom.io), a hackable text editor for the 21st Century. ## Usage This package provides a cross-platform "About Atom" view that displays information about your Atom installation, which currently includes the current version, the license, and the Terms of Use. ## Contributing Always feel free to help out! Whether it's filing bugs and feature requests or working on some of the open issues, Atom's [contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md) will help get you started while the [guide for contributing to packages](https://github.com/atom/atom/blob/master/docs/contributing-to-packages.md) has some extra information. ## License [MIT License](https://opensource.org/licenses/MIT) - see the [LICENSE](https://github.com/atom/about/blob/master/LICENSE.md) for more details. ================================================ FILE: packages/about/lib/about.js ================================================ const { CompositeDisposable, Emitter } = require('atom'); const AboutView = require('./components/about-view'); // Deferred requires let shell; module.exports = class About { constructor(initialState) { this.subscriptions = new CompositeDisposable(); this.emitter = new Emitter(); this.state = initialState; this.views = { aboutView: null }; this.subscriptions.add( atom.workspace.addOpener(uriToOpen => { if (uriToOpen === this.state.uri) { return this.deserialize(); } }) ); this.subscriptions.add( atom.commands.add('atom-workspace', 'about:view-release-notes', () => { shell = shell || require('electron').shell; shell.openExternal( this.state.updateManager.getReleaseNotesURLForCurrentVersion() ); }) ); } destroy() { if (this.views.aboutView) this.views.aboutView.destroy(); this.views.aboutView = null; if (this.state.updateManager) this.state.updateManager.dispose(); this.setState({ updateManager: null }); this.subscriptions.dispose(); } setState(newState) { if (newState && typeof newState === 'object') { let { state } = this; this.state = Object.assign({}, state, newState); this.didChange(); } } didChange() { this.emitter.emit('did-change'); } onDidChange(callback) { this.emitter.on('did-change', callback); } deserialize(state) { if (!this.views.aboutView) { this.setState(state); this.views.aboutView = new AboutView({ uri: this.state.uri, updateManager: this.state.updateManager, currentAtomVersion: this.state.currentAtomVersion, currentElectronVersion: this.state.currentElectronVersion, currentChromeVersion: this.state.currentChromeVersion, currentNodeVersion: this.state.currentNodeVersion, availableVersion: this.state.updateManager.getAvailableVersion() }); this.handleStateChanges(); } return this.views.aboutView; } handleStateChanges() { this.onDidChange(() => { if (this.views.aboutView) { this.views.aboutView.update({ updateManager: this.state.updateManager, currentAtomVersion: this.state.currentAtomVersion, currentElectronVersion: this.state.currentElectronVersion, currentChromeVersion: this.state.currentChromeVersion, currentNodeVersion: this.state.currentNodeVersion, availableVersion: this.state.updateManager.getAvailableVersion() }); } }); this.state.updateManager.onDidChange(() => { this.didChange(); }); } }; ================================================ FILE: packages/about/lib/components/about-status-bar.js ================================================ const { CompositeDisposable } = require('atom'); const etch = require('etch'); const EtchComponent = require('../etch-component'); const $ = etch.dom; module.exports = class AboutStatusBar extends EtchComponent { constructor() { super(); this.subscriptions = new CompositeDisposable(); this.subscriptions.add( atom.tooltips.add(this.element, { title: 'An update will be installed the next time Atom is relaunched.

Click the squirrel icon for more information.' }) ); } handleClick() { atom.workspace.open('atom://about'); } render() { return $.div( { className: 'about-release-notes inline-block', onclick: this.handleClick.bind(this) }, $.span({ type: 'button', className: 'icon icon-squirrel' }) ); } destroy() { super.destroy(); this.subscriptions.dispose(); } }; ================================================ FILE: packages/about/lib/components/about-view.js ================================================ const { Disposable } = require('atom'); const etch = require('etch'); const { shell } = require('electron'); const AtomLogo = require('./atom-logo'); const EtchComponent = require('../etch-component'); const UpdateView = require('./update-view'); const $ = etch.dom; module.exports = class AboutView extends EtchComponent { handleAtomVersionClick(e) { e.preventDefault(); atom.clipboard.write(this.props.currentAtomVersion); } handleElectronVersionClick(e) { e.preventDefault(); atom.clipboard.write(this.props.currentElectronVersion); } handleChromeVersionClick(e) { e.preventDefault(); atom.clipboard.write(this.props.currentChromeVersion); } handleNodeVersionClick(e) { e.preventDefault(); atom.clipboard.write(this.props.currentNodeVersion); } handleReleaseNotesClick(e) { e.preventDefault(); shell.openExternal( this.props.updateManager.getReleaseNotesURLForAvailableVersion() ); } handleLicenseClick(e) { e.preventDefault(); atom.commands.dispatch( atom.views.getView(atom.workspace), 'application:open-license' ); } handleTermsOfUseClick(e) { e.preventDefault(); shell.openExternal('https://atom.io/terms'); } handleHowToUpdateClick(e) { e.preventDefault(); shell.openExternal( 'https://flight-manual.atom.io/getting-started/sections/installing-atom/' ); } handleShowMoreClick(e) { e.preventDefault(); var showMoreDiv = document.querySelector('.show-more'); var showMoreText = document.querySelector('.about-more-expand'); switch (showMoreText.textContent) { case 'Show more': showMoreDiv.classList.toggle('hidden'); showMoreText.textContent = 'Hide'; break; case 'Hide': showMoreDiv.classList.toggle('hidden'); showMoreText.textContent = 'Show more'; break; } } render() { return $.div( { className: 'pane-item native-key-bindings about' }, $.div( { className: 'about-container' }, $.header( { className: 'about-header' }, $.a( { className: 'about-atom-io', href: 'https://atom.io' }, $(AtomLogo) ), $.div( { className: 'about-header-info' }, $.span( { className: 'about-version-container inline-block atom', onclick: this.handleAtomVersionClick.bind(this) }, $.span( { className: 'about-version' }, `${this.props.currentAtomVersion} ${process.arch}` ), $.span({ className: 'icon icon-clippy about-copy-version' }) ), $.a( { className: 'about-header-release-notes', onclick: this.handleReleaseNotesClick.bind(this) }, 'Release Notes' ) ), $.span( { className: 'about-version-container inline-block show-more-expand', onclick: this.handleShowMoreClick.bind(this) }, $.span({ className: 'about-more-expand' }, 'Show more') ), $.div( { className: 'show-more hidden about-more-info' }, $.div( { className: 'about-more-info' }, $.span( { className: 'about-version-container inline-block electron', onclick: this.handleElectronVersionClick.bind(this) }, $.span( { className: 'about-more-version' }, `Electron: ${this.props.currentElectronVersion} ` ), $.span({ className: 'icon icon-clippy about-copy-version' }) ) ), $.div( { className: 'about-more-info' }, $.span( { className: 'about-version-container inline-block chrome', onclick: this.handleChromeVersionClick.bind(this) }, $.span( { className: 'about-more-version' }, `Chrome: ${this.props.currentChromeVersion} ` ), $.span({ className: 'icon icon-clippy about-copy-version' }) ) ), $.div( { className: 'about-more-info' }, $.span( { className: 'about-version-container inline-block node', onclick: this.handleNodeVersionClick.bind(this) }, $.span( { className: 'about-more-version' }, `Node: ${this.props.currentNodeVersion} ` ), $.span({ className: 'icon icon-clippy about-copy-version' }) ) ) ) ) ), $(UpdateView, { updateManager: this.props.updateManager, availableVersion: this.props.availableVersion, viewUpdateReleaseNotes: this.handleReleaseNotesClick.bind(this), viewUpdateInstructions: this.handleHowToUpdateClick.bind(this) }), $.div( { className: 'about-actions group-item' }, $.div( { className: 'btn-group' }, $.button( { className: 'btn view-license', onclick: this.handleLicenseClick.bind(this) }, 'License' ), $.button( { className: 'btn terms-of-use', onclick: this.handleTermsOfUseClick.bind(this) }, 'Terms of Use' ) ) ), $.div( { className: 'about-love group-start' }, $.span({ className: 'icon icon-code' }), $.span({ className: 'inline' }, ' with '), $.span({ className: 'icon icon-heart' }), $.span({ className: 'inline' }, ' by '), $.a({ className: 'icon icon-logo-github', href: 'https://github.com' }) ), $.div( { className: 'about-credits group-item' }, $.span({ className: 'inline' }, 'And the awesome '), $.a( { href: 'https://github.com/atom/atom/contributors' }, 'Atom Community' ) ) ); } serialize() { return { deserializer: this.constructor.name, uri: this.props.uri }; } onDidChangeTitle() { return new Disposable(); } onDidChangeModified() { return new Disposable(); } getTitle() { return 'About'; } getIconName() { return 'info'; } }; ================================================ FILE: packages/about/lib/components/atom-logo.js ================================================ const etch = require('etch'); const EtchComponent = require('../etch-component'); const $ = etch.dom; module.exports = class AtomLogo extends EtchComponent { render() { return $.svg( { className: 'about-logo', width: '330px', height: '68px', viewBox: '0 0 330 68' }, $.g( { stroke: 'none', 'stroke-width': '1', fill: 'none', 'fill-rule': 'evenodd' }, $.g( { transform: 'translate(2.000000, 1.000000)' }, $.g( { transform: 'translate(96.000000, 8.000000)', fill: 'currentColor' }, $.path({ d: 'M185.498,3.399 C185.498,2.417 186.34,1.573 187.324,1.573 L187.674,1.573 C188.447,1.573 189.01,1.995 189.5,2.628 L208.676,30.862 L227.852,2.628 C228.272,1.995 228.905,1.573 229.676,1.573 L230.028,1.573 C231.01,1.573 231.854,2.417 231.854,3.399 L231.854,49.403 C231.854,50.387 231.01,51.231 230.028,51.231 C229.044,51.231 228.202,50.387 228.202,49.403 L228.202,8.246 L210.151,34.515 C209.729,35.148 209.237,35.428 208.606,35.428 C207.973,35.428 207.481,35.148 207.061,34.515 L189.01,8.246 L189.01,49.475 C189.01,50.457 188.237,51.231 187.254,51.231 C186.27,51.231 185.498,50.458 185.498,49.475 L185.498,3.399 L185.498,3.399 Z' }), $.path({ d: 'M113.086,26.507 L113.086,26.367 C113.086,12.952 122.99,0.941 137.881,0.941 C152.77,0.941 162.533,12.811 162.533,26.225 L162.533,26.367 C162.533,39.782 152.629,51.792 137.74,51.792 C122.85,51.792 113.086,39.923 113.086,26.507 M158.74,26.507 L158.74,26.367 C158.74,14.216 149.89,4.242 137.74,4.242 C125.588,4.242 116.879,14.075 116.879,26.225 L116.879,26.367 C116.879,38.518 125.729,48.491 137.881,48.491 C150.031,48.491 158.74,38.658 158.74,26.507' }), $.path({ d: 'M76.705,5.155 L60.972,5.155 C60.06,5.155 59.287,4.384 59.287,3.469 C59.287,2.556 60.059,1.783 60.972,1.783 L96.092,1.783 C97.004,1.783 97.778,2.555 97.778,3.469 C97.778,4.383 97.005,5.155 96.092,5.155 L80.358,5.155 L80.358,49.405 C80.358,50.387 79.516,51.231 78.532,51.231 C77.55,51.231 76.706,50.387 76.706,49.405 L76.706,5.155 L76.705,5.155 Z' }), $.path({ d: 'M0.291,48.562 L21.291,3.05 C21.783,1.995 22.485,1.292 23.75,1.292 L23.891,1.292 C25.155,1.292 25.858,1.995 26.348,3.05 L47.279,48.421 C47.49,48.843 47.56,49.194 47.56,49.546 C47.56,50.458 46.788,51.231 45.803,51.231 C44.961,51.231 44.329,50.599 43.978,49.826 L38.219,37.183 L9.21,37.183 L3.45,49.897 C3.099,50.739 2.538,51.231 1.694,51.231 C0.781,51.231 0.008,50.529 0.008,49.685 C0.009,49.404 0.08,48.983 0.291,48.562 L0.291,48.562 Z M36.673,33.882 L23.749,5.437 L10.755,33.882 L36.673,33.882 L36.673,33.882 Z' }) ), $.g( {}, $.path({ d: 'M40.363,32.075 C40.874,34.44 39.371,36.77 37.006,37.282 C34.641,37.793 32.311,36.29 31.799,33.925 C31.289,31.56 32.791,29.23 35.156,28.718 C37.521,28.207 39.851,29.71 40.363,32.075', fill: 'currentColor' }), $.path({ d: 'M48.578,28.615 C56.851,45.587 58.558,61.581 52.288,64.778 C45.822,68.076 33.326,56.521 24.375,38.969 C15.424,21.418 13.409,4.518 19.874,1.221 C22.689,-0.216 26.648,1.166 30.959,4.629', stroke: 'currentColor', 'stroke-width': '3.08', 'stroke-linecap': 'round' }), $.path({ d: 'M7.64,39.45 C2.806,36.94 -0.009,33.915 0.154,30.79 C0.531,23.542 16.787,18.497 36.462,19.52 C56.137,20.544 71.781,27.249 71.404,34.497 C71.241,37.622 68.127,40.338 63.06,42.333', stroke: 'currentColor', 'stroke-width': '3.08', 'stroke-linecap': 'round' }), $.path({ d: 'M28.828,59.354 C23.545,63.168 18.843,64.561 15.902,62.653 C9.814,58.702 13.572,42.102 24.296,25.575 C35.02,9.048 48.649,-1.149 54.736,2.803 C57.566,4.639 58.269,9.208 57.133,15.232', stroke: 'currentColor', 'stroke-width': '3.08', 'stroke-linecap': 'round' }) ) ) ) ); } }; ================================================ FILE: packages/about/lib/components/update-view.js ================================================ const etch = require('etch'); const EtchComponent = require('../etch-component'); const UpdateManager = require('../update-manager'); const $ = etch.dom; module.exports = class UpdateView extends EtchComponent { constructor(props) { super(props); if ( this.props.updateManager.getAutoUpdatesEnabled() && this.props.updateManager.getState() === UpdateManager.State.Idle ) { this.props.updateManager.checkForUpdate(); } } handleAutoUpdateCheckbox(e) { atom.config.set('core.automaticallyUpdate', e.target.checked); } shouldUpdateActionButtonBeDisabled() { let { state } = this.props.updateManager; return ( state === UpdateManager.State.CheckingForUpdate || state === UpdateManager.State.DownloadingUpdate ); } executeUpdateAction() { if ( this.props.updateManager.state === UpdateManager.State.UpdateAvailableToInstall ) { this.props.updateManager.restartAndInstallUpdate(); } else { this.props.updateManager.checkForUpdate(); } } renderUpdateStatus() { let updateStatus = ''; switch (this.props.updateManager.state) { case UpdateManager.State.Idle: updateStatus = $.div( { className: 'about-updates-item is-shown about-default-update-message' }, this.props.updateManager.getAutoUpdatesEnabled() ? 'Atom will check for updates automatically' : 'Automatic updates are disabled please check manually' ); break; case UpdateManager.State.CheckingForUpdate: updateStatus = $.div( { className: 'about-updates-item app-checking-for-updates' }, $.span( { className: 'about-updates-label icon icon-search' }, 'Checking for updates...' ) ); break; case UpdateManager.State.DownloadingUpdate: updateStatus = $.div( { className: 'about-updates-item app-downloading-update' }, $.span({ className: 'loading loading-spinner-tiny inline-block' }), $.span({ className: 'about-updates-label' }, 'Downloading update') ); break; case UpdateManager.State.UpdateAvailableToInstall: updateStatus = $.div( { className: 'about-updates-item app-update-available-to-install' }, $.span( { className: 'about-updates-label icon icon-squirrel' }, 'New update' ), $.span( { className: 'about-updates-version' }, this.props.availableVersion ), $.a( { className: 'about-updates-release-notes', onclick: this.props.viewUpdateReleaseNotes }, 'Release Notes' ) ); break; case UpdateManager.State.UpToDate: updateStatus = $.div( { className: 'about-updates-item app-up-to-date' }, $.span({ className: 'icon icon-check' }), $.span( { className: 'about-updates-label is-strong' }, 'Atom is up to date!' ) ); break; case UpdateManager.State.Unsupported: updateStatus = $.div( { className: 'about-updates-item app-unsupported' }, $.span( { className: 'about-updates-label is-strong' }, 'Your system does not support automatic updates' ), $.a( { className: 'about-updates-instructions', onclick: this.props.viewUpdateInstructions }, 'How to update' ) ); break; case UpdateManager.State.Error: updateStatus = $.div( { className: 'about-updates-item app-update-error' }, $.span({ className: 'icon icon-x' }), $.span( { className: 'about-updates-label app-error-message is-strong' }, this.props.updateManager.getErrorMessage() ) ); break; } return updateStatus; } render() { return $.div( { className: 'about-updates group-start' }, $.div( { className: 'about-updates-box' }, $.div({ className: 'about-updates-status' }, this.renderUpdateStatus()), $.button( { className: 'btn about-update-action-button', disabled: this.shouldUpdateActionButtonBeDisabled(), onclick: this.executeUpdateAction.bind(this), style: { display: this.props.updateManager.state === UpdateManager.State.Unsupported ? 'none' : 'block' } }, this.props.updateManager.state === 'update-available' ? 'Restart and install' : 'Check now' ) ), $.div( { className: 'about-auto-updates', style: { display: this.props.updateManager.state === UpdateManager.State.Unsupported ? 'none' : 'block' } }, $.label( {}, $.input({ className: 'input-checkbox', type: 'checkbox', checked: this.props.updateManager.getAutoUpdatesEnabled(), onchange: this.handleAutoUpdateCheckbox.bind(this) }), $.span({}, 'Automatically download updates') ) ) ); } }; ================================================ FILE: packages/about/lib/etch-component.js ================================================ const etch = require('etch'); /* Public: Abstract class for handling the initialization boilerplate of an Etch component. */ module.exports = class EtchComponent { constructor(props) { this.props = props; etch.initialize(this); EtchComponent.setScheduler(atom.views); } /* Public: Gets the scheduler Etch uses for coordinating DOM updates. Returns a {Scheduler} */ static getScheduler() { return etch.getScheduler(); } /* Public: Sets the scheduler Etch uses for coordinating DOM updates. * `scheduler` {Scheduler} */ static setScheduler(scheduler) { etch.setScheduler(scheduler); } /* Public: Updates the component's properties and re-renders it. Only the properties you specify in this object will update – any other properties the component stores will be unaffected. * `props` an {Object} representing the properties you want to update */ update(props) { let oldProps = this.props; this.props = Object.assign({}, oldProps, props); return etch.update(this); } /* Public: Destroys the component, removing it from the DOM. */ destroy() { etch.destroy(this); } render() { throw new Error('Etch components must implement a `render` method'); } }; ================================================ FILE: packages/about/lib/main.js ================================================ const { CompositeDisposable } = require('atom'); const semver = require('semver'); const UpdateManager = require('./update-manager'); const About = require('./about'); const StatusBarView = require('./components/about-status-bar'); let updateManager; // The local storage key for the available update version. const AvailableUpdateVersion = 'about:version-available'; const AboutURI = 'atom://about'; module.exports = { activate() { this.subscriptions = new CompositeDisposable(); this.createModel(); let availableVersion = window.localStorage.getItem(AvailableUpdateVersion); if ( atom.getReleaseChannel() === 'dev' || (availableVersion && semver.lte(availableVersion, atom.getVersion())) ) { this.clearUpdateState(); } this.subscriptions.add( updateManager.onDidChange(() => { if ( updateManager.getState() === UpdateManager.State.UpdateAvailableToInstall ) { window.localStorage.setItem( AvailableUpdateVersion, updateManager.getAvailableVersion() ); this.showStatusBarIfNeeded(); } }) ); this.subscriptions.add( atom.commands.add('atom-workspace', 'about:clear-update-state', () => { this.clearUpdateState(); }) ); }, deactivate() { this.model.destroy(); if (this.statusBarTile) this.statusBarTile.destroy(); if (updateManager) { updateManager.dispose(); updateManager = undefined; } }, clearUpdateState() { window.localStorage.removeItem(AvailableUpdateVersion); }, consumeStatusBar(statusBar) { this.statusBar = statusBar; this.showStatusBarIfNeeded(); }, deserializeAboutView(state) { if (!this.model) { this.createModel(); } return this.model.deserialize(state); }, createModel() { updateManager = updateManager || new UpdateManager(); this.model = new About({ uri: AboutURI, currentAtomVersion: atom.getVersion(), currentElectronVersion: process.versions.electron, currentChromeVersion: process.versions.chrome, currentNodeVersion: process.version, updateManager: updateManager }); }, isUpdateAvailable() { let availableVersion = window.localStorage.getItem(AvailableUpdateVersion); return availableVersion && semver.gt(availableVersion, atom.getVersion()); }, showStatusBarIfNeeded() { if (this.isUpdateAvailable() && this.statusBar) { let statusBarView = new StatusBarView(); if (this.statusBarTile) { this.statusBarTile.destroy(); } this.statusBarTile = this.statusBar.addRightTile({ item: statusBarView, priority: -100 }); return this.statusBarTile; } } }; ================================================ FILE: packages/about/lib/update-manager.js ================================================ const { Emitter, CompositeDisposable } = require('atom'); const Unsupported = 'unsupported'; const Idle = 'idle'; const CheckingForUpdate = 'checking'; const DownloadingUpdate = 'downloading'; const UpdateAvailableToInstall = 'update-available'; const UpToDate = 'no-update-available'; const ErrorState = 'error'; let UpdateManager = class UpdateManager { constructor() { this.emitter = new Emitter(); this.currentVersion = atom.getVersion(); this.availableVersion = atom.getVersion(); this.resetState(); this.listenForAtomEvents(); } listenForAtomEvents() { this.subscriptions = new CompositeDisposable(); this.subscriptions.add( atom.autoUpdater.onDidBeginCheckingForUpdate(() => { this.setState(CheckingForUpdate); }), atom.autoUpdater.onDidBeginDownloadingUpdate(() => { this.setState(DownloadingUpdate); }), atom.autoUpdater.onDidCompleteDownloadingUpdate(({ releaseVersion }) => { this.setAvailableVersion(releaseVersion); }), atom.autoUpdater.onUpdateNotAvailable(() => { this.setState(UpToDate); }), atom.autoUpdater.onUpdateError(() => { this.setState(ErrorState); }), atom.config.observe('core.automaticallyUpdate', value => { this.autoUpdatesEnabled = value; this.emitDidChange(); }) ); // TODO: When https://github.com/atom/electron/issues/4587 is closed we can add this support. // atom.autoUpdater.onUpdateAvailable => // @find('.about-updates-item').removeClass('is-shown') // @updateAvailable.addClass('is-shown') } dispose() { this.subscriptions.dispose(); } onDidChange(callback) { return this.emitter.on('did-change', callback); } emitDidChange() { this.emitter.emit('did-change'); } getAutoUpdatesEnabled() { return ( this.autoUpdatesEnabled && this.state !== UpdateManager.State.Unsupported ); } setAutoUpdatesEnabled(enabled) { return atom.config.set('core.automaticallyUpdate', enabled); } getErrorMessage() { return atom.autoUpdater.getErrorMessage(); } getState() { return this.state; } setState(state) { this.state = state; this.emitDidChange(); } resetState() { this.state = atom.autoUpdater.platformSupportsUpdates() ? atom.autoUpdater.getState() : Unsupported; this.emitDidChange(); } getAvailableVersion() { return this.availableVersion; } setAvailableVersion(version) { this.availableVersion = version; if (this.availableVersion !== this.currentVersion) { this.state = UpdateAvailableToInstall; } else { this.state = UpToDate; } this.emitDidChange(); } checkForUpdate() { atom.autoUpdater.checkForUpdate(); } restartAndInstallUpdate() { atom.autoUpdater.restartAndInstallUpdate(); } getReleaseNotesURLForCurrentVersion() { return this.getReleaseNotesURLForVersion(this.currentVersion); } getReleaseNotesURLForAvailableVersion() { return this.getReleaseNotesURLForVersion(this.availableVersion); } getReleaseNotesURLForVersion(appVersion) { // Dev versions will not have a releases page if (appVersion.indexOf('dev') > -1) { return 'https://atom.io/releases'; } if (!appVersion.startsWith('v')) { appVersion = `v${appVersion}`; } const releaseRepo = appVersion.indexOf('nightly') > -1 ? 'atom-nightly-releases' : 'atom'; return `https://github.com/atom/${releaseRepo}/releases/tag/${appVersion}`; } }; UpdateManager.State = { Unsupported: Unsupported, Idle: Idle, CheckingForUpdate: CheckingForUpdate, DownloadingUpdate: DownloadingUpdate, UpdateAvailableToInstall: UpdateAvailableToInstall, UpToDate: UpToDate, Error: ErrorState }; module.exports = UpdateManager; ================================================ FILE: packages/about/package.json ================================================ { "name": "about", "author": "Machisté N. Quintana ", "main": "./lib/main", "version": "1.9.1", "description": "View useful information about your Atom installation.", "keywords": [], "repository": "https://github.com/atom/atom", "license": "MIT", "scripts": { "lint": "standard" }, "engines": { "atom": ">=1.7 <2.0.0" }, "dependencies": { "etch": "0.9.0", "semver": "^5.5.0" }, "devDependencies": { "standard": "^11.0.0" }, "consumedServices": { "status-bar": { "versions": { "^1.0.0": "consumeStatusBar" } } }, "deserializers": { "AboutView": "deserializeAboutView" }, "standard": { "env": [ "browser", "node", "atomtest", "jasmine" ], "globals": [ "atom" ] } } ================================================ FILE: packages/about/spec/about-spec.js ================================================ describe('About', () => { let workspaceElement; beforeEach(async () => { let storage = {}; spyOn(window.localStorage, 'setItem').andCallFake((key, value) => { storage[key] = value; }); spyOn(window.localStorage, 'getItem').andCallFake(key => { return storage[key]; }); workspaceElement = atom.views.getView(atom.workspace); await atom.packages.activatePackage('about'); }); it('deserializes correctly', () => { let deserializedAboutView = atom.deserializers.deserialize({ deserializer: 'AboutView', uri: 'atom://about' }); expect(deserializedAboutView).toBeTruthy(); }); describe('when the about:about-atom command is triggered', () => { it('shows the About Atom view', async () => { // Attaching the workspaceElement to the DOM is required to allow the // `toBeVisible()` matchers to work. Anything testing visibility or focus // requires that the workspaceElement is on the DOM. Tests that attach the // workspaceElement to the DOM are generally slower than those off DOM. jasmine.attachToDOM(workspaceElement); expect(workspaceElement.querySelector('.about')).not.toExist(); await atom.workspace.open('atom://about'); let aboutElement = workspaceElement.querySelector('.about'); expect(aboutElement).toBeVisible(); }); }); describe('when the Atom version number is clicked', () => { it('copies the version number to the clipboard', async () => { await atom.workspace.open('atom://about'); jasmine.attachToDOM(workspaceElement); let aboutElement = workspaceElement.querySelector('.about'); let versionContainer = aboutElement.querySelector('.atom'); versionContainer.click(); expect(atom.clipboard.read()).toBe(atom.getVersion()); }); }); describe('when the show more link is clicked', () => { it('expands to show additional version numbers', async () => { await atom.workspace.open('atom://about'); jasmine.attachToDOM(workspaceElement); let aboutElement = workspaceElement.querySelector('.about'); let showMoreElement = aboutElement.querySelector('.show-more-expand'); let moreInfoElement = workspaceElement.querySelector('.show-more'); showMoreElement.click(); expect(moreInfoElement).toBeVisible(); }); }); describe('when the Electron version number is clicked', () => { it('copies the version number to the clipboard', async () => { await atom.workspace.open('atom://about'); jasmine.attachToDOM(workspaceElement); let aboutElement = workspaceElement.querySelector('.about'); let versionContainer = aboutElement.querySelector('.electron'); versionContainer.click(); expect(atom.clipboard.read()).toBe(process.versions.electron); }); }); describe('when the Chrome version number is clicked', () => { it('copies the version number to the clipboard', async () => { await atom.workspace.open('atom://about'); jasmine.attachToDOM(workspaceElement); let aboutElement = workspaceElement.querySelector('.about'); let versionContainer = aboutElement.querySelector('.chrome'); versionContainer.click(); expect(atom.clipboard.read()).toBe(process.versions.chrome); }); }); describe('when the Node version number is clicked', () => { it('copies the version number to the clipboard', async () => { await atom.workspace.open('atom://about'); jasmine.attachToDOM(workspaceElement); let aboutElement = workspaceElement.querySelector('.about'); let versionContainer = aboutElement.querySelector('.node'); versionContainer.click(); expect(atom.clipboard.read()).toBe(process.version); }); }); }); ================================================ FILE: packages/about/spec/about-status-bar-spec.js ================================================ const { conditionPromise } = require('./helpers/async-spec-helpers'); const MockUpdater = require('./mocks/updater'); describe('the status bar', () => { let atomVersion; let workspaceElement; beforeEach(async () => { let storage = {}; spyOn(window.localStorage, 'setItem').andCallFake((key, value) => { storage[key] = value; }); spyOn(window.localStorage, 'getItem').andCallFake(key => { return storage[key]; }); spyOn(atom, 'getVersion').andCallFake(() => { return atomVersion; }); workspaceElement = atom.views.getView(atom.workspace); await atom.packages.activatePackage('status-bar'); await atom.workspace.open('sample.js'); jasmine.attachToDOM(workspaceElement); }); afterEach(async () => { await atom.packages.deactivatePackage('about'); await atom.packages.deactivatePackage('status-bar'); }); describe('on a stable version', function() { beforeEach(async () => { atomVersion = '1.2.3'; await atom.packages.activatePackage('about'); }); describe('with no update', () => { it('does not show the view', () => { expect(workspaceElement).not.toContain('.about-release-notes'); }); }); describe('with an update', () => { it('shows the view when the update finishes downloading', () => { MockUpdater.finishDownloadingUpdate('42.0.0'); expect(workspaceElement).toContain('.about-release-notes'); }); describe('clicking on the status', () => { it('opens the about page', async () => { MockUpdater.finishDownloadingUpdate('42.0.0'); workspaceElement.querySelector('.about-release-notes').click(); await conditionPromise(() => workspaceElement.querySelector('.about') ); expect(workspaceElement.querySelector('.about')).toExist(); }); }); it('continues to show the squirrel until Atom is updated to the new version', async () => { MockUpdater.finishDownloadingUpdate('42.0.0'); expect(workspaceElement).toContain('.about-release-notes'); await atom.packages.deactivatePackage('about'); expect(workspaceElement).not.toContain('.about-release-notes'); await atom.packages.activatePackage('about'); await Promise.resolve(); // Service consumption hooks are deferred until the next tick expect(workspaceElement).toContain('.about-release-notes'); await atom.packages.deactivatePackage('about'); expect(workspaceElement).not.toContain('.about-release-notes'); atomVersion = '42.0.0'; await atom.packages.activatePackage('about'); await Promise.resolve(); // Service consumption hooks are deferred until the next tick expect(workspaceElement).not.toContain('.about-release-notes'); }); it('does not show the view if Atom is updated to a newer version than notified', async () => { MockUpdater.finishDownloadingUpdate('42.0.0'); await atom.packages.deactivatePackage('about'); atomVersion = '43.0.0'; await atom.packages.activatePackage('about'); await Promise.resolve(); // Service consumption hooks are deferred until the next tick expect(workspaceElement).not.toContain('.about-release-notes'); }); }); }); describe('on a beta version', function() { beforeEach(async () => { atomVersion = '1.2.3-beta4'; await atom.packages.activatePackage('about'); }); describe('with no update', () => { it('does not show the view', () => { expect(workspaceElement).not.toContain('.about-release-notes'); }); }); describe('with an update', () => { it('shows the view when the update finishes downloading', () => { MockUpdater.finishDownloadingUpdate('42.0.0'); expect(workspaceElement).toContain('.about-release-notes'); }); describe('clicking on the status', () => { it('opens the about page', async () => { MockUpdater.finishDownloadingUpdate('42.0.0'); workspaceElement.querySelector('.about-release-notes').click(); await conditionPromise(() => workspaceElement.querySelector('.about') ); expect(workspaceElement.querySelector('.about')).toExist(); }); }); it('continues to show the squirrel until Atom is updated to the new version', async () => { MockUpdater.finishDownloadingUpdate('42.0.0'); expect(workspaceElement).toContain('.about-release-notes'); await atom.packages.deactivatePackage('about'); expect(workspaceElement).not.toContain('.about-release-notes'); await atom.packages.activatePackage('about'); await Promise.resolve(); // Service consumption hooks are deferred until the next tick expect(workspaceElement).toContain('.about-release-notes'); await atom.packages.deactivatePackage('about'); expect(workspaceElement).not.toContain('.about-release-notes'); atomVersion = '42.0.0'; await atom.packages.activatePackage('about'); await Promise.resolve(); // Service consumption hooks are deferred until the next tick expect(workspaceElement).not.toContain('.about-release-notes'); }); it('does not show the view if Atom is updated to a newer version than notified', async () => { MockUpdater.finishDownloadingUpdate('42.0.0'); await atom.packages.deactivatePackage('about'); atomVersion = '43.0.0'; await atom.packages.activatePackage('about'); await Promise.resolve(); // Service consumption hooks are deferred until the next tick expect(workspaceElement).not.toContain('.about-release-notes'); }); }); }); describe('on a development version', function() { beforeEach(async () => { atomVersion = '1.2.3-dev-0123abcd'; await atom.packages.activatePackage('about'); }); describe('with no update', () => { it('does not show the view', () => { expect(workspaceElement).not.toContain('.about-release-notes'); }); }); describe('with a previously downloaded update', () => { it('does not show the view', () => { window.localStorage.setItem('about:version-available', '42.0.0'); expect(workspaceElement).not.toContain('.about-release-notes'); }); }); }); }); ================================================ FILE: packages/about/spec/helpers/async-spec-helpers.js ================================================ /** @babel */ const { now } = Date; const { setTimeout } = global; export async function conditionPromise(condition) { const startTime = now(); while (true) { await timeoutPromise(100); if (await condition()) { return; } if (now() - startTime > 5000) { throw new Error('Timed out waiting on condition'); } } } export function timeoutPromise(timeout) { return new Promise(function(resolve) { setTimeout(resolve, timeout); }); } ================================================ FILE: packages/about/spec/mocks/updater.js ================================================ module.exports = { updateError() { atom.autoUpdater.emitter.emit('update-error'); }, checkForUpdate() { atom.autoUpdater.emitter.emit('did-begin-checking-for-update'); }, updateNotAvailable() { atom.autoUpdater.emitter.emit('update-not-available'); }, downloadUpdate() { atom.autoUpdater.emitter.emit('did-begin-downloading-update'); }, finishDownloadingUpdate(releaseVersion) { atom.autoUpdater.emitter.emit('did-complete-downloading-update', { releaseVersion }); } }; ================================================ FILE: packages/about/spec/update-manager-spec.js ================================================ const UpdateManager = require('../lib/update-manager'); describe('UpdateManager', () => { let updateManager; beforeEach(() => { updateManager = new UpdateManager(); }); describe('::getReleaseNotesURLForVersion', () => { it('returns atom.io releases when dev version', () => { expect( updateManager.getReleaseNotesURLForVersion('1.7.0-dev-e44b57d') ).toContain('atom.io/releases'); }); it('returns the page for the release when not a dev version', () => { expect(updateManager.getReleaseNotesURLForVersion('1.7.0')).toContain( 'atom/atom/releases/tag/v1.7.0' ); expect(updateManager.getReleaseNotesURLForVersion('v1.7.0')).toContain( 'atom/atom/releases/tag/v1.7.0' ); expect( updateManager.getReleaseNotesURLForVersion('1.7.0-beta10') ).toContain('atom/atom/releases/tag/v1.7.0-beta10'); expect( updateManager.getReleaseNotesURLForVersion('1.7.0-nightly10') ).toContain('atom/atom-nightly-releases/releases/tag/v1.7.0-nightly10'); }); }); }); ================================================ FILE: packages/about/spec/update-view-spec.js ================================================ const { shell } = require('electron'); const main = require('../lib/main'); const AboutView = require('../lib/components/about-view'); const UpdateView = require('../lib/components/update-view'); const MockUpdater = require('./mocks/updater'); describe('UpdateView', () => { let aboutElement; let updateManager; let workspaceElement; let scheduler; beforeEach(async () => { let storage = {}; spyOn(window.localStorage, 'setItem').andCallFake((key, value) => { storage[key] = value; }); spyOn(window.localStorage, 'getItem').andCallFake(key => { return storage[key]; }); workspaceElement = atom.views.getView(atom.workspace); await atom.packages.activatePackage('about'); spyOn(atom.autoUpdater, 'getState').andReturn('idle'); spyOn(atom.autoUpdater, 'checkForUpdate'); spyOn(atom.autoUpdater, 'platformSupportsUpdates').andReturn(true); }); describe('when the About page is open', () => { beforeEach(async () => { jasmine.attachToDOM(workspaceElement); await atom.workspace.open('atom://about'); aboutElement = workspaceElement.querySelector('.about'); updateManager = main.model.state.updateManager; scheduler = AboutView.getScheduler(); }); describe('when the updates are not supported by the platform', () => { beforeEach(async () => { atom.autoUpdater.platformSupportsUpdates.andReturn(false); updateManager.resetState(); await scheduler.getNextUpdatePromise(); }); it('hides the auto update UI and shows the update instructions link', async () => { expect( aboutElement.querySelector('.about-update-action-button') ).not.toBeVisible(); expect( aboutElement.querySelector('.about-auto-updates') ).not.toBeVisible(); }); it('opens the update instructions page when the instructions link is clicked', async () => { spyOn(shell, 'openExternal'); let link = aboutElement.querySelector( '.app-unsupported .about-updates-instructions' ); link.click(); let args = shell.openExternal.mostRecentCall.args; expect(shell.openExternal).toHaveBeenCalled(); expect(args[0]).toContain('installing-atom'); }); }); describe('when updates are supported by the platform', () => { beforeEach(async () => { atom.autoUpdater.platformSupportsUpdates.andReturn(true); updateManager.resetState(); await scheduler.getNextUpdatePromise(); }); it('shows the auto update UI', () => { expect(aboutElement.querySelector('.about-updates')).toBeVisible(); }); it('shows the correct panels when the app checks for updates and there is no update available', async () => { expect( aboutElement.querySelector('.about-default-update-message') ).toBeVisible(); MockUpdater.checkForUpdate(); await scheduler.getNextUpdatePromise(); expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible(); expect( aboutElement.querySelector('.app-checking-for-updates') ).toBeVisible(); MockUpdater.updateNotAvailable(); await scheduler.getNextUpdatePromise(); expect(aboutElement.querySelector('.app-up-to-date')).toBeVisible(); expect( aboutElement.querySelector('.app-checking-for-updates') ).not.toBeVisible(); }); it('shows the correct panels when the app checks for updates and encounters an error', async () => { expect( aboutElement.querySelector('.about-default-update-message') ).toBeVisible(); MockUpdater.checkForUpdate(); await scheduler.getNextUpdatePromise(); expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible(); expect( aboutElement.querySelector('.app-checking-for-updates') ).toBeVisible(); spyOn(atom.autoUpdater, 'getErrorMessage').andReturn( 'an error message' ); MockUpdater.updateError(); await scheduler.getNextUpdatePromise(); expect(aboutElement.querySelector('.app-update-error')).toBeVisible(); expect( aboutElement.querySelector('.app-error-message').textContent ).toBe('an error message'); expect( aboutElement.querySelector('.app-checking-for-updates') ).not.toBeVisible(); expect( aboutElement.querySelector('.about-update-action-button').disabled ).toBe(false); expect( aboutElement.querySelector('.about-update-action-button').textContent ).toBe('Check now'); }); it('shows the correct panels and button states when the app checks for updates and an update is downloaded', async () => { expect( aboutElement.querySelector('.about-default-update-message') ).toBeVisible(); expect( aboutElement.querySelector('.about-update-action-button').disabled ).toBe(false); expect( aboutElement.querySelector('.about-update-action-button').textContent ).toBe('Check now'); MockUpdater.checkForUpdate(); await scheduler.getNextUpdatePromise(); expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible(); expect( aboutElement.querySelector('.app-checking-for-updates') ).toBeVisible(); expect( aboutElement.querySelector('.about-update-action-button').disabled ).toBe(true); expect( aboutElement.querySelector('.about-update-action-button').textContent ).toBe('Check now'); MockUpdater.downloadUpdate(); await scheduler.getNextUpdatePromise(); expect( aboutElement.querySelector('.app-checking-for-updates') ).not.toBeVisible(); expect( aboutElement.querySelector('.app-downloading-update') ).toBeVisible(); // TODO: at some point it would be nice to be able to cancel an update download, and then this would be a cancel button expect( aboutElement.querySelector('.about-update-action-button').disabled ).toBe(true); expect( aboutElement.querySelector('.about-update-action-button').textContent ).toBe('Check now'); MockUpdater.finishDownloadingUpdate('42.0.0'); await scheduler.getNextUpdatePromise(); expect( aboutElement.querySelector('.app-downloading-update') ).not.toBeVisible(); expect( aboutElement.querySelector('.app-update-available-to-install') ).toBeVisible(); expect( aboutElement.querySelector( '.app-update-available-to-install .about-updates-version' ).textContent ).toBe('42.0.0'); expect( aboutElement.querySelector('.about-update-action-button').disabled ).toBe(false); expect( aboutElement.querySelector('.about-update-action-button').textContent ).toBe('Restart and install'); }); it('opens the release notes for the downloaded release when the release notes link are clicked', async () => { MockUpdater.finishDownloadingUpdate('1.2.3'); await scheduler.getNextUpdatePromise(); spyOn(shell, 'openExternal'); let link = aboutElement.querySelector( '.app-update-available-to-install .about-updates-release-notes' ); link.click(); let args = shell.openExternal.mostRecentCall.args; expect(shell.openExternal).toHaveBeenCalled(); expect(args[0]).toContain('/v1.2.3'); }); it('executes checkForUpdate() when the check for update button is clicked', () => { let button = aboutElement.querySelector('.about-update-action-button'); button.click(); expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled(); }); it('executes restartAndInstallUpdate() when the restart and install button is clicked', async () => { spyOn(atom.autoUpdater, 'restartAndInstallUpdate'); MockUpdater.finishDownloadingUpdate('42.0.0'); await scheduler.getNextUpdatePromise(); let button = aboutElement.querySelector('.about-update-action-button'); button.click(); expect(atom.autoUpdater.restartAndInstallUpdate).toHaveBeenCalled(); }); it("starts in the same state as atom's AutoUpdateManager", async () => { atom.autoUpdater.getState.andReturn('downloading'); updateManager.resetState(); await scheduler.getNextUpdatePromise(); expect( aboutElement.querySelector('.app-checking-for-updates') ).not.toBeVisible(); expect( aboutElement.querySelector('.app-downloading-update') ).toBeVisible(); expect( aboutElement.querySelector('.about-update-action-button').disabled ).toBe(true); expect( aboutElement.querySelector('.about-update-action-button').textContent ).toBe('Check now'); }); describe('when core.automaticallyUpdate is toggled', () => { beforeEach(async () => { expect(atom.config.get('core.automaticallyUpdate')).toBe(true); atom.autoUpdater.checkForUpdate.reset(); }); it('shows the auto update UI', async () => { expect( aboutElement.querySelector('.about-auto-updates input').checked ).toBe(true); expect( aboutElement.querySelector('.about-default-update-message') ).toBeVisible(); expect( aboutElement.querySelector('.about-default-update-message') .textContent ).toBe('Atom will check for updates automatically'); atom.config.set('core.automaticallyUpdate', false); await scheduler.getNextUpdatePromise(); expect( aboutElement.querySelector('.about-auto-updates input').checked ).toBe(false); expect( aboutElement.querySelector('.about-default-update-message') ).toBeVisible(); expect( aboutElement.querySelector('.about-default-update-message') .textContent ).toBe('Automatic updates are disabled please check manually'); }); it('updates config and the UI when the checkbox is used to toggle', async () => { expect( aboutElement.querySelector('.about-auto-updates input').checked ).toBe(true); aboutElement.querySelector('.about-auto-updates input').click(); await scheduler.getNextUpdatePromise(); expect(atom.config.get('core.automaticallyUpdate')).toBe(false); expect( aboutElement.querySelector('.about-auto-updates input').checked ).toBe(false); expect( aboutElement.querySelector('.about-default-update-message') ).toBeVisible(); expect( aboutElement.querySelector('.about-default-update-message') .textContent ).toBe('Automatic updates are disabled please check manually'); aboutElement.querySelector('.about-auto-updates input').click(); await scheduler.getNextUpdatePromise(); expect(atom.config.get('core.automaticallyUpdate')).toBe(true); expect( aboutElement.querySelector('.about-auto-updates input').checked ).toBe(true); expect( aboutElement.querySelector('.about-default-update-message') ).toBeVisible(); expect( aboutElement.querySelector('.about-default-update-message') .textContent ).toBe('Atom will check for updates automatically'); }); describe('checking for updates', function() { afterEach(() => { this.updateView = null; }); it('checks for update when the about page is shown', () => { expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled(); this.updateView = new UpdateView({ updateManager: updateManager, availableVersion: '9999.0.0', viewUpdateReleaseNotes: () => {} }); expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled(); }); it('does not check for update when the about page is shown and the update manager is not in the idle state', () => { atom.autoUpdater.getState.andReturn('downloading'); updateManager.resetState(); expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled(); this.updateView = new UpdateView({ updateManager: updateManager, availableVersion: '9999.0.0', viewUpdateReleaseNotes: () => {} }); expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled(); }); it('does not check for update when the about page is shown and auto updates are turned off', () => { atom.config.set('core.automaticallyUpdate', false); expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled(); this.updateView = new UpdateView({ updateManager: updateManager, availableVersion: '9999.0.0', viewUpdateReleaseNotes: () => {} }); expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled(); }); }); }); }); }); describe('when the About page is not open and an update is downloaded', () => { it('should display the new version when it is opened', async () => { MockUpdater.finishDownloadingUpdate('42.0.0'); jasmine.attachToDOM(workspaceElement); await atom.workspace.open('atom://about'); aboutElement = workspaceElement.querySelector('.about'); updateManager = main.model.state.updateManager; scheduler = AboutView.getScheduler(); expect( aboutElement.querySelector('.app-update-available-to-install') ).toBeVisible(); expect( aboutElement.querySelector( '.app-update-available-to-install .about-updates-version' ).textContent ).toBe('42.0.0'); expect( aboutElement.querySelector('.about-update-action-button').disabled ).toBe(false); expect( aboutElement.querySelector('.about-update-action-button').textContent ).toBe('Restart and install'); }); }); }); ================================================ FILE: packages/about/styles/about.less ================================================ @import "ui-variables"; @import "variables"; .about { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; -webkit-user-select: none; cursor: default; overflow: auto; text-align: center; font-size: 1.25em; line-height: 1.4; padding: 4em; color: @text-color; background-color: @base-background-color; button { cursor: default; } a:focus { // Don't use Bootstrap default here color: inherit; } img, a { -webkit-user-drag: none; } .input-checkbox { margin-top: -.2em; } // used to group different elements .group-start { margin-top: 4em; } .group-item { margin-top: 1.5em; } } .about-container { width: 100%; max-width: 500px; } // Header -------------------------------- .about-atom-io:hover { .about-logo { color: @atom-green; } } .about-logo { display: block; width: 100%; max-width: 280px; margin: 0 auto 1em auto; color: @text-color-highlight; transition: color 0.2s; } .about-version-container { &:hover { color: lighten(@text-color, 15%); } &:active { color: lighten(@text-color, 30%); } } .about-version { margin-right: .5em; font-size: 1.25em; vertical-align: middle; } .about-more-version { color: @text-color-subtle; font-size: .9em; } .about-header-release-notes { vertical-align: middle; margin-left: 1em; } // Updates -------------------------------- .about-updates { width: 100%; max-width: 500px; } .about-updates-box { display: flex; align-items: center; padding: @component-padding; border: 1px solid @base-border-color; border-radius: @component-border-radius * 2; background-color: @background-color-highlight; } .about-updates-status { flex: 1; margin-left: .5em; text-align: left; } .about-updates-item, .about-default-update-message .about-updates-label { display: block; } .about-updates-label { color: @text-color-subtle; &.is-strong { color: @text-color; } } .about-updates-version { margin: 0 .4em; } .about-updates-release-notes, .about-updates-instructions { margin: 0 1em 0 1.5em; } .about-auto-updates { margin-top: 1em; input { margin-right: .5em; } } // Love -------------------------------- .about-love { .icon::before { // Make these octicons look good inlined with text position: relative; width: auto; height: auto; margin-right: 0; font-size: 1.5em; vertical-align: text-top; } .icon-logo-github::before { font-size: 3.6em; height: .36em; } } .about-credits { color: @text-color-subtle; } // the blue squirrel -------------------------------- .about-release-notes { color: @background-color-info; &:hover { color: lighten(@background-color-info, 15%); } } ================================================ FILE: packages/about/styles/variables.less ================================================ @atom-green: #40a977; ================================================ FILE: packages/atom-dark-syntax/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/atom-dark-syntax/README.md ================================================ # Atom Dark Syntax theme A dark syntax theme for Atom. This theme is installed by default with Atom and can be activated by going to the _Themes_ section in the Settings view (`cmd-,`) and selecting it from the _Syntax Themes_ dropdown menu. ![](https://f.cloud.github.com/assets/671378/2264549/f49e9bf2-9e73-11e3-9329-e2d59dd1b119.png) ================================================ FILE: packages/atom-dark-syntax/index.less ================================================ // Atom Dark Syntax theme @import "styles/syntax-variables.less"; @import "styles/editor.less"; @import "styles/syntax-legacy/_base.less"; @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; @import "styles/syntax/html.less"; ================================================ FILE: packages/atom-dark-syntax/package.json ================================================ { "name": "atom-dark-syntax", "theme": "syntax", "version": "0.29.1", "description": "A dark theme for syntax", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.50.0" } } ================================================ FILE: packages/atom-dark-syntax/styles/editor.less ================================================ atom-text-editor { background-color: @syntax-background-color; color: @syntax-text-color; .invisible-character { color: @syntax-invisible-character-color; } .indent-guide { color: @syntax-indent-guide-color; } .wrap-guide { background-color: @syntax-wrap-guide-color; } .gutter { background-color: @syntax-gutter-background-color; } .gutter .cursor-line { background-color: @syntax-gutter-background-color-selected; } .line-number.cursor-line-no-selection { background-color: @syntax-gutter-background-color-selected; } .gutter .line-number.folded, .gutter .line-number:after, .fold-marker:after { color: #fba0e3; } .invisible { color: @syntax-text-color; } .cursor { border-color: @syntax-cursor-color; } .selection .region { background-color: @syntax-selection-color; } .bracket-matcher .region { border-bottom: 1px solid #f8de7e; margin-top: -1px; opacity: .7; } } ================================================ FILE: packages/atom-dark-syntax/styles/syntax/base.less ================================================ /* This defines styling rules for syntax classes. See the naming conventions for a list of syntax classes: https://flight-manual.atom.io/hacking-atom/sections/syntax-naming-conventions When styling rules conflict: - The last rule overrides previous rules. - The rule with most classes and pseudo-classes overrides the last rule. */ // if for return .syntax--keyword { color: #96CBFE; // global let def class &.syntax--storage { color: #96CBFE; } // int char float &.syntax--type { color: #FFFFB6; } // and del not &.syntax--operator { color: #96CBFE; } // super &.syntax--function { color: #C6C5FE; } // this self &.syntax--variable { color: #C6C5FE; } // = + && | << ? &.syntax--symbolic { color: #EDEDED; } } // identifier .syntax--entity { color: #C5C8C6; // variable &.syntax--variable { color: #C5C8C6; } // self cls iota &.syntax--support { color: #C6C5FE; } // @entity.decorator &.syntax--decorator:last-child { color: #FFD2A7; } // label: &.syntax--label { text-decoration: underline; } // import package &.syntax--package { color: #FFD2A7; } // function method &.syntax--function { color: #FFD2A7; } // add &.syntax--operator { color: #FFD2A7; // %>% <=> &.syntax--symbolic { color: #EDEDED; } } // String Class int rune list &.syntax--type { color: #FFFFB6; } // div span &.syntax--tag { color: #96CBFE; } // href src alt &.syntax--attribute { color: #FF73FD; } } // () [] {} => @ .syntax--punctuation { color: #C5C8C6; // . -> &.syntax--accessor.syntax--member { color: #EDEDED; } } // "string" .syntax--string { color: #A8FF60; // :immutable &.syntax--immutable { color: #A8FF60; } // ${variable} %().2f &.syntax--part { color: #00A0A0; } // /^reg[ex]?p/ &.syntax--regexp { color: #A8FF60; &.syntax--group { color: #A8FF60; background-color: @syntax-background-color; } // \g \" .syntax--constant.syntax--character.syntax--escape { color: #A8FF60; // \n \W \d . &.syntax--code { color: #00A0A0; } } // ^ $ \b ? + i &.syntax--language { color: #96CBFE; } // \1 &.syntax--variable { color: #C5C8C6; } // ( ) [^ ] (?= ) | r" / &.syntax--punctuation { color: #E9C062; } } } // literal true nil .syntax--constant { color: #FF73FD; // 4 1.3 Infinity &.syntax--numeric { color: #FF73FD; } // < 'a' &.syntax--character { color: #A8FF60; // \" \' \g \. &.syntax--escape { color: #A8FF60; } // \u2661 \n \t \W . &.syntax--code { color: #00A0A0; } } } // text .syntax--text { color: #C5C8C6; } // __formatted__ .syntax--markup { // # Heading &.syntax--heading { color: #eee; } // - item &.syntax--list { color: #555; } // > quote &.syntax--quote { color: #555; } // `raw` &.syntax--raw { color: #aaa; } // url.com (path) &.syntax--link { color: #555; } // [alt] ![alt] &.syntax--alt { color: #ddd; } } // /* comment */ .syntax--comment { color: #8A8A8A; // @param TODO NOTE &.syntax--caption { color: lighten(#8A8A8A, 6); font-weight: bold; } // variable function type &.syntax--term { color: lighten(#8A8A8A, 9); } // { } / . &.syntax--punctuation { color: #8A8A8A; font-weight: normal; } } // 0invalid .syntax--invalid:not(.syntax--punctuation) { // §illegal &.syntax--illegal { color: #FD5FF1 !important; background-color: rgba(86, 45, 86, 0.75) !important; } // obsolete() &.syntax--deprecated { color: #FD5FF1 !important; text-decoration: underline !important; } } ================================================ FILE: packages/atom-dark-syntax/styles/syntax/css.less ================================================ .syntax--source.syntax--css { .syntax--entity { // function() &.syntax--function { color: #C5C8C6; // url rgb &.syntax--support { color: #DAD085; } } // .class :pseudo-class attribute &.syntax--selector { color: #FF73FD; // div span &.syntax--tag { color: #96CBFE; text-decoration: underline; } // #id &.syntax--id { color: #8B98AB; } // .class &.syntax--class { color: #62B1FE; } } // property: constant &.syntax--property { // height position border &.syntax--support { color: #EDEDED; } } // --variable &.syntax--variable { color: #C6C5FE; } // @keyframes keyframe &.syntax--keyframe { color: #C6C5FE; } } // property: constant .syntax--constant { color: #C5C8C6; // flex solid bold &.syntax--support { color: #F9EE98; } // 4 1.3 &.syntax--numeric { color: #99CC99; // px % cm hz &.syntax--unit { color: #99CC99; } } // screen print &.syntax--media { color: #FFD2A7; } // #b294bb blue red &.syntax--color { color: #99CC99; } // from to 50% &.syntax--offset { color: #FFD2A7; // % &.syntax--unit { color: #FFD2A7; } } } // . : :: # [] () .syntax--punctuation { color: #C5C8C6; // * &.syntax--wildcard { color: #96CBFE; text-decoration: underline; } } } ================================================ FILE: packages/atom-dark-syntax/styles/syntax/html.less ================================================ .syntax--source.syntax--html { .syntax--punctuation { // < /> &.syntax--tag { color: #96CBFE; } } .syntax--meta { // &.syntax--doctype { color: #8A8A8A; } } } ================================================ FILE: packages/atom-dark-syntax/styles/syntax-legacy/_base.less ================================================ .syntax--comment { color: #8a8a8a; } .syntax--entity { color: #FFD2A7; &.syntax--name.syntax--type { text-decoration: underline; color: #FFFFB6; } &.syntax--other.syntax--inherited-class { color: #9B5C2E; } } .syntax--keyword { color: #96CBFE; &.syntax--control { color: #96CBFE; } &.syntax--operator { color: #EDEDED; } } .syntax--storage { color: #CFCB90; &.syntax--modifier { color: #96CBFE; } } .syntax--constant { color: #99CC99; &.syntax--numeric { color: #FF73FD; } } .syntax--variable { color: #C6C5FE; } .syntax--invalid.syntax--deprecated { text-decoration: underline; color: #FD5FF1; } .syntax--invalid.syntax--illegal { color: #FD5FF1; background-color: rgba(86, 45, 86, 0.75); } // String interpolation in Ruby, CoffeeScript, and others .syntax--string { .syntax--source, .syntax--meta.syntax--embedded.syntax--line { color: #EDEDED; } .syntax--punctuation.syntax--section.syntax--embedded { color: #00A0A0; .syntax--source { color: #00A0A0; // Required for the end of embedded strings in Ruby #716 } } } .syntax--string { color: #A8FF60; .syntax--constant { color: #00A0A0; } &.syntax--regexp { color: #E9C062; .syntax--constant.syntax--character.syntax--escape, .syntax--source.syntax--ruby.syntax--embedded, .syntax--string.syntax--regexp.syntax--arbitrary-repetition { color: #FF8000; } &.syntax--group { color: #C6A24F; background-color: rgba(255, 255, 255, 0.06); } &.syntax--character-class { color: #B18A3D; } } .syntax--variable { color: #8A9A95; } } .syntax--support { color: #FFFFB6; &.syntax--function { color: #DAD085; } &.syntax--constant { color: #FFD2A7; } &.syntax--type.syntax--property-name.syntax--css { color: #EDEDED; } } .syntax--source .syntax--entity.syntax--name.syntax--tag, .syntax--source .syntax--punctuation.syntax--tag { color: #96CBFE; } .syntax--source .syntax--entity.syntax--other.syntax--attribute-name { color: #FF73FD; } .syntax--entity { &.syntax--other.syntax--attribute-name { color: #FF73FD; } &.syntax--name.syntax--tag.syntax--namespace, &.syntax--other.syntax--attribute-name.syntax--namespace { color: #E18964; } } .syntax--meta { &.syntax--preprocessor.syntax--c { color: #8996A8; } &.syntax--preprocessor.syntax--c .syntax--keyword { color: #AFC4DB; } &.syntax--cast { color: #676767; } &.syntax--sgml.syntax--html .syntax--meta.syntax--doctype, &.syntax--sgml.syntax--html .syntax--meta.syntax--doctype .syntax--entity, &.syntax--sgml.syntax--html .syntax--meta.syntax--doctype .syntax--string, &.syntax--xml-processing, &.syntax--xml-processing .syntax--entity, &.syntax--xml-processing .syntax--string { color: #8a8a8a; } &.syntax--tag .syntax--entity, &.syntax--tag > .syntax--punctuation, &.syntax--tag.syntax--inline .syntax--entity { color: #FF73FD; } &.syntax--tag .syntax--name, &.syntax--tag.syntax--inline .syntax--name, &.syntax--tag > .syntax--punctuation { color: #96CBFE; } &.syntax--selector.syntax--css .syntax--entity.syntax--name.syntax--tag { text-decoration: underline; color: #96CBFE; } &.syntax--selector.syntax--css .syntax--entity.syntax--other.syntax--attribute-name.syntax--tag.syntax--pseudo-class { color: #8F9D6A; } &.syntax--selector.syntax--css .syntax--entity.syntax--other.syntax--attribute-name.syntax--id { color: #8B98AB; } &.syntax--selector.syntax--css .syntax--entity.syntax--other.syntax--attribute-name.syntax--class { color: #62B1FE; } &.syntax--property-group .syntax--support.syntax--constant.syntax--property-value.syntax--css, &.syntax--property-value .syntax--support.syntax--constant.syntax--property-value.syntax--css { color: #F9EE98; } &.syntax--preprocessor.syntax--at-rule .syntax--keyword.syntax--control.syntax--at-rule { color: #8693A5; } &.syntax--property-value .syntax--support.syntax--constant.syntax--named-color.syntax--css, &.syntax--property-value .syntax--constant { color: #87C38A; } &.syntax--constructor.syntax--argument.syntax--css { color: #8F9D6A; } &.syntax--diff, &.syntax--diff.syntax--header { color: #F8F8F8; background-color: #0E2231; } &.syntax--separator { color: #60A633; background-color: #242424; } &.syntax--line.syntax--entry.syntax--logfile, &.syntax--line.syntax--exit.syntax--logfile { background-color: rgba(238, 238, 238, 0.16); } &.syntax--line.syntax--error.syntax--logfile { background-color: #751012; } } // Markdown Styles .syntax--source.syntax--gfm { color: #999; } .syntax--gfm { .syntax--markup.syntax--heading { color: #eee; } .syntax--link { color: #555; } .syntax--variable.syntax--list, .syntax--support.syntax--quote { color: #555; } .syntax--link .syntax--entity { color: #ddd; } .syntax--raw { color: #aaa; } } .syntax--markdown { .syntax--paragraph { color: #999; } .syntax--heading { color: #eee; } .syntax--raw { color: #aaa; } .syntax--link { color: #555; .syntax--string { color: #555; &.syntax--title { color: #ddd; } } } } ================================================ FILE: packages/atom-dark-syntax/styles/syntax-variables.less ================================================ // This defines all syntax variables that syntax themes must implement when they // include a syntax-variables.less file. // General colors @syntax-text-color: #c5c8c6; @syntax-cursor-color: white; @syntax-selection-color: #444; @syntax-selection-flash-color: #eee; @syntax-background-color: #1d1f21; // Guide colors @syntax-wrap-guide-color: rgba(197, 200, 198, .1); @syntax-indent-guide-color: rgba(197, 200, 198, .2); @syntax-invisible-character-color: rgba(197, 200, 198, .2); // For find and replace markers @syntax-result-marker-color: #888; @syntax-result-marker-color-selected: white; // Gutter colors @syntax-gutter-text-color: @syntax-text-color; @syntax-gutter-text-color-selected: @syntax-gutter-text-color; @syntax-gutter-background-color: lighten(@syntax-background-color, 5%); @syntax-gutter-background-color-selected: rgba(255, 255, 255, 0.14); // For git diff info. i.e. in the gutter @syntax-color-renamed: #96CBFE; @syntax-color-added: #A8FF60; @syntax-color-modified: #E9C062; @syntax-color-removed: #CC6666; // For language entity colors @syntax-color-variable: #C6C5FE; @syntax-color-constant: #99CC99; @syntax-color-property: #EDEDED; @syntax-color-value: #F9EE98; @syntax-color-function: #DAD085; @syntax-color-method: @syntax-color-function; @syntax-color-class: #62B1FE; @syntax-color-keyword: #96CBFE; @syntax-color-tag: #96CBFE; @syntax-color-attribute: #FF73FD; @syntax-color-import: @syntax-color-keyword; @syntax-color-snippet: @syntax-color-constant; ================================================ FILE: packages/atom-dark-ui/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/atom-dark-ui/README.md ================================================ # Atom Dark UI theme A dark UI theme for Atom. This theme is installed by default with Atom and can be activated by going to the _Themes_ section in the Settings view (`cmd-,`) and selecting it from the _UI Themes_ drop-down menu. ![](https://f.cloud.github.com/assets/671378/2265086/c6897dba-9e7b-11e3-945d-551cac610717.png) ================================================ FILE: packages/atom-dark-ui/index.less ================================================ // Atom Dark UI theme @import "styles/ui-variables.less"; @import "styles/ui-mixins.less"; @import "styles/atom.less"; @import "styles/buttons.less"; @import "styles/editor.less"; @import "styles/git.less"; @import "styles/lists.less"; @import "styles/messages.less"; @import "styles/nav.less"; @import "styles/overlays.less"; @import "styles/panels.less"; @import "styles/panes.less"; @import "styles/progress.less"; @import "styles/sites.less"; @import "styles/tabs.less"; @import "styles/text.less"; @import "styles/tooltips.less"; @import "styles/tree-view.less"; @import "styles/utilities.less"; ================================================ FILE: packages/atom-dark-ui/package.json ================================================ { "name": "atom-dark-ui", "theme": "ui", "version": "0.53.3", "description": "A dark UI theme for Atom", "license": "MIT", "repository": "https://github.com/atom/atom", "engines": { "atom": ">0.40.0" } } ================================================ FILE: packages/atom-dark-ui/styles/atom.less ================================================ * { box-sizing: border-box; } atom-workspace { background-color: @app-background-color; } .scrollbars-visible-always { ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: @scrollbar-background-color; } ::-webkit-scrollbar-thumb { background: @scrollbar-color; border-radius: 5px; box-shadow: 0 0 1px black inset; } } ================================================ FILE: packages/atom-dark-ui/styles/buttons.less ================================================ .btn-background (@color, @hover-color, @selected-color, @text-color) { color: @text-color; background-color: transparent; background-image: -webkit-linear-gradient(@color, darken(@color, 5%)); &:focus { color: @text-color; } &:hover { color: @text-color-highlight; background-image: -webkit-linear-gradient(@hover-color, darken(@hover-color, 5%)); } &.selected, &.selected:hover { color: @text-color-highlight; background-image: -webkit-linear-gradient(darken(@selected-color, 5%), @selected-color); &:hover { background-image: -webkit-linear-gradient(@selected-color, darken(@selected-color, 5%)); } } } .btn-variant (@color) { @bg: darken(@color, 10%); @hover: @color; @selected: @color; .btn-background(@bg, @hover, @selected, @text-color-highlight); } .btn { .btn-background(@button-background-color, @button-background-color-hover, @button-background-color-selected, @text-color); } .btn.btn-primary { .btn-variant(@background-color-info); } .btn.btn-info { .btn-variant(@background-color-info); } .btn.btn-success { .btn-variant(@background-color-success); } .btn.btn-warning { .btn-variant(@background-color-warning); } .btn.btn-error { .btn-variant(@background-color-error); } .caret { border-top: 5px solid #fff; margin-top: -1px; } ================================================ FILE: packages/atom-dark-ui/styles/dropdowns.less ================================================ .dropdown-menu { background-color: @overlay-background-color; border-radius: @component-border-radius; border: 1px solid @base-border-color; padding: 0; > li > a { .text(normal); } > li > a:hover { .text(highlight); background-color: @background-color-highlight; } } ================================================ FILE: packages/atom-dark-ui/styles/editor.less ================================================ atom-text-editor[mini] { color: @text-color-highlight; background-color: @input-background-color; border: 1px solid @input-border-color; box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); border-radius: @component-border-radius; padding-left: @component-padding/2; .cursor { border-color: #fff; } .selection .region { background-color: lighten(@input-background-color, 10%); } } atom-text-editor[mini].is-focused { background-color: lighten(@input-background-color, 5%); .selection .region { background-color: desaturate(@background-color-info, 50%); } } // FIXME: these should go in syntax themes? atom-text-editor { .gutter.drop-shadow { -webkit-box-shadow: -2px 0 10px 2px #222; } } ================================================ FILE: packages/atom-dark-ui/styles/git.less ================================================ .status { .text(normal); } .status-added { .text(success); } .status-ignored { .text(subtle); } .status-modified { .text(warning); } .status-removed { .text(error); } .status-renamed { .text(info); } ================================================ FILE: packages/atom-dark-ui/styles/lists.less ================================================ @import "octicon-mixins.less"; // https://github.com/atom/atom/blob/master/static/variables/octicon-mixins.less .list-group, .list-tree { li:not(.list-nested-item), li.list-nested-item > .list-item { .text(normal); } .generate-list-item-text-color(@class) { li:not(.list-nested-item).text-@{class}, li.list-nested-item.text-@{class} > .list-item { .text(@class); } } .generate-list-item-text-color(subtle); .generate-list-item-text-color(info); .generate-list-item-text-color(success); .generate-list-item-text-color(warning); .generate-list-item-text-color(error); .generate-list-item-text-color(selected); .generate-list-item-status-color(@color, @status) { li:not(.list-nested-item).status-@{status}, li.list-nested-item.status-@{status} > .list-item { color: @color; } li:not(.list-nested-item).selected.status-@{status}, li.list-nested-item.selected.status-@{status} > .list-item { color: @color; } } .generate-list-item-status-color(@text-color-subtle, ignored); .generate-list-item-status-color(@text-color-added, added); .generate-list-item-status-color(@text-color-renamed, renamed); .generate-list-item-status-color(@text-color-modified, modified); .generate-list-item-status-color(@text-color-removed, removed); li:not(.list-nested-item).selected, li.list-nested-item.selected > .list-item { .text(selected); } } .select-list ol.list-group, &.select-list ol.list-group { li.two-lines { .secondary-line { color: @text-color-subtle; } &.selected .secondary-line { color: @text-color; text-shadow: none; } } // We want to highlight the background of the list items because we dont // know their size. li.selected { background-color: @background-color-selected; &:before{ display: none; } } &.mark-active{ @active-icon-size: 14px; // pad in front of the text where the icon would be We'll pad the non- // active items with a 'fake' icon so other classes can pad the item // without worrying about the icon padding. li:before { content: ''; background-color: transparent; position: static; display: inline-block; left: auto; right: auto; height: @active-icon-size; width: @active-icon-size; } > li:not(.active):before { margin-right: @component-icon-padding; } li.active { .octicon(check, @active-icon-size); &:before { margin-right: @component-icon-padding; color: @text-color-success; } } } } .select-list.popover-list { background-color: @overlay-background-color; box-shadow: 0 0 10px @base-border-color; padding: @component-padding/2; border-radius: @component-border-radius; border: 1px solid @overlay-border-color; atom-text-editor { margin-bottom: @component-padding/2; } .list-group li { padding-left: @component-padding/2; } } .ui-sortable { li { line-height: 2.5; } // For sortable lists in the settings view li.ui-sortable-placeholder { visibility: visible !important; background-color: darken(@pane-item-background-color, 10%); } } li.ui-draggable-dragging, li.ui-sortable-helper { line-height: @component-line-height; height: @component-line-height; border: 0; border-radius: 0; list-style: none; padding: 0 @component-padding; background: @background-color-highlight; box-shadow: 0 0 1px @base-border-color; } ================================================ FILE: packages/atom-dark-ui/styles/messages.less ================================================ ul.background-message { font-weight: bold; color: rgba(0, 0, 0, .2); } ================================================ FILE: packages/atom-dark-ui/styles/nav.less ================================================ .nav-tabs { border-bottom: 1px solid @base-border-color; li { a, &.active a { border: none; margin-right: 0px; margin-bottom: 1px; } a:hover, &.active a, &.active a:hover { background-color: @background-color-highlight; border: none; color: @text-color-selected; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } &.active a { background-color: @tab-background-color-active; } } } ================================================ FILE: packages/atom-dark-ui/styles/overlays.less ================================================ atom-panel.modal, .overlay { color: @text-color; background-color: @overlay-background-color; padding: @component-padding; border: 1px solid @overlay-border-color; box-shadow: 0 0 10px @base-border-color; border-radius: @component-border-radius; atom-text-editor[mini] { margin-bottom: @component-padding; } .select-list ol.list-group, &.select-list ol.list-group { background-color: lighten(@overlay-background-color, 3%); li { padding: @component-padding; border-bottom: 1px solid @overlay-background-color; &.two-lines { padding: @component-padding/2 @component-padding; } .status.icon { float: right; margin-left: @component-icon-padding; &:before { margin-right: 0; } } &.selected { .status.icon { color: @text-color-selected; } } } } } ================================================ FILE: packages/atom-dark-ui/styles/panels.less ================================================ @import "buttons.less"; .panel { &.bordered { border: 1px solid @base-border-color; border-radius: @component-border-radius; } } atom-panel, .tool-panel { .text(normal); position: relative; background-color: @tool-panel-background-color; &.bottom, &.panel-bottom, &.footer, &.panel-footer { border-top: 1px solid @tool-panel-border-color; box-shadow: inset 0 1px 0 @background-color-highlight; } &.left, &.panel-left { border-right: 1px solid @tool-panel-border-color; } &.right, &.panel-right { border-left: 1px solid @tool-panel-border-color; } } .inset-panel { position: relative; background-color: @inset-panel-background-color; } .is-blurred { atom-panel, .inset-panel { } } .panel-heading { .text(normal); border-bottom: 1px solid @panel-heading-border-color; border-top: 1px solid fadein(@background-color-highlight, 10%); background-color: transparent; background-image: -webkit-linear-gradient(@panel-heading-background-color, darken(@panel-heading-background-color, 10%)); .btn { padding-left: 8px; padding-right: 8px; @bg: lighten(@button-background-color, 10%); @hover: lighten(@button-background-color-hover, 10%); @selected: lighten(@button-background-color-selected, 10%); @text: lighten(@text-color, 10%); .btn-background(@bg, @hover, @selected, @text); } } ================================================ FILE: packages/atom-dark-ui/styles/panes.less ================================================ .pane-item { .panel { border-color: fadeout(@inset-panel-border-color, 30%); } } atom-pane-container { atom-pane { background-color: lighten(@app-background-color, 4%); &:focus { background-color: @app-background-color; } } atom-pane-axis.horizontal > * { border-right: 1px solid @pane-item-border-color; &:last-child { border-right: none; } } atom-pane-axis.vertical > * { border-bottom: 1px solid @pane-item-border-color; &:last-child { border-bottom: none; } } } ================================================ FILE: packages/atom-dark-ui/styles/progress.less ================================================ .loading-spinner(@size) { width: @size; height: @size; display: block; background-image: url(images/octocat-spinner-128.gif); background-repeat: no-repeat; background-size: cover; &.inline-block { display: inline-block; } } .loading-spinner-large { .loading-spinner(64px); } .loading-spinner-medium { .loading-spinner(50px); } .loading-spinner-small { .loading-spinner(32px); } .loading-spinner-tiny { .loading-spinner(20px); } // Much learning from: // http://css-tricks.com/html5-progress-element/ @progress-height: 16px; @progress-shine-gradient: -webkit-linear-gradient(top, rgba(255, 255, 255, .15), rgba(0, 0, 0, .15)); progress { height: @progress-height; -webkit-appearance: none; border-radius: @component-border-radius; background-color: #666; background-image: -webkit-linear-gradient(-30deg, transparent 33%, rgba(0, 0, 0, .1) 33%, rgba(0,0, 0, .1) 66%, transparent 66%), @progress-shine-gradient; border-radius: 2px; background-size: 25px @progress-height, 100% 100%, 100% 100%; -webkit-animation: animate-stripes 5s linear 6; // stop animation after 6 runs (30s) to limit CPU usage } progress::-webkit-progress-bar { background-color: transparent; } progress::-webkit-progress-value { border-radius: @component-border-radius; background-image: @progress-shine-gradient; background-color: @background-color-success; } progress[value] { background-image: @progress-shine-gradient; -webkit-animation: none; } @-webkit-keyframes animate-stripes { 100% { background-position: 100px 0px; } } ================================================ FILE: packages/atom-dark-ui/styles/sites.less ================================================ .ui-site(@num, @color) { .ui-site-@{num} { background-color: @color; } } .ui-site(1, @ui-site-color-1); .ui-site(2, @ui-site-color-2); .ui-site(3, @ui-site-color-3); .ui-site(4, @ui-site-color-4); .ui-site(5, @ui-site-color-5); ================================================ FILE: packages/atom-dark-ui/styles/tabs.less ================================================ @tab-radius: 3px; @modified-icon-width: 8px; @tab-skew: 30deg; @tab-top-padding: 5px; @tab-bottom-border-height: 5px; @tab-border: 1px solid @tab-border-color; @tab-bar-bottom-border-color: #111; @tab-max-width: 160px; .tab-bar { height: @tab-height + @tab-top-padding + @tab-bottom-border-height; background: @tab-bar-background-color; box-shadow: inset 0 -8px 8px -4px rgba(0,0,0, .15); padding: 0 10px 0 25px; overflow-x: auto; overflow-y: hidden; &::-webkit-scrollbar { display: none; } .tab { position: relative; top: @tab-top-padding; max-width: @tab-max-width; height: @tab-height; line-height: @tab-height; padding: 0; margin: 0 20px 0 5px; color: @text-color; transition: color .1s ease-in; border: none; &, &:before, &:after { background-image: -webkit-linear-gradient(top, lighten(#333, 7%), #333); height: @tab-height; } &:before, &:after { content: ''; position: absolute; top: 0px; width: 25px; height: @tab-height; } // left angled edge &:before { left: -14px; border-top-left-radius: @tab-radius; box-shadow: inset 1px 1px 0 @tab-border-color, -4px 0px 4px rgba(0,0,0,.1); -webkit-transform: skewX(-@tab-skew); } // right angled edge &:after { right: -14px; border-top-right-radius: @tab-radius; box-shadow: inset -1px 1px 0 @tab-border-color, 4px 0px 4px rgba(0,0,0,.1); -webkit-transform: skewX(@tab-skew); } .close-icon { right: 0; z-index: 3; text-align: right; line-height: @tab-height; color: @text-color; &:hover { color: inherit; } } &.modified:not(:hover) .close-icon { right: 0; top: @tab-height/2 - @modified-icon-width/2 + 1px; width: @modified-icon-width; height: @modified-icon-width; } &.modified:hover .close-icon:hover { color: @text-color-highlight; } .title { position: relative; z-index: 1; margin-top: -@tab-top-padding; padding-top: @tab-top-padding; padding-right: 10px; } } .tab.active { z-index: 1; color: @text-color-highlight; box-shadow: inset -1px 1px 0 @tab-border-color, 4px -4px 4px rgba(0,0,0,.1); .close-icon { line-height: @tab-height - 1px; color: @text-color; } &, &:before, &:after { background-image: -webkit-linear-gradient(top, lighten(@tab-background-color-active, 7%), @tab-background-color-active); height: @tab-height + 1px; } &:before { box-shadow: inset 1px 1px 0 @tab-border-color, -4px -4px 4px rgba(0,0,0,.1); } &:after { box-shadow: inset -1px 1px 0 @tab-border-color, 4px -4px 4px rgba(0,0,0,.1); } } .tab:hover { color: @text-color-highlight; } .tab.active:hover .close-icon { color: @text-color; &:hover { color: inherit; } } .placeholder { height: @tab-height + @tab-top-padding + @tab-bottom-border-height; pointer-events: none; &:before { margin-left: -9px; // center between tabs } &:after { top: @tab-height + @tab-top-padding + @tab-bottom-border-height - 2px; margin-left: -10px; // center between tabs } } } // border .tab-bar + .item-views::before { content: ""; position: absolute; top: -5px; height: @tab-bottom-border-height; left: 0; right: 0; background-color: @tab-background-color-active; border-top: 1px solid @tab-border-color; border-bottom: 1px solid @tab-bar-bottom-border-color; pointer-events: none; } ================================================ FILE: packages/atom-dark-ui/styles/text.less ================================================ h1, h2, h3 { line-height: 1em; margin-bottom: 15px } h1 { font-size: 2em; } h2 { font-size: 1.5em; } h3 { font-size: 1.2em; } p { line-height: 1.6; margin-bottom: 15px; } label { font-weight: normal; } pre { box-shadow: none; color: @text-color; background: @inset-panel-background-color; border-radius: @component-border-radius; border: none; margin: 0; } code { .text(highlight); background: @background-color-highlight; border-radius: @component-border-radius; } .markdown-preview code { text-shadow: none; } .selected { .text(highlight); } .text-smaller { font-size: 0.9em; } .text-subtle { .text(subtle); } .text-highlight { .text(highlight); } .text-error { .text(error); } .text-info { .text(info); &:hover { color: @text-color-info; } } .text-warning { .text(warning); &:hover { color: @text-color-warning; } } .text-success { .text(success); &:hover { color: @text-color-success; } } .highlight { color: @text-color-highlight; font-weight: bold; text-shadow: none; background-color: @background-color-highlight; border-radius: @component-border-radius; padding: 1px 3px; } .highlight-color(@name, @color, @text-color) { .highlight-@{name} { color: lighten(saturate(@text-color, 0%), 30%); font-weight: bold; text-shadow: none; background-color: fadeout(@color, 60%); border-radius: @component-border-radius; padding: 1px 3px; } } .highlight-color(info, @background-color-info, @text-color-info); .highlight-color(warning, @background-color-warning, @text-color-warning); .highlight-color(error, @background-color-error, @text-color-error); .highlight-color(success, @background-color-success, @text-color-success); .results-view .path-details.list-item { color: darken(@text-color-highlight, 18%); } ================================================ FILE: packages/atom-dark-ui/styles/tooltips.less ================================================ .tooltip { @tip-background-color: #fff; @tip-text-color: #333; white-space: nowrap; .keystroke { font-family: Helvetica, Arial, sans-serif; font-size: 13px; color: #777; padding-left: 2px; } &.in { opacity: 1; } .tooltip-inner { line-height: 19px; border-radius: @component-border-radius; background-color: @tip-background-color; color: @tip-text-color; white-space: nowrap; max-width: none; } &.top .tooltip-arrow { border-top-color: @tip-background-color; } &.top-left .tooltip-arrow { border-top-color: @tip-background-color; } &.top-right .tooltip-arrow { border-top-color: @tip-background-color; } &.right .tooltip-arrow { border-right-color: @tip-background-color; } &.left .tooltip-arrow { border-left-color: @tip-background-color; } &.bottom .tooltip-arrow { border-bottom-color: @tip-background-color; } &.bottom-left .tooltip-arrow { border-bottom-color: @tip-background-color; } &.bottom-right .tooltip-arrow { border-bottom-color: @tip-background-color; } } ================================================ FILE: packages/atom-dark-ui/styles/tree-view.less ================================================ .tree-view { font-size: @font-size; background: @tree-view-background-color; .selected:before { background: #444; box-shadow: inset -3px 0 0 rgba(0,0,0, .05); } } .tree-view-resizer { .tree-view-resize-handle { width: 8px; } } .focusable-panel { opacity: 1; box-shadow: inset -3px 0 0 rgba(0,0,0, .05); &:focus { background: #282828; .selected:before { background: @background-color-selected; } } } [data-show-on-right-side=true] { .tree-view .selected:before, .focusable-panel { box-shadow: inset 3px 0 0 rgba(0,0,0, .05); } } ================================================ FILE: packages/atom-dark-ui/styles/ui-mixins.less ================================================ // Pattern matching; ish is cray. // http://lesscss.org/#-pattern-matching-and-guard-expressions .text(normal) { font-weight: normal; color: @text-color; text-shadow: none; } .text(subtle) { font-weight: normal; color: @text-color-subtle; text-shadow: none; } .text(highlight) { font-weight: normal; color: @text-color-highlight; text-shadow: 0 1px 0 rgba(0,0,0, .5); } .text(selected) { .text(highlight) } .text(info) { color: @text-color-info; text-shadow: none; } .text(success) { color: @text-color-success; text-shadow: none; } .text(warning) { color: @text-color-warning; text-shadow: none; } .text(error) { color: @text-color-error; text-shadow: none; } ================================================ FILE: packages/atom-dark-ui/styles/ui-variables.less ================================================ // Colors @text-color: #aaa; @text-color-subtle: #555; @text-color-highlight: #fff; @text-color-selected: @text-color-highlight; @text-color-info: #5293d8; @text-color-success: #2BDA77; @text-color-warning: #ff982d; @text-color-error: #c00; @text-color-ignored: @text-color-subtle; @text-color-added: @text-color-success; @text-color-renamed: @text-color-info; @text-color-modified: @text-color-warning; @text-color-removed: @text-color-error; @background-color-info: #0098ff; @background-color-success: #17ca65; @background-color-warning: #ffaa2c; @background-color-error: #c00; @background-color-highlight: rgba(255, 255, 255, 0.07); @background-color-selected: #4182C4; @app-background-color: #333; @base-background-color: lighten(@tool-panel-background-color, 5%); @base-border-color: rgba(0, 0, 0, 0.5); @pane-item-background-color: @base-background-color; @pane-item-border-color: rgba(0, 0, 0, 0.5); @input-background-color: #212224; @input-border-color: @base-border-color; @tool-panel-background-color: #1d1f21; @tool-panel-border-color: @base-border-color; @inset-panel-background-color: #161719; @inset-panel-border-color: @base-border-color; @panel-heading-background-color: #43484d; @panel-heading-border-color: fadein(@base-border-color, 10%); @overlay-background-color: #202123; @overlay-border-color: @background-color-highlight; @button-background-color: #43484d; @button-background-color-hover: lighten(@button-background-color, 5%); @button-background-color-selected: #5c6064; @button-border-color: @base-border-color; @tab-bar-background-color: #222; @tab-bar-border-color: darken(@tab-background-color-active, 10%); @tab-background-color: #333; @tab-background-color-active: #222; @tab-border-color: #484848; @tree-view-background-color: #303030; @tree-view-border-color: @tool-panel-border-color; @scrollbar-background-color: #222425; // Needs to be opaque -> atom/atom/issues/4578 @scrollbar-color: rgba(92, 92, 92, 0.5); @ui-site-color-1: @background-color-success; // green @ui-site-color-2: @background-color-info; // blue @ui-site-color-3: @background-color-warning; // orange @ui-site-color-4: #db2ff4; // purple @ui-site-color-5: #f5e11d; // yellow // Sizes @font-size: 11px; @input-font-size: 14px; @disclosure-arrow-size: 12px; @component-padding: 10px; @component-icon-padding: 5px; @component-icon-size: 16px; @component-line-height: 25px; @component-border-radius: 2px; @tab-height: 26px; // Other @font-family: system-ui; ================================================ FILE: packages/atom-dark-ui/styles/utilities.less ================================================ .key-binding { background: -webkit-linear-gradient( rgba(100, 100, 100, 0.5), rgba(70,70,70, 0.5)); -webkit-box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.1); display: inline-block; line-height: 100%; border-radius: @component-border-radius; margin-left: @component-icon-padding; font-family: Helvetica, Arial, sans-serif; font-size: @font-size - 1px; padding: @component-padding / 2; } .badge { .text(highlight); background: @background-color-highlight; } ================================================ FILE: packages/atom-light-syntax/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/atom-light-syntax/README.md ================================================ # Atom Light Syntax theme A light syntax theme for Atom. This theme is installed by default with Atom and can be activated by going to the _Themes_ section in the Settings view (`cmd-,`) and selecting it from the _Syntax Themes_ dropdown menu. ![](https://f.cloud.github.com/assets/671378/2264690/886ce496-9e75-11e3-971a-9a24f359c481.png) ================================================ FILE: packages/atom-light-syntax/index.less ================================================ // Atom Light Syntax theme @import "styles/syntax-variables.less"; @import 'styles/editor.less'; @import 'styles/syntax-legacy/_base.less'; @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; ================================================ FILE: packages/atom-light-syntax/package.json ================================================ { "name": "atom-light-syntax", "theme": "syntax", "version": "0.29.1", "description": "A light syntax theme", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.40.0" } } ================================================ FILE: packages/atom-light-syntax/styles/editor.less ================================================ atom-text-editor { background-color: @syntax-background-color; color: @syntax-text-color; .invisible-character { color: @syntax-invisible-character-color; } .indent-guide { color: @syntax-indent-guide-color; } .wrap-guide { background-color: @syntax-wrap-guide-color; } .gutter { color: @syntax-gutter-text-color; background: @syntax-gutter-background-color; } .gutter .line-number.folded, .gutter .line-number:after, .fold-marker:after { color: #e87b00; } .invisible { color: #555; } .selection .region { background-color: #e1e1e1; } .bracket-matcher .region { background-color: #C9C9C9; opacity: .7; border-bottom: 0 none; } &.is-focused { .cursor { border-color: @syntax-cursor-color; } .selection .region { background-color: @syntax-selection-color; } .line-number.cursor-line-no-selection, .line.cursor-line { background-color: @syntax-gutter-background-color-selected; } } } ================================================ FILE: packages/atom-light-syntax/styles/syntax/base.less ================================================ /* This defines styling rules for syntax classes. See the naming conventions for a list of syntax classes: https://flight-manual.atom.io/hacking-atom/sections/syntax-naming-conventions When styling rules conflict: - The last rule overrides previous rules. - The rule with most classes and pseudo-classes overrides the last rule. */ // if for and del = && .syntax--keyword { color: #222; font-weight: bold; // global let def class &.syntax--storage { color: #222; font-weight: bold; } // int char float &.syntax--type { color: #458; font-weight: normal; } // super &.syntax--function { color: #008080; } // this self &.syntax--variable { color: #008080; } } // identifier .syntax--entity { color: #555; // function(parameter) &.syntax--parameter { color: #555; } // self cls iota &.syntax--support { color: #008080; } // @entity.decorator &.syntax--decorator:last-child { color: #900; } // label: &.syntax--label { text-decoration: underline; } // function method &.syntax--function { color: #900; } // add &.syntax--operator { color: #900; // %>% <=> &.syntax--symbolic { color: #555; } } // String Class int rune list &.syntax--type { color: #458; } // div span &.syntax--tag { color: #008080; } // href src alt &.syntax--attribute { color: #458; font-weight: bold; } } // () [] {} => @ .syntax--punctuation { // . -> &.syntax--accessor.syntax--member { color: #222; font-weight: bold; } } // "string" .syntax--string { color: #D14; // :immutable &.syntax--immutable { color: #D14; } // {placeholder} %().2f &.syntax--part { color: #606aa1; } // ${ } &.syntax--interpolation { color: #222; } // /^reg[ex]?p/ &.syntax--regexp { color: #D14; // ^ $ \b ? + i &.syntax--language { color: #222; } // \1 &.syntax--variable { color: #008080; } // ( ) [^ ] (?= ) | r" / &.syntax--punctuation { color: #222; } } } // literal 4 1.3 .syntax--constant { color: #D14; // < 'a' &.syntax--character { color: #D14; // \" \' \g \. &.syntax--escape { color: #D14; } // \u2661 \n \t \W . &.syntax--code { color: #606aa1; } } // true false nil &.syntax--language { color: #D14; } } // text .syntax--text { color: #555; } // __formatted__ .syntax--markup { // # Heading &.syntax--heading { color: #111; } // 1. * - &.syntax--list.syntax--punctuation { color: #888; } // url.com (path) &.syntax--link { color: #888; } } // /* comment */ .syntax--comment { color: #999988; font-style: italic; // @param TODO NOTE &.syntax--caption { color: lighten(#999988, 6); font-weight: bold; } // variable function type &.syntax--term { color: lighten(#999988, 9); } // { } / . &.syntax--punctuation { color: #999988; font-weight: normal; } } // 0invalid .syntax--invalid:not(.syntax--punctuation) { // §illegal &.syntax--illegal { color: #F8F8F0 !important; background-color: #00A8C6 !important; } // obsolete() &.syntax--deprecated { color: #F8F8F0 !important; background-color: #8FBE00 !important; } } ================================================ FILE: packages/atom-light-syntax/styles/syntax/css.less ================================================ .syntax--source.syntax--css { .syntax--entity { // function() &.syntax--function { color: #555; // url rgb &.syntax--support { color: #458; } } // .class :pseudo-class attribute &.syntax--selector { color: #458; font-weight: bold; // div span &.syntax--tag { color: #008080; font-weight: normal; } } // href src alt .syntax--attribute { color: #458; font-weight: bold; } // property: constant &.syntax--property { color: #555; // height position border &.syntax--support { font-weight: bold; color: #333; } } // --variable &.syntax--variable { color: #008080; } // @keyframes keyframe &.syntax--keyframe { color: #606aa1; } } // property: constant .syntax--constant { color: #555; // flex solid bold &.syntax--support { color: #099; } // 4 1.3 &.syntax--numeric { color: #099; // px % cm hz &.syntax--unit { color: #445588; font-weight: bold; } } // screen print &.syntax--media { color: #099; } // #b294bb blue red &.syntax--color { color: #099; } // [attribute=attribute-value] &.syntax--attribute-value { color: #D14; } } // . : :: # .syntax--punctuation.syntax--selector { color: #458; font-weight: bold; // * &.syntax--wildcard { color: #008080; font-weight: normal; } // [] &.syntax--attribute { color: #555; font-weight: normal; } } } ================================================ FILE: packages/atom-light-syntax/styles/syntax-legacy/_base.less ================================================ .syntax--comment { color: #999988; font-style: italic; } .syntax--string { color: #D14; } // String interpolation in Ruby, CoffeeScript, and others .syntax--string { .syntax--source, .syntax--meta.syntax--embedded.syntax--line { color: #5A5A5A; } .syntax--punctuation.syntax--section.syntax--embedded { color: #920B2D; .syntax--source { color: #920B2D; // Required for the end of embedded strings in Ruby #716 } } } .syntax--constant { &.syntax--numeric { color: #D14; } &.syntax--language { color: #606aa1; } &.syntax--character, &.syntax--other { color: #606aa1; } &.syntax--symbol { color: #990073; } &.syntax--numeric.syntax--line-number.syntax--find-in-files .syntax--match { color: rgba(143, 190, 0, 0.63); } } .syntax--variable { color: #008080; &.syntax--parameter { color: #606aa1; } } // Keywords .syntax--keyword { color: #222; font-weight: bold; &.syntax--unit { color: #445588; } &.syntax--special-method { color: #0086B3; } } .syntax--storage { color: #222; &.syntax--type { color: #222; } } .syntax--entity { &.syntax--name.syntax--class { text-decoration: underline; color: #606aa1; } &.syntax--other.syntax--inherited-class { text-decoration: underline; color: #606aa1; } &.syntax--name.syntax--function { color: #900; } &.syntax--name.syntax--tag { color: #008080; } &.syntax--other.syntax--attribute-name { color: #458; font-weight: bold; } &.syntax--name.syntax--filename.syntax--find-in-files { color: #E6DB74; } } .syntax--support { &.syntax--constant, &.syntax--function, &.syntax--type { color: #458; } &.syntax--class { color: #008080; } } .syntax--invalid { color: #F8F8F0; background-color: #00A8C6; &.syntax--deprecated { color: #F8F8F0; background-color: #8FBE00; } } .syntax--meta { &.syntax--structure.syntax--dictionary.syntax--json > .syntax--string.syntax--quoted.syntax--double.syntax--json, &.syntax--structure.syntax--dictionary.syntax--json > .syntax--string.syntax--quoted.syntax--double.syntax--json .syntax--punctuation.syntax--string { color: #000080; } &.syntax--structure.syntax--dictionary.syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--double.syntax--json { color: #d14; } &.syntax--diff, &.syntax--diff.syntax--header { color: #75715E; } } // CSS Styles .syntax--css { &.syntax--support.syntax--property-name { font-weight: bold; color: #333; } &.syntax--constant { color: #099; } } // Markdown .syntax--source.syntax--gfm { color: #444; } .syntax--gfm { .syntax--markup.syntax--heading { color: #111; } & .syntax--link { color: #888; } .syntax--variable.syntax--list { color: #888; } } .syntax--markdown { .syntax--paragraph { color: #444; } .syntax--heading { color: #111; } .syntax--link { color: #888; .syntax--string { color: #888; } } } ================================================ FILE: packages/atom-light-syntax/styles/syntax-variables.less ================================================ // This defines all syntax variables that syntax themes must implement when they // include a syntax-variables.less file. // General colors @syntax-text-color: #555; @syntax-cursor-color: black; @syntax-selection-color: #afc4da; @syntax-selection-flash-color: #69c; @syntax-background-color: white; // Guide colors @syntax-wrap-guide-color: rgba(85, 85, 85, .2); @syntax-indent-guide-color: rgba(85, 85, 85, .2); @syntax-invisible-character-color: rgba(85, 85, 85, .2); // For find and replace markers @syntax-result-marker-color: #999; @syntax-result-marker-color-selected: black; // Gutter colors @syntax-gutter-text-color: @syntax-text-color; @syntax-gutter-text-color-selected: @syntax-gutter-text-color; @syntax-gutter-background-color: white; @syntax-gutter-background-color-selected: rgba(255, 255, 134, 0.34); // For git diff info. i.e. in the gutter @syntax-color-renamed: #96CBFE; @syntax-color-added: #718C00; @syntax-color-modified: #ff982d; @syntax-color-removed: #D14; // For language entity colors @syntax-color-variable: #008080; @syntax-color-constant: #099; @syntax-color-property: #333; @syntax-color-value: @syntax-color-constant; @syntax-color-function: #900; @syntax-color-method: @syntax-color-function; @syntax-color-class: #606aa1; @syntax-color-keyword: #222; @syntax-color-tag: #008080; @syntax-color-attribute: #458; @syntax-color-import: @syntax-color-keyword; @syntax-color-snippet: @syntax-color-constant; ================================================ FILE: packages/atom-light-ui/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/atom-light-ui/README.md ================================================ # Atom Light UI theme A light UI theme for Atom. This theme is installed by default with Atom and can be activated by going to the _Themes_ section in the Settings view (`cmd-,`) and selecting it from the _UI Themes_ drop-down menu. ![](https://f.cloud.github.com/assets/671378/2265022/bb148a20-9e7a-11e3-81c8-bf5965d48183.png) ================================================ FILE: packages/atom-light-ui/index.less ================================================ // Atom Light UI theme @import "styles/ui-variables.less"; @import "styles/ui-mixins.less"; @import "styles/atom.less"; @import "styles/utilities.less"; @import "styles/text.less"; @import "styles/git.less"; @import "styles/sites.less"; @import "styles/messages.less"; @import "styles/progress.less"; @import "styles/buttons.less"; @import "styles/panels.less"; @import "styles/panes.less"; @import "styles/lists.less"; @import "styles/overlays.less"; @import "styles/editor.less"; @import "styles/tabs.less"; @import "styles/tooltips.less"; @import "styles/tree-view.less"; ================================================ FILE: packages/atom-light-ui/package.json ================================================ { "name": "atom-light-ui", "theme": "ui", "version": "0.46.3", "description": "A light UI theme for Atom", "repository": "https://github.com/atom/atom.git", "license": "MIT", "engines": { "atom": ">0.50.0" } } ================================================ FILE: packages/atom-light-ui/styles/atom.less ================================================ atom-workspace { background-color: @app-background-color; } .scrollbars-visible-always { ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: @scrollbar-background-color; } ::-webkit-scrollbar-thumb { background: @scrollbar-color; border-radius: 5px; box-shadow: 0 0 1px white inset; } } ================================================ FILE: packages/atom-light-ui/styles/buttons.less ================================================ .btn-background (@color, @hover-color, @selected-color, @border-color, @text-color, @text-color-hover) { @border-shadow: inset 0 0 0 1px @border-color; @active-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); color: @text-color; background-color: transparent; background-image: -webkit-linear-gradient(@color, darken(@color, 8%)); box-shadow: @border-shadow; &:hover { color: @text-color-hover; background-image: -webkit-linear-gradient(@hover-color, darken(@hover-color, 8%)); } &:active, &.selected:hover:active { box-shadow: @active-shadow, @border-shadow; } &.selected, &.selected:hover { color: @text-color-selected; box-shadow: inset 0 2px 5px rgba(0, 0, 0,.3), @border-shadow; text-shadow: 0 0 2px rgba(0, 0, 0, 0.3); background-image: -webkit-linear-gradient(darken(@selected-color, 8%), @selected-color); } &.selected:hover { box-shadow: @border-shadow; background-image: -webkit-linear-gradient(@selected-color, darken(@selected-color, 8%)); } } .btn-variant (@color) { @bg: darken(@color, 10%); @hover: @color; @selected: @color; @border: fadeout(darken(@color, 20%), 50%); .btn-background(@bg, @hover, @selected, @border, @text-color-selected, @text-color-selected); } .btn { .btn-background(@button-background-color, @button-background-color-hover, @button-background-color-selected, @button-border-color, @text-color, @text-color-highlight); } .btn.btn-primary { .btn-variant(@background-color-info); } .btn.btn-info { .btn-variant(@background-color-info); } .btn.btn-success { .btn-variant(@background-color-success); } .btn.btn-warning { .btn-variant(@background-color-warning); } .btn.btn-error { .btn-variant(@background-color-error); } .btn-group > .btn { border: none; } ================================================ FILE: packages/atom-light-ui/styles/editor.less ================================================ atom-text-editor[mini] { color: lighten(@text-color, 15%); background-color: darken(@input-background-color, 1%); border: 1px solid lighten(@input-border-color, 10%); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); border-radius: @component-border-radius; padding-left: @component-padding/2; .cursor { border-color: #000; } .selection .region { background-color: rgba(0, 0, 0, .2); } .placeholder-text { color: lighten(@text-color-subtle, 10%); } } atom-text-editor[mini].is-focused { color: @text-color; background-color: @input-background-color; border-color: @input-border-color; .placeholder-text { color: @text-color-subtle; } .selection .region { background-color: lighten(@background-color-info, 30%); } } // FIXME: these should go in syntax themes? atom-text-editor { .gutter.drop-shadow { -webkit-box-shadow: -2px 0 10px 2px #222; } } ================================================ FILE: packages/atom-light-ui/styles/git.less ================================================ .status { .text(normal); } .status-added { .text(success); } .status-ignored { .text(subtle); } .status-modified { .text(warning); } .status-removed { .text(error); } .status-renamed { .text(info); } ================================================ FILE: packages/atom-light-ui/styles/lists.less ================================================ @import "octicon-mixins.less"; // https://github.com/atom/atom/blob/master/static/variables/octicon-mixins.less .list-group, .list-tree { li:not(.list-nested-item), li.list-nested-item > .list-item { .text(normal); } .generate-list-item-text-color(@class) { li:not(.list-nested-item).text-@{class}, li.list-nested-item.text-@{class} > .list-item { .text(@class); } } .generate-list-item-text-color(subtle); .generate-list-item-text-color(info); .generate-list-item-text-color(success); .generate-list-item-text-color(warning); .generate-list-item-text-color(error); .generate-list-item-text-color(selected); .generate-list-item-status-color(@color, @status) { li:not(.list-nested-item).status-@{status}, li.list-nested-item.status-@{status} > .list-item { color: @color; } li:not(.list-nested-item).selected.status-@{status}, li.list-nested-item.selected.status-@{status} > .list-item { color: darken(@color, 7%); } } .generate-list-item-status-color(@text-color-subtle, ignored); .generate-list-item-status-color(@text-color-added, added); .generate-list-item-status-color(@text-color-renamed, renamed); .generate-list-item-status-color(@text-color-modified, modified); .generate-list-item-status-color(@text-color-removed, removed); li:not(.list-nested-item).selected, li.list-nested-item.selected > .list-item { .text(selected); } } .select-list ol.list-group, &.select-list ol.list-group { li.two-lines { .secondary-line { color: @text-color-subtle; } &.selected .secondary-line { color: lighten(@text-color-subtle, 10%); text-shadow: none; } } // We want to highlight the background of the list items because we dont // know their size. li.selected { background-color: @background-color-selected; &:before{ display: none; } } &.mark-active{ @active-icon-size: 14px; // pad in front of the text where the icon would be We'll pad the non- // active items with a 'fake' icon so other classes can pad the item // without worrying about the icon padding. li:before { content: ''; background-color: transparent; position: static; display: inline-block; left: auto; right: auto; height: @active-icon-size; width: @active-icon-size; } > li:not(.active):before { margin-right: @component-icon-padding; } li.active { .octicon(check, @active-icon-size); &:before { margin-right: @component-icon-padding; color: @text-color-success; } } } } .select-list.popover-list { background-color: @overlay-background-color; box-shadow: 0 0 10px @base-border-color; padding: @component-padding/2; border-radius: @component-border-radius; border: 1px solid @overlay-border-color; atom-text-editor { margin-bottom: @component-padding/2; } .list-group li { padding-left: @component-padding/2; } } .ui-sortable { li { line-height: 2.5; } // For sortable lists in the settings view li.ui-sortable-placeholder { visibility: visible !important; background-color: darken(@pane-item-background-color, 10%); } } li.ui-draggable-dragging, li.ui-sortable-helper { line-height: @component-line-height; height: @component-line-height; border: 0; border-radius: 0; list-style: none; padding: 0 @component-padding; background: @background-color-highlight; box-shadow: 0 0 1px @base-border-color; } ================================================ FILE: packages/atom-light-ui/styles/messages.less ================================================ ul.background-message { font-weight: bold; color: rgba(0, 0, 0, .18); } ================================================ FILE: packages/atom-light-ui/styles/overlays.less ================================================ atom-panel.modal, .overlay { color: @text-color; background-color: @overlay-background-color; padding: @component-padding; border: 1px solid @overlay-border-color; box-shadow: 0 0 10px @base-border-color; border-radius: @component-border-radius; atom-text-editor[mini] { margin-bottom: @component-padding; } .select-list ol.list-group, &.select-list ol.list-group { background-color: @inset-panel-background-color; li { padding: @component-padding; border-top: 1px solid @inset-panel-border-color; border-left: 1px solid @inset-panel-border-color; border-right: 1px solid @inset-panel-border-color; &:last-child { border-bottom: 1px solid @inset-panel-border-color; } &.two-lines { padding: @component-padding/2 @component-padding; } &.selected { color: @text-color; background-color: @background-color-highlight; } .status.icon { float: right; margin-left: @component-icon-padding; &:before { margin-right: 0; } } } } } ================================================ FILE: packages/atom-light-ui/styles/panels.less ================================================ @import "buttons.less"; .panel { &.bordered { border: 1px solid @base-border-color; border-radius: @component-border-radius; } } atom-panel, .tool-panel { .text(normal); position: relative; background-color: @tool-panel-background-color; &.bottom, &.panel-bottom, &.footer, &.footer-bottom { border-top: 1px solid @tool-panel-border-color; } &.left, &.panel-left { border-right: 1px solid @tool-panel-border-color; } &.right, &.panel-right { border-left: 1px solid @tool-panel-border-color; } .inset-panel { border-radius: @component-border-radius; border: 1px solid @tool-panel-border-color; } } .inset-panel { position: relative; background-color: @inset-panel-background-color; } .panel-heading { border-bottom: none; padding: @component-padding - 2px @component-padding; background-color: transparent; background-image: -webkit-linear-gradient(@panel-heading-background-color, darken(@panel-heading-background-color, 10%)); .btn { @bg: lighten(@button-background-color, 10%); @hover: lighten(@button-background-color-hover, 10%); @selected: lighten(@button-background-color-selected, 10%); @text: lighten(@text-color, 10%); .btn-background(@bg, @hover, @selected, @button-border-color, @text, @text); } } ================================================ FILE: packages/atom-light-ui/styles/panes.less ================================================ atom-pane-container { atom-pane { background-color: lighten(@app-background-color, 3%); &:focus { background-color: @app-background-color; } } atom-pane-axis.horizontal > * { border-right: 1px solid @pane-item-border-color; &:last-child { border-right: none; } } atom-pane-axis.vertical > * { border-bottom: 1px solid @pane-item-border-color; &:last-child { border-bottom: none; } } } ================================================ FILE: packages/atom-light-ui/styles/progress.less ================================================ .loading-spinner(@size) { width: @size; height: @size; display: block; background-image: url(images/octocat-spinner-128.gif); background-repeat: no-repeat; background-size: cover; &.inline-block { display: inline-block; } } .loading-spinner-large { .loading-spinner(64px); } .loading-spinner-medium { .loading-spinner(50px); } .loading-spinner-small { .loading-spinner(32px); } .loading-spinner-tiny { .loading-spinner(20px); } // Much learning from: // http://css-tricks.com/html5-progress-element/ @progress-height: 16px; @progress-shine-gradient: -webkit-linear-gradient(top, rgba(255, 255, 255, .15), rgba(0, 0, 0, .15)); progress { height: @progress-height; -webkit-appearance: none; border-radius: @component-border-radius; background-color: #ccc; background-image: -webkit-linear-gradient(-30deg, transparent 33%, rgba(0, 0, 0, .1) 33%, rgba(0,0, 0, .1) 66%, transparent 66%), @progress-shine-gradient; border-radius: 2px; background-size: 25px @progress-height, 100% 100%, 100% 100%; -webkit-animation: animate-stripes 5s linear 6; // stop animation after 6 runs (30s) to limit CPU usage } progress::-webkit-progress-bar { background-color: transparent; } progress::-webkit-progress-value { border-radius: @component-border-radius; background-image: @progress-shine-gradient; background-color: @background-color-info; } progress[value] { background-image: @progress-shine-gradient; -webkit-animation: none; } @-webkit-keyframes animate-stripes { 100% { background-position: 100px 0px; } } ================================================ FILE: packages/atom-light-ui/styles/sites.less ================================================ .ui-site(@num, @color) { .ui-site-@{num} { background-color: @color; } } .ui-site(1, @ui-site-color-1); .ui-site(2, @ui-site-color-2); .ui-site(3, @ui-site-color-3); .ui-site(4, @ui-site-color-4); .ui-site(5, @ui-site-color-5); ================================================ FILE: packages/atom-light-ui/styles/tabs.less ================================================ @tab-radius: 3px; @modified-icon-width: 8px; @tab-skew: 30deg; @tab-top-padding: 5px; @tab-bottom-border-height: 5px; @tab-border: 1px solid @tab-border-color; @tab-bar-bottom-border-color: @tab-border-color; @tab-max-width: 160px; .tab-bar { height: @tab-height + @tab-top-padding + @tab-bottom-border-height; background-image: -webkit-linear-gradient(top, @tab-bar-background-color, lighten(@tab-bar-background-color, 9%)); box-shadow: inset 0 -8px 8px -4px rgba(0,0,0, .15); padding: 0 10px 0 25px; overflow-x: auto; overflow-y: hidden; &::-webkit-scrollbar { display: none; } .tab { position: relative; top: @tab-top-padding; max-width: @tab-max-width; height: @tab-height; line-height: @tab-height; color: @text-color; padding: 0; margin: 0 20px 0 5px; box-shadow: inset -1px -1px 1px rgba(0,0,0, .05); transition: color .1s ease-in; &, &:before, &:after { background-image: -webkit-linear-gradient(top, @tab-background-color, darken(@tab-background-color, 6%)); border-top: @tab-border; } &:before, &:after { content: ''; position: absolute; top: -1px; width: 25px; height: @tab-height; } // left angled edge &:before { left: -14px; border-top-left-radius: @tab-radius; border-left: @tab-border; box-shadow: inset 1px -1px 1px rgba(0,0,0, .05); -webkit-transform: skewX(-@tab-skew); } // right angled edge &:after { right: -14px; border-top-right-radius: @tab-radius; border-right: @tab-border; box-shadow: inset -1px -1px 1px rgba(0,0,0, .05); -webkit-transform: skewX(@tab-skew); } .close-icon { right: 0; z-index: 3; text-align: right; line-height: @tab-height; color: @text-color; &:hover { color: inherit; } } &.modified:not(:hover) .close-icon { right: 0; top: @tab-height/2 - @modified-icon-width/2 + 1px; width: @modified-icon-width; height: @modified-icon-width; } &.modified:hover .close-icon:hover { color: @text-color-highlight; } .title { position: relative; z-index: 1; margin-top: -@tab-top-padding - 1px; padding-top: @tab-top-padding + 1px; padding-right: 10px; } } .tab.active { z-index: 1; color: @text-color-highlight; .close-icon { line-height: @tab-height - 1px; color: @text-color; } &, &:before, &:after { background: @tab-background-color-active; height: @tab-height + 1px; box-shadow: none; } } .tab:hover { color: @text-color-highlight; } .tab.active:hover .close-icon { color: @text-color; &:hover { color: inherit; } } .placeholder { height: @tab-height + @tab-top-padding + @tab-bottom-border-height; pointer-events: none; &:before { margin-left: -9px; // center between tabs } &:after { top: @tab-height + @tab-top-padding + @tab-bottom-border-height - 2px; margin-left: -10px; // center between tabs } } } // border .tab-bar + .item-views::before { content: ""; position: absolute; top: -5px; height: @tab-bottom-border-height; left: 0; right: 0; background-color: @tab-background-color-active; border-top: 1px solid @tab-border-color; border-bottom: 1px solid @tab-bar-bottom-border-color; pointer-events: none; } ================================================ FILE: packages/atom-light-ui/styles/text.less ================================================ h1, h2, h3 { line-height: 1em; margin-bottom: 15px } h1 { font-size: 2em; } h2 { font-size: 1.5em; } h3 { font-size: 1.2em; } p { line-height: 1.6; margin-bottom: 15px; } label { font-weight: normal; } pre { box-shadow: none; color: @text-color; background: @inset-panel-background-color; border-radius: @component-border-radius; border: none; margin: 0; } code { .text(highlight); background: @background-color-highlight; border-radius: @component-border-radius; } .selected { .text(highlight); } .text-smaller { font-size: 0.9em; } .text-subtle { .text(subtle); } .text-highlight { .text(highlight); } .text-error { .text(error); } .text-info { .text(info); &:hover { color: @text-color-info; } } .text-warning { .text(warning); &:hover { color: @text-color-warning; } } .text-success { .text(success); &:hover { color: @text-color-success; } } .highlight { color: @text-color-highlight; font-weight: bold; text-shadow: none; background-color: @background-color-highlight; border-radius: @component-border-radius; padding: 1px 3px; } .highlight-color(@name, @color) { .highlight-@{name} { color: @text-color-highlight; font-weight: bold; text-shadow: none; background-color: @color; border-radius: @component-border-radius; padding: 1px 3px; } } .highlight-color(info, @background-color-info); .highlight-color(warning, @background-color-warning); .highlight-color(error, @background-color-error); .highlight-color(success, @background-color-success); ================================================ FILE: packages/atom-light-ui/styles/tooltips.less ================================================ .tooltip { @tip-background-color: #333; @tip-text-color: #fff; white-space: nowrap; .keystroke { font-family: Helvetica, Arial, sans-serif; font-size: 13px; color: #c0c0c0; padding-left: 2px; } &.in { opacity: 1; } .tooltip-inner { line-height: 19px; border-radius: @component-border-radius; background-color: @tip-background-color; color: @tip-text-color; white-space: nowrap; max-width: none; } &.top .tooltip-arrow { border-top-color: @tip-background-color; } &.top-left .tooltip-arrow { border-top-color: @tip-background-color; } &.top-right .tooltip-arrow { border-top-color: @tip-background-color; } &.right .tooltip-arrow { border-right-color: @tip-background-color; } &.left .tooltip-arrow { border-left-color: @tip-background-color; } &.bottom .tooltip-arrow { border-bottom-color: @tip-background-color; } &.bottom-left .tooltip-arrow { border-bottom-color: @tip-background-color; } &.bottom-right .tooltip-arrow { border-bottom-color: @tip-background-color; } } ================================================ FILE: packages/atom-light-ui/styles/tree-view.less ================================================ .tree-view { font-size: @font-size; background: @tree-view-background-color; .selected:before { background: #d0d0d0; } } .tree-view-resizer { .tree-view-resize-handle { width: 8px; } } .focusable-panel { opacity: 1; background-image: -webkit-gradient(linear, left top, left bottom, from(#e8ecf1), to(#ebebeb)); background-image: -webkit-linear-gradient(top, #e8ecf1, #ebebeb); &:focus { background-image: -webkit-gradient(linear, left top, left bottom, from(#e8ecf1),to(#d1d8e0)); background-image: -webkit-linear-gradient(top, #e8ecf1, #d1d8e0); .selected:before { background: @background-color-selected; } } } ================================================ FILE: packages/atom-light-ui/styles/ui-mixins.less ================================================ // Pattern matching; ish is cray. // http://lesscss.org/#-pattern-matching-and-guard-expressions .text(normal) { font-weight: normal; color: @text-color; text-shadow: 0 1px 0 rgba(255, 255, 255, .5); } .text(subtle) { font-weight: normal; color: @text-color-subtle; text-shadow: none; } .text(highlight) { font-weight: normal; color: @text-color-highlight; } .text(selected) { font-weight: normal; color: @text-color-selected; text-shadow: none; } .text(info) { color: @text-color-info; text-shadow: none; } .text(success) { color: @text-color-success; text-shadow: none; } .text(warning) { color: @text-color-warning; text-shadow: none; } .text(error) { color: @text-color-error; text-shadow: none; } ================================================ FILE: packages/atom-light-ui/styles/ui-variables.less ================================================ // Colors @text-color: #444; @text-color-subtle: #999; @text-color-highlight: #000; @text-color-selected: #fff; @text-color-info: #5293d8; @text-color-success: #45A815; @text-color-warning: #CD8E00; @text-color-error: #c00; @text-color-ignored: @text-color-subtle; @text-color-added: @text-color-success; @text-color-renamed: @text-color-info; @text-color-modified: @text-color-warning; @text-color-removed: @text-color-error; @background-color-info: #0098ff; @background-color-success: #17ca65; @background-color-warning: #ff4800; @background-color-error: #c00; @background-color-highlight: rgba(0, 0, 0, .1); @background-color-selected: #6aa5e9; @app-background-color: #ccc; @base-background-color: #f4f4f4; @base-border-color: #9f9f9f; @pane-item-background-color: @base-background-color; @pane-item-border-color: @base-border-color; @input-background-color: white; @input-border-color: fadeout(@base-border-color, 10%); @tool-panel-background-color: @base-background-color; @tool-panel-border-color: @base-border-color; @inset-panel-background-color: #fff; @inset-panel-border-color: fadeout(@base-border-color, 10%); @panel-heading-background-color: #c3c3c3; @panel-heading-border-color: transparent; @overlay-background-color: #ececec; @overlay-border-color: @base-border-color; @button-background-color: @base-background-color; @button-background-color-hover: lighten(@button-background-color, 5%); @button-background-color-selected: #888; @button-border-color: rgba(0, 0, 0, 0.15); @tab-bar-background-color: #d8d8d8; @tab-bar-border-color: #ddd; @tab-background-color: #e8e8e8; @tab-background-color-active: #f0f0f0; @tab-border-color: lighten(@base-border-color, 10%); @tree-view-background-color: #eee; @tree-view-border-color: @base-border-color; @scrollbar-background-color: #F9F9F9; // Needs to be opaque -> atom/atom/issues/4578 @scrollbar-color: #C1C1C1; @ui-site-color-1: @background-color-success; // green @ui-site-color-2: @background-color-info; // blue @ui-site-color-3: @background-color-warning; // orange @ui-site-color-4: #db2ff4; // purple @ui-site-color-5: #f5e11d; // yellow // Sizes @font-size: 11px; @input-font-size: 14px; @disclosure-arrow-size: 12px; @component-padding: 10px; @component-icon-padding: 5px; @component-icon-size: 16px; @component-line-height: 25px; @component-border-radius: 2px; @tab-height: 26px; // Other @font-family: system-ui; ================================================ FILE: packages/atom-light-ui/styles/utilities.less ================================================ .key-binding { background: #fff; border: 1px solid lighten(@base-border-color, 20%); text-shadow: none; display: inline-block; line-height: 100%; border-radius: @component-border-radius; margin-left: @component-icon-padding; font-family: Helvetica, Arial, sans-serif; font-size: @font-size - 1px; padding: @component-padding / 2; } .badge { .text(highlight); background: @background-color-highlight; } ================================================ FILE: packages/autoflow/.coffeelintignore ================================================ spec/fixtures ================================================ FILE: packages/autoflow/.gitignore ================================================ node_modules ================================================ FILE: packages/autoflow/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/autoflow/README.md ================================================ # Autoflow package Format the current selection to have lines no longer than 80 characters using `cmd-alt-q` on macOS and `ctrl-shift-q` on Windows and Linux. If nothing is selected, the current paragraph will be reflowed. This package uses the config value of `editor.preferredLineLength` when set to determine desired line length. ================================================ FILE: packages/autoflow/coffeelint.json ================================================ { "max_line_length": { "level": "ignore" }, "no_empty_param_list": { "level": "error" }, "arrow_spacing": { "level": "error" }, "no_interpolation_in_single_quotes": { "level": "error" }, "no_debugger": { "level": "error" }, "prefer_english_operator": { "level": "error" }, "colon_assignment_spacing": { "spacing": { "left": 0, "right": 1 }, "level": "error" }, "braces_spacing": { "spaces": 0, "level": "error" }, "spacing_after_comma": { "level": "error" }, "no_stand_alone_at": { "level": "error" } } ================================================ FILE: packages/autoflow/keymaps/autoflow.cson ================================================ '.platform-darwin atom-text-editor': 'alt-cmd-q': 'autoflow:reflow-selection' '.platform-win32 atom-text-editor, .platform-linux atom-text-editor': 'ctrl-shift-q': 'autoflow:reflow-selection' ================================================ FILE: packages/autoflow/lib/autoflow.coffee ================================================ _ = require 'underscore-plus' CharacterPattern = /// [ ^\s ] /// module.exports = activate: -> @commandDisposable = atom.commands.add 'atom-text-editor', 'autoflow:reflow-selection': (event) => @reflowSelection(event.currentTarget.getModel()) deactivate: -> @commandDisposable?.dispose() @commandDisposable = null reflowSelection: (editor) -> range = editor.getSelectedBufferRange() range = editor.getCurrentParagraphBufferRange() if range.isEmpty() return unless range? reflowOptions = wrapColumn: @getPreferredLineLength(editor) tabLength: @getTabLength(editor) reflowedText = @reflow(editor.getTextInRange(range), reflowOptions) editor.getBuffer().setTextInRange(range, reflowedText) reflow: (text, {wrapColumn, tabLength}) -> paragraphs = [] # Convert all \r\n and \r to \n. The text buffer will normalize them later text = text.replace(/\r\n?/g, '\n') leadingVerticalSpace = text.match(/^\s*\n/) if leadingVerticalSpace text = text.substr(leadingVerticalSpace.length) else leadingVerticalSpace = '' trailingVerticalSpace = text.match(/\n\s*$/) if trailingVerticalSpace text = text.substr(0, text.length - trailingVerticalSpace.length) else trailingVerticalSpace = '' paragraphBlocks = text.split(/\n\s*\n/g) if tabLength tabLengthInSpaces = Array(tabLength + 1).join(' ') else tabLengthInSpaces = '' for block in paragraphBlocks blockLines = block.split('\n') # For LaTeX tags surrounding the text, we simply ignore them, and # reproduce them verbatim in the wrapped text. beginningLinesToIgnore = [] endingLinesToIgnore = [] latexTagRegex = /^\s*\\\w+(\[.*\])?\{\w+\}(\[.*\])?\s*$/g # e.g. \begin{verbatim} latexTagStartRegex = /^\s*\\\w+\s*\{\s*$/g # e.g. \item{ latexTagEndRegex = /^\s*\}\s*$/g # e.g. } while blockLines.length > 0 and ( blockLines[0].match(latexTagRegex) or blockLines[0].match(latexTagStartRegex)) beginningLinesToIgnore.push(blockLines[0]) blockLines.shift() while blockLines.length > 0 and ( blockLines[blockLines.length - 1].match(latexTagRegex) or blockLines[blockLines.length - 1].match(latexTagEndRegex)) endingLinesToIgnore.unshift(blockLines[blockLines.length - 1]) blockLines.pop() # The paragraph might be a LaTeX section with no text, only tags: # \documentclass{article} # In that case, we have nothing to reflow. # Push the tags verbatim and continue to the next paragraph. unless blockLines.length > 0 paragraphs.push(block) continue # TODO: this could be more language specific. Use the actual comment char. # Remember that `-` has to be the last character in the character class. linePrefix = blockLines[0].match(/^\s*(\/\/|\/\*|;;|#'|\|\|\||--|[#%*>-])?\s*/g)[0] linePrefixTabExpanded = linePrefix if tabLengthInSpaces linePrefixTabExpanded = linePrefix.replace(/\t/g, tabLengthInSpaces) if linePrefix escapedLinePrefix = _.escapeRegExp(linePrefix) blockLines = blockLines.map (blockLine) -> blockLine.replace(///^#{escapedLinePrefix}///, '') blockLines = blockLines.map (blockLine) -> blockLine.replace(/^\s+/, '') lines = [] currentLine = [] currentLineLength = linePrefixTabExpanded.length wrappedLinePrefix = linePrefix .replace(/^(\s*)\/\*/, '$1 ') .replace(/^(\s*)-(?!-)/, '$1 ') firstLine = true for segment in @segmentText(blockLines.join(' ')) if @wrapSegment(segment, currentLineLength, wrapColumn) # Independent of line prefix don't mess with it on the first line if firstLine isnt true # Handle C comments if linePrefix.search(/^\s*\/\*/) isnt -1 or linePrefix.search(/^\s*-(?!-)/) isnt -1 linePrefix = wrappedLinePrefix lines.push(linePrefix + currentLine.join('')) currentLine = [] currentLineLength = linePrefixTabExpanded.length firstLine = false currentLine.push(segment) currentLineLength += segment.length lines.push(linePrefix + currentLine.join('')) wrappedLines = beginningLinesToIgnore.concat(lines.concat(endingLinesToIgnore)) paragraphs.push(wrappedLines.join('\n').replace(/\s+\n/g, '\n')) return leadingVerticalSpace + paragraphs.join('\n\n') + trailingVerticalSpace getTabLength: (editor) -> atom.config.get('editor.tabLength', scope: editor.getRootScopeDescriptor()) ? 2 getPreferredLineLength: (editor) -> atom.config.get('editor.preferredLineLength', scope: editor.getRootScopeDescriptor()) wrapSegment: (segment, currentLineLength, wrapColumn) -> CharacterPattern.test(segment) and (currentLineLength + segment.length > wrapColumn) and (currentLineLength > 0 or segment.length < wrapColumn) segmentText: (text) -> segments = [] re = /[\s]+|[^\s]+/g segments.push(match[0]) while match = re.exec(text) segments ================================================ FILE: packages/autoflow/menus/autoflow.cson ================================================ 'menu': [ { 'label': 'Edit' 'submenu': [ { 'label': 'Reflow Selection' 'command': 'autoflow:reflow-selection' } ] } ] ================================================ FILE: packages/autoflow/package.json ================================================ { "name": "autoflow", "version": "0.29.4", "main": "./lib/autoflow", "description": "Format the current selection to have lines no longer than 80 characters.\n\nThis packages uses the config value of `editor.preferredLineLength` when set.", "activationCommands": { "atom-text-editor": [ "autoflow:reflow-selection" ] }, "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": "*" }, "dependencies": { "underscore-plus": "^1.7.0" }, "devDependencies": { "coffeelint": "^1.9.7" } } ================================================ FILE: packages/autoflow/spec/autoflow-spec.coffee ================================================ describe "Autoflow package", -> [autoflow, editor, editorElement] = [] tabLength = 4 describe "autoflow:reflow-selection", -> beforeEach -> activationPromise = null waitsForPromise -> atom.workspace.open() runs -> editor = atom.workspace.getActiveTextEditor() editorElement = atom.views.getView(editor) atom.config.set('editor.preferredLineLength', 30) atom.config.set('editor.tabLength', tabLength) activationPromise = atom.packages.activatePackage('autoflow') atom.commands.dispatch editorElement, 'autoflow:reflow-selection' waitsForPromise -> activationPromise it "uses the preferred line length based on the editor's scope", -> atom.config.set('editor.preferredLineLength', 4, scopeSelector: '.text.plain.null-grammar') editor.setText("foo bar") editor.selectAll() atom.commands.dispatch editorElement, 'autoflow:reflow-selection' expect(editor.getText()).toBe """ foo bar """ it "rearranges line breaks in the current selection to ensure lines are shorter than config.editor.preferredLineLength honoring tabLength", -> editor.setText "\t\tThis is the first paragraph and it is longer than the preferred line length so it should be reflowed.\n\n\t\tThis is a short paragraph.\n\n\t\tAnother long paragraph, it should also be reflowed with the use of this single command." editor.selectAll() atom.commands.dispatch editorElement, 'autoflow:reflow-selection' exedOut = editor.getText().replace(/\t/g, Array(tabLength+1).join 'X') expect(exedOut).toBe "XXXXXXXXThis is the first\nXXXXXXXXparagraph and it is\nXXXXXXXXlonger than the\nXXXXXXXXpreferred line length\nXXXXXXXXso it should be\nXXXXXXXXreflowed.\n\nXXXXXXXXThis is a short\nXXXXXXXXparagraph.\n\nXXXXXXXXAnother long\nXXXXXXXXparagraph, it should\nXXXXXXXXalso be reflowed with\nXXXXXXXXthe use of this single\nXXXXXXXXcommand." it "rearranges line breaks in the current selection to ensure lines are shorter than config.editor.preferredLineLength", -> editor.setText """ This is the first paragraph and it is longer than the preferred line length so it should be reflowed. This is a short paragraph. Another long paragraph, it should also be reflowed with the use of this single command. """ editor.selectAll() atom.commands.dispatch editorElement, 'autoflow:reflow-selection' expect(editor.getText()).toBe """ This is the first paragraph and it is longer than the preferred line length so it should be reflowed. This is a short paragraph. Another long paragraph, it should also be reflowed with the use of this single command. """ it "is not confused when the selection boundary is between paragraphs", -> editor.setText """ v--- SELECTION STARTS AT THE BEGINNING OF THE NEXT LINE (pos 1,0) The preceding newline should not be considered part of this paragraph. The newline at the end of this paragraph should be preserved and not converted into a space. ^--- SELECTION ENDS AT THE BEGINNING OF THE PREVIOUS LINE (pos 6,0) """ editor.setCursorBufferPosition([1, 0]) editor.selectToBufferPosition([6, 0]) atom.commands.dispatch editorElement, 'autoflow:reflow-selection' expect(editor.getText()).toBe """ v--- SELECTION STARTS AT THE BEGINNING OF THE NEXT LINE (pos 1,0) The preceding newline should not be considered part of this paragraph. The newline at the end of this paragraph should be preserved and not converted into a space. ^--- SELECTION ENDS AT THE BEGINNING OF THE PREVIOUS LINE (pos 6,0) """ it "reflows the current paragraph if nothing is selected", -> editor.setText """ This is a preceding paragraph, which shouldn't be modified by a reflow of the following paragraph. The quick brown fox jumps over the lazy dog. The preceding sentence contains every letter in the entire English alphabet, which has absolutely no relevance to this test. This is a following paragraph, which shouldn't be modified by a reflow of the preciding paragraph. """ editor.setCursorBufferPosition([3, 5]) atom.commands.dispatch editorElement, 'autoflow:reflow-selection' expect(editor.getText()).toBe """ This is a preceding paragraph, which shouldn't be modified by a reflow of the following paragraph. The quick brown fox jumps over the lazy dog. The preceding sentence contains every letter in the entire English alphabet, which has absolutely no relevance to this test. This is a following paragraph, which shouldn't be modified by a reflow of the preciding paragraph. """ it "allows for single words that exceed the preferred wrap column length", -> editor.setText("this-is-a-super-long-word-that-shouldn't-break-autoflow and these are some smaller words") editor.selectAll() atom.commands.dispatch editorElement, 'autoflow:reflow-selection' expect(editor.getText()).toBe """ this-is-a-super-long-word-that-shouldn't-break-autoflow and these are some smaller words """ describe "reflowing text", -> beforeEach -> autoflow = require("../lib/autoflow") it 'respects current paragraphs', -> text = ''' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper sagittis. Maecenas et enim eu orci tincidunt adipiscing aliquam ligula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper tincidunt adipiscing lacinia a dui. Etiam quis erat dolor. rutrum nisl fermentum rhoncus. Duis blandit ligula facilisis fermentum. ''' res = ''' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper sagittis. Maecenas et enim eu orci tincidunt adipiscing aliquam ligula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper tincidunt adipiscing lacinia a dui. Etiam quis erat dolor. rutrum nisl fermentum rhoncus. Duis blandit ligula facilisis fermentum. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'respects indentation', -> text = ''' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper sagittis. Maecenas et enim eu orci tincidunt adipiscing aliquam ligula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper tincidunt adipiscing lacinia a dui. Etiam quis erat dolor. rutrum nisl fermentum rhoncus. Duis blandit ligula facilisis fermentum ''' res = ''' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper sagittis. Maecenas et enim eu orci tincidunt adipiscing aliquam ligula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper tincidunt adipiscing lacinia a dui. Etiam quis erat dolor. rutrum nisl fermentum rhoncus. Duis blandit ligula facilisis fermentum ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'respects prefixed text (comments!)', -> text = ''' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper sagittis. Maecenas et enim eu orci tincidunt adipiscing aliquam ligula. # Lorem ipsum dolor sit amet, consectetur adipiscing elit. # Phasellus gravida # nibh id magna ullamcorper # tincidunt adipiscing lacinia a dui. Etiam quis erat dolor. # rutrum nisl fermentum rhoncus. Duis blandit ligula facilisis fermentum ''' res = ''' Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida nibh id magna ullamcorper sagittis. Maecenas et enim eu orci tincidunt adipiscing aliquam ligula. # Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida # nibh id magna ullamcorper tincidunt adipiscing lacinia a dui. Etiam quis # erat dolor. rutrum nisl fermentum rhoncus. Duis blandit ligula facilisis # fermentum ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'respects multiple prefixes (js/c comments)', -> text = ''' // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida et enim eu orci tincidunt adipiscing aliquam ligula. ''' res = ''' // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida et // enim eu orci tincidunt adipiscing aliquam ligula. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly handles * prefix', -> text = ''' * Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida et enim eu orci tincidunt adipiscing aliquam ligula. * soidjfiojsoidj foi ''' res = ''' * Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus gravida et * enim eu orci tincidunt adipiscing aliquam ligula. * soidjfiojsoidj foi ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it "does not throw invalid regular expression errors (regression)", -> text = ''' *** Lorem ipsum dolor sit amet ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual text it 'handles different initial indentation', -> text = ''' Magna ea magna fugiat nisi minim in id duis. Culpa sit sint consequat quis elit magna pariatur incididunt proident laborum deserunt est aliqua reprehenderit. Occaecat et ex non do Lorem irure adipisicing mollit excepteur eu ullamco consectetur. Ex ex Lorem duis labore quis ad exercitation elit dolor non adipisicing. Pariatur commodo ullamco culpa dolor sunt enim. Ullamco dolore do ea nulla ut commodo minim consequat cillum ad velit quis. ''' res = ''' Magna ea magna fugiat nisi minim in id duis. Culpa sit sint consequat quis elit magna pariatur incididunt proident laborum deserunt est aliqua reprehenderit. Occaecat et ex non do Lorem irure adipisicing mollit excepteur eu ullamco consectetur. Ex ex Lorem duis labore quis ad exercitation elit dolor non adipisicing. Pariatur commodo ullamco culpa dolor sunt enim. Ullamco dolore do ea nulla ut commodo minim consequat cillum ad velit quis. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly handles CRLF', -> text = "This is the first line and it is longer than the preferred line length so it should be reflowed.\r\nThis is a short line which should\r\nbe reflowed with the following line.\rAnother long line, it should also be reflowed with everything above it when it is all reflowed." res = ''' This is the first line and it is longer than the preferred line length so it should be reflowed. This is a short line which should be reflowed with the following line. Another long line, it should also be reflowed with everything above it when it is all reflowed. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'handles cyrillic text', -> text = ''' В начале июля, в чрезвычайно жаркое время, под вечер, один молодой человек вышел из своей каморки, которую нанимал от жильцов в С-м переулке, на улицу и медленно, как бы в нерешимости, отправился к К-ну мосту. ''' res = ''' В начале июля, в чрезвычайно жаркое время, под вечер, один молодой человек вышел из своей каморки, которую нанимал от жильцов в С-м переулке, на улицу и медленно, как бы в нерешимости, отправился к К-ну мосту. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'handles `yo` character properly', -> # Because there're known problems with this character in major regex engines text = 'Ё Ё Ё' res = ''' Ё Ё Ё ''' expect(autoflow.reflow(text, wrapColumn: 2)).toEqual res it 'properly reflows // comments ', -> text = ''' // Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' // Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard // sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical // fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest // quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro // actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia // sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher // direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. // Meditation microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly reflows /* comments ', -> text = ''' /* Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. */ ''' res = ''' /* Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. */ ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly reflows pound comments ', -> text = ''' # Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' # Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha # banh mi, cold-pressed retro whatever ethical man braid asymmetrical # fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa # leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually # aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial # letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, # tacos pickled fanny pack literally meh pinterest slow-carb. Meditation # microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly reflows - list items ', -> text = ''' - Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' - Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly reflows % comments ', -> text = ''' % Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' % Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha % banh mi, cold-pressed retro whatever ethical man braid asymmetrical % fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa % leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually % aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial % letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, % tacos pickled fanny pack literally meh pinterest slow-carb. Meditation % microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it "properly reflows roxygen comments ", -> text = ''' #' Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' #' Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard #' sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical #' fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest #' quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro #' actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia #' sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher #' direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. #' Meditation microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it "properly reflows -- comments ", -> text = ''' -- Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' -- Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard -- sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical -- fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest -- quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro -- actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia -- sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher -- direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. -- Meditation microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it "properly reflows ||| comments ", -> text = ''' ||| Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' ||| Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard ||| sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical ||| fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest ||| quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro ||| actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia ||| sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher ||| direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. ||| Meditation microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly reflows ;; comments ', -> text = ''' ;; Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' ;; Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard ;; sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical ;; fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest ;; quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro ;; actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia ;; sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher ;; direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. ;; Meditation microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'does not treat lines starting with a single semicolon as ;; comments', -> text = ''' ;! Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' ;! Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly reflows > ascii email inclusions ', -> text = ''' > Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha banh mi, cold-pressed retro whatever ethical man braid asymmetrical fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, tacos pickled fanny pack literally meh pinterest slow-carb. Meditation microdosing distillery 8-bit humblebrag migas. ''' res = ''' > Beard pinterest actually brunch brooklyn jean shorts YOLO. Knausgaard sriracha > banh mi, cold-pressed retro whatever ethical man braid asymmetrical > fingerstache narwhal. Intelligentsia wolf photo booth, tumblr pinterest quinoa > leggings four loko poutine. DIY tattooed drinking vinegar, wolf retro actually > aesthetic austin keffiyeh marfa beard. Marfa trust fund salvia sartorial > letterpress, keffiyeh plaid butcher. Swag try-hard dreamcatcher direct trade, > tacos pickled fanny pack literally meh pinterest slow-carb. Meditation > microdosing distillery 8-bit humblebrag migas. ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it "doesn't allow special characters to surpass wrapColumn", -> test = ''' Imagine that I'm writing some LaTeX code. I start a comment, but change my mind. % Now I'm just kind of trucking along, doing some math and stuff. For instance, $3 + 4 = 7$. But maybe I'm getting really crazy and I use subtraction. It's kind of an obscure technique, but often it goes a bit like this: let $x = 2 + 2$, so $x - 1 = 3$ (quick maths). That's OK I guess, but now look at this cool thing called set theory: $\\{n + 42 : n \\in \\mathbb{N}\\}$. Wow. Neat. But we all know why we're really here. If you peer deep down into your heart, and you stare into the depths of yourself: is $P = NP$? Beware, though; many have tried and failed to answer this question. It is by no means for the faint of heart. ''' res = ''' Imagine that I'm writing some LaTeX code. I start a comment, but change my mind. % Now I'm just kind of trucking along, doing some math and stuff. For instance, $3 + 4 = 7$. But maybe I'm getting really crazy and I use subtraction. It's kind of an obscure technique, but often it goes a bit like this: let $x = 2 + 2$, so $x - 1 = 3$ (quick maths). That's OK I guess, but now look at this cool thing called set theory: $\\{n + 42 : n \\in \\mathbb{N}\\}$. Wow. Neat. But we all know why we're really here. If you peer deep down into your heart, and you stare into the depths of yourself: is $P = NP$? Beware, though; many have tried and failed to answer this question. It is by no means for the faint of heart. ''' expect(autoflow.reflow(test, wrapColumn: 80)).toEqual res describe 'LaTeX', -> it 'properly reflows text around LaTeX tags', -> text = ''' \\begin{verbatim} Lorem ipsum dolor sit amet, nisl odio amet, et tempor netus neque at at blandit, vel vestibulum libero dolor, semper lobortis ligula praesent. Eget condimentum integer, porta sagittis nam, fusce vitae a vitae augue. Nec semper quis sed ut, est porttitor praesent. Nisl velit quam dolore velit quam, elementum neque pellentesque pulvinar et vestibulum. \\end{verbatim} ''' res = ''' \\begin{verbatim} Lorem ipsum dolor sit amet, nisl odio amet, et tempor netus neque at at blandit, vel vestibulum libero dolor, semper lobortis ligula praesent. Eget condimentum integer, porta sagittis nam, fusce vitae a vitae augue. Nec semper quis sed ut, est porttitor praesent. Nisl velit quam dolore velit quam, elementum neque pellentesque pulvinar et vestibulum. \\end{verbatim} ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly reflows text inside LaTeX tags', -> text = ''' \\item{ Lorem ipsum dolor sit amet, nisl odio amet, et tempor netus neque at at blandit, vel vestibulum libero dolor, semper lobortis ligula praesent. Eget condimentum integer, porta sagittis nam, fusce vitae a vitae augue. Nec semper quis sed ut, est porttitor praesent. Nisl velit quam dolore velit quam, elementum neque pellentesque pulvinar et vestibulum. } ''' res = ''' \\item{ Lorem ipsum dolor sit amet, nisl odio amet, et tempor netus neque at at blandit, vel vestibulum libero dolor, semper lobortis ligula praesent. Eget condimentum integer, porta sagittis nam, fusce vitae a vitae augue. Nec semper quis sed ut, est porttitor praesent. Nisl velit quam dolore velit quam, elementum neque pellentesque pulvinar et vestibulum. } ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'properly reflows text inside nested LaTeX tags', -> text = ''' \\begin{enumerate}[label=(\\alph*)] \\item{ Lorem ipsum dolor sit amet, nisl odio amet, et tempor netus neque at at blandit, vel vestibulum libero dolor, semper lobortis ligula praesent. Eget condimentum integer, porta sagittis nam, fusce vitae a vitae augue. Nec semper quis sed ut, est porttitor praesent. Nisl velit quam dolore velit quam, elementum neque pellentesque pulvinar et vestibulum. } \\end{enumerate} ''' res = ''' \\begin{enumerate}[label=(\\alph*)] \\item{ Lorem ipsum dolor sit amet, nisl odio amet, et tempor netus neque at at blandit, vel vestibulum libero dolor, semper lobortis ligula praesent. Eget condimentum integer, porta sagittis nam, fusce vitae a vitae augue. Nec semper quis sed ut, est porttitor praesent. Nisl velit quam dolore velit quam, elementum neque pellentesque pulvinar et vestibulum. } \\end{enumerate} ''' expect(autoflow.reflow(text, wrapColumn: 80)).toEqual res it 'does not attempt to reflow a selection that contains only LaTeX tags and nothing else', -> text = ''' \\begin{enumerate} \\end{enumerate} ''' expect(autoflow.reflow(text, wrapColumn: 5)).toEqual text ================================================ FILE: packages/base16-tomorrow-dark-theme/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/base16-tomorrow-dark-theme/README.md ================================================ # Base16 Tomorrow Dark Syntax theme Atom theme using the ever popular [Base16 Tomorrow](http://chriskempson.com/projects/base16/) dark colors. ![Base16 Tomorrow light](https://cloud.githubusercontent.com/assets/378023/10118589/f108a568-64b6-11e5-8438-eb34dc9b40a1.png) ## Install This theme is installed by default with Atom and can be activated by going to the _Themes_ section in the Settings view (`cmd-,`) and selecting it from the _Syntax Themes_ drop-down menu. A [light version](../base16-tomorrow-light-theme) of this theme is also available. ================================================ FILE: packages/base16-tomorrow-dark-theme/index.less ================================================ // Base16 Tomorrow Dark theme @import "styles/syntax-variables.less"; @import "styles/editor.less"; @import "styles/syntax-legacy/_base.less"; @import "styles/syntax-legacy/cs.less"; @import "styles/syntax-legacy/json.less"; @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; ================================================ FILE: packages/base16-tomorrow-dark-theme/package.json ================================================ { "name": "base16-tomorrow-dark-theme", "theme": "syntax", "version": "1.6.0", "description": "Base16 dark theme for Atom", "keywords": [ "base16", "dark", "syntax" ], "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.49.0" } } ================================================ FILE: packages/base16-tomorrow-dark-theme/styles/colors.less ================================================ // Base16 Tomorrow // A color scheme by Chris Kempson (http://chriskempson.com) // Grayscale @black: #1d1f21; // 00 @very-dark-gray: #282a2e; // 01 @dark-gray: #373b41; // 02 @gray: #969896; // 03 @light-gray: #b4b7b4; // 04 @very-light-gray: #c5c8c6; // 05 @almost-white: #e0e0e0; // 06 @white: #ffffff; // 07 // Colors @red: #cc6666; // 08 @orange: #de935f; // 09 @yellow: #f0c674; // 0A @green: #b5bd68; // 0B @cyan: #8abeb7; // 0C @blue: #81a2be; // 0D @purple: #b294bb; // 0E @brown: #a3685a; // 0F ================================================ FILE: packages/base16-tomorrow-dark-theme/styles/editor.less ================================================ // Editor styles (background, gutter, guides) atom-text-editor { background-color: @syntax-background-color; color: @syntax-text-color; .wrap-guide { background-color: @syntax-wrap-guide-color; } .indent-guide { color: @syntax-indent-guide-color; } .invisible-character { color: @syntax-invisible-character-color; } .gutter { background-color: @syntax-gutter-background-color; color: @syntax-gutter-text-color; .line-number { padding: 0 0.25em 0 0.5em; -webkit-font-smoothing: antialiased; &.cursor-line { background-color: @syntax-gutter-background-color-selected; color: @syntax-gutter-text-color-selected; } &.cursor-line-no-selection { color: @syntax-gutter-text-color-selected; } } } .gutter .line-number.folded, .gutter .line-number:after, .fold-marker:after { color: @syntax-result-marker-color; } .invisible { color: @syntax-text-color; } .cursor { color: @syntax-cursor-color; } .selection .region { background-color: @syntax-selection-color; } .bracket-matcher .region { border-color: @syntax-result-marker-color; } } ================================================ FILE: packages/base16-tomorrow-dark-theme/styles/syntax/base.less ================================================ /* This defines styling rules for syntax classes. See the naming conventions for a list of syntax classes: https://flight-manual.atom.io/hacking-atom/sections/syntax-naming-conventions When styling rules conflict: - The last rule overrides previous rules. - The rule with most classes and pseudo-classes overrides the last rule. */ // if for return .syntax--keyword { color: @purple; // global let def class &.syntax--storage { color: @purple; } // int char float &.syntax--type { color: @yellow; } // and del not &.syntax--operator { color: @purple; } // super &.syntax--function { color: @red; } // this self &.syntax--variable { color: @red; } // = + && | << ? &.syntax--symbolic { color: @syntax-text-color; } } // identifier .syntax--entity { color: @syntax-text-color; // self cls iota &.syntax--support { color: @red; } // @entity.decorator &.syntax--decorator:last-child { color: @blue; } // label: &.syntax--label { text-decoration: underline; } // function method &.syntax--function { color: @blue; // len print &.syntax--support { color: @cyan; } } // add &.syntax--operator { color: @blue; // %>% <=> &.syntax--symbolic { color: @syntax-text-color; } } // String Class int rune list &.syntax--type { color: @yellow; } // div span &.syntax--tag { color: @red; } // href src alt &.syntax--attribute { color: @orange; } } // () [] {} => @ .syntax--punctuation { color: @syntax-text-color; // { } ~~~ &.syntax--embedded { color: @brown; } } // "string" .syntax--string { color: @green; // :immutable &.syntax--immutable { color: @green; } // {placeholder} %().2f &.syntax--part { color: @cyan; } // ${ } &.syntax--interpolation { color: @brown; } // /^reg[ex]?p/ &.syntax--regexp { color: @green; // ^ $ \b ? + i &.syntax--language { color: @purple; } // \1 &.syntax--variable { color: @blue; } // ( ) [^ ] (?= ) | r" / &.syntax--punctuation { color: @brown; } } } // literal 4 1.3 true nil .syntax--constant { color: @orange; // < 'a' &.syntax--character { color: @green; // \" \' \g \. &.syntax--escape { color: @green; } // \u2661 \n \t \W . &.syntax--code { color: @cyan; } } } // text .syntax--text { color: @syntax-text-color; } // __formatted__ .syntax--markup { // # Heading &.syntax--heading { color: @red; } // 1. * - &.syntax--list.syntax--punctuation { color: @red; } // **bold** &.syntax--bold { color: @orange; font-weight: bold; } // *italic* &.syntax--italic { color: @purple; font-style: italic; } // `raw` &.syntax--raw { color: @green; } // url.com (path) &.syntax--link { color: @blue; } // [alt] ![alt] &.syntax--alt { color: @cyan; } // {++ inserted ++} &.syntax--inserted { color: @green; .syntax--punctuation { color: @green; } } // {== highlighted ==} &.syntax--inserted { color: @green; .syntax--punctuation { color: @green; } } // {-- deleted --} &.syntax--deleted { color: @red; .syntax--punctuation { color: @red; } } // {~~ from~>to ~~} &.syntax--changed { color: @purple; .syntax--punctuation { color: @purple; } } // {>> commented <<} &.syntax--commented { color: @gray; .syntax--punctuation { color: @gray; } } } // /* comment */ .syntax--comment { color: @gray; // @param TODO NOTE &.syntax--caption { color: lighten(@gray, 3); font-weight: bold; } // variable function type &.syntax--term { color: lighten(@gray, 7); } // { } / . &.syntax--punctuation { color: @gray; font-weight: normal; } } // 0invalid .syntax--invalid:not(.syntax--punctuation) { // §illegal &.syntax--illegal { background-color: @red; color: @syntax-background-color; } } ================================================ FILE: packages/base16-tomorrow-dark-theme/styles/syntax/css.less ================================================ .syntax--source.syntax--css { .syntax--entity { // function() &.syntax--function { color: @syntax-text-color; // url rgb &.syntax--support { color: @cyan; } } // .class :pseudo-class attribute &.syntax--selector { color: @orange; // div span &.syntax--tag { color: @red; } // #id &.syntax--id { color: @blue; } } // property: constant &.syntax--property { color: @syntax-text-color; } // --variable &.syntax--variable { color: @red; } // @keyframes keyframe &.syntax--keyframe { color: @red; } } // property: constant .syntax--constant { color: @syntax-text-color; // flex solid bold &.syntax--support { color: @orange; } // 3px 4em &.syntax--numeric { color: @orange; } // screen print &.syntax--media { color: @orange; } // from to 50% &.syntax--offset { color: @syntax-text-color; // % &.syntax--unit { color: @syntax-text-color; } } // #b294bb &.syntax--color { color: @cyan; // blue red &.syntax--support { color: @orange; } } // [attribute=attribute-value] &.syntax--attribute-value { color: @green; } } .syntax--punctuation { // . : :: &.syntax--selector { color: @orange; // * &.syntax--wildcard { color: @red; } // # &.syntax--id { color: @blue; } // [] &.syntax--attribute { color: @syntax-text-color; } } } } ================================================ FILE: packages/base16-tomorrow-dark-theme/styles/syntax-legacy/_base.less ================================================ // Language syntax highlighting .syntax--comment { color: @gray; .syntax--markup.syntax--link { color: @gray; } } .syntax--entity { &.syntax--name.syntax--type { color: @yellow; } &.syntax--other.syntax--inherited-class { color: @green; } } .syntax--keyword { color: @purple; &.syntax--control { color: @purple; } &.syntax--operator { color: @syntax-text-color; } &.syntax--other.syntax--special-method { color: @blue; } &.syntax--other.syntax--unit { color: @orange; } } .syntax--storage { color: @purple; } .syntax--constant { color: @orange; &.syntax--character.syntax--escape { color: @cyan; } &.syntax--numeric { color: @orange; } &.syntax--other.syntax--color { color: @cyan; } &.syntax--other.syntax--symbol { color: @cyan; } } .syntax--variable { color: @red; &.syntax--interpolation { color: @brown; } &.syntax--parameter.syntax--function { color: @syntax-text-color; } } .syntax--invalid.syntax--illegal { background-color: @red; color: @syntax-background-color; } .syntax--string { color: @green; &.syntax--regexp { color: @cyan; .syntax--source.syntax--ruby.syntax--embedded { color: @yellow; } } &.syntax--other.syntax--link { color: @red; } } .syntax--punctuation { &.syntax--definition { &.syntax--parameters, &.syntax--array { color: @syntax-text-color; } &.syntax--heading, &.syntax--identity { color: @blue; } &.syntax--bold { color: @yellow; font-weight: bold; } &.syntax--italic { color: @purple; font-style: italic; } } &.syntax--section { &.syntax--embedded { color: @brown; } &.syntax--method, &.syntax--class, &.syntax--inner-class { color: @syntax-text-color; } } } .syntax--support { &.syntax--class { color: @yellow; } &.syntax--function { color: @cyan; &.syntax--any-method { color: @blue; } } } .syntax--entity { &.syntax--name.syntax--function { color: @blue; } &.syntax--name.syntax--class, &.syntax--name.syntax--type.syntax--class { color: @yellow; } &.syntax--name.syntax--section { color: @blue; } &.syntax--name.syntax--tag { color: @red; } &.syntax--other.syntax--attribute-name { color: @orange; &.syntax--id { color: @blue; } } } .syntax--meta { &.syntax--class { color: @yellow; &.syntax--body { color: @syntax-text-color; } } &.syntax--link { color: @orange; } &.syntax--method-call, &.syntax--method { color: @syntax-text-color; } &.syntax--require { color: @blue; } &.syntax--selector { color: @purple; } &.syntax--separator { background-color: #373b41; color: @syntax-text-color; } &.syntax--tag { color: @syntax-text-color; } } .syntax--none { color: @syntax-text-color; } .syntax--markup { &.syntax--bold { color: @orange; font-weight: bold; } &.syntax--changed { color: @purple; } &.syntax--deleted { color: @red; } &.syntax--italic { color: @purple; font-style: italic; } &.syntax--heading { color: @red; .syntax--punctuation.syntax--definition.syntax--heading { color: @blue; } } &.syntax--link { color: @blue; } &.syntax--inserted { color: @green; } &.syntax--quote { color: @orange; } &.syntax--raw { color: @green; } } .syntax--source.syntax--gfm { .syntax--markup { -webkit-font-smoothing: auto; } .syntax--link .syntax--entity { color: @cyan; } } ================================================ FILE: packages/base16-tomorrow-dark-theme/styles/syntax-legacy/cs.less ================================================ .syntax--source.syntax--cs { .syntax--keyword.syntax--operator { color: @purple; } } ================================================ FILE: packages/base16-tomorrow-dark-theme/styles/syntax-legacy/json.less ================================================ .syntax--source.syntax--json { .syntax--meta.syntax--structure.syntax--dictionary.syntax--json { & > .syntax--string.syntax--quoted.syntax--json { & > .syntax--punctuation.syntax--string { color: @red; } color: @red; } } .syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json { & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json, & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation { color: @green; } & > .syntax--constant.syntax--language.syntax--json { color: @cyan; } } } ================================================ FILE: packages/base16-tomorrow-dark-theme/styles/syntax-variables.less ================================================ @import "colors.less"; // Official Syntax Variables // General colors @syntax-text-color: @very-light-gray; @syntax-cursor-color: @white; @syntax-selection-color: @dark-gray; @syntax-selection-flash-color: @very-light-gray; @syntax-background-color: @black; // Guide colors @syntax-wrap-guide-color: mix(@gray, @dark-gray, 25%); @syntax-indent-guide-color: mix(@gray, @dark-gray, 25%); @syntax-invisible-character-color: mix(@gray, @dark-gray, 25%); // For find and replace markers @syntax-result-marker-color: @gray; @syntax-result-marker-color-selected: @white; // Gutter colors @syntax-gutter-text-color: @gray; @syntax-gutter-text-color-selected: @very-light-gray; @syntax-gutter-background-color: @syntax-background-color; @syntax-gutter-background-color-selected: @syntax-selection-color; // For git diff info. i.e. in the gutter @syntax-color-renamed: @blue; @syntax-color-added: @green; @syntax-color-modified: @orange; @syntax-color-removed: @red; // For language entity colors @syntax-color-variable: @red; @syntax-color-constant: @orange; @syntax-color-property: @syntax-text-color; @syntax-color-value: @green; @syntax-color-function: @blue; @syntax-color-method: @blue; @syntax-color-class: @yellow; @syntax-color-keyword: @purple; @syntax-color-tag: @red; @syntax-color-attribute: @orange; @syntax-color-import: @purple; @syntax-color-snippet: @green; ================================================ FILE: packages/base16-tomorrow-light-theme/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/base16-tomorrow-light-theme/README.md ================================================ # Base16 Tomorrow Light Syntax theme Atom theme using the ever popular [Base16 Tomorrow](http://chriskempson.github.io/base16/#tomorrow) light colors. ![Base16 Tomorrow light](https://cloud.githubusercontent.com/assets/378023/10118588/f1002474-64b6-11e5-9107-b6bedee9777a.png) ## Install This theme is installed by default with Atom and can be activated by going to the _Themes_ section in the Settings view (`cmd-,`) and selecting it from the _Syntax Themes_ drop-down menu. A [dark version](../base16-tomorrow-dark-theme) of this theme is also available. ================================================ FILE: packages/base16-tomorrow-light-theme/index.less ================================================ // Base16 Tomorrow Light @import "styles/syntax-variables.less"; @import "styles/editor.less"; @import "styles/syntax-legacy/_base.less"; @import "styles/syntax-legacy/cs.less"; @import "styles/syntax-legacy/json.less"; @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; ================================================ FILE: packages/base16-tomorrow-light-theme/package.json ================================================ { "name": "base16-tomorrow-light-theme", "theme": "syntax", "version": "1.6.0", "description": "Base16 light theme for Atom", "keywords": [ "base16", "light", "syntax" ], "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.49.0" } } ================================================ FILE: packages/base16-tomorrow-light-theme/styles/colors.less ================================================ // Base16 Tomorrow // A color scheme by Chris Kempson (http://chriskempson.com) // Grayscale @black: #1d1f21; // 00 @very-dark-gray: #282a2e; // 01 @dark-gray: #373b41; // 02 @gray: #969896; // 03 @light-gray: #b4b7b4; // 04 @very-light-gray: #c5c8c6; // 05 @almost-white: #e0e0e0; // 06 @white: #ffffff; // 07 // Colors @red: #cc6666; // 08 @orange: #de935f; // 09 @yellow: #f0c674; // 0A @green: #b5bd68; // 0B @cyan: #8abeb7; // 0C @blue: #81a2be; // 0D @purple: #b294bb; // 0E @brown: #a3685a; // 0F ================================================ FILE: packages/base16-tomorrow-light-theme/styles/editor.less ================================================ // Editor styles (background, gutter, guides) atom-text-editor { background-color: @syntax-background-color; color: @syntax-text-color; .wrap-guide { background-color: @syntax-wrap-guide-color; } .indent-guide { color: @syntax-indent-guide-color; } .invisible-character { color: @syntax-invisible-character-color; } .gutter { background-color: @syntax-gutter-background-color; color: @syntax-gutter-text-color; .line-number { padding: 0 0.25em 0 0.5em; -webkit-font-smoothing: antialiased; &.cursor-line { background-color: @syntax-gutter-background-color-selected; color: @syntax-gutter-text-color-selected; } &.cursor-line-no-selection { color: @syntax-gutter-text-color-selected; } } } .gutter .line-number.folded, .gutter .line-number:after, .fold-marker:after { color: @syntax-result-marker-color; } .invisible { color: @syntax-text-color; } .cursor { color: @syntax-cursor-color; } .selection .region { background-color: @syntax-selection-color; } .bracket-matcher .region { border-color: @syntax-result-marker-color; } } ================================================ FILE: packages/base16-tomorrow-light-theme/styles/syntax/base.less ================================================ /* This defines styling rules for syntax classes. See the naming conventions for a list of syntax classes: https://flight-manual.atom.io/hacking-atom/sections/syntax-naming-conventions When styling rules conflict: - The last rule overrides previous rules. - The rule with most classes and pseudo-classes overrides the last rule. */ // if for return .syntax--keyword { color: @purple; // global let def class &.syntax--storage { color: @purple; } // int char float &.syntax--type { color: @yellow; } // and del not &.syntax--operator { color: @purple; } // super &.syntax--function { color: @red; } // this self &.syntax--variable { color: @red; } // = + && | << ? &.syntax--symbolic { color: @syntax-text-color; } } // identifier .syntax--entity { color: @syntax-text-color; // self cls iota &.syntax--support { color: @red; } // @entity.decorator &.syntax--decorator:last-child { color: @blue; } // label: &.syntax--label { text-decoration: underline; } // function method &.syntax--function { color: @blue; // len print &.syntax--support { color: @cyan; } } // add &.syntax--operator { color: @blue; // %>% <=> &.syntax--symbolic { color: @syntax-text-color; } } // String Class int rune list &.syntax--type { color: @yellow; } // div span &.syntax--tag { color: @red; } // href src alt &.syntax--attribute { color: @orange; } } // () [] {} => @ .syntax--punctuation { color: @syntax-text-color; // { } ~~~ &.syntax--embedded { color: @brown; } } // "string" .syntax--string { color: @green; // :immutable &.syntax--immutable { color: @green; } // {placeholder} %().2f &.syntax--part { color: @cyan; } // ${ } &.syntax--interpolation { color: @brown; } // /^reg[ex]?p/ &.syntax--regexp { color: @green; // ^ $ \b ? + i &.syntax--language { color: @purple; } // \1 &.syntax--variable { color: @blue; } // ( ) [^ ] (?= ) | r" / &.syntax--punctuation { color: @brown; } } } // literal 4 1.3 true nil .syntax--constant { color: @orange; // < 'a' &.syntax--character { color: @green; // \" \' \g \. &.syntax--escape { color: @green; } // \u2661 \n \t \W . &.syntax--code { color: @cyan; } } } // text .syntax--text { color: @syntax-text-color; } // __formatted__ .syntax--markup { // # Heading &.syntax--heading { color: @red; } // 1. * - &.syntax--list.syntax--punctuation { color: @red; } // **bold** &.syntax--bold { color: @orange; font-weight: bold; } // *italic* &.syntax--italic { color: @purple; font-style: italic; } // `raw` &.syntax--raw { color: @green; } // url.com (path) &.syntax--link { color: @blue; } // [alt] ![alt] &.syntax--alt { color: @cyan; } // {++ inserted ++} &.syntax--inserted { color: @green; .syntax--punctuation { color: @green; } } // {== highlighted ==} &.syntax--inserted { color: @green; .syntax--punctuation { color: @green; } } // {-- deleted --} &.syntax--deleted { color: @red; .syntax--punctuation { color: @red; } } // {~~ from~>to ~~} &.syntax--changed { color: @purple; .syntax--punctuation { color: @purple; } } // {>> commented <<} &.syntax--commented { color: @gray; .syntax--punctuation { color: @gray; } } } // /* comment */ .syntax--comment { color: @gray; // @param TODO NOTE &.syntax--caption { color: lighten(@gray, 3); font-weight: bold; } // variable function type &.syntax--term { color: lighten(@gray, 7); } // { } / . &.syntax--punctuation { color: @gray; font-weight: normal; } } // 0invalid .syntax--invalid:not(.syntax--punctuation) { // §illegal &.syntax--illegal { background-color: @red; color: @syntax-background-color; } } ================================================ FILE: packages/base16-tomorrow-light-theme/styles/syntax/css.less ================================================ .syntax--source.syntax--css { .syntax--entity { // function() &.syntax--function { color: @syntax-text-color; // url rgb &.syntax--support { color: @cyan; } } // .class :pseudo-class attribute &.syntax--selector { color: @orange; // div span &.syntax--tag { color: @red; } // #id &.syntax--id { color: @blue; } } // property: constant &.syntax--property { color: @syntax-text-color; } // --variable &.syntax--variable { color: @red; } // @keyframes keyframe &.syntax--keyframe { color: @red; } } // property: constant .syntax--constant { color: @syntax-text-color; // flex solid bold &.syntax--support { color: @orange; } // 3px 4em &.syntax--numeric { color: @orange; } // screen print &.syntax--media { color: @orange; } // from to 50% &.syntax--offset { color: @syntax-text-color; // % &.syntax--unit { color: @syntax-text-color; } } // #b294bb &.syntax--color { color: @cyan; // blue red &.syntax--support { color: @orange; } } // [attribute=attribute-value] &.syntax--attribute-value { color: @green; } } .syntax--punctuation { // . : :: &.syntax--selector { color: @orange; // * &.syntax--wildcard { color: @red; } // # &.syntax--id { color: @blue; } // [] &.syntax--attribute { color: @syntax-text-color; } } } } ================================================ FILE: packages/base16-tomorrow-light-theme/styles/syntax-legacy/_base.less ================================================ // Language syntax highlighting .syntax--comment { color: @gray; .syntax--markup.syntax--link { color: @gray; } } .syntax--entity { &.syntax--name.syntax--type { color: @yellow; } &.syntax--other.syntax--inherited-class { color: @green; } } .syntax--keyword { color: @purple; &.syntax--control { color: @purple; } &.syntax--operator { color: @syntax-text-color; } &.syntax--other.syntax--special-method { color: @blue; } &.syntax--other.syntax--unit { color: @orange; } } .syntax--storage { color: @purple; } .syntax--constant { color: @orange; &.syntax--character.syntax--escape { color: @cyan; } &.syntax--numeric { color: @orange; } &.syntax--other.syntax--color { color: @cyan; } &.syntax--other.syntax--symbol { color: @cyan; } } .syntax--variable { color: @red; &.syntax--interpolation { color: @brown; } &.syntax--parameter.syntax--function { color: @syntax-text-color; } } .syntax--invalid.syntax--illegal { background-color: @red; color: @syntax-background-color; } .syntax--string { color: @green; &.syntax--regexp { color: @cyan; .syntax--source.syntax--ruby.syntax--embedded { color: @yellow; } } &.syntax--other.syntax--link { color: @red; } } .syntax--punctuation { &.syntax--definition { &.syntax--parameters, &.syntax--array { color: @syntax-text-color; } &.syntax--heading, &.syntax--identity { color: @blue; } &.syntax--bold { color: @yellow; font-weight: bold; } &.syntax--italic { color: @purple; font-style: italic; } } &.syntax--section { &.syntax--embedded { color: @brown; } &.syntax--method, &.syntax--class, &.syntax--inner-class { color: @syntax-text-color; } } } .syntax--support { &.syntax--class { color: @yellow; } &.syntax--function { color: @cyan; &.syntax--any-method { color: @blue; } } } .syntax--entity { &.syntax--name.syntax--function { color: @blue; } &.syntax--name.syntax--class, &.syntax--name.syntax--type.syntax--class { color: @yellow; } &.syntax--name.syntax--section { color: @blue; } &.syntax--name.syntax--tag { color: @red; } &.syntax--other.syntax--attribute-name { color: @orange; &.syntax--id { color: @blue; } } } .syntax--meta { &.syntax--class { color: @yellow; &.syntax--body { color: @syntax-text-color; } } &.syntax--link { color: @orange; } &.syntax--method-call, &.syntax--method { color: @syntax-text-color; } &.syntax--require { color: @blue; } &.syntax--selector { color: @purple; } &.syntax--separator { background-color: #373b41; color: @syntax-text-color; } &.syntax--tag { color: @syntax-text-color; } } .syntax--none { color: @syntax-text-color; } .syntax--markup { &.syntax--bold { color: @orange; font-weight: bold; } &.syntax--changed { color: @purple; } &.syntax--deleted { color: @red; } &.syntax--italic { color: @purple; font-style: italic; } &.syntax--heading { color: @red; .syntax--punctuation.syntax--definition.syntax--heading { color: @blue; } } &.syntax--link { color: @blue; } &.syntax--inserted { color: @green; } &.syntax--quote { color: @orange; } &.syntax--raw { color: @green; } } .syntax--source.syntax--gfm { .syntax--markup { -webkit-font-smoothing: auto; } .syntax--link .syntax--entity { color: @cyan; } } ================================================ FILE: packages/base16-tomorrow-light-theme/styles/syntax-legacy/cs.less ================================================ .syntax--source.syntax--cs { .syntax--keyword.syntax--operator { color: @purple; } } ================================================ FILE: packages/base16-tomorrow-light-theme/styles/syntax-legacy/json.less ================================================ .syntax--source.syntax--json { .syntax--meta.syntax--structure.syntax--dictionary.syntax--json { & > .syntax--string.syntax--quoted.syntax--json { & > .syntax--punctuation.syntax--string { color: @red; } color: @red; } } .syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json { & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json, & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation { color: @green; } & > .syntax--constant.syntax--language.syntax--json { color: @cyan; } } } ================================================ FILE: packages/base16-tomorrow-light-theme/styles/syntax-variables.less ================================================ @import "colors.less"; // Official Syntax Variables // General colors @syntax-text-color: @black; @syntax-cursor-color: @black; @syntax-selection-color: @almost-white; @syntax-selection-flash-color: @very-dark-gray; @syntax-background-color: @white; // Guide colors @syntax-wrap-guide-color: @very-light-gray; @syntax-indent-guide-color: @very-light-gray; @syntax-invisible-character-color: @very-light-gray; // For find and replace markers @syntax-result-marker-color: @light-gray; @syntax-result-marker-color-selected: @very-light-gray; // Gutter colors @syntax-gutter-text-color: @light-gray; @syntax-gutter-text-color-selected: @dark-gray; @syntax-gutter-background-color: @syntax-background-color; @syntax-gutter-background-color-selected: @syntax-selection-color; // For git diff info. i.e. in the gutter @syntax-color-renamed: @blue; @syntax-color-added: @green; @syntax-color-modified: @orange; @syntax-color-removed: @red; // For language entity colors @syntax-color-variable: @red; @syntax-color-constant: @orange; @syntax-color-property: @syntax-text-color; @syntax-color-value: @green; @syntax-color-function: @blue; @syntax-color-method: @blue; @syntax-color-class: @yellow; @syntax-color-keyword: @purple; @syntax-color-tag: @red; @syntax-color-attribute: @orange; @syntax-color-import: @purple; @syntax-color-snippet: @green; ================================================ FILE: packages/dalek/.gitignore ================================================ .DS_Store npm-debug.log node_modules ================================================ FILE: packages/dalek/LICENSE.md ================================================ Copyright (c) 2016 GitHub, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/dalek/README.md ================================================ # dalek **EXTERMINATEs** core packages installed in `~/.atom/packages`. ## Why worry? When people install core Atom packages as if they are community packages, it can cause many problems that are very hard to diagnose. This package is intended to notify people when they are in this precarious position so they can take corrective action. ## I got a warning, what do I do? 1. Note down the packages named in the notification 1. Exit Atom 1. Open a command prompt 1. For each package named in the notification, execute `apm uninstall [package-name]` 1. Start Atom again normally to verify that the warning notification no longer appears ## I have more questions. Where can I ask them? Please feel free to ask on [the official Atom message board](https://github.com/atom/atom/discussions). ================================================ FILE: packages/dalek/lib/dalek.js ================================================ /** @babel */ const fs = require('fs'); const path = require('path'); module.exports = { async enumerate() { if (atom.inDevMode()) { return []; } const duplicatePackages = []; const names = atom.packages.getAvailablePackageNames(); for (let name of names) { if (atom.packages.isBundledPackage(name)) { const isDuplicatedPackage = await this.isInstalledAsCommunityPackage( name ); if (isDuplicatedPackage) { duplicatePackages.push(name); } } } return duplicatePackages; }, async isInstalledAsCommunityPackage(name) { const availablePackagePaths = atom.packages.getPackageDirPaths(); for (let packagePath of availablePackagePaths) { const candidate = path.join(packagePath, name); if (fs.existsSync(candidate)) { const realPath = await this.realpath(candidate); if (realPath === candidate) { return true; } } } return false; }, realpath(path) { return new Promise((resolve, reject) => { fs.realpath(path, function(error, realpath) { if (error) { reject(error); } else { resolve(realpath); } }); }); } }; ================================================ FILE: packages/dalek/lib/main.js ================================================ /** @babel */ const dalek = require('./dalek'); const Grim = require('grim'); module.exports = { activate() { atom.packages.onDidActivateInitialPackages(async () => { const duplicates = await dalek.enumerate(); for (let i = 0; i < duplicates.length; i++) { const duplicate = duplicates[i]; Grim.deprecate( `You have the core package "${duplicate}" installed as a community package. See https://github.com/atom/atom/blob/master/packages/dalek/README.md for how this causes problems and instructions on how to correct the situation.`, { packageName: duplicate } ); } }); } }; ================================================ FILE: packages/dalek/package.json ================================================ { "name": "dalek", "main": "./lib/main", "version": "0.2.2", "description": "EXTERMINATEs built-in packages installed in ~/.atom/packages", "keywords": [], "repository": "https://github.com/atom/atom", "license": "MIT", "atomTestRunner": "./test/runner", "engines": { "atom": ">=1.12.7 <2.0.0" }, "dependencies": { "grim": "^2.0.1" }, "devDependencies": { "atom-mocha-test-runner": "^1.0.0", "sinon": "9.0.3", "standard": "^8.6.0" }, "standard": { "env": { "jasmine": true }, "globals": [ "atom" ] } } ================================================ FILE: packages/dalek/test/dalek.test.js ================================================ /** @babel */ const assert = require('assert'); const fs = require('fs'); const sinon = require('sinon'); const path = require('path'); const dalek = require('../lib/dalek'); describe('dalek', function() { describe('enumerate', function() { let availablePackages = {}; let realPaths = {}; let bundledPackages = []; let packageDirPaths = []; let sandbox = null; beforeEach(function() { availablePackages = { 'an-unduplicated-installed-package': path.join( 'Users', 'username', '.atom', 'packages', 'an-unduplicated-installed-package' ), 'duplicated-package': path.join( 'Users', 'username', '.atom', 'packages', 'duplicated-package' ), 'unduplicated-package': path.join( `${atom.getLoadSettings().resourcePath}`, 'node_modules', 'unduplicated-package' ) }; atom.devMode = false; bundledPackages = ['duplicated-package', 'unduplicated-package']; packageDirPaths = [path.join('Users', 'username', '.atom', 'packages')]; sandbox = sinon.createSandbox(); sandbox .stub(dalek, 'realpath') .callsFake(filePath => Promise.resolve(realPaths[filePath] || filePath) ); sandbox.stub(atom.packages, 'isBundledPackage').callsFake(packageName => { return bundledPackages.includes(packageName); }); sandbox .stub(atom.packages, 'getAvailablePackageNames') .callsFake(() => Object.keys(availablePackages)); sandbox.stub(atom.packages, 'getPackageDirPaths').callsFake(() => { return packageDirPaths; }); sandbox.stub(fs, 'existsSync').callsFake(candidate => { return ( Object.values(availablePackages).includes(candidate) && !candidate.includes(atom.getLoadSettings().resourcePath) ); }); }); afterEach(function() { sandbox.restore(); }); it('returns a list of duplicate names', async function() { assert.deepEqual(await dalek.enumerate(), ['duplicated-package']); }); describe('when in dev mode', function() { beforeEach(function() { atom.devMode = true; }); it('always returns an empty list', async function() { assert.deepEqual(await dalek.enumerate(), []); }); }); describe('when a package is symlinked into the package directory', async function() { beforeEach(function() { const realPath = path.join('Users', 'username', 'duplicated-package'); const packagePath = path.join( 'Users', 'username', '.atom', 'packages', 'duplicated-package' ); realPaths[packagePath] = realPath; }); it('is not included in the list of duplicate names', async function() { assert.deepEqual(await dalek.enumerate(), []); }); }); }); }); ================================================ FILE: packages/dalek/test/runner.js ================================================ const createRunner = require('atom-mocha-test-runner').createRunner; module.exports = createRunner({ testSuffixes: ['test.js'] }); ================================================ FILE: packages/deprecation-cop/.coffeelintignore ================================================ spec/fixtures ================================================ FILE: packages/deprecation-cop/.gitignore ================================================ .DS_Store npm-debug.log node_modules ================================================ FILE: packages/deprecation-cop/LICENSE.md ================================================ Copyright (c) 2011-2018 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/deprecation-cop/README.md ================================================ # Deprecation Cop package Shows a list of deprecated methods calls. Ideally it should show nothing! ![https://github-images.s3.amazonaws.com/skitch/Deprecation_Cop_-__Users_corey_github_deprecation-cop-20140414-144618.jpg](https://github-images.s3.amazonaws.com/skitch/Deprecation_Cop_-__Users_corey_github_deprecation-cop-20140414-144618.jpg) ================================================ FILE: packages/deprecation-cop/coffeelint.json ================================================ { "max_line_length": { "level": "ignore" }, "no_empty_param_list": { "level": "error" }, "arrow_spacing": { "level": "error" }, "no_interpolation_in_single_quotes": { "level": "error" }, "no_debugger": { "level": "error" }, "prefer_english_operator": { "level": "error" }, "colon_assignment_spacing": { "spacing": { "left": 0, "right": 1 }, "level": "error" }, "braces_spacing": { "spaces": 0, "level": "error" }, "spacing_after_comma": { "level": "error" }, "no_stand_alone_at": { "level": "error" } } ================================================ FILE: packages/deprecation-cop/lib/deprecation-cop-status-bar-view.coffee ================================================ {CompositeDisposable, Disposable} = require 'atom' _ = require 'underscore-plus' Grim = require 'grim' module.exports = class DeprecationCopStatusBarView lastLength: null toolTipDisposable: null constructor: -> @subscriptions = new CompositeDisposable @element = document.createElement('div') @element.classList.add('deprecation-cop-status', 'inline-block', 'text-warning') @element.setAttribute('tabindex', -1) @icon = document.createElement('span') @icon.classList.add('icon', 'icon-alert') @element.appendChild(@icon) @deprecationNumber = document.createElement('span') @deprecationNumber.classList.add('deprecation-number') @deprecationNumber.textContent = '0' @element.appendChild(@deprecationNumber) clickHandler = -> workspaceElement = atom.views.getView(atom.workspace) atom.commands.dispatch workspaceElement, 'deprecation-cop:view' @element.addEventListener('click', clickHandler) @subscriptions.add(new Disposable(=> @element.removeEventListener('click', clickHandler))) @update() debouncedUpdateDeprecatedSelectorCount = _.debounce(@update, 1000) @subscriptions.add Grim.on 'updated', @update # TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. if atom.styles.onDidUpdateDeprecations? @subscriptions.add(atom.styles.onDidUpdateDeprecations(debouncedUpdateDeprecatedSelectorCount)) destroy: -> @subscriptions.dispose() @element.remove() getDeprecatedCallCount: -> Grim.getDeprecations().map((d) -> d.getStackCount()).reduce(((a, b) -> a + b), 0) getDeprecatedStyleSheetsCount: -> # TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. if atom.styles.getDeprecations? Object.keys(atom.styles.getDeprecations()).length else 0 update: => length = @getDeprecatedCallCount() + @getDeprecatedStyleSheetsCount() return if @lastLength is length @lastLength = length @deprecationNumber.textContent = "#{_.pluralize(length, 'deprecation')}" @toolTipDisposable?.dispose() @toolTipDisposable = atom.tooltips.add @element, title: "#{_.pluralize(length, 'call')} to deprecated methods" if length is 0 @element.style.display = 'none' else @element.style.display = '' ================================================ FILE: packages/deprecation-cop/lib/deprecation-cop-view.js ================================================ /** @babel */ /** @jsx etch.dom */ import _ from 'underscore-plus'; import { CompositeDisposable } from 'atom'; import etch from 'etch'; import fs from 'fs-plus'; import Grim from 'grim'; import { marked } from 'marked'; import path from 'path'; import { shell } from 'electron'; export default class DeprecationCopView { constructor({ uri }) { this.uri = uri; this.subscriptions = new CompositeDisposable(); this.subscriptions.add( Grim.on('updated', () => { etch.update(this); }) ); // TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. if (atom.styles.onDidUpdateDeprecations) { this.subscriptions.add( atom.styles.onDidUpdateDeprecations(() => { etch.update(this); }) ); } etch.initialize(this); this.subscriptions.add( atom.commands.add(this.element, { 'core:move-up': () => { this.scrollUp(); }, 'core:move-down': () => { this.scrollDown(); }, 'core:page-up': () => { this.pageUp(); }, 'core:page-down': () => { this.pageDown(); }, 'core:move-to-top': () => { this.scrollToTop(); }, 'core:move-to-bottom': () => { this.scrollToBottom(); } }) ); } serialize() { return { deserializer: this.constructor.name, uri: this.getURI(), version: 1 }; } destroy() { this.subscriptions.dispose(); return etch.destroy(this); } update() { return etch.update(this); } render() { return (
Deprecated calls
    {this.renderDeprecatedCalls()}
Deprecated selectors
    {this.renderDeprecatedSelectors()}
); } renderDeprecatedCalls() { const deprecationsByPackageName = this.getDeprecatedCallsByPackageName(); const packageNames = Object.keys(deprecationsByPackageName); if (packageNames.length === 0) { return
  • No deprecated calls
  • ; } else { return packageNames.sort().map(packageName => (
  • event.target.parentElement.classList.toggle('collapsed') } > {packageName || 'atom core'} {` (${_.pluralize( deprecationsByPackageName[packageName].length, 'deprecation' )})`}
  • )); } } renderDeprecatedSelectors() { const deprecationsByPackageName = this.getDeprecatedSelectorsByPackageName(); const packageNames = Object.keys(deprecationsByPackageName); if (packageNames.length === 0) { return
  • No deprecated selectors
  • ; } else { return packageNames.map(packageName => (
  • event.target.parentElement.classList.toggle('collapsed') } > {packageName}
  • )); } } renderPackageActionsIfNeeded(packageName) { if (packageName && atom.packages.getLoadedPackage(packageName)) { return (
    ); } else { return ''; } } encodeURI(str) { return encodeURI(str) .replace(/#/g, '%23') .replace(/;/g, '%3B') .replace(/%20/g, '+'); } renderSelectorIssueURLIfNeeded(packageName, issueTitle, issueBody) { const repoURL = this.getRepoURL(packageName); if (repoURL) { const issueURL = `${repoURL}/issues/new?title=${this.encodeURI( issueTitle )}&body=${this.encodeURI(issueBody)}`; return (
    ); } else { return ''; } } renderIssueURLIfNeeded(packageName, deprecation, issueURL) { if (packageName && issueURL) { const repoURL = this.getRepoURL(packageName); const issueTitle = `${deprecation.getOriginName()} is deprecated.`; return (
    ); } else { return ''; } } buildIssueURL(packageName, deprecation, stack) { const repoURL = this.getRepoURL(packageName); if (repoURL) { const title = `${deprecation.getOriginName()} is deprecated.`; const stacktrace = stack .map(({ functionName, location }) => `${functionName} (${location})`) .join('\n'); const body = `${deprecation.getMessage()}\n\`\`\`\n${stacktrace}\n\`\`\``; return `${repoURL}/issues/new?title=${encodeURI(title)}&body=${encodeURI( body )}`; } else { return null; } } async openIssueURL(repoURL, issueURL, issueTitle) { const issue = await this.findSimilarIssue(repoURL, issueTitle); if (issue) { shell.openExternal(issue.html_url); } else if (process.platform === 'win32') { // Windows will not launch URLs greater than ~2000 bytes so we need to shrink it shell.openExternal((await this.shortenURL(issueURL)) || issueURL); } else { shell.openExternal(issueURL); } } async findSimilarIssue(repoURL, issueTitle) { const url = 'https://api.github.com/search/issues'; const repo = repoURL.replace(/http(s)?:\/\/(\d+\.)?github.com\//gi, ''); const query = `${issueTitle} repo:${repo}`; const response = await window.fetch( `${url}?q=${encodeURI(query)}&sort=created`, { method: 'GET', headers: { Accept: 'application/vnd.github.v3+json', 'Content-Type': 'application/json' } } ); if (response.ok) { const data = await response.json(); if (data.items) { const issues = {}; for (const issue of data.items) { if (issue.title.includes(issueTitle) && !issues[issue.state]) { issues[issue.state] = issue; } } return issues.open || issues.closed; } } } async shortenURL(url) { let encodedUrl = encodeURIComponent(url).substr(0, 5000); // is.gd has 5000 char limit let incompletePercentEncoding = encodedUrl.indexOf( '%', encodedUrl.length - 2 ); if (incompletePercentEncoding >= 0) { // Handle an incomplete % encoding cut-off encodedUrl = encodedUrl.substr(0, incompletePercentEncoding); } let result = await fetch('https://is.gd/create.php?format=simple', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `url=${encodedUrl}` }); return result.text(); } getRepoURL(packageName) { const loadedPackage = atom.packages.getLoadedPackage(packageName); if ( loadedPackage && loadedPackage.metadata && loadedPackage.metadata.repository ) { const url = loadedPackage.metadata.repository.url || loadedPackage.metadata.repository; return url.replace(/\.git$/, ''); } else { return null; } } getDeprecatedCallsByPackageName() { const deprecatedCalls = Grim.getDeprecations(); deprecatedCalls.sort((a, b) => b.getCallCount() - a.getCallCount()); const deprecatedCallsByPackageName = {}; for (const deprecation of deprecatedCalls) { const stacks = deprecation.getStacks(); stacks.sort((a, b) => b.callCount - a.callCount); for (const stack of stacks) { let packageName = null; if (stack.metadata && stack.metadata.packageName) { packageName = stack.metadata.packageName; } else { packageName = (this.getPackageName(stack) || '').toLowerCase(); } deprecatedCallsByPackageName[packageName] = deprecatedCallsByPackageName[packageName] || []; deprecatedCallsByPackageName[packageName].push({ deprecation, stack }); } } return deprecatedCallsByPackageName; } getDeprecatedSelectorsByPackageName() { const deprecatedSelectorsByPackageName = {}; if (atom.styles.getDeprecations) { const deprecatedSelectorsBySourcePath = atom.styles.getDeprecations(); for (const sourcePath of Object.keys(deprecatedSelectorsBySourcePath)) { const deprecation = deprecatedSelectorsBySourcePath[sourcePath]; const components = sourcePath.split(path.sep); const packagesComponentIndex = components.indexOf('packages'); let packageName = null; let packagePath = null; if (packagesComponentIndex === -1) { packageName = 'Other'; // could be Atom Core or the personal style sheet packagePath = ''; } else { packageName = components[packagesComponentIndex + 1]; packagePath = components .slice(0, packagesComponentIndex + 1) .join(path.sep); } deprecatedSelectorsByPackageName[packageName] = deprecatedSelectorsByPackageName[packageName] || []; deprecatedSelectorsByPackageName[packageName].push({ packagePath, sourcePath, deprecation }); } } return deprecatedSelectorsByPackageName; } getPackageName(stack) { const packagePaths = this.getPackagePathsByPackageName(); for (const [packageName, packagePath] of packagePaths) { if ( packagePath.includes('.atom/dev/packages') || packagePath.includes('.atom/packages') ) { packagePaths.set(packageName, fs.absolute(packagePath)); } } for (let i = 1; i < stack.length; i++) { const { fileName } = stack[i]; // Empty when it was run from the dev console if (!fileName) { return null; } // Continue to next stack entry if call is in node_modules if (fileName.includes(`${path.sep}node_modules${path.sep}`)) { continue; } for (const [packageName, packagePath] of packagePaths) { const relativePath = path.relative(packagePath, fileName); if (!/^\.\./.test(relativePath)) { return packageName; } } if (atom.getUserInitScriptPath() === fileName) { return `Your local ${path.basename(fileName)} file`; } } return null; } getPackagePathsByPackageName() { if (this.packagePathsByPackageName) { return this.packagePathsByPackageName; } else { this.packagePathsByPackageName = new Map(); for (const pack of atom.packages.getLoadedPackages()) { this.packagePathsByPackageName.set(pack.name, pack.path); } return this.packagePathsByPackageName; } } checkForUpdates() { atom.workspace.open('atom://config/updates'); } disablePackage(packageName) { if (packageName) { atom.packages.disablePackage(packageName); } } openLocation(location) { let pathToOpen = location.replace('file://', ''); if (process.platform === 'win32') { pathToOpen = pathToOpen.replace(/^\//, ''); } atom.open({ pathsToOpen: [pathToOpen] }); } getURI() { return this.uri; } getTitle() { return 'Deprecation Cop'; } getIconName() { return 'alert'; } scrollUp() { this.element.scrollTop -= document.body.offsetHeight / 20; } scrollDown() { this.element.scrollTop += document.body.offsetHeight / 20; } pageUp() { this.element.scrollTop -= this.element.offsetHeight; } pageDown() { this.element.scrollTop += this.element.offsetHeight; } scrollToTop() { this.element.scrollTop = 0; } scrollToBottom() { this.element.scrollTop = this.element.scrollHeight; } } ================================================ FILE: packages/deprecation-cop/lib/main.js ================================================ const { Disposable, CompositeDisposable } = require('atom'); const DeprecationCopView = require('./deprecation-cop-view'); const DeprecationCopStatusBarView = require('./deprecation-cop-status-bar-view'); const ViewURI = 'atom://deprecation-cop'; class DeprecationCopPackage { activate() { this.disposables = new CompositeDisposable(); this.disposables.add( atom.workspace.addOpener(uri => { if (uri === ViewURI) { return this.deserializeDeprecationCopView({ uri }); } }) ); this.disposables.add( atom.commands.add('atom-workspace', 'deprecation-cop:view', () => { atom.workspace.open(ViewURI); }) ); } deactivate() { this.disposables.dispose(); const pane = atom.workspace.paneForURI(ViewURI); if (pane) { pane.destroyItem(pane.itemForURI(ViewURI)); } } deserializeDeprecationCopView(state) { return new DeprecationCopView(state); } consumeStatusBar(statusBar) { const statusBarView = new DeprecationCopStatusBarView(); const statusBarTile = statusBar.addRightTile({ item: statusBarView, priority: 150 }); this.disposables.add( new Disposable(() => { statusBarView.destroy(); }) ); this.disposables.add( new Disposable(() => { statusBarTile.destroy(); }) ); } } module.exports = new DeprecationCopPackage(); ================================================ FILE: packages/deprecation-cop/package.json ================================================ { "name": "deprecation-cop", "main": "./lib/main", "version": "0.56.9", "description": "Shows a list of deprecated calls", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.50.0" }, "dependencies": { "etch": "0.9.0", "fs-plus": "^3.0.0", "grim": "^2.0.1", "marked": "^4.0.10", "underscore-plus": "^1.7.0" }, "consumedServices": { "status-bar": { "versions": { "^1.0.0": "consumeStatusBar" } } }, "deserializers": { "DeprecationCopView": "deserializeDeprecationCopView" }, "devDependencies": { "coffeelint": "^1.9.7" } } ================================================ FILE: packages/deprecation-cop/spec/deprecation-cop-spec.coffee ================================================ DeprecationCopView = require '../lib/deprecation-cop-view' describe "DeprecationCop", -> [activationPromise, workspaceElement] = [] beforeEach -> workspaceElement = atom.views.getView(atom.workspace) activationPromise = atom.packages.activatePackage('deprecation-cop') expect(atom.workspace.getActivePane().getActiveItem()).not.toExist() describe "when the deprecation-cop:view event is triggered", -> it "displays the deprecation cop pane", -> atom.commands.dispatch workspaceElement, 'deprecation-cop:view' waitsForPromise -> activationPromise deprecationCopView = null waitsFor -> deprecationCopView = atom.workspace.getActivePane().getActiveItem() runs -> expect(deprecationCopView instanceof DeprecationCopView).toBeTruthy() describe "deactivating the package", -> it "removes the deprecation cop pane item", -> atom.commands.dispatch workspaceElement, 'deprecation-cop:view' waitsForPromise -> activationPromise waitsForPromise -> Promise.resolve(atom.packages.deactivatePackage('deprecation-cop')) # Wrapped for Promise & non-Promise deactivate runs -> expect(atom.workspace.getActivePane().getActiveItem()).not.toExist() ================================================ FILE: packages/deprecation-cop/spec/deprecation-cop-status-bar-view-spec.coffee ================================================ path = require 'path' Grim = require 'grim' DeprecationCopView = require '../lib/deprecation-cop-view' _ = require 'underscore-plus' describe "DeprecationCopStatusBarView", -> [deprecatedMethod, statusBarView, workspaceElement] = [] beforeEach -> # jasmine.Clock.useMock() cannot mock _.debounce # http://stackoverflow.com/questions/13707047/spec-for-async-functions-using-jasmine spyOn(_, 'debounce').andCallFake (func) -> -> func.apply(this, arguments) jasmine.snapshotDeprecations() workspaceElement = atom.views.getView(atom.workspace) jasmine.attachToDOM(workspaceElement) waitsForPromise -> atom.packages.activatePackage('status-bar') waitsForPromise -> atom.packages.activatePackage('deprecation-cop') waitsFor -> statusBarView = workspaceElement.querySelector('.deprecation-cop-status') afterEach -> jasmine.restoreDeprecationsSnapshot() it "adds the status bar view when activated", -> expect(statusBarView).toExist() expect(statusBarView.textContent).toBe '0 deprecations' expect(statusBarView).not.toShow() it "increments when there are deprecated methods", -> deprecatedMethod = -> Grim.deprecate("This isn't used") anotherDeprecatedMethod = -> Grim.deprecate("This either") expect(statusBarView.style.display).toBe 'none' expect(statusBarView.offsetHeight).toBe(0) deprecatedMethod() expect(statusBarView.textContent).toBe '1 deprecation' expect(statusBarView.offsetHeight).toBeGreaterThan(0) deprecatedMethod() expect(statusBarView.textContent).toBe '2 deprecations' expect(statusBarView.offsetHeight).toBeGreaterThan(0) anotherDeprecatedMethod() expect(statusBarView.textContent).toBe '3 deprecations' expect(statusBarView.offsetHeight).toBeGreaterThan(0) # TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. if atom.styles.getDeprecations? it "increments when there are deprecated selectors", -> atom.styles.addStyleSheet(""" atom-text-editor::shadow { color: red; } """, sourcePath: 'file-1') expect(statusBarView.textContent).toBe '1 deprecation' expect(statusBarView).toBeVisible() atom.styles.addStyleSheet(""" atom-text-editor::shadow { color: blue; } """, sourcePath: 'file-2') expect(statusBarView.textContent).toBe '2 deprecations' expect(statusBarView).toBeVisible() it 'opens deprecation cop tab when clicked', -> expect(atom.workspace.getActivePane().getActiveItem()).not.toExist() waitsFor (done) -> atom.workspace.onDidOpen ({item}) -> expect(item instanceof DeprecationCopView).toBe true done() statusBarView.click() ================================================ FILE: packages/deprecation-cop/spec/deprecation-cop-view-spec.coffee ================================================ Grim = require 'grim' path = require 'path' _ = require 'underscore-plus' etch = require 'etch' describe "DeprecationCopView", -> [deprecationCopView, workspaceElement] = [] beforeEach -> spyOn(_, 'debounce').andCallFake (func) -> -> func.apply(this, arguments) workspaceElement = atom.views.getView(atom.workspace) jasmine.attachToDOM(workspaceElement) jasmine.snapshotDeprecations() Grim.clearDeprecations() deprecatedMethod = -> Grim.deprecate("A test deprecation. This isn't used") deprecatedMethod() spyOn(Grim, 'deprecate') # Don't fail tests if when using deprecated APIs in deprecation cop's activation activationPromise = atom.packages.activatePackage('deprecation-cop') atom.commands.dispatch workspaceElement, 'deprecation-cop:view' waitsForPromise -> activationPromise waitsFor -> deprecationCopView = atom.workspace.getActivePane().getActiveItem() runs -> jasmine.unspy(Grim, 'deprecate') afterEach -> jasmine.restoreDeprecationsSnapshot() it "displays deprecated methods", -> expect(deprecationCopView.element.textContent).toMatch /Deprecated calls/ expect(deprecationCopView.element.textContent).toMatch /This isn't used/ # TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. if atom.styles.getDeprecations? it "displays deprecated selectors", -> atom.styles.addStyleSheet("atom-text-editor::shadow { color: red }", sourcePath: path.join('some-dir', 'packages', 'package-1', 'file-1.css')) atom.styles.addStyleSheet("atom-text-editor::shadow { color: yellow }", context: 'atom-text-editor', sourcePath: path.join('some-dir', 'packages', 'package-1', 'file-2.css')) atom.styles.addStyleSheet('atom-text-editor::shadow { color: blue }', sourcePath: path.join('another-dir', 'packages', 'package-2', 'file-3.css')) atom.styles.addStyleSheet('atom-text-editor::shadow { color: gray }', sourcePath: path.join('another-dir', 'node_modules', 'package-3', 'file-4.css')) promise = etch.getScheduler().getNextUpdatePromise() waitsForPromise -> promise runs -> packageItems = deprecationCopView.element.querySelectorAll("ul.selectors > li") expect(packageItems.length).toBe(3) expect(packageItems[0].textContent).toMatch /package-1/ expect(packageItems[1].textContent).toMatch /package-2/ expect(packageItems[2].textContent).toMatch /Other/ packageDeprecationItems = packageItems[0].querySelectorAll("li.source-file") expect(packageDeprecationItems.length).toBe(2) expect(packageDeprecationItems[0].textContent).toMatch /atom-text-editor/ expect(packageDeprecationItems[0].querySelector("a").href).toMatch('some-dir/packages/package-1/file-1.css') expect(packageDeprecationItems[1].textContent).toMatch /:host/ expect(packageDeprecationItems[1].querySelector("a").href).toMatch('some-dir/packages/package-1/file-2.css') it 'skips stack entries which go through node_modules/ files when determining package name', -> stack = [ { "functionName": "function0" "location": path.normalize "/Users/user/.atom/packages/package1/node_modules/atom-space-pen-viewslib/space-pen.js:55:66" "fileName": path.normalize "/Users/user/.atom/packages/package1/node_modules/atom-space-pen-views/lib/space-pen.js" } { "functionName": "function1" "location": path.normalize "/Users/user/.atom/packages/package1/node_modules/atom-space-pen-viewslib/space-pen.js:15:16" "fileName": path.normalize "/Users/user/.atom/packages/package1/node_modules/atom-space-pen-views/lib/space-pen.js" } { "functionName": "function2" "location": path.normalize "/Users/user/.atom/packages/package2/lib/module.js:13:14" "fileName": path.normalize "/Users/user/.atom/packages/package2/lib/module.js" } ] packagePathsByPackageName = new Map([ ['package1', path.normalize("/Users/user/.atom/packages/package1")], ['package2', path.normalize("/Users/user/.atom/packages/package2")] ]) spyOn(deprecationCopView, 'getPackagePathsByPackageName').andReturn(packagePathsByPackageName) packageName = deprecationCopView.getPackageName(stack) expect(packageName).toBe("package2") ================================================ FILE: packages/deprecation-cop/styles/deprecation-cop.less ================================================ // The ui-variables file is provided by base themes provided by Atom. // // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less // for a full listing of what's available. @import "ui-variables"; .deprecation-cop-status { .icon.icon-alert:before { // It's a really big icon... width: 17px; } } .deprecation-cop { overflow: auto; -webkit-flex: 1; background-color: @app-background-color; .deprecation-overview { &:after { content: ''; clear: both; display: block; } } .deprecation-info { padding: 0 @component-padding; } .deprecation-info:hover { background-color: @background-color-selected; } .deprecation-detail.list-item { white-space: normal; clear: both; .deprecation-message { padding: 5px 0 5px 28px; line-height: 1.4; font-size: @font-size; p:last-child { margin-bottom: 0; } } .icon-alert { margin-top: 5px; float: left; } } .collapsed > ul { display: none; } .list { list-style-type: none; padding: 0; } .stack-trace { background-color: @tool-panel-background-color; padding: @component-padding; margin-bottom: @component-padding; } a { color: @text-color-highlight; } } ================================================ FILE: packages/dev-live-reload/.gitignore ================================================ node_modules ================================================ FILE: packages/dev-live-reload/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/dev-live-reload/README.md ================================================ # Dev Live Reload package This live reloads the Atom `.less` files. You edit styles and they are magically reflected in any running Atom windows. Magic! :tophat: :sparkles: :rabbit2: Installed by default on Atom windows running in dev mode. Use the "Application: Open Dev" command to open a new dev mode window. Use meta-shift-ctrl-r to reload all core and package stylesheets. This package is __experimental__, it does not handle the following: * File additions to a theme. New files will not be watched. ![gif](https://f.cloud.github.com/assets/69169/1387004/d2dc45f2-3b84-11e3-877e-cac8c51e9702.gif) ================================================ FILE: packages/dev-live-reload/keymaps/dev-live-reload.cson ================================================ '.platform-darwin': 'cmd-ctrl-R': 'dev-live-reload:reload-all' '.platform-win32': 'alt-ctrl-R': 'dev-live-reload:reload-all' ================================================ FILE: packages/dev-live-reload/lib/base-theme-watcher.js ================================================ const fs = require('fs-plus'); const path = require('path'); const Watcher = require('./watcher'); module.exports = class BaseThemeWatcher extends Watcher { constructor() { super(); this.stylesheetsPath = path.dirname( atom.themes.resolveStylesheet('../static/atom.less') ); this.watch(); } watch() { const filePaths = fs .readdirSync(this.stylesheetsPath) .filter(filePath => path.extname(filePath).includes('less')); for (const filePath of filePaths) { this.watchFile(path.join(this.stylesheetsPath, filePath)); } } loadStylesheet() { this.loadAllStylesheets(); } loadAllStylesheets() { atom.themes.reloadBaseStylesheets(); } }; ================================================ FILE: packages/dev-live-reload/lib/main.js ================================================ module.exports = { activate(state) { if (!atom.inDevMode() || atom.inSpecMode()) return; if (atom.packages.hasActivatedInitialPackages()) { this.startWatching(); } else { this.activatedDisposable = atom.packages.onDidActivateInitialPackages( () => this.startWatching() ); } }, deactivate() { if (this.activatedDisposable) this.activatedDisposable.dispose(); if (this.commandDisposable) this.commandDisposable.dispose(); if (this.uiWatcher) this.uiWatcher.destroy(); }, startWatching() { const UIWatcher = require('./ui-watcher'); this.uiWatcher = new UIWatcher({ themeManager: atom.themes }); this.commandDisposable = atom.commands.add( 'atom-workspace', 'dev-live-reload:reload-all', () => this.uiWatcher.reloadAll() ); if (this.activatedDisposable) this.activatedDisposable.dispose(); } }; ================================================ FILE: packages/dev-live-reload/lib/package-watcher.js ================================================ const fs = require('fs-plus'); const Watcher = require('./watcher'); module.exports = class PackageWatcher extends Watcher { static supportsPackage(pack, type) { if (pack.getType() === type && pack.getStylesheetPaths().length) return true; return false; } constructor(pack) { super(); this.pack = pack; this.watch(); } watch() { const watchedPaths = []; const watchPath = stylesheet => { if (!watchedPaths.includes(stylesheet)) this.watchFile(stylesheet); watchedPaths.push(stylesheet); }; const stylesheetsPath = this.pack.getStylesheetsPath(); if (fs.isDirectorySync(stylesheetsPath)) { this.watchDirectory(stylesheetsPath); } const stylesheetPaths = new Set(this.pack.getStylesheetPaths()); const onFile = stylesheetPath => stylesheetPaths.add(stylesheetPath); const onFolder = () => true; fs.traverseTreeSync(stylesheetsPath, onFile, onFolder); for (let stylesheet of stylesheetPaths) { watchPath(stylesheet); } } loadStylesheet(pathName) { if (pathName.includes('variables')) this.emitGlobalsChanged(); this.loadAllStylesheets(); } loadAllStylesheets() { console.log('Reloading package', this.pack.name); this.pack.reloadStylesheets(); } }; ================================================ FILE: packages/dev-live-reload/lib/ui-watcher.js ================================================ const { CompositeDisposable } = require('atom'); const BaseThemeWatcher = require('./base-theme-watcher'); const PackageWatcher = require('./package-watcher'); module.exports = class UIWatcher { constructor() { this.subscriptions = new CompositeDisposable(); this.reloadAll = this.reloadAll.bind(this); this.watchers = []; this.baseTheme = this.createWatcher(new BaseThemeWatcher()); this.watchPackages(); } watchPackages() { this.watchedThemes = new Map(); this.watchedPackages = new Map(); for (const theme of atom.themes.getActiveThemes()) { this.watchTheme(theme); } for (const pack of atom.packages.getActivePackages()) { this.watchPackage(pack); } this.watchForPackageChanges(); } watchForPackageChanges() { this.subscriptions.add( atom.themes.onDidChangeActiveThemes(() => { // We need to destroy all theme watchers as all theme packages are destroyed // when a theme changes. for (const theme of this.watchedThemes.values()) { theme.destroy(); } this.watchedThemes.clear(); // Rewatch everything! for (const theme of atom.themes.getActiveThemes()) { this.watchTheme(theme); } }) ); this.subscriptions.add( atom.packages.onDidActivatePackage(pack => this.watchPackage(pack)) ); this.subscriptions.add( atom.packages.onDidDeactivatePackage(pack => { // This only handles packages - onDidChangeActiveThemes handles themes const watcher = this.watchedPackages.get(pack.name); if (watcher) watcher.destroy(); this.watchedPackages.delete(pack.name); }) ); } watchTheme(theme) { if (PackageWatcher.supportsPackage(theme, 'theme')) { this.watchedThemes.set( theme.name, this.createWatcher(new PackageWatcher(theme)) ); } } watchPackage(pack) { if (PackageWatcher.supportsPackage(pack, 'atom')) { this.watchedPackages.set( pack.name, this.createWatcher(new PackageWatcher(pack)) ); } } createWatcher(watcher) { watcher.onDidChangeGlobals(() => { console.log('Global changed, reloading all styles'); this.reloadAll(); }); watcher.onDidDestroy(() => this.watchers.splice(this.watchers.indexOf(watcher), 1) ); this.watchers.push(watcher); return watcher; } reloadAll() { this.baseTheme.loadAllStylesheets(); for (const pack of atom.packages.getActivePackages()) { if (PackageWatcher.supportsPackage(pack, 'atom')) { pack.reloadStylesheets(); } } for (const theme of atom.themes.getActiveThemes()) { if (PackageWatcher.supportsPackage(theme, 'theme')) { theme.reloadStylesheets(); } } } destroy() { this.subscriptions.dispose(); this.baseTheme.destroy(); for (const pack of this.watchedPackages.values()) { pack.destroy(); } for (const theme of this.watchedThemes.values()) { theme.destroy(); } } }; ================================================ FILE: packages/dev-live-reload/lib/watcher.js ================================================ const { CompositeDisposable, File, Directory, Emitter } = require('atom'); const path = require('path'); module.exports = class Watcher { constructor() { this.destroy = this.destroy.bind(this); this.emitter = new Emitter(); this.disposables = new CompositeDisposable(); this.entities = []; // Used for specs } onDidDestroy(callback) { this.emitter.on('did-destroy', callback); } onDidChangeGlobals(callback) { this.emitter.on('did-change-globals', callback); } destroy() { this.disposables.dispose(); this.entities = null; this.emitter.emit('did-destroy'); this.emitter.dispose(); } watch() { // override me } loadStylesheet(stylesheetPath) { // override me } loadAllStylesheets() { // override me } emitGlobalsChanged() { this.emitter.emit('did-change-globals'); } watchDirectory(directoryPath) { if (this.isInAsarArchive(directoryPath)) return; const entity = new Directory(directoryPath); this.disposables.add(entity.onDidChange(() => this.loadAllStylesheets())); this.entities.push(entity); } watchGlobalFile(filePath) { const entity = new File(filePath); this.disposables.add(entity.onDidChange(() => this.emitGlobalsChanged())); this.entities.push(entity); } watchFile(filePath) { if (this.isInAsarArchive(filePath)) return; const reloadFn = () => this.loadStylesheet(entity.getPath()); const entity = new File(filePath); this.disposables.add(entity.onDidChange(reloadFn)); this.disposables.add(entity.onDidDelete(reloadFn)); this.disposables.add(entity.onDidRename(reloadFn)); this.entities.push(entity); } isInAsarArchive(pathToCheck) { const { resourcePath } = atom.getLoadSettings(); return ( pathToCheck.startsWith(`${resourcePath}${path.sep}`) && path.extname(resourcePath) === '.asar' ); } }; ================================================ FILE: packages/dev-live-reload/menus/dev-live-reload.cson ================================================ 'menu': [ 'label': 'Packages' 'submenu': [ 'label': 'Dev Live Reload' 'submenu': [ { 'label': 'Reload All Styles', 'command': 'dev-live-reload:reload-all' } ] ] ] ================================================ FILE: packages/dev-live-reload/package.json ================================================ { "name": "dev-live-reload", "main": "./lib/main", "version": "0.48.1", "description": "Live reload atom themes and packages.", "repository": "https://github.com/atom/atom", "license": "MIT", "dependencies": { "fs-plus": "^3.0.0" }, "engines": { "atom": "*" }, "devDependencies": { "standard": "^10.0.3" }, "standard": { "env": { "atomtest": true, "browser": true, "jasmine": true, "node": true }, "globals": [ "atom" ] } } ================================================ FILE: packages/dev-live-reload/spec/async-spec-helpers.js ================================================ /** @babel */ export async function conditionPromise( condition, description = 'anonymous condition' ) { const startTime = Date.now(); while (true) { await timeoutPromise(100); if (await condition()) { return; } if (Date.now() - startTime > 5000) { throw new Error('Timed out waiting on ' + description); } } } export function timeoutPromise(timeout) { return new Promise(function(resolve) { global.setTimeout(resolve, timeout); }); } ================================================ FILE: packages/dev-live-reload/spec/dev-live-reload-spec.js ================================================ describe('Dev Live Reload', () => { describe('package activation', () => { let [pack, mainModule] = []; beforeEach(() => { pack = atom.packages.loadPackage('dev-live-reload'); pack.requireMainModule(); mainModule = pack.mainModule; spyOn(mainModule, 'startWatching'); }); describe('when the window is not in dev mode', () => { beforeEach(() => spyOn(atom, 'inDevMode').andReturn(false)); it('does not watch files', async () => { spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true); await atom.packages.activatePackage('dev-live-reload'); expect(mainModule.startWatching).not.toHaveBeenCalled(); }); }); describe('when the window is in spec mode', () => { beforeEach(() => spyOn(atom, 'inSpecMode').andReturn(true)); it('does not watch files', async () => { spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true); await atom.packages.activatePackage('dev-live-reload'); expect(mainModule.startWatching).not.toHaveBeenCalled(); }); }); describe('when the window is in dev mode', () => { beforeEach(() => { spyOn(atom, 'inDevMode').andReturn(true); spyOn(atom, 'inSpecMode').andReturn(false); }); it('watches files', async () => { spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true); await atom.packages.activatePackage('dev-live-reload'); expect(mainModule.startWatching).toHaveBeenCalled(); }); }); describe('when the window is in both dev mode and spec mode', () => { beforeEach(() => { spyOn(atom, 'inDevMode').andReturn(true); spyOn(atom, 'inSpecMode').andReturn(true); }); it('does not watch files', async () => { spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true); await atom.packages.activatePackage('dev-live-reload'); expect(mainModule.startWatching).not.toHaveBeenCalled(); }); }); describe('when the package is activated before initial packages have been activated', () => { beforeEach(() => { spyOn(atom, 'inDevMode').andReturn(true); spyOn(atom, 'inSpecMode').andReturn(false); }); it('waits until all initial packages have been activated before watching files', async () => { await atom.packages.activatePackage('dev-live-reload'); expect(mainModule.startWatching).not.toHaveBeenCalled(); atom.packages.emitter.emit('did-activate-initial-packages'); expect(mainModule.startWatching).toHaveBeenCalled(); }); }); }); describe('package deactivation', () => { beforeEach(() => { spyOn(atom, 'inDevMode').andReturn(true); spyOn(atom, 'inSpecMode').andReturn(false); }); it('stops watching all files', async () => { spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true); const { mainModule } = await atom.packages.activatePackage( 'dev-live-reload' ); expect(mainModule.uiWatcher).not.toBeNull(); spyOn(mainModule.uiWatcher, 'destroy'); await atom.packages.deactivatePackage('dev-live-reload'); expect(mainModule.uiWatcher.destroy).toHaveBeenCalled(); }); it('unsubscribes from the onDidActivateInitialPackages subscription if it is disabled before all initial packages are activated', async () => { const { mainModule } = await atom.packages.activatePackage( 'dev-live-reload' ); expect(mainModule.activatedDisposable.disposed).toBe(false); await atom.packages.deactivatePackage('dev-live-reload'); expect(mainModule.activatedDisposable.disposed).toBe(true); spyOn(mainModule, 'startWatching'); atom.packages.emitter.emit('did-activate-initial-packages'); expect(mainModule.startWatching).not.toHaveBeenCalled(); }); it('removes its commands', async () => { spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true); await atom.packages.activatePackage('dev-live-reload'); expect( atom.commands .findCommands({ target: atom.views.getView(atom.workspace) }) .filter(command => command.name.startsWith('dev-live-reload')).length ).toBeGreaterThan(0); await atom.packages.deactivatePackage('dev-live-reload'); expect( atom.commands .findCommands({ target: atom.views.getView(atom.workspace) }) .filter(command => command.name.startsWith('dev-live-reload')).length ).toBe(0); }); }); }); ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-index/index.coffee ================================================ module.exports = activate: -> ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-styles-folder/package.cson ================================================ {} ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-styles-folder/styles/3.css ================================================ #jasmine-content { font-size: 3px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-styles-folder/styles/sub/1.css ================================================ #jasmine-content { font-size: 1px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-styles-folder/styles/sub/2.less ================================================ @size: 2px; #jasmine-content { font-size: @size; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-styles-manifest/package.cson ================================================ styleSheets: ['2', '1'] ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-styles-manifest/styles/1.css ================================================ #jasmine-content { font-size: 1px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-styles-manifest/styles/2.less ================================================ @size: 2px; #jasmine-content { font-size: @size; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/package-with-styles-manifest/styles/3.css ================================================ #jasmine-content { font-size: 3px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/index.less ================================================ @import "styles/first"; @import "styles/second"; @import "styles/last"; ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/package.cson ================================================ styleSheets: ['2', '1'] ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/package.json ================================================ { "theme": true } ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/styles/1.css ================================================ #jasmine-content { font-size: 1px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/styles/2.less ================================================ @size: 2px; #jasmine-content { font-size: @size; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/styles/3.css ================================================ #jasmine-content { font-size: 3px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/styles/first.less ================================================ .editor { padding-top: 101px; padding-right: 101px; padding-bottom: 101px; color: red; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/styles/last.less ================================================ .editor { /* padding-top: 103px; padding-right: 103px;*/ padding-bottom: 103px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/styles/second.less ================================================ @import "ui-variables"; @number: 102px; .editor { /* padding-top: 102px;*/ padding-right: @number; padding-bottom: @number; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/packages/styles/ui-variables.less ================================================ // Variables different from the original are marked 'Changed' @text-color: #333; @text-color-subtle: #777; @text-color-highlight: #111; @text-color-selected: @text-color-highlight; @text-color-info: #5293d8; @text-color-success: #1fe977; @text-color-warning: #f78a46; @text-color-error: #c00; @background-color-info: #0098ff; @background-color-success: #17ca65; @background-color-warning: #ff4800; @background-color-error: #c00; @background-color-highlight: rgba(255, 255, 255, 0.10); @background-color-selected: @background-color-highlight; @app-background-color: #00f; // Changed @base-background-color: #fff; @base-border-color: #eee; @pane-item-background-color: @base-background-color; @pane-item-border-color: @base-border-color; @input-background-color: #f00; // Changed @input-border-color: @base-border-color; @tool-panel-background-color: #f4f4f4; @tool-panel-border-color: @base-border-color; @inset-panel-background-color: #eee; @inset-panel-border-color: @base-border-color; @panel-heading-background-color: #ddd; @panel-heading-border-color: transparent; @overlay-background-color: #f4f4f4; @overlay-border-color: @base-border-color; @button-background-color: #ccc; @button-background-color-hover: lighten(@button-background-color, 5%); @button-background-color-selected: @button-background-color-hover; @button-border-color: #aaa; @tab-bar-background-color: #fff; @tab-bar-border-color: darken(@tab-background-color-active, 10%); @tab-background-color: #f4f4f4; @tab-background-color-active: #fff; @tab-border-color: @base-border-color; @tree-view-background-color: @tool-panel-background-color; @tree-view-border-color: @tool-panel-border-color; @ui-site-color-1: @background-color-success; // green @ui-site-color-2: @background-color-info; // blue @ui-site-color-3: @background-color-warning; // orange @ui-site-color-4: #db2ff4; // purple @ui-site-color-5: #f5e11d; // yellow @font-size: 12px; @disclosure-arrow-size: 12px; @component-padding: 150px; @component-icon-padding: 5px; @component-icon-size: 16px; @component-line-height: 25px; @component-border-radius: 2px; @tab-height: 30px; @font-family: Arial; ================================================ FILE: packages/dev-live-reload/spec/fixtures/static/atom.less ================================================ ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-index-less/index.less ================================================ @padding: 4321px; atom-text-editor { padding-top: @padding; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-index-less/package.json ================================================ { "theme": "ui" } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-multiple-imported-files/index.less ================================================ @import "styles/first"; @import "styles/second"; @import "styles/last"; ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-multiple-imported-files/package.json ================================================ { "name": "theme-with-multiple-imported-files", "theme": "ui" } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-multiple-imported-files/styles/first.less ================================================ .editor { padding-top: 101px; padding-right: 101px; padding-bottom: 101px; color: red; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-multiple-imported-files/styles/last.less ================================================ .editor { /* padding-top: 103px; padding-right: 103px;*/ padding-bottom: 103px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-multiple-imported-files/styles/second.less ================================================ @import "ui-variables"; @number: 102px; .editor { /* padding-top: 102px;*/ padding-right: @number; padding-bottom: @number; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-multiple-imported-files/styles/ui-variables.less ================================================ // Variables different from the original are marked 'Changed' @text-color: #333; @text-color-subtle: #777; @text-color-highlight: #111; @text-color-selected: @text-color-highlight; @text-color-info: #5293d8; @text-color-success: #1fe977; @text-color-warning: #f78a46; @text-color-error: #c00; @background-color-info: #0098ff; @background-color-success: #17ca65; @background-color-warning: #ff4800; @background-color-error: #c00; @background-color-highlight: rgba(255, 255, 255, 0.10); @background-color-selected: @background-color-highlight; @app-background-color: #00f; // Changed @base-background-color: #fff; @base-border-color: #eee; @pane-item-background-color: @base-background-color; @pane-item-border-color: @base-border-color; @input-background-color: #f00; // Changed @input-border-color: @base-border-color; @tool-panel-background-color: #f4f4f4; @tool-panel-border-color: @base-border-color; @inset-panel-background-color: #eee; @inset-panel-border-color: @base-border-color; @panel-heading-background-color: #ddd; @panel-heading-border-color: transparent; @overlay-background-color: #f4f4f4; @overlay-border-color: @base-border-color; @button-background-color: #ccc; @button-background-color-hover: lighten(@button-background-color, 5%); @button-background-color-selected: @button-background-color-hover; @button-border-color: #aaa; @tab-bar-background-color: #fff; @tab-bar-border-color: darken(@tab-background-color-active, 10%); @tab-background-color: #f4f4f4; @tab-background-color-active: #fff; @tab-border-color: @base-border-color; @tree-view-background-color: @tool-panel-background-color; @tree-view-border-color: @tool-panel-border-color; @ui-site-color-1: @background-color-success; // green @ui-site-color-2: @background-color-info; // blue @ui-site-color-3: @background-color-warning; // orange @ui-site-color-4: #db2ff4; // purple @ui-site-color-5: #f5e11d; // yellow @font-size: 12px; @disclosure-arrow-size: 12px; @component-padding: 150px; @component-icon-padding: 5px; @component-icon-size: 16px; @component-line-height: 25px; @component-border-radius: 2px; @tab-height: 30px; @font-family: Arial; ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-package-file/package.json ================================================ { "theme": "ui", "styleSheets": ["first.css", "second.less", "last.css"] } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-package-file/styles/first.css ================================================ atom-text-editor { padding-top: 101px; padding-right: 101px; padding-bottom: 101px; color: red; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-package-file/styles/last.css ================================================ atom-text-editor { /* padding-top: 103px; padding-right: 103px;*/ padding-bottom: 103px; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-package-file/styles/second.less ================================================ @number: 102px; atom-text-editor { /* padding-top: 102px;*/ padding-right: @number; padding-bottom: @number; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-syntax-variables/package.json ================================================ { "theme": "syntax", "styleSheets": ["editor.less"] } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-syntax-variables/styles/editor.less ================================================ ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-ui-variables/package.json ================================================ { "theme": "ui", "styleSheets": ["editor.less"] } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-ui-variables/styles/editor.less ================================================ @import "ui-variables"; atom-text-editor { padding-top: @component-padding; padding-right: @component-padding; padding-bottom: @component-padding; color: @input-background-color; } ================================================ FILE: packages/dev-live-reload/spec/fixtures/theme-with-ui-variables/styles/ui-variables.less ================================================ // Variables different from the original are marked 'Changed' @text-color: #333; @text-color-subtle: #777; @text-color-highlight: #111; @text-color-selected: @text-color-highlight; @text-color-info: #5293d8; @text-color-success: #1fe977; @text-color-warning: #f78a46; @text-color-error: #c00; @background-color-info: #0098ff; @background-color-success: #17ca65; @background-color-warning: #ff4800; @background-color-error: #c00; @background-color-highlight: rgba(255, 255, 255, 0.10); @background-color-selected: @background-color-highlight; @app-background-color: #00f; // Changed @base-background-color: #fff; @base-border-color: #eee; @pane-item-background-color: @base-background-color; @pane-item-border-color: @base-border-color; @input-background-color: #f00; // Changed @input-border-color: @base-border-color; @tool-panel-background-color: #f4f4f4; @tool-panel-border-color: @base-border-color; @inset-panel-background-color: #eee; @inset-panel-border-color: @base-border-color; @panel-heading-background-color: #ddd; @panel-heading-border-color: transparent; @overlay-background-color: #f4f4f4; @overlay-border-color: @base-border-color; @button-background-color: #ccc; @button-background-color-hover: lighten(@button-background-color, 5%); @button-background-color-selected: @button-background-color-hover; @button-border-color: #aaa; @tab-bar-background-color: #fff; @tab-bar-border-color: darken(@tab-background-color-active, 10%); @tab-background-color: #f4f4f4; @tab-background-color-active: #fff; @tab-border-color: @base-border-color; @tree-view-background-color: @tool-panel-background-color; @tree-view-border-color: @tool-panel-border-color; @ui-site-color-1: @background-color-success; // green @ui-site-color-2: @background-color-info; // blue @ui-site-color-3: @background-color-warning; // orange @ui-site-color-4: #db2ff4; // purple @ui-site-color-5: #f5e11d; // yellow @font-size: 12px; @disclosure-arrow-size: 12px; @component-padding: 150px; @component-icon-padding: 5px; @component-icon-size: 16px; @component-line-height: 25px; @component-border-radius: 2px; @tab-height: 30px; @font-family: Arial; ================================================ FILE: packages/dev-live-reload/spec/ui-watcher-spec.js ================================================ const path = require('path'); const UIWatcher = require('../lib/ui-watcher'); const { conditionPromise } = require('./async-spec-helpers'); describe('UIWatcher', () => { let uiWatcher = null; beforeEach(() => atom.packages.packageDirPaths.push(path.join(__dirname, 'fixtures')) ); afterEach(() => uiWatcher && uiWatcher.destroy()); describe("when a base theme's file changes", () => { beforeEach(() => { spyOn(atom.themes, 'resolveStylesheet').andReturn( path.join(__dirname, 'fixtures', 'static', 'atom.less') ); uiWatcher = new UIWatcher(); }); it('reloads all the base styles', () => { spyOn(atom.themes, 'reloadBaseStylesheets'); expect(uiWatcher.baseTheme.entities[0].getPath()).toContain( `${path.sep}static${path.sep}` ); uiWatcher.baseTheme.entities[0].emitter.emit('did-change'); expect(atom.themes.reloadBaseStylesheets).toHaveBeenCalled(); }); }); it("watches all the style sheets in the theme's styles folder", async () => { const packagePath = path.join( __dirname, 'fixtures', 'package-with-styles-folder' ); await atom.packages.activatePackage(packagePath); uiWatcher = new UIWatcher(); const lastWatcher = uiWatcher.watchers[uiWatcher.watchers.length - 1]; expect(lastWatcher.entities.length).toBe(4); expect(lastWatcher.entities[0].getPath()).toBe( path.join(packagePath, 'styles') ); expect(lastWatcher.entities[1].getPath()).toBe( path.join(packagePath, 'styles', '3.css') ); expect(lastWatcher.entities[2].getPath()).toBe( path.join(packagePath, 'styles', 'sub', '1.css') ); expect(lastWatcher.entities[3].getPath()).toBe( path.join(packagePath, 'styles', 'sub', '2.less') ); }); describe('when a package stylesheet file changes', async () => { beforeEach(async () => { await atom.packages.activatePackage( path.join(__dirname, 'fixtures', 'package-with-styles-manifest') ); uiWatcher = new UIWatcher(); }); it('reloads all package styles', () => { const pack = atom.packages.getActivePackages()[0]; spyOn(pack, 'reloadStylesheets'); uiWatcher.watchers[ uiWatcher.watchers.length - 1 ].entities[1].emitter.emit('did-change'); expect(pack.reloadStylesheets).toHaveBeenCalled(); }); }); describe('when a package does not have a stylesheet', () => { beforeEach(async () => { await atom.packages.activatePackage('package-with-index'); uiWatcher = new UIWatcher(); }); it('does not create a PackageWatcher', () => { expect(uiWatcher.watchedPackages['package-with-index']).toBeUndefined(); }); }); describe('when a package global file changes', () => { beforeEach(async () => { atom.config.set('core.themes', [ 'theme-with-ui-variables', 'theme-with-multiple-imported-files' ]); await atom.themes.activateThemes(); uiWatcher = new UIWatcher(); }); afterEach(() => atom.themes.deactivateThemes()); it('reloads every package when the variables file changes', () => { let varEntity; for (const theme of atom.themes.getActiveThemes()) { spyOn(theme, 'reloadStylesheets'); } for (const entity of uiWatcher.watchedThemes.get( 'theme-with-multiple-imported-files' ).entities) { if (entity.getPath().indexOf('variables') > -1) varEntity = entity; } varEntity.emitter.emit('did-change'); for (const theme of atom.themes.getActiveThemes()) { expect(theme.reloadStylesheets).toHaveBeenCalled(); } }); }); describe('watcher lifecycle', () => { it('starts watching a package if it is activated after initial startup', async () => { uiWatcher = new UIWatcher(); expect(uiWatcher.watchedPackages.size).toBe(0); await atom.packages.activatePackage( path.join(__dirname, 'fixtures', 'package-with-styles-folder') ); expect( uiWatcher.watchedPackages.get('package-with-styles-folder') ).not.toBeUndefined(); }); it('unwatches a package after it is deactivated', async () => { await atom.packages.activatePackage( path.join(__dirname, 'fixtures', 'package-with-styles-folder') ); uiWatcher = new UIWatcher(); const watcher = uiWatcher.watchedPackages.get( 'package-with-styles-folder' ); expect(watcher).not.toBeUndefined(); const watcherDestructionSpy = jasmine.createSpy('watcher-on-did-destroy'); watcher.onDidDestroy(watcherDestructionSpy); await atom.packages.deactivatePackage('package-with-styles-folder'); expect( uiWatcher.watchedPackages.get('package-with-styles-folder') ).toBeUndefined(); expect(uiWatcher.watchedPackages.size).toBe(0); expect(watcherDestructionSpy).toHaveBeenCalled(); }); it('does not watch activated packages after the UI watcher has been destroyed', async () => { uiWatcher = new UIWatcher(); uiWatcher.destroy(); await atom.packages.activatePackage( path.join(__dirname, 'fixtures', 'package-with-styles-folder') ); expect(uiWatcher.watchedPackages.size).toBe(0); }); }); describe('minimal theme packages', () => { let pack = null; beforeEach(async () => { atom.config.set('core.themes', [ 'theme-with-syntax-variables', 'theme-with-index-less' ]); await atom.themes.activateThemes(); uiWatcher = new UIWatcher(); pack = atom.themes.getActiveThemes()[0]; }); afterEach(() => atom.themes.deactivateThemes()); it('watches themes without a styles directory', () => { spyOn(pack, 'reloadStylesheets'); spyOn(atom.themes, 'reloadBaseStylesheets'); const watcher = uiWatcher.watchedThemes.get('theme-with-index-less'); expect(watcher.entities.length).toBe(1); watcher.entities[0].emitter.emit('did-change'); expect(pack.reloadStylesheets).toHaveBeenCalled(); expect(atom.themes.reloadBaseStylesheets).not.toHaveBeenCalled(); }); }); describe('theme packages', () => { let pack = null; beforeEach(async () => { atom.config.set('core.themes', [ 'theme-with-syntax-variables', 'theme-with-multiple-imported-files' ]); await atom.themes.activateThemes(); uiWatcher = new UIWatcher(); pack = atom.themes.getActiveThemes()[0]; }); afterEach(() => atom.themes.deactivateThemes()); it('reloads the theme when anything within the theme changes', () => { spyOn(pack, 'reloadStylesheets'); spyOn(atom.themes, 'reloadBaseStylesheets'); const watcher = uiWatcher.watchedThemes.get( 'theme-with-multiple-imported-files' ); expect(watcher.entities.length).toBe(6); watcher.entities[2].emitter.emit('did-change'); expect(pack.reloadStylesheets).toHaveBeenCalled(); expect(atom.themes.reloadBaseStylesheets).not.toHaveBeenCalled(); watcher.entities[watcher.entities.length - 1].emitter.emit('did-change'); expect(atom.themes.reloadBaseStylesheets).toHaveBeenCalled(); }); it('unwatches when a theme is deactivated', async () => { jasmine.useRealClock(); atom.config.set('core.themes', []); await conditionPromise( () => !uiWatcher.watchedThemes['theme-with-multiple-imported-files'] ); }); it('watches a new theme when it is deactivated', async () => { jasmine.useRealClock(); atom.config.set('core.themes', [ 'theme-with-syntax-variables', 'theme-with-package-file' ]); await conditionPromise(() => uiWatcher.watchedThemes.get('theme-with-package-file') ); pack = atom.themes.getActiveThemes()[0]; spyOn(pack, 'reloadStylesheets'); expect(pack.name).toBe('theme-with-package-file'); const watcher = uiWatcher.watchedThemes.get('theme-with-package-file'); watcher.entities[2].emitter.emit('did-change'); expect(pack.reloadStylesheets).toHaveBeenCalled(); }); }); }); ================================================ FILE: packages/exception-reporting/.gitignore ================================================ node_modules ================================================ FILE: packages/exception-reporting/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/exception-reporting/README.md ================================================ ## Exception Reporting package Reports uncaught exceptions in Atom to [bugsnag](https://bugsnag.com). ================================================ FILE: packages/exception-reporting/lib/main.js ================================================ /** @babel */ import { CompositeDisposable } from 'atom'; let reporter; function getReporter() { if (!reporter) { const Reporter = require('./reporter'); reporter = new Reporter(); } return reporter; } export default { activate() { this.subscriptions = new CompositeDisposable(); if (!atom.config.get('exception-reporting.userId')) { atom.config.set('exception-reporting.userId', require('node-uuid').v4()); } this.subscriptions.add( atom.onDidThrowError(({ message, url, line, column, originalError }) => { try { getReporter().reportUncaughtException(originalError); } catch (secondaryException) { try { console.error( 'Error reporting uncaught exception', secondaryException ); getReporter().reportUncaughtException(secondaryException); } catch (error) {} } }) ); if (atom.onDidFailAssertion != null) { this.subscriptions.add( atom.onDidFailAssertion(error => { try { getReporter().reportFailedAssertion(error); } catch (secondaryException) { try { console.error( 'Error reporting assertion failure', secondaryException ); getReporter().reportUncaughtException(secondaryException); } catch (error) {} } }) ); } } }; ================================================ FILE: packages/exception-reporting/lib/reporter.js ================================================ /** @babel */ import os from 'os'; import stackTrace from 'stack-trace'; import fs from 'fs-plus'; import path from 'path'; const API_KEY = '7ddca14cb60cbd1cd12d1b252473b076'; const LIB_VERSION = require('../package.json')['version']; const StackTraceCache = new WeakMap(); export default class Reporter { constructor(params = {}) { this.request = params.request || window.fetch; this.alwaysReport = params.hasOwnProperty('alwaysReport') ? params.alwaysReport : false; this.reportPreviousErrors = params.hasOwnProperty('reportPreviousErrors') ? params.reportPreviousErrors : true; this.resourcePath = this.normalizePath( params.resourcePath || process.resourcesPath ); this.reportedErrors = []; this.reportedAssertionFailures = []; } buildNotificationJSON(error, params) { return { apiKey: API_KEY, notifier: { name: 'Atom', version: LIB_VERSION, url: 'https://www.atom.io' }, events: [ { payloadVersion: '2', exceptions: [this.buildExceptionJSON(error, params.projectRoot)], severity: params.severity, user: { id: params.userId }, app: { version: params.appVersion, releaseStage: params.releaseStage }, device: { osVersion: params.osVersion }, metaData: error.metadata } ] }; } buildExceptionJSON(error, projectRoot) { return { errorClass: error.constructor.name, message: error.message, stacktrace: this.buildStackTraceJSON(error, projectRoot) }; } buildStackTraceJSON(error, projectRoot) { return this.parseStackTrace(error).map(callSite => { return { file: this.scrubPath(callSite.getFileName()), method: callSite.getMethodName() || callSite.getFunctionName() || 'none', lineNumber: callSite.getLineNumber(), columnNumber: callSite.getColumnNumber(), inProject: !/node_modules/.test(callSite.getFileName()) }; }); } normalizePath(pathToNormalize) { return pathToNormalize .replace('file:///', '') // Sometimes it's a uri .replace(/\\/g, '/'); // Unify path separators across Win/macOS/Linux } scrubPath(pathToScrub) { const absolutePath = this.normalizePath(pathToScrub); if (this.isBundledFile(absolutePath)) { return this.normalizePath(path.relative(this.resourcePath, absolutePath)); } else { return absolutePath .replace(this.normalizePath(fs.getHomeDirectory()), '~') // Remove users home dir .replace(/.*(\/packages\/.*)/, '$1'); // Remove everything before app.asar or packages } } getDefaultNotificationParams() { return { userId: atom.config.get('exception-reporting.userId'), appVersion: atom.getVersion(), releaseStage: this.getReleaseChannel(atom.getVersion()), projectRoot: atom.getLoadSettings().resourcePath, osVersion: `${os.platform()}-${os.arch()}-${os.release()}` }; } getReleaseChannel(version) { return version.indexOf('beta') > -1 ? 'beta' : version.indexOf('dev') > -1 ? 'dev' : 'stable'; } performRequest(json) { this.request.call(null, 'https://notify.bugsnag.com', { method: 'POST', headers: new Headers({ 'Content-Type': 'application/json' }), body: JSON.stringify(json) }); } shouldReport(error) { if (this.alwaysReport) return true; // Used in specs if (atom.config.get('core.telemetryConsent') !== 'limited') return false; if (atom.inDevMode()) return false; const topFrame = this.parseStackTrace(error)[0]; const fileName = topFrame ? topFrame.getFileName() : null; return ( fileName && (this.isBundledFile(fileName) || this.isTeletypeFile(fileName)) ); } parseStackTrace(error) { let callSites = StackTraceCache.get(error); if (callSites) { return callSites; } else { callSites = stackTrace.parse(error); StackTraceCache.set(error, callSites); return callSites; } } requestPrivateMetadataConsent(error, message, reportFn) { let notification, dismissSubscription; function reportWithoutPrivateMetadata() { if (dismissSubscription) { dismissSubscription.dispose(); } delete error.privateMetadata; delete error.privateMetadataDescription; reportFn(error); if (notification) { notification.dismiss(); } } function reportWithPrivateMetadata() { if (error.metadata == null) { error.metadata = {}; } for (let key in error.privateMetadata) { let value = error.privateMetadata[key]; error.metadata[key] = value; } reportWithoutPrivateMetadata(); } const name = error.privateMetadataRequestName; if (name != null) { if (localStorage.getItem(`private-metadata-request:${name}`)) { return reportWithoutPrivateMetadata(error); } else { localStorage.setItem(`private-metadata-request:${name}`, true); } } notification = atom.notifications.addInfo(message, { detail: error.privateMetadataDescription, description: 'Are you willing to submit this information to a private server for debugging purposes?', dismissable: true, buttons: [ { text: 'No', onDidClick: reportWithoutPrivateMetadata }, { text: 'Yes, Submit for Debugging', onDidClick: reportWithPrivateMetadata } ] }); dismissSubscription = notification.onDidDismiss( reportWithoutPrivateMetadata ); } addPackageMetadata(error) { let activePackages = atom.packages.getActivePackages(); const availablePackagePaths = atom.packages.getPackageDirPaths(); if (activePackages.length > 0) { let userPackages = {}; let bundledPackages = {}; for (let pack of atom.packages.getActivePackages()) { if (availablePackagePaths.includes(path.dirname(pack.path))) { userPackages[pack.name] = pack.metadata.version; } else { bundledPackages[pack.name] = pack.metadata.version; } } if (error.metadata == null) { error.metadata = {}; } error.metadata.bundledPackages = bundledPackages; error.metadata.userPackages = userPackages; } } addPreviousErrorsMetadata(error) { if (!this.reportPreviousErrors) return; if (!error.metadata) error.metadata = {}; error.metadata.previousErrors = this.reportedErrors.map( error => error.message ); error.metadata.previousAssertionFailures = this.reportedAssertionFailures.map( error => error.message ); } reportUncaughtException(error) { if (!this.shouldReport(error)) return; this.addPackageMetadata(error); this.addPreviousErrorsMetadata(error); if ( error.privateMetadata != null && error.privateMetadataDescription != null ) { this.requestPrivateMetadataConsent( error, 'The Atom team would like to collect the following information to resolve this error:', error => this.reportUncaughtException(error) ); return; } let params = this.getDefaultNotificationParams(); params.severity = 'error'; this.performRequest(this.buildNotificationJSON(error, params)); this.reportedErrors.push(error); } reportFailedAssertion(error) { if (!this.shouldReport(error)) return; this.addPackageMetadata(error); this.addPreviousErrorsMetadata(error); if ( error.privateMetadata != null && error.privateMetadataDescription != null ) { this.requestPrivateMetadataConsent( error, 'The Atom team would like to collect some information to resolve an unexpected condition:', error => this.reportFailedAssertion(error) ); return; } let params = this.getDefaultNotificationParams(); params.severity = 'warning'; this.performRequest(this.buildNotificationJSON(error, params)); this.reportedAssertionFailures.push(error); } // Used in specs setRequestFunction(requestFunction) { this.request = requestFunction; } isBundledFile(fileName) { return this.normalizePath(fileName).indexOf(this.resourcePath) === 0; } isTeletypeFile(fileName) { const teletypePath = atom.packages.resolvePackagePath('teletype'); return ( teletypePath && this.normalizePath(fileName).indexOf(teletypePath) === 0 ); } } Reporter.API_KEY = API_KEY; Reporter.LIB_VERSION = LIB_VERSION; ================================================ FILE: packages/exception-reporting/package.json ================================================ { "name": "exception-reporting", "main": "./lib/main", "version": "0.43.1", "description": "Reports uncaught Atom exceptions to the Atom team via bugsnag.com", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.48.0" }, "dependencies": { "fs-plus": "^3.0.0", "node-uuid": "~1.4.7", "stack-trace": "0.0.9", "underscore-plus": "^1.7.0" }, "devDependencies": { "semver": "^5.3.0" } } ================================================ FILE: packages/exception-reporting/spec/reporter-spec.js ================================================ const Reporter = require('../lib/reporter'); const semver = require('semver'); const os = require('os'); const path = require('path'); const fs = require('fs-plus'); let osVersion = `${os.platform()}-${os.arch()}-${os.release()}`; let getReleaseChannel = version => { return version.indexOf('beta') > -1 ? 'beta' : version.indexOf('dev') > -1 ? 'dev' : 'stable'; }; describe('Reporter', () => { let reporter, requests, initialStackTraceLimit, initialFsGetHomeDirectory, mockActivePackages; beforeEach(() => { reporter = new Reporter({ request: (url, options) => requests.push(Object.assign({ url }, options)), alwaysReport: true, reportPreviousErrors: false }); requests = []; mockActivePackages = []; spyOn(atom.packages, 'getActivePackages').andCallFake( () => mockActivePackages ); initialStackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = 1; initialFsGetHomeDirectory = fs.getHomeDirectory; }); afterEach(() => { fs.getHomeDirectory = initialFsGetHomeDirectory; Error.stackTraceLimit = initialStackTraceLimit; }); describe('.reportUncaughtException(error)', () => { it('posts errors originated inside Atom Core to BugSnag', () => { const repositoryRootPath = path.join(__dirname, '..'); reporter = new Reporter({ request: (url, options) => requests.push(Object.assign({ url }, options)), alwaysReport: true, reportPreviousErrors: false, resourcePath: repositoryRootPath }); let error = new Error(); Error.captureStackTrace(error); reporter.reportUncaughtException(error); let [lineNumber, columnNumber] = error.stack .match(/.js:(\d+):(\d+)/) .slice(1) .map(s => parseInt(s)); expect(requests.length).toBe(1); let [request] = requests; expect(request.method).toBe('POST'); expect(request.url).toBe('https://notify.bugsnag.com'); expect(request.headers.get('Content-Type')).toBe('application/json'); let body = JSON.parse(request.body); // Delete `inProject` field because tests may fail when run as part of Atom core // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`) delete body.events[0].exceptions[0].stacktrace[0].inProject; expect(body).toEqual({ apiKey: Reporter.API_KEY, notifier: { name: 'Atom', version: Reporter.LIB_VERSION, url: 'https://www.atom.io' }, events: [ { payloadVersion: '2', exceptions: [ { errorClass: 'Error', message: '', stacktrace: [ { method: semver.gt(process.versions.electron, '1.6.0') ? 'Spec.' : '', file: 'spec/reporter-spec.js', lineNumber: lineNumber, columnNumber: columnNumber } ] } ], severity: 'error', user: {}, app: { version: atom.getVersion(), releaseStage: getReleaseChannel(atom.getVersion()) }, device: { osVersion: osVersion } } ] }); }); it('posts errors originated outside Atom Core to BugSnag', () => { fs.getHomeDirectory = () => path.join(__dirname, '..', '..'); let error = new Error(); Error.captureStackTrace(error); reporter.reportUncaughtException(error); let [lineNumber, columnNumber] = error.stack .match(/.js:(\d+):(\d+)/) .slice(1) .map(s => parseInt(s)); expect(requests.length).toBe(1); let [request] = requests; expect(request.method).toBe('POST'); expect(request.url).toBe('https://notify.bugsnag.com'); expect(request.headers.get('Content-Type')).toBe('application/json'); let body = JSON.parse(request.body); // Delete `inProject` field because tests may fail when run as part of Atom core // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`) delete body.events[0].exceptions[0].stacktrace[0].inProject; expect(body).toEqual({ apiKey: Reporter.API_KEY, notifier: { name: 'Atom', version: Reporter.LIB_VERSION, url: 'https://www.atom.io' }, events: [ { payloadVersion: '2', exceptions: [ { errorClass: 'Error', message: '', stacktrace: [ { method: semver.gt(process.versions.electron, '1.6.0') ? 'Spec.' : '', file: '~/exception-reporting/spec/reporter-spec.js', lineNumber: lineNumber, columnNumber: columnNumber } ] } ], severity: 'error', user: {}, app: { version: atom.getVersion(), releaseStage: getReleaseChannel(atom.getVersion()) }, device: { osVersion: osVersion } } ] }); }); describe('when the error object has `privateMetadata` and `privateMetadataDescription` fields', () => { let [error, notification] = []; beforeEach(() => { atom.notifications.clear(); spyOn(atom.notifications, 'addInfo').andCallThrough(); error = new Error(); Error.captureStackTrace(error); error.metadata = { foo: 'bar' }; error.privateMetadata = { baz: 'quux' }; error.privateMetadataDescription = 'The contents of baz'; }); it('posts a notification asking for consent', () => { reporter.reportUncaughtException(error); expect(atom.notifications.addInfo).toHaveBeenCalled(); }); it('submits the error with the private metadata if the user consents', () => { spyOn(reporter, 'reportUncaughtException').andCallThrough(); reporter.reportUncaughtException(error); reporter.reportUncaughtException.reset(); notification = atom.notifications.getNotifications()[0]; let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]; expect(notificationOptions.buttons[1].text).toMatch(/Yes/); notificationOptions.buttons[1].onDidClick(); expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error); expect(reporter.reportUncaughtException.callCount).toBe(1); expect(error.privateMetadata).toBeUndefined(); expect(error.privateMetadataDescription).toBeUndefined(); expect(error.metadata).toEqual({ foo: 'bar', baz: 'quux' }); expect(notification.isDismissed()).toBe(true); }); it('submits the error without the private metadata if the user does not consent', () => { spyOn(reporter, 'reportUncaughtException').andCallThrough(); reporter.reportUncaughtException(error); reporter.reportUncaughtException.reset(); notification = atom.notifications.getNotifications()[0]; let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]; expect(notificationOptions.buttons[0].text).toMatch(/No/); notificationOptions.buttons[0].onDidClick(); expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error); expect(reporter.reportUncaughtException.callCount).toBe(1); expect(error.privateMetadata).toBeUndefined(); expect(error.privateMetadataDescription).toBeUndefined(); expect(error.metadata).toEqual({ foo: 'bar' }); expect(notification.isDismissed()).toBe(true); }); it('submits the error without the private metadata if the user dismisses the notification', () => { spyOn(reporter, 'reportUncaughtException').andCallThrough(); reporter.reportUncaughtException(error); reporter.reportUncaughtException.reset(); notification = atom.notifications.getNotifications()[0]; notification.dismiss(); expect(reporter.reportUncaughtException).toHaveBeenCalledWith(error); expect(reporter.reportUncaughtException.callCount).toBe(1); expect(error.privateMetadata).toBeUndefined(); expect(error.privateMetadataDescription).toBeUndefined(); expect(error.metadata).toEqual({ foo: 'bar' }); }); }); it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => { mockActivePackages = [ { name: 'user-1', path: '/Users/user/.atom/packages/user-1', metadata: { version: '1.0.0' } }, { name: 'user-2', path: '/Users/user/.atom/packages/user-2', metadata: { version: '1.2.0' } }, { name: 'bundled-1', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-1', metadata: { version: '1.0.0' } }, { name: 'bundled-2', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2', metadata: { version: '1.2.0' } } ]; const packageDirPaths = ['/Users/user/.atom/packages']; spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths); let error = new Error(); Error.captureStackTrace(error); reporter.reportUncaughtException(error); expect(error.metadata.userPackages).toEqual({ 'user-1': '1.0.0', 'user-2': '1.2.0' }); expect(error.metadata.bundledPackages).toEqual({ 'bundled-1': '1.0.0', 'bundled-2': '1.2.0' }); }); it('adds previous error messages and assertion failures to the reported metadata', () => { reporter.reportPreviousErrors = true; reporter.reportUncaughtException(new Error('A')); reporter.reportUncaughtException(new Error('B')); reporter.reportFailedAssertion(new Error('X')); reporter.reportFailedAssertion(new Error('Y')); reporter.reportUncaughtException(new Error('C')); expect(requests.length).toBe(5); const lastRequest = requests[requests.length - 1]; const body = JSON.parse(lastRequest.body); console.log(body); expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B']); expect(body.events[0].metaData.previousAssertionFailures).toEqual([ 'X', 'Y' ]); }); }); describe('.reportFailedAssertion(error)', () => { it('posts warnings to bugsnag', () => { fs.getHomeDirectory = () => path.join(__dirname, '..', '..'); let error = new Error(); Error.captureStackTrace(error); reporter.reportFailedAssertion(error); let [lineNumber, columnNumber] = error.stack .match(/.js:(\d+):(\d+)/) .slice(1) .map(s => parseInt(s)); expect(requests.length).toBe(1); let [request] = requests; expect(request.method).toBe('POST'); expect(request.url).toBe('https://notify.bugsnag.com'); expect(request.headers.get('Content-Type')).toBe('application/json'); let body = JSON.parse(request.body); // Delete `inProject` field because tests may fail when run as part of Atom core // (i.e. when this test file will be located under `node_modules/exception-reporting/spec`) delete body.events[0].exceptions[0].stacktrace[0].inProject; expect(body).toEqual({ apiKey: Reporter.API_KEY, notifier: { name: 'Atom', version: Reporter.LIB_VERSION, url: 'https://www.atom.io' }, events: [ { payloadVersion: '2', exceptions: [ { errorClass: 'Error', message: '', stacktrace: [ { method: semver.gt(process.versions.electron, '1.6.0') ? 'Spec.' : '', file: '~/exception-reporting/spec/reporter-spec.js', lineNumber: lineNumber, columnNumber: columnNumber } ] } ], severity: 'warning', user: {}, app: { version: atom.getVersion(), releaseStage: getReleaseChannel(atom.getVersion()) }, device: { osVersion: osVersion } } ] }); }); describe('when the error object has `privateMetadata` and `privateMetadataDescription` fields', () => { let [error, notification] = []; beforeEach(() => { atom.notifications.clear(); spyOn(atom.notifications, 'addInfo').andCallThrough(); error = new Error(); Error.captureStackTrace(error); error.metadata = { foo: 'bar' }; error.privateMetadata = { baz: 'quux' }; error.privateMetadataDescription = 'The contents of baz'; }); it('posts a notification asking for consent', () => { reporter.reportFailedAssertion(error); expect(atom.notifications.addInfo).toHaveBeenCalled(); }); it('submits the error with the private metadata if the user consents', () => { spyOn(reporter, 'reportFailedAssertion').andCallThrough(); reporter.reportFailedAssertion(error); reporter.reportFailedAssertion.reset(); notification = atom.notifications.getNotifications()[0]; let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]; expect(notificationOptions.buttons[1].text).toMatch(/Yes/); notificationOptions.buttons[1].onDidClick(); expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error); expect(reporter.reportFailedAssertion.callCount).toBe(1); expect(error.privateMetadata).toBeUndefined(); expect(error.privateMetadataDescription).toBeUndefined(); expect(error.metadata).toEqual({ foo: 'bar', baz: 'quux' }); expect(notification.isDismissed()).toBe(true); }); it('submits the error without the private metadata if the user does not consent', () => { spyOn(reporter, 'reportFailedAssertion').andCallThrough(); reporter.reportFailedAssertion(error); reporter.reportFailedAssertion.reset(); notification = atom.notifications.getNotifications()[0]; let notificationOptions = atom.notifications.addInfo.argsForCall[0][1]; expect(notificationOptions.buttons[0].text).toMatch(/No/); notificationOptions.buttons[0].onDidClick(); expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error); expect(reporter.reportFailedAssertion.callCount).toBe(1); expect(error.privateMetadata).toBeUndefined(); expect(error.privateMetadataDescription).toBeUndefined(); expect(error.metadata).toEqual({ foo: 'bar' }); expect(notification.isDismissed()).toBe(true); }); it('submits the error without the private metadata if the user dismisses the notification', () => { spyOn(reporter, 'reportFailedAssertion').andCallThrough(); reporter.reportFailedAssertion(error); reporter.reportFailedAssertion.reset(); notification = atom.notifications.getNotifications()[0]; notification.dismiss(); expect(reporter.reportFailedAssertion).toHaveBeenCalledWith(error); expect(reporter.reportFailedAssertion.callCount).toBe(1); expect(error.privateMetadata).toBeUndefined(); expect(error.privateMetadataDescription).toBeUndefined(); expect(error.metadata).toEqual({ foo: 'bar' }); }); it("only notifies the user once for a given 'privateMetadataRequestName'", () => { let fakeStorage = {}; spyOn(global.localStorage, 'setItem').andCallFake( (key, value) => (fakeStorage[key] = value) ); spyOn(global.localStorage, 'getItem').andCallFake( key => fakeStorage[key] ); error.privateMetadataRequestName = 'foo'; reporter.reportFailedAssertion(error); expect(atom.notifications.addInfo).toHaveBeenCalled(); atom.notifications.addInfo.reset(); reporter.reportFailedAssertion(error); expect(atom.notifications.addInfo).not.toHaveBeenCalled(); let error2 = new Error(); Error.captureStackTrace(error2); error2.privateMetadataDescription = 'Something about you'; error2.privateMetadata = { baz: 'quux' }; error2.privateMetadataRequestName = 'bar'; reporter.reportFailedAssertion(error2); expect(atom.notifications.addInfo).toHaveBeenCalled(); }); }); it('treats packages located in atom.packages.getPackageDirPaths as user packages', () => { mockActivePackages = [ { name: 'user-1', path: '/Users/user/.atom/packages/user-1', metadata: { version: '1.0.0' } }, { name: 'user-2', path: '/Users/user/.atom/packages/user-2', metadata: { version: '1.2.0' } }, { name: 'bundled-1', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-1', metadata: { version: '1.0.0' } }, { name: 'bundled-2', path: '/Applications/Atom.app/Contents/Resources/app.asar/node_modules/bundled-2', metadata: { version: '1.2.0' } } ]; const packageDirPaths = ['/Users/user/.atom/packages']; spyOn(atom.packages, 'getPackageDirPaths').andReturn(packageDirPaths); let error = new Error(); Error.captureStackTrace(error); reporter.reportFailedAssertion(error); expect(error.metadata.userPackages).toEqual({ 'user-1': '1.0.0', 'user-2': '1.2.0' }); expect(error.metadata.bundledPackages).toEqual({ 'bundled-1': '1.0.0', 'bundled-2': '1.2.0' }); }); it('adds previous error messages and assertion failures to the reported metadata', () => { reporter.reportPreviousErrors = true; reporter.reportUncaughtException(new Error('A')); reporter.reportUncaughtException(new Error('B')); reporter.reportFailedAssertion(new Error('X')); reporter.reportFailedAssertion(new Error('Y')); reporter.reportFailedAssertion(new Error('C')); expect(requests.length).toBe(5); const lastRequest = requests[requests.length - 1]; const body = JSON.parse(lastRequest.body); expect(body.events[0].metaData.previousErrors).toEqual(['A', 'B']); expect(body.events[0].metaData.previousAssertionFailures).toEqual([ 'X', 'Y' ]); }); }); }); ================================================ FILE: packages/git-diff/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/git-diff/README.md ================================================ # Git Diff package [![OS X Build Status](https://travis-ci.org/atom/git-diff.svg?branch=master)](https://travis-ci.org/atom/git-diff) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/9auj52cs0vso66nv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/git-diff/branch/master) [![Dependency Status](https://david-dm.org/atom/git-diff.svg)](https://david-dm.org/atom/git-diff) Marks lines in the editor gutter that have been added, edited, or deleted since the last commit. * alt-g up to move the cursor to the previous diff in the editor * alt-g down to move the cursor to the next diff in the editor ![](https://f.cloud.github.com/assets/671378/2241519/04791a24-9cd6-11e3-9a12-164cabe81d58.png) ================================================ FILE: packages/git-diff/keymaps/git-diff.cson ================================================ 'atom-text-editor': 'alt-g down': 'git-diff:move-to-next-diff' 'alt-g up': 'git-diff:move-to-previous-diff' 'alt-g d': 'git-diff:toggle-diff-list' ================================================ FILE: packages/git-diff/lib/diff-list-view.js ================================================ 'use babel'; import SelectListView from 'atom-select-list'; import repositoryForPath from './helpers'; export default class DiffListView { constructor() { this.selectListView = new SelectListView({ emptyMessage: 'No diffs in file', items: [], filterKeyForItem: diff => diff.lineText, elementForItem: diff => { const li = document.createElement('li'); li.classList.add('two-lines'); const primaryLine = document.createElement('div'); primaryLine.classList.add('primary-line'); primaryLine.textContent = diff.lineText; li.appendChild(primaryLine); const secondaryLine = document.createElement('div'); secondaryLine.classList.add('secondary-line'); secondaryLine.textContent = `-${diff.oldStart},${diff.oldLines} +${ diff.newStart },${diff.newLines}`; li.appendChild(secondaryLine); return li; }, didConfirmSelection: diff => { this.cancel(); const bufferRow = diff.newStart > 0 ? diff.newStart - 1 : diff.newStart; this.editor.setCursorBufferPosition([bufferRow, 0], { autoscroll: true }); this.editor.moveToFirstCharacterOfLine(); }, didCancelSelection: () => { this.cancel(); } }); this.selectListView.element.classList.add('diff-list-view'); this.panel = atom.workspace.addModalPanel({ item: this.selectListView, visible: false }); } attach() { this.previouslyFocusedElement = document.activeElement; this.selectListView.reset(); this.panel.show(); this.selectListView.focus(); } cancel() { this.panel.hide(); if (this.previouslyFocusedElement) { this.previouslyFocusedElement.focus(); this.previouslyFocusedElement = null; } } destroy() { this.cancel(); this.panel.destroy(); return this.selectListView.destroy(); } async toggle() { const editor = atom.workspace.getActiveTextEditor(); if (this.panel.isVisible()) { this.cancel(); } else if (editor) { this.editor = editor; const repository = await repositoryForPath(this.editor.getPath()); let diffs = repository ? repository.getLineDiffs(this.editor.getPath(), this.editor.getText()) : []; if (!diffs) diffs = []; for (let diff of diffs) { const bufferRow = diff.newStart > 0 ? diff.newStart - 1 : diff.newStart; const lineText = this.editor.lineTextForBufferRow(bufferRow); diff.lineText = lineText ? lineText.trim() : ''; } await this.selectListView.update({ items: diffs }); this.attach(); } } } ================================================ FILE: packages/git-diff/lib/git-diff-view.js ================================================ 'use babel'; import { CompositeDisposable } from 'atom'; import repositoryForPath from './helpers'; const MAX_BUFFER_LENGTH_TO_DIFF = 2 * 1024 * 1024; /** * @describe Handles per-editor event and repository subscriptions. * @param editor {Atom.TextEditor} - The editor this view will manage. */ export default class GitDiffView { constructor(editor, editorElement) { // These are the only members guaranteed to exist. this.subscriptions = new CompositeDisposable(); this.editor = editor; this.editorElement = editorElement; this.repository = null; this.markers = new Map(); // Assign `null` to all possible child vars here so the JS engine doesn't // have to re-evaluate the microcode when we do eventually need them. this.releaseChildren(); // I know this looks janky but it works. Class methods are available // before the constructor is executed. It's a micro-opt above lambdas. const subscribeToRepository = this.subscribeToRepository.bind(this); // WARNING: This gets handed to requestAnimationFrame, so it must be bound. this.updateDiffs = this.updateDiffs.bind(this); subscribeToRepository(); this.subscriptions.add( atom.project.onDidChangePaths(subscribeToRepository) ); } /** * @describe Handles tear down of destructables and subscriptions. * Does not handle release of memory. This method should only be called * just before this object is freed, and should only tear down the main * object components that are guarunteed to exist at all times. */ destroy() { this.subscriptions.dispose(); this.destroyChildren(); this.markers.clear(); } /** * @describe Destroys this objects children (non-freeing), it's intended * to be an ease-of use function for maintaing this object. This method * should only tear down objects that are selectively allocated upon * repository discovery. * * Example: this.diffs only exists when we have a repository. */ destroyChildren() { if (this._animationId) cancelAnimationFrame(this._animationId); if (this.diffs) for (const diff of this.diffs) this.markers.get(diff).destroy(); } /** * @describe The memory releasing complement function of `destroyChildren`. * frees the memory allocated at all child object storage locations * when there is no repository. */ releaseChildren() { this.diffs = null; this._repoSubs = null; this._animationId = null; this.editorPath = null; this.buffer = null; } /** * @describe handles all subscriptions based on the repository in focus */ async subscribeToRepository() { if (this._repoSubs !== null) { this._repoSubs.dispose(); this.subscriptions.remove(this._repoSubs); } // Don't cache the path unless we know we need it. let editorPath = this.editor.getPath(); this.repository = await repositoryForPath(editorPath); if (this.repository !== null) { this.editorPath = editorPath; this.buffer = this.editor.getBuffer(); const subscribeToRepository = this.subscribeToRepository.bind(this); const updateIconDecoration = this.updateIconDecoration.bind(this); const scheduleUpdate = this.scheduleUpdate.bind(this); this._repoSubs = new CompositeDisposable( this.repository.onDidDestroy(subscribeToRepository), this.repository.onDidChangeStatuses(scheduleUpdate), this.repository.onDidChangeStatus(changedPath => { if (changedPath === this.editorPath) scheduleUpdate(); }), this.editor.onDidStopChanging(scheduleUpdate), this.editor.onDidChangePath(() => { this.editorPath = this.editor.getPath(); this.buffer = this.editor.getBuffer(); scheduleUpdate(); }), atom.commands.add( this.editorElement, 'git-diff:move-to-next-diff', this.moveToNextDiff.bind(this) ), atom.commands.add( this.editorElement, 'git-diff:move-to-previous-diff', this.moveToPreviousDiff.bind(this) ), atom.config.onDidChange( 'git-diff.showIconsInEditorGutter', updateIconDecoration ), atom.config.onDidChange('editor.showLineNumbers', updateIconDecoration), this.editorElement.onDidAttach(updateIconDecoration) ); // Every time the repo is changed, the editor needs to be reinitialized. this.subscriptions.add(this._repoSubs); updateIconDecoration(); scheduleUpdate(); } else { this.destroyChildren(); this.releaseChildren(); } } moveToNextDiff() { const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1; let nextDiffLineNumber = null; let firstDiffLineNumber = null; for (const { newStart } of this.diffs) { if (newStart > cursorLineNumber) { if (nextDiffLineNumber == null) nextDiffLineNumber = newStart - 1; nextDiffLineNumber = Math.min(newStart - 1, nextDiffLineNumber); } if (firstDiffLineNumber == null) firstDiffLineNumber = newStart - 1; firstDiffLineNumber = Math.min(newStart - 1, firstDiffLineNumber); } // Wrap around to the first diff in the file if ( atom.config.get('git-diff.wrapAroundOnMoveToDiff') && nextDiffLineNumber == null ) { nextDiffLineNumber = firstDiffLineNumber; } this.moveToLineNumber(nextDiffLineNumber); } moveToPreviousDiff() { const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1; let previousDiffLineNumber = null; let lastDiffLineNumber = null; for (const { newStart } of this.diffs) { if (newStart < cursorLineNumber) { previousDiffLineNumber = Math.max(newStart - 1, previousDiffLineNumber); } lastDiffLineNumber = Math.max(newStart - 1, lastDiffLineNumber); } // Wrap around to the last diff in the file if ( atom.config.get('git-diff.wrapAroundOnMoveToDiff') && previousDiffLineNumber === null ) { previousDiffLineNumber = lastDiffLineNumber; } this.moveToLineNumber(previousDiffLineNumber); } updateIconDecoration() { const gutter = this.editorElement.querySelector('.gutter'); if (gutter) { if ( atom.config.get('editor.showLineNumbers') && atom.config.get('git-diff.showIconsInEditorGutter') ) { gutter.classList.add('git-diff-icon'); } else { gutter.classList.remove('git-diff-icon'); } } } moveToLineNumber(lineNumber) { if (lineNumber !== null) { this.editor.setCursorBufferPosition([lineNumber, 0]); this.editor.moveToFirstCharacterOfLine(); } } scheduleUpdate() { // Use Chromium native requestAnimationFrame because it yields // to the browser, is standard and doesn't involve extra JS overhead. if (this._animationId) cancelAnimationFrame(this._animationId); this._animationId = requestAnimationFrame(this.updateDiffs); } /** * @describe Uses text markers in the target editor to visualize * git modifications, additions, and deletions. The current algorithm * just redraws the markers each call. */ updateDiffs() { if (this.buffer.getLength() < MAX_BUFFER_LENGTH_TO_DIFF) { // Before we redraw the diffs, tear down the old markers. if (this.diffs) for (const diff of this.diffs) this.markers.get(diff).destroy(); this.markers.clear(); const text = this.buffer.getText(); this.diffs = this.repository.getLineDiffs(this.editorPath, text); this.diffs = this.diffs || []; // Sanitize type to array. for (const diff of this.diffs) { const { newStart, oldLines, newLines } = diff; const startRow = newStart - 1; const endRow = newStart + newLines - 1; let mark; if (oldLines === 0 && newLines > 0) { mark = this.markRange(startRow, endRow, 'git-line-added'); } else if (newLines === 0 && oldLines > 0) { if (startRow < 0) { mark = this.markRange(0, 0, 'git-previous-line-removed'); } else { mark = this.markRange(startRow, startRow, 'git-line-removed'); } } else { mark = this.markRange(startRow, endRow, 'git-line-modified'); } this.markers.set(diff, mark); } } } markRange(startRow, endRow, klass) { const marker = this.editor.markBufferRange([[startRow, 0], [endRow, 0]], { invalidate: 'never' }); this.editor.decorateMarker(marker, { type: 'line-number', class: klass }); return marker; } } ================================================ FILE: packages/git-diff/lib/helpers.js ================================================ 'use babel'; import { Directory } from 'atom'; export default async function(goalPath) { if (goalPath) { return atom.project.repositoryForDirectory(new Directory(goalPath)); } return null; } ================================================ FILE: packages/git-diff/lib/main.js ================================================ 'use babel'; import { CompositeDisposable } from 'atom'; import GitDiffView from './git-diff-view'; import DiffListView from './diff-list-view'; let diffListView = null; let diffViews = new Set(); let subscriptions = null; export default { activate(state) { subscriptions = new CompositeDisposable(); subscriptions.add( atom.workspace.observeTextEditors(editor => { const editorElement = atom.views.getView(editor); const diffView = new GitDiffView(editor, editorElement); diffViews.add(diffView); const listViewCommand = 'git-diff:toggle-diff-list'; const editorSubs = new CompositeDisposable( atom.commands.add(editorElement, listViewCommand, () => { if (diffListView == null) diffListView = new DiffListView(); diffListView.toggle(); }), editor.onDidDestroy(() => { diffView.destroy(); diffViews.delete(diffView); editorSubs.dispose(); subscriptions.remove(editorSubs); }) ); subscriptions.add(editorSubs); }) ); }, deactivate() { diffListView = null; for (const diffView of diffViews) diffView.destroy(); diffViews.clear(); subscriptions.dispose(); subscriptions = null; } }; ================================================ FILE: packages/git-diff/menus/git-diff.cson ================================================ 'menu': [ { 'label': 'Packages' 'submenu': [ 'label': 'Git Diff' 'submenu': [ { 'label': 'Move to Next Diff', 'command': 'git-diff:move-to-next-diff' } { 'label': 'Move to Previous Diff', 'command': 'git-diff:move-to-previous-diff' } { 'label': 'Toggle Diff List', 'command': 'git-diff:toggle-diff-list' } ] ] } ] ================================================ FILE: packages/git-diff/package.json ================================================ { "name": "git-diff", "version": "1.3.9", "main": "./lib/main", "description": "Marks lines in the editor gutter that have been added, edited, or deleted since the last commit.", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": "*" }, "dependencies": { "atom-select-list": "^0.7.0" }, "devDependencies": { "fs-plus": "^3.0.0", "temp": "~0.8.1" }, "configSchema": { "showIconsInEditorGutter": { "type": "boolean", "default": false, "description": "Show colored icons for added (`+`), modified (`·`) and removed (`-`) lines in the editor's gutter, instead of colored markers (`|`)." }, "wrapAroundOnMoveToDiff": { "type": "boolean", "default": true, "description": "Wraps around to the first/last diff in the file when moving to next/previous diff." } } } ================================================ FILE: packages/git-diff/spec/diff-list-view-spec.js ================================================ const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); describe('git-diff:toggle-diff-list', () => { let diffListView, editor; beforeEach(() => { const projectPath = temp.mkdirSync('git-diff-spec-'); fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath); fs.moveSync( path.join(projectPath, 'git.git'), path.join(projectPath, '.git') ); atom.project.setPaths([projectPath]); jasmine.attachToDOM(atom.workspace.getElement()); waitsForPromise(() => atom.packages.activatePackage('git-diff')); waitsForPromise(() => atom.workspace.open('sample.js')); runs(() => { editor = atom.workspace.getActiveTextEditor(); editor.setCursorBufferPosition([8, 30]); editor.insertText('a'); atom.commands.dispatch(editor.getElement(), 'git-diff:toggle-diff-list'); }); waitsFor(() => { diffListView = document.querySelector('.diff-list-view'); return diffListView && diffListView.querySelectorAll('li').length > 0; }); }); it('shows a list of all diff hunks', () => { diffListView = document.querySelector('.diff-list-view ol'); expect(diffListView.textContent).toBe( 'while (items.length > 0) {a-9,1 +9,1' ); }); it('moves the cursor to the selected hunk', () => { editor.setCursorBufferPosition([0, 0]); atom.commands.dispatch( document.querySelector('.diff-list-view'), 'core:confirm' ); expect(editor.getCursorBufferPosition()).toEqual([8, 4]); }); }); ================================================ FILE: packages/git-diff/spec/fixtures/working-dir/git.git/HEAD ================================================ ref: refs/heads/master ================================================ FILE: packages/git-diff/spec/fixtures/working-dir/git.git/config ================================================ [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = false ================================================ FILE: packages/git-diff/spec/fixtures/working-dir/git.git/objects/90/820108a054b6f49c0d21031313244b6f7d69dc ================================================ xM0a=\@B;Sbt bL<]|RJi6yJ-e5N1dP 4?'~FGhq(ܔv*Lk]ANx)xmC ================================================ FILE: packages/git-diff/spec/fixtures/working-dir/git.git/refs/heads/master ================================================ 065a272b55ec2ee84530dffd60b6869f7bf5d99c ================================================ FILE: packages/git-diff/spec/fixtures/working-dir/sample.js ================================================ module.exports.quicksort = function () { var sort = function (items) { if (items.length <= 1) return items var pivot = items.shift() var current var left = [] var right = [] while (items.length > 0) { current = items.shift() current < pivot ? left.push(current) : right.push(current) } return sort(left) .concat(pivot) .concat(sort(right)) } return sort(Array.apply(this, arguments)) } ================================================ FILE: packages/git-diff/spec/fixtures/working-dir/sample.txt ================================================ Some text. ================================================ FILE: packages/git-diff/spec/git-diff-spec.js ================================================ const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); describe('GitDiff package', () => { let editor, editorElement, projectPath, screenUpdates; beforeEach(() => { screenUpdates = 0; spyOn(window, 'requestAnimationFrame').andCallFake(fn => { fn(); screenUpdates++; }); spyOn(window, 'cancelAnimationFrame').andCallFake(i => null); projectPath = temp.mkdirSync('git-diff-spec-'); const otherPath = temp.mkdirSync('some-other-path-'); fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath); fs.moveSync( path.join(projectPath, 'git.git'), path.join(projectPath, '.git') ); atom.project.setPaths([otherPath, projectPath]); jasmine.attachToDOM(atom.workspace.getElement()); waitsForPromise(async () => { await atom.workspace.open(path.join(projectPath, 'sample.js')); await atom.packages.activatePackage('git-diff'); }); runs(() => { editor = atom.workspace.getActiveTextEditor(); editorElement = atom.views.getView(editor); }); }); afterEach(() => { temp.cleanup(); }); describe('when the editor has no changes', () => { it("doesn't mark the editor", () => { waitsFor(() => screenUpdates > 0); runs(() => expect(editor.getMarkers().length).toBe(0)); }); }); describe('when the editor has modified lines', () => { it('highlights the modified lines', () => { expect(editorElement.querySelectorAll('.git-line-modified').length).toBe( 0 ); editor.insertText('a'); advanceClock(editor.getBuffer().stoppedChangingDelay); waitsFor(() => editor.getMarkers().length > 0); runs(() => { expect( editorElement.querySelectorAll('.git-line-modified').length ).toBe(1); expect(editorElement.querySelector('.git-line-modified')).toHaveData( 'buffer-row', 0 ); }); }); }); describe('when the editor has added lines', () => { it('highlights the added lines', () => { expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0); editor.moveToEndOfLine(); editor.insertNewline(); editor.insertText('a'); advanceClock(editor.getBuffer().stoppedChangingDelay); waitsFor(() => editor.getMarkers().length > 0); runs(() => { expect(editorElement.querySelectorAll('.git-line-added').length).toBe( 1 ); expect(editorElement.querySelector('.git-line-added')).toHaveData( 'buffer-row', 1 ); }); }); }); describe('when the editor has removed lines', () => { it('highlights the line preceeding the deleted lines', () => { expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0); editor.setCursorBufferPosition([5]); editor.deleteLine(); advanceClock(editor.getBuffer().stoppedChangingDelay); waitsFor(() => editor.getMarkers().length > 0); runs(() => { expect(editorElement.querySelectorAll('.git-line-removed').length).toBe( 1 ); expect(editorElement.querySelector('.git-line-removed')).toHaveData( 'buffer-row', 4 ); }); }); }); describe('when the editor has removed the first line', () => { it('highlights the line preceeding the deleted lines', () => { expect(editorElement.querySelectorAll('.git-line-added').length).toBe(0); editor.setCursorBufferPosition([0, 0]); editor.deleteLine(); advanceClock(editor.getBuffer().stoppedChangingDelay); waitsFor(() => editor.getMarkers().length > 0); runs(() => { expect( editorElement.querySelectorAll('.git-previous-line-removed').length ).toBe(1); expect( editorElement.querySelector('.git-previous-line-removed') ).toHaveData('buffer-row', 0); }); }); }); describe('when a modified line is restored to the HEAD version contents', () => { it('removes the diff highlight', () => { expect(editorElement.querySelectorAll('.git-line-modified').length).toBe( 0 ); editor.insertText('a'); advanceClock(editor.getBuffer().stoppedChangingDelay); waitsFor( () => editorElement.querySelectorAll('.git-line-modified').length > 0 ); runs(() => { expect( editorElement.querySelectorAll('.git-line-modified').length ).toBe(1); editor.backspace(); advanceClock(editor.getBuffer().stoppedChangingDelay); }); waitsFor( () => editorElement.querySelectorAll('.git-line-modified').length < 1 ); runs(() => { expect( editorElement.querySelectorAll('.git-line-modified').length ).toBe(0); }); }); }); describe('when a modified file is opened', () => { it('highlights the changed lines', () => { fs.writeFileSync( path.join(projectPath, 'sample.txt'), 'Some different text.' ); waitsForPromise(() => atom.workspace.open(path.join(projectPath, 'sample.txt')) ); runs(() => { editor = atom.workspace.getActiveTextEditor(); editorElement = editor.getElement(); }); waitsFor(() => editor.getMarkers().length > 0); runs(() => { expect( editorElement.querySelectorAll('.git-line-modified').length ).toBe(1); expect(editorElement.querySelector('.git-line-modified')).toHaveData( 'buffer-row', 0 ); }); }); }); describe('when the project paths change', () => { it("doesn't try to use the destroyed git repository", () => { editor.deleteLine(); atom.project.setPaths([temp.mkdirSync('no-repository')]); advanceClock(editor.getBuffer().stoppedChangingDelay); waitsFor(() => editor.getMarkers().length === 0); runs(() => { expect(editor.getMarkers().length).toBe(0); }); }); }); describe('move-to-next-diff/move-to-previous-diff events', () => { it('moves the cursor to first character of the next/previous diff line', () => { editor.insertText('a'); waitsFor(() => editor.getMarkers().length > 0); runs(() => { editor.setCursorBufferPosition([5]); editor.deleteLine(); advanceClock(editor.getBuffer().stoppedChangingDelay); editor.setCursorBufferPosition([0]); atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff'); expect(editor.getCursorBufferPosition()).toEqual([4, 4]); atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff'); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); }); it('wraps around to the first/last diff in the file', () => { editor.insertText('a'); waitsFor(() => editor.getMarkers().length > 0); runs(() => { editor.setCursorBufferPosition([5]); editor.deleteLine(); advanceClock(editor.getBuffer().stoppedChangingDelay); editor.setCursorBufferPosition([0]); atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff'); expect(editor.getCursorBufferPosition().toArray()).toEqual([4, 4]); atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff'); expect(editor.getCursorBufferPosition().toArray()).toEqual([0, 0]); atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff'); expect(editor.getCursorBufferPosition().toArray()).toEqual([4, 4]); }); }); describe('when the wrapAroundOnMoveToDiff config option is false', () => { beforeEach(() => atom.config.set('git-diff.wrapAroundOnMoveToDiff', false) ); it('does not wraps around to the first/last diff in the file', () => { editor.insertText('a'); editor.setCursorBufferPosition([5]); editor.deleteLine(); advanceClock(editor.getBuffer().stoppedChangingDelay); waitsFor(() => editor.getMarkers().length > 0); runs(() => { editor.setCursorBufferPosition([0]); atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff'); expect(editor.getCursorBufferPosition()).toEqual([4, 4]); atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff'); expect(editor.getCursorBufferPosition()).toEqual([4, 4]); atom.commands.dispatch( editorElement, 'git-diff:move-to-previous-diff' ); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); atom.commands.dispatch( editorElement, 'git-diff:move-to-previous-diff' ); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); }); }); }); describe('when the showIconsInEditorGutter config option is true', () => { beforeEach(() => { atom.config.set('git-diff.showIconsInEditorGutter', true); }); it('the gutter has a git-diff-icon class', () => { waitsFor(() => screenUpdates > 0); runs(() => { expect(editorElement.querySelector('.gutter')).toHaveClass( 'git-diff-icon' ); }); }); it('keeps the git-diff-icon class when editor.showLineNumbers is toggled', () => { waitsFor(() => screenUpdates > 0); runs(() => { atom.config.set('editor.showLineNumbers', false); expect(editorElement.querySelector('.gutter')).not.toHaveClass( 'git-diff-icon' ); atom.config.set('editor.showLineNumbers', true); expect(editorElement.querySelector('.gutter')).toHaveClass( 'git-diff-icon' ); }); }); it('removes the git-diff-icon class when the showIconsInEditorGutter config option set to false', () => { waitsFor(() => screenUpdates > 0); runs(() => { atom.config.set('git-diff.showIconsInEditorGutter', false); expect(editorElement.querySelector('.gutter')).not.toHaveClass( 'git-diff-icon' ); }); }); }); }); ================================================ FILE: packages/git-diff/spec/git-diff-subfolder-spec.js ================================================ const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); describe('GitDiff when targeting nested repository', () => { let editor, editorElement, projectPath, screenUpdates; beforeEach(() => { screenUpdates = 0; spyOn(window, 'requestAnimationFrame').andCallFake(fn => { fn(); screenUpdates++; }); spyOn(window, 'cancelAnimationFrame').andCallFake(i => null); projectPath = temp.mkdirSync('git-diff-spec-'); fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath); fs.moveSync( path.join(projectPath, 'git.git'), path.join(projectPath, '.git') ); // The nested repo doesn't need to be managed by the temp module because // it's a part of our test environment. const nestedPath = path.join(projectPath, 'nested-repository'); // Initialize the repository contents. fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), nestedPath); fs.moveSync( path.join(nestedPath, 'git.git'), path.join(nestedPath, '.git') ); atom.project.setPaths([projectPath]); jasmine.attachToDOM(atom.workspace.getElement()); waitsForPromise(async () => { await atom.workspace.open(path.join(nestedPath, 'sample.js')); await atom.packages.activatePackage('git-diff'); }); runs(() => { editor = atom.workspace.getActiveTextEditor(); editorElement = atom.views.getView(editor); }); }); afterEach(() => { temp.cleanup(); }); describe('When git-diff targets a file in a nested git-repository', () => { /*** * Non-hack regression prevention for nested repositories. If we know * that our project path contains two repositories, we can ensure that * git-diff is targeting the correct one by creating an artificial change * in the ancestor repository, which is percieved differently within the * child. In this case, creating a new file will not generate markers in * the ancestor repo, even if there are changes; but changes will be * marked within the child repo. So all we have to do is check if * markers exist and we know we're targeting the proper repository, * If no markers exist, we're targeting an ancestor repo. */ it('uses the innermost repository', () => { editor.insertText('a'); waitsFor(() => screenUpdates > 0); runs(() => { expect( editorElement.querySelectorAll('.git-line-modified').length ).toBe(1); }); }); }); }); ================================================ FILE: packages/git-diff/spec/init-spec.js ================================================ const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); const commands = [ 'git-diff:toggle-diff-list', 'git-diff:move-to-next-diff', 'git-diff:move-to-previous-diff' ]; describe('git-diff', () => { let editor, element; beforeEach(() => { const projectPath = temp.mkdirSync('git-diff-spec-'); fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath); fs.moveSync( path.join(projectPath, 'git.git'), path.join(projectPath, '.git') ); atom.project.setPaths([projectPath]); jasmine.attachToDOM(atom.workspace.getElement()); waitsForPromise(() => atom.workspace.open('sample.js')); runs(() => { editor = atom.workspace.getActiveTextEditor(); element = atom.views.getView(editor); }); }); describe('When the module is deactivated', () => { it('removes all registered command hooks after deactivation.', () => { waitsForPromise(() => atom.packages.activatePackage('git-diff')); waitsForPromise(() => atom.packages.deactivatePackage('git-diff')); runs(() => { // NOTE: don't use enable and disable from the Public API. expect(atom.packages.isPackageActive('git-diff')).toBe(false); atom.commands .findCommands({ target: element }) .filter(({ name }) => commands.includes(name)) .forEach(command => expect(commands).not.toContain(command.name)); }); }); }); }); ================================================ FILE: packages/git-diff/styles/git-diff.less ================================================ @import "syntax-variables"; @import "octicon-utf-codes"; @import "octicon-mixins"; atom-text-editor { .gutter .line-number { &.git-line-modified { border-left: 2px solid @syntax-color-modified; padding-left: ~"calc(0.5em - 2px)"; } &.git-line-added { border-left: 2px solid @syntax-color-added; padding-left: ~"calc(0.5em - 2px)"; } @size: 4px; &.git-line-removed:before, &.git-previous-line-removed:before { position: absolute; left: 0; height: 0; width: 0; content: ""; border: solid transparent; border-left-color: @syntax-color-removed; border-width: @size; margin-top: -@size; pointer-events: none; } &.git-line-removed:before { bottom: -@size; } &.git-previous-line-removed:before { top: 0; } } .gutter.git-diff-icon .line-number { border-left-width: 0; padding-left: 1.4em; // space for diff icon &:before { .octicon-font(); content: ""; display: inline-block; position: absolute; top: .2em; left: .4em; height: 0px; // make sure it doesnt affect the gutter line height. width: 1em; font-size: .75em; } &.git-line-modified:before { content: @primitive-dot; color: @syntax-color-modified; } &.git-line-added:before { content: @plus; color: @syntax-color-added; } &.git-line-removed:before, &.git-previous-line-removed:before { border: none; // reset triangle content: @dash; color: @syntax-color-removed; } &.git-line-removed:before { top: 1em; } &.git-previous-line-removed:before { top: 0; } } } ================================================ FILE: packages/go-to-line/.gitignore ================================================ .DS_Store Thumbs.db node_modules npm-debug.log ================================================ FILE: packages/go-to-line/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/go-to-line/README.md ================================================ # Go To Line package Move the cursor to a specific line in the editor using ctrl-g. ![](https://f.cloud.github.com/assets/671378/2241602/fdd88c4c-9cd8-11e3-9d14-74844ec7da01.png) ================================================ FILE: packages/go-to-line/keymaps/go-to-line.cson ================================================ '.platform-darwin, .platform-win32, .platform-linux': 'ctrl-g': 'go-to-line:toggle' '.go-to-line atom-text-editor[mini]': 'enter': 'core:confirm', 'escape': 'core:cancel' '.platform-darwin .go-to-line atom-text-editor[mini]': 'cmd-w': 'core:cancel' '.platform-win32 .go-to-line atom-text-editor[mini]': 'ctrl-w': 'core:cancel' '.platform-linux .go-to-line atom-text-editor[mini]': 'ctrl-w': 'core:cancel' ================================================ FILE: packages/go-to-line/lib/go-to-line-view.js ================================================ 'use babel'; import { Point, TextEditor } from 'atom'; class GoToLineView { constructor() { this.miniEditor = new TextEditor({ mini: true }); this.miniEditor.element.addEventListener('blur', this.close.bind(this)); this.message = document.createElement('div'); this.message.classList.add('message'); this.element = document.createElement('div'); this.element.classList.add('go-to-line'); this.element.appendChild(this.miniEditor.element); this.element.appendChild(this.message); this.panel = atom.workspace.addModalPanel({ item: this, visible: false }); atom.commands.add('atom-text-editor', 'go-to-line:toggle', () => { this.toggle(); return false; }); atom.commands.add(this.miniEditor.element, 'core:confirm', () => { this.navigate(); }); atom.commands.add(this.miniEditor.element, 'core:cancel', () => { this.close(); }); this.miniEditor.onWillInsertText(arg => { if (arg.text.match(/[^0-9:]/)) { arg.cancel(); } }); this.miniEditor.onDidChange(() => { this.navigate({ keepOpen: true }); }); } toggle() { this.panel.isVisible() ? this.close() : this.open(); } close() { if (!this.panel.isVisible()) return; this.miniEditor.setText(''); this.panel.hide(); if (this.miniEditor.element.hasFocus()) { this.restoreFocus(); } } navigate(options = {}) { const lineNumber = this.miniEditor.getText(); const editor = atom.workspace.getActiveTextEditor(); if (!options.keepOpen) { this.close(); } if (!editor || !lineNumber.length) return; const currentRow = editor.getCursorBufferPosition().row; const rowLineNumber = lineNumber.split(/:+/)[0] || ''; const row = rowLineNumber.length > 0 ? parseInt(rowLineNumber) - 1 : currentRow; const columnLineNumber = lineNumber.split(/:+/)[1] || ''; const column = columnLineNumber.length > 0 ? parseInt(columnLineNumber) - 1 : -1; const position = new Point(row, column); editor.setCursorBufferPosition(position); editor.unfoldBufferRow(row); if (column < 0) { editor.moveToFirstCharacterOfLine(); } editor.scrollToBufferPosition(position, { center: true }); } storeFocusedElement() { this.previouslyFocusedElement = document.activeElement; return this.previouslyFocusedElement; } restoreFocus() { if ( this.previouslyFocusedElement && this.previouslyFocusedElement.parentElement ) { return this.previouslyFocusedElement.focus(); } atom.views.getView(atom.workspace).focus(); } open() { if (this.panel.isVisible() || !atom.workspace.getActiveTextEditor()) return; this.storeFocusedElement(); this.panel.show(); this.message.textContent = 'Enter a or : to go there. Examples: "3" for row 3 or "2:7" for row 2 and column 7'; this.miniEditor.element.focus(); } } export default { activate() { return new GoToLineView(); } }; ================================================ FILE: packages/go-to-line/menus/go-to-line.cson ================================================ 'menu': [ 'label': 'Edit' 'submenu': [ 'label': 'Go to Line' 'command': 'go-to-line:toggle' ] ] ================================================ FILE: packages/go-to-line/package.json ================================================ { "name": "go-to-line", "version": "0.33.0", "main": "./lib/go-to-line-view", "description": "Jump to a specific editor line number with `ctrl-g`.", "license": "MIT", "scripts": { "lint": "standard" }, "activationCommands": { "atom-text-editor": [ "go-to-line:toggle" ] }, "repository": "https://github.com/atom/atom", "engines": { "atom": "*" }, "devDependencies": { "standard": "^8.6.0" }, "standard": { "globals": [ "atom", "waitsForPromise" ], "ignore": [ "spec/fixtures" ] } } ================================================ FILE: packages/go-to-line/spec/fixtures/sample.js ================================================ var quicksort = function () { var sort = function (items) { if (items.length <= 1) return items var pivot = items.shift() var current var left = [] var right = [] while (items.length > 0) { current = items.shift() current < pivot ? left.push(current) : right.push(current) } return sort(left) .concat(pivot) .concat(sort(right)) } return sort(Array.apply(this, arguments)) } // adapted from: // https://github.com/nzakas/computer-science-in-javascript/tree/master/algorithms/sorting/merge-sort-recursive var mergeSort = function (items) { var merge = function (left, right) { var result = [] var il = 0 var ir = 0 while (il < left.length && ir < right.length) { if (left[il] < right[ir]) { result.push(left[il++]) } else { result.push(right[ir++]) } } return result.concat(left.slice(il)).concat(right.slice(ir)) } if (items.length < 2) { return items } var middle = Math.floor(items.length / 2) var left = items.slice(0, middle) var right = items.slice(middle) var params = merge(mergeSort(left), mergeSort(right)) // Add the arguments to replace everything between 0 and last item in the array params.unshift(0, items.length) items.splice.apply(items, params) return items } // adapted from: // https://github.com/nzakas/computer-science-in-javascript/blob/master/algorithms/sorting/bubble-sort/bubble-sort.js var bubbleSort = function (items) { var swap = function (items, firstIndex, secondIndex) { var temp = items[firstIndex] items[firstIndex] = items[secondIndex] items[secondIndex] = temp } var len = items.length var i var j var stop for (i = 0; i < len; i++) { for (j = 0, stop = len - i; j < stop; j++) { if (items[j] > items[j + 1]) { swap(items, j, j + 1) } } } return items } module.exports = { bubbleSort, mergeSort, quicksort } ================================================ FILE: packages/go-to-line/spec/go-to-line-spec.js ================================================ 'use babel'; /* eslint-env jasmine */ import GoToLineView from '../lib/go-to-line-view'; describe('GoToLine', () => { let editor = null; let editorView = null; let goToLine = null; beforeEach(() => { waitsForPromise(() => { return atom.workspace.open('sample.js'); }); runs(() => { const workspaceElement = atom.views.getView(atom.workspace); workspaceElement.style.height = '200px'; workspaceElement.style.width = '1000px'; jasmine.attachToDOM(workspaceElement); editor = atom.workspace.getActiveTextEditor(); editorView = atom.views.getView(editor); goToLine = GoToLineView.activate(); editor.setCursorBufferPosition([1, 0]); }); }); describe('when go-to-line:toggle is triggered', () => { it('adds a modal panel', () => { expect(goToLine.panel.isVisible()).toBeFalsy(); atom.commands.dispatch(editorView, 'go-to-line:toggle'); expect(goToLine.panel.isVisible()).toBeTruthy(); }); }); describe('when entering a line number', () => { it('only allows 0-9 and the colon character to be entered in the mini editor', () => { expect(goToLine.miniEditor.getText()).toBe(''); goToLine.miniEditor.insertText('a'); expect(goToLine.miniEditor.getText()).toBe(''); goToLine.miniEditor.insertText('path/file.txt:56'); expect(goToLine.miniEditor.getText()).toBe(''); goToLine.miniEditor.insertText(':'); expect(goToLine.miniEditor.getText()).toBe(':'); goToLine.miniEditor.setText(''); goToLine.miniEditor.insertText('4'); expect(goToLine.miniEditor.getText()).toBe('4'); }); }); describe('when typing line numbers (auto-navigation)', () => { it('automatically scrolls to the desired line', () => { goToLine.miniEditor.insertText('19'); expect(editor.getCursorBufferPosition()).toEqual([18, 0]); }); }); describe('when typing line and column numbers (auto-navigation)', () => { it('automatically scrolls to the desired line and column', () => { goToLine.miniEditor.insertText('3:8'); expect(editor.getCursorBufferPosition()).toEqual([2, 7]); }); }); describe('when entering a line number and column number', () => { it('moves the cursor to the column number of the line specified', () => { expect(goToLine.miniEditor.getText()).toBe(''); goToLine.miniEditor.insertText('3:14'); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); expect(editor.getCursorBufferPosition()).toEqual([2, 13]); }); it('centers the selected line', () => { goToLine.miniEditor.insertText('45:4'); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); const rowsPerPage = editor.getRowsPerPage(); const currentRow = editor.getCursorBufferPosition().row; expect(editor.getFirstVisibleScreenRow()).toBe( Math.ceil(currentRow - rowsPerPage / 2) ); expect(editor.getLastVisibleScreenRow()).toBe( currentRow + Math.floor(rowsPerPage / 2) ); }); }); describe('when entering a line number greater than the number of rows in the buffer', () => { it('moves the cursor position to the first character of the last line', () => { atom.commands.dispatch(editorView, 'go-to-line:toggle'); expect(goToLine.panel.isVisible()).toBeTruthy(); expect(goToLine.miniEditor.getText()).toBe(''); goToLine.miniEditor.insertText('78'); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); expect(goToLine.panel.isVisible()).toBeFalsy(); expect(editor.getCursorBufferPosition()).toEqual([77, 0]); }); }); describe('when entering a column number greater than the number in the specified line', () => { it('moves the cursor position to the last character of the specified line', () => { atom.commands.dispatch(editorView, 'go-to-line:toggle'); expect(goToLine.panel.isVisible()).toBeTruthy(); expect(goToLine.miniEditor.getText()).toBe(''); goToLine.miniEditor.insertText('3:43'); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); expect(goToLine.panel.isVisible()).toBeFalsy(); expect(editor.getCursorBufferPosition()).toEqual([2, 39]); }); }); describe('when core:confirm is triggered', () => { describe('when a line number has been entered', () => { it('moves the cursor to the first character of the line', () => { goToLine.miniEditor.insertText('3'); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); expect(editor.getCursorBufferPosition()).toEqual([2, 4]); }); }); describe('when the line number entered is nested within foldes', () => { it('unfolds all folds containing the given row', () => { expect(editor.indentationForBufferRow(9)).toEqual(3); editor.foldAll(); expect(editor.screenRowForBufferRow(9)).toEqual(0); goToLine.miniEditor.insertText('10'); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); expect(editor.getCursorBufferPosition()).toEqual([9, 6]); }); }); }); describe('when no line number has been entered', () => { it('closes the view and does not update the cursor position', () => { atom.commands.dispatch(editorView, 'go-to-line:toggle'); expect(goToLine.panel.isVisible()).toBeTruthy(); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); expect(goToLine.panel.isVisible()).toBeFalsy(); expect(editor.getCursorBufferPosition()).toEqual([1, 0]); }); }); describe('when no line number has been entered, but a column number has been entered', () => { it('navigates to the column of the current line', () => { atom.commands.dispatch(editorView, 'go-to-line:toggle'); expect(goToLine.panel.isVisible()).toBeTruthy(); goToLine.miniEditor.insertText('4:1'); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); expect(goToLine.panel.isVisible()).toBeFalsy(); expect(editor.getCursorBufferPosition()).toEqual([3, 0]); atom.commands.dispatch(editorView, 'go-to-line:toggle'); expect(goToLine.panel.isVisible()).toBeTruthy(); goToLine.miniEditor.insertText(':19'); atom.commands.dispatch(goToLine.miniEditor.element, 'core:confirm'); expect(goToLine.panel.isVisible()).toBeFalsy(); expect(editor.getCursorBufferPosition()).toEqual([3, 18]); }); }); describe('when core:cancel is triggered', () => { it('closes the view and does not update the cursor position', () => { atom.commands.dispatch(editorView, 'go-to-line:toggle'); expect(goToLine.panel.isVisible()).toBeTruthy(); atom.commands.dispatch(goToLine.miniEditor.element, 'core:cancel'); expect(goToLine.panel.isVisible()).toBeFalsy(); expect(editor.getCursorBufferPosition()).toEqual([1, 0]); }); }); }); ================================================ FILE: packages/grammar-selector/README.md ================================================ # Grammar Selector package Pick the grammar used for syntax highlighting using ctrl-shift-L or by clicking the current grammar name in the status bar. ![](https://f.cloud.github.com/assets/671378/2241618/b7661f08-9cd9-11e3-8276-fe1c02955901.png) ================================================ FILE: packages/grammar-selector/keymaps/grammar-selector.cson ================================================ '.platform-darwin atom-text-editor': 'ctrl-L': 'grammar-selector:show' '.platform-win32 atom-text-editor': 'ctrl-L': 'grammar-selector:show' '.platform-linux atom-text-editor': 'ctrl-L': 'grammar-selector:show' ================================================ FILE: packages/grammar-selector/lib/grammar-list-view.js ================================================ const SelectListView = require('atom-select-list'); module.exports = class GrammarListView { constructor() { this.autoDetect = { name: 'Auto Detect' }; this.selectListView = new SelectListView({ itemsClassList: ['mark-active'], items: [], filterKeyForItem: grammar => grammar.name, elementForItem: grammar => { const grammarName = grammar.name || grammar.scopeName; const element = document.createElement('li'); if (grammar === this.currentGrammar) { element.classList.add('active'); } element.textContent = grammarName; element.dataset.grammar = grammarName; const div = document.createElement('div'); div.classList.add('pull-right'); if (isTreeSitter(grammar)) { const parser = document.createElement('span'); parser.classList.add( 'grammar-selector-parser', 'badge', 'badge-success' ); parser.textContent = 'Tree-sitter'; parser.setAttribute( 'title', '(Recommended) A faster parser with improved syntax highlighting & code navigation support.' ); div.appendChild(parser); } if (grammar.scopeName) { const scopeName = document.createElement('scopeName'); scopeName.classList.add('badge', 'badge-info'); scopeName.textContent = grammar.scopeName; div.appendChild(scopeName); element.appendChild(div); } return element; }, didConfirmSelection: grammar => { this.cancel(); if (grammar === this.autoDetect) { atom.textEditors.clearGrammarOverride(this.editor); } else { atom.grammars.assignGrammar(this.editor, grammar); } }, didCancelSelection: () => { this.cancel(); } }); this.selectListView.element.classList.add('grammar-selector'); } destroy() { this.cancel(); return this.selectListView.destroy(); } cancel() { if (this.panel != null) { this.panel.destroy(); } this.panel = null; this.currentGrammar = null; if (this.previouslyFocusedElement) { this.previouslyFocusedElement.focus(); this.previouslyFocusedElement = null; } } attach() { this.previouslyFocusedElement = document.activeElement; if (this.panel == null) { this.panel = atom.workspace.addModalPanel({ item: this.selectListView }); } this.selectListView.focus(); this.selectListView.reset(); } async toggle() { if (this.panel != null) { this.cancel(); return; } const editor = atom.workspace.getActiveTextEditor(); if (editor) { this.editor = editor; this.currentGrammar = this.editor.getGrammar(); if (this.currentGrammar === atom.grammars.nullGrammar) { this.currentGrammar = this.autoDetect; } let grammars = atom.grammars .getGrammars({ includeTreeSitter: true }) .filter(grammar => { return grammar !== atom.grammars.nullGrammar && grammar.name; }); if (atom.config.get('grammar-selector.hideDuplicateTextMateGrammars')) { const blacklist = new Set(); grammars.forEach(grammar => { if (isTreeSitter(grammar)) { blacklist.add(grammar.name); } }); grammars = grammars.filter( grammar => isTreeSitter(grammar) || !blacklist.has(grammar.name) ); } grammars.sort((a, b) => { if (a.scopeName === 'text.plain') { return -1; } else if (b.scopeName === 'text.plain') { return 1; } else if (a.name === b.name) { return compareGrammarType(a, b); } return a.name.localeCompare(b.name); }); grammars.unshift(this.autoDetect); await this.selectListView.update({ items: grammars }); this.attach(); } } }; function isTreeSitter(grammar) { return grammar.constructor.name === 'TreeSitterGrammar'; } function compareGrammarType(a, b) { if (isTreeSitter(a)) { return -1; } else if (isTreeSitter(b)) { return 1; } return 0; } ================================================ FILE: packages/grammar-selector/lib/grammar-status-view.js ================================================ const { Disposable } = require('atom'); module.exports = class GrammarStatusView { constructor(statusBar) { this.statusBar = statusBar; this.element = document.createElement('grammar-selector-status'); this.element.classList.add('grammar-status', 'inline-block'); this.grammarLink = document.createElement('a'); this.grammarLink.classList.add('inline-block'); this.element.appendChild(this.grammarLink); this.activeItemSubscription = atom.workspace.observeActiveTextEditor( this.subscribeToActiveTextEditor.bind(this) ); this.configSubscription = atom.config.observe( 'grammar-selector.showOnRightSideOfStatusBar', this.attach.bind(this) ); const clickHandler = event => { event.preventDefault(); atom.commands.dispatch( atom.views.getView(atom.workspace.getActiveTextEditor()), 'grammar-selector:show' ); }; this.element.addEventListener('click', clickHandler); this.clickSubscription = new Disposable(() => { this.element.removeEventListener('click', clickHandler); }); } attach() { if (this.tile) { this.tile.destroy(); } this.tile = atom.config.get('grammar-selector.showOnRightSideOfStatusBar') ? this.statusBar.addRightTile({ item: this.element, priority: 10 }) : this.statusBar.addLeftTile({ item: this.element, priority: 10 }); } destroy() { if (this.activeItemSubscription) { this.activeItemSubscription.dispose(); } if (this.grammarSubscription) { this.grammarSubscription.dispose(); } if (this.clickSubscription) { this.clickSubscription.dispose(); } if (this.configSubscription) { this.configSubscription.dispose(); } if (this.tile) { this.tile.destroy(); } if (this.tooltip) { this.tooltip.dispose(); } } subscribeToActiveTextEditor() { if (this.grammarSubscription) { this.grammarSubscription.dispose(); this.grammarSubscription = null; } const editor = atom.workspace.getActiveTextEditor(); if (editor) { this.grammarSubscription = editor.onDidChangeGrammar( this.updateGrammarText.bind(this) ); } this.updateGrammarText(); } updateGrammarText() { atom.views.updateDocument(() => { const editor = atom.workspace.getActiveTextEditor(); const grammar = editor ? editor.getGrammar() : null; if (this.tooltip) { this.tooltip.dispose(); this.tooltip = null; } if (grammar) { let grammarName = null; if (grammar === atom.grammars.nullGrammar) { grammarName = 'Plain Text'; } else { grammarName = grammar.name || grammar.scopeName; } this.grammarLink.textContent = grammarName; this.grammarLink.dataset.grammar = grammarName; this.element.style.display = ''; this.tooltip = atom.tooltips.add(this.element, { title: `File uses the ${grammarName} grammar` }); } else { this.element.style.display = 'none'; } }); } }; ================================================ FILE: packages/grammar-selector/lib/main.js ================================================ const GrammarListView = require('./grammar-list-view'); const GrammarStatusView = require('./grammar-status-view'); let commandDisposable = null; let grammarListView = null; let grammarStatusView = null; module.exports = { activate() { commandDisposable = atom.commands.add( 'atom-text-editor', 'grammar-selector:show', () => { if (!grammarListView) grammarListView = new GrammarListView(); grammarListView.toggle(); } ); }, deactivate() { if (commandDisposable) commandDisposable.dispose(); commandDisposable = null; if (grammarStatusView) grammarStatusView.destroy(); grammarStatusView = null; if (grammarListView) grammarListView.destroy(); grammarListView = null; }, consumeStatusBar(statusBar) { grammarStatusView = new GrammarStatusView(statusBar); grammarStatusView.attach(); } }; ================================================ FILE: packages/grammar-selector/menus/grammar-selector.cson ================================================ 'menu': [ 'label': 'Edit' 'submenu': [ 'label': 'Select Grammar' 'command': 'grammar-selector:show' ] ] 'context-menu': '.overlayer': [ 'label': 'Change Grammar' 'command': 'grammar-selector:show' ] ================================================ FILE: packages/grammar-selector/package.json ================================================ { "name": "grammar-selector", "version": "0.50.1", "main": "./lib/main", "description": "Select the grammar to use for the current editor with `ctrl-shift-L`.", "license": "MIT", "repository": "https://github.com/atom/atom", "engines": { "atom": "*" }, "dependencies": { "atom-select-list": "^0.7.0" }, "consumedServices": { "status-bar": { "versions": { "^1.0.0": "consumeStatusBar" } } }, "devDependencies": { "standard": "^10.0.3" }, "standard": { "globals": [ "atom", "beforeEach", "describe", "expect", "it", "jasmine", "spyOn" ] }, "configSchema": { "showOnRightSideOfStatusBar": { "type": "boolean", "default": true, "description": "Show the active pane item's language on the right side of Atom's status bar, instead of the left." }, "hideDuplicateTextMateGrammars": { "type": "boolean", "default": true, "description": "Hides the TextMate grammar when there is an existing Tree-sitter grammar" } } } ================================================ FILE: packages/grammar-selector/spec/fixtures/language-with-no-name/grammars/a.json ================================================ { "scopeName": "source.a", "fileTypes": [ ".a", ".aa", "a" ] } ================================================ FILE: packages/grammar-selector/spec/fixtures/language-with-no-name/package.json ================================================ { "name": "language-test", "version": "1.0.0" } ================================================ FILE: packages/grammar-selector/spec/grammar-selector-spec.js ================================================ const path = require('path'); const SelectListView = require('atom-select-list'); describe('GrammarSelector', () => { let [editor, textGrammar, jsGrammar] = []; beforeEach(async () => { jasmine.attachToDOM(atom.views.getView(atom.workspace)); atom.config.set('grammar-selector.showOnRightSideOfStatusBar', false); atom.config.set('grammar-selector.hideDuplicateTextMateGrammars', false); await atom.packages.activatePackage('status-bar'); await atom.packages.activatePackage('grammar-selector'); await atom.packages.activatePackage('language-text'); await atom.packages.activatePackage('language-javascript'); await atom.packages.activatePackage( path.join(__dirname, 'fixtures', 'language-with-no-name') ); editor = await atom.workspace.open('sample.js'); textGrammar = atom.grammars.grammarForScopeName('text.plain'); expect(textGrammar).toBeTruthy(); jsGrammar = atom.grammars.grammarForScopeName('source.js'); expect(jsGrammar).toBeTruthy(); expect(editor.getGrammar()).toBe(jsGrammar); }); describe('when grammar-selector:show is triggered', () => it('displays a list of all the available grammars', async () => { const grammarView = (await getGrammarView(editor)).element; // -1 for removing nullGrammar, +1 for adding "Auto Detect" // Tree-sitter names the regex and JSDoc grammars expect(grammarView.querySelectorAll('li').length).toBe( atom.grammars .getGrammars({ includeTreeSitter: true }) .filter(g => g.name).length ); expect(grammarView.querySelectorAll('li')[0].textContent).toBe( 'Auto Detect' ); expect(grammarView.textContent.includes('source.a')).toBe(false); grammarView .querySelectorAll('li') .forEach(li => expect(li.textContent).not.toBe(atom.grammars.nullGrammar.name) ); expect(grammarView.textContent.includes('Tree-sitter')).toBe(true); // check we are showing and labelling Tree-sitter grammars })); describe('when a grammar is selected', () => it('sets the new grammar on the editor', async () => { const grammarView = await getGrammarView(editor); grammarView.props.didConfirmSelection(textGrammar); expect(editor.getGrammar()).toBe(textGrammar); })); describe('when auto-detect is selected', () => it('restores the auto-detected grammar on the editor', async () => { let grammarView = await getGrammarView(editor); grammarView.props.didConfirmSelection(textGrammar); expect(editor.getGrammar()).toBe(textGrammar); grammarView = await getGrammarView(editor); grammarView.props.didConfirmSelection(grammarView.items[0]); expect(editor.getGrammar()).toBe(jsGrammar); })); describe("when the editor's current grammar is the null grammar", () => it('displays Auto Detect as the selected grammar', async () => { editor.setGrammar(atom.grammars.nullGrammar); const grammarView = (await getGrammarView(editor)).element; expect(grammarView.querySelector('li.active').textContent).toBe( 'Auto Detect' ); })); describe('when editor is untitled', () => it('sets the new grammar on the editor', async () => { editor = await atom.workspace.open(); expect(editor.getGrammar()).not.toBe(jsGrammar); const grammarView = await getGrammarView(editor); grammarView.props.didConfirmSelection(jsGrammar); expect(editor.getGrammar()).toBe(jsGrammar); })); describe('Status bar grammar label', () => { let [grammarStatus, grammarTile, statusBar] = []; beforeEach(async () => { statusBar = document.querySelector('status-bar'); [grammarTile] = statusBar.getLeftTiles().slice(-1); grammarStatus = grammarTile.getItem(); // Wait for status bar service hook to fire while (!grammarStatus || !grammarStatus.textContent) { await atom.views.getNextUpdatePromise(); grammarStatus = document.querySelector('.grammar-status'); } }); it('displays the name of the current grammar', () => { expect(grammarStatus.querySelector('a').textContent).toBe('JavaScript'); expect(getTooltipText(grammarStatus)).toBe( 'File uses the JavaScript grammar' ); }); it('displays Plain Text when the current grammar is the null grammar', async () => { editor.setGrammar(atom.grammars.nullGrammar); await atom.views.getNextUpdatePromise(); expect(grammarStatus.querySelector('a').textContent).toBe('Plain Text'); expect(grammarStatus).toBeVisible(); expect(getTooltipText(grammarStatus)).toBe( 'File uses the Plain Text grammar' ); editor.setGrammar(atom.grammars.grammarForScopeName('source.js')); await atom.views.getNextUpdatePromise(); expect(grammarStatus.querySelector('a').textContent).toBe('JavaScript'); expect(grammarStatus).toBeVisible(); }); it('hides the label when the current grammar is null', async () => { jasmine.attachToDOM(editor.getElement()); spyOn(editor, 'getGrammar').andReturn(null); editor.setGrammar(atom.grammars.nullGrammar); await atom.views.getNextUpdatePromise(); expect(grammarStatus.offsetHeight).toBe(0); }); describe('when the grammar-selector.showOnRightSideOfStatusBar setting changes', () => it('moves the item to the preferred side of the status bar', () => { expect(statusBar.getLeftTiles().map(tile => tile.getItem())).toContain( grammarStatus ); expect( statusBar.getRightTiles().map(tile => tile.getItem()) ).not.toContain(grammarStatus); atom.config.set('grammar-selector.showOnRightSideOfStatusBar', true); expect( statusBar.getLeftTiles().map(tile => tile.getItem()) ).not.toContain(grammarStatus); expect(statusBar.getRightTiles().map(tile => tile.getItem())).toContain( grammarStatus ); atom.config.set('grammar-selector.showOnRightSideOfStatusBar', false); expect(statusBar.getLeftTiles().map(tile => tile.getItem())).toContain( grammarStatus ); expect( statusBar.getRightTiles().map(tile => tile.getItem()) ).not.toContain(grammarStatus); })); describe("when the editor's grammar changes", () => it('displays the new grammar of the editor', async () => { editor.setGrammar(atom.grammars.grammarForScopeName('text.plain')); await atom.views.getNextUpdatePromise(); expect(grammarStatus.querySelector('a').textContent).toBe('Plain Text'); expect(getTooltipText(grammarStatus)).toBe( 'File uses the Plain Text grammar' ); editor.setGrammar(atom.grammars.grammarForScopeName('source.a')); await atom.views.getNextUpdatePromise(); expect(grammarStatus.querySelector('a').textContent).toBe('source.a'); expect(getTooltipText(grammarStatus)).toBe( 'File uses the source.a grammar' ); })); describe('when toggling hideDuplicateTextMateGrammars', () => { it('shows only the Tree-sitter if true and both exist', async () => { // the main JS grammar has both a TextMate and Tree-sitter implementation atom.config.set('grammar-selector.hideDuplicateTextMateGrammars', true); const grammarView = await getGrammarView(editor); const observedNames = new Set(); grammarView.element.querySelectorAll('li').forEach(li => { const name = li.getAttribute('data-grammar'); expect(observedNames.has(name)).toBe(false); observedNames.add(name); }); // check the seen JS is actually the Tree-sitter one const list = atom.workspace.getModalPanels()[0].item; for (const item of list.items) { if (item.name === 'JavaScript') { expect(item.constructor.name === 'TreeSitterGrammar'); } } }); it('shows both if false', async () => { await atom.packages.activatePackage('language-c'); // punctuation making it sort wrong atom.config.set( 'grammar-selector.hideDuplicateTextMateGrammars', false ); await getGrammarView(editor); let cppCount = 0; const listItems = atom.workspace.getModalPanels()[0].item.items; for (let i = 0; i < listItems.length; i++) { const grammar = listItems[i]; const name = grammar.name; if (cppCount === 0 && name === 'C++') { expect(grammar.constructor.name).toBe('TreeSitterGrammar'); // first C++ entry should be Tree-sitter cppCount++; } else if (cppCount === 1) { expect(name).toBe('C++'); expect(grammar.constructor.name).toBe('Grammar'); // immediate next grammar should be the TextMate version cppCount++; } else { expect(name).not.toBe('C++'); // there should not be any other C++ grammars } } expect(cppCount).toBe(2); // ensure we actually saw both grammars }); }); describe('for every Tree-sitter grammar', () => { it('adds a label to identify it as Tree-sitter', async () => { const grammarView = await getGrammarView(editor); const elements = grammarView.element.querySelectorAll('li'); const listItems = atom.workspace.getModalPanels()[0].item.items; for (let i = 0; i < listItems.length; i++) { if (listItems[i].constructor.name === 'TreeSitterGrammar') { expect( elements[i].childNodes[1].childNodes[0].className.startsWith( 'grammar-selector-parser' ) ).toBe(true); } } }); }); describe('when clicked', () => it('shows the grammar selector modal', () => { const eventHandler = jasmine.createSpy('eventHandler'); atom.commands.add( editor.getElement(), 'grammar-selector:show', eventHandler ); grammarStatus.click(); expect(eventHandler).toHaveBeenCalled(); })); describe('when the package is deactivated', () => it('removes the view', () => { spyOn(grammarTile, 'destroy'); atom.packages.deactivatePackage('grammar-selector'); expect(grammarTile.destroy).toHaveBeenCalled(); })); }); }); function getTooltipText(element) { const [tooltip] = atom.tooltips.findTooltips(element); return tooltip.getTitle(); } async function getGrammarView(editor) { atom.commands.dispatch(editor.getElement(), 'grammar-selector:show'); await SelectListView.getScheduler().getNextUpdatePromise(); return atom.workspace.getModalPanels()[0].getItem(); } ================================================ FILE: packages/grammar-selector/styles/grammar-selector.less ================================================ @import "ui-variables"; .grammar-status a, .grammar-status a:hover { color: @text-color; } .grammar-selector-parser { margin-right: @component-padding; } ================================================ FILE: packages/incompatible-packages/.gitignore ================================================ .DS_Store npm-debug.log node_modules ================================================ FILE: packages/incompatible-packages/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/incompatible-packages/README.md ================================================ # Incompatible Packages package Displays a list of installed Atom packages that have native module dependencies that are not compatible with the current version of Atom. ![](https://cloud.githubusercontent.com/assets/671378/3767534/3f099820-18ce-11e4-9fa0-feef7947aab2.png) ================================================ FILE: packages/incompatible-packages/lib/incompatible-packages-component.js ================================================ /** @babel */ /** @jsx etch.dom */ import etch from 'etch'; import VIEW_URI from './view-uri'; const REBUILDING = 'rebuilding'; const REBUILD_FAILED = 'rebuild-failed'; const REBUILD_SUCCEEDED = 'rebuild-succeeded'; export default class IncompatiblePackagesComponent { constructor(packageManager) { this.rebuildStatuses = new Map(); this.rebuildFailureOutputs = new Map(); this.rebuildInProgress = false; this.rebuiltPackageCount = 0; this.packageManager = packageManager; this.loaded = false; etch.initialize(this); if (this.packageManager.getActivePackages().length > 0) { this.populateIncompatiblePackages(); } else { global.setImmediate(this.populateIncompatiblePackages.bind(this)); } this.element.addEventListener('click', event => { if (event.target === this.refs.rebuildButton) { this.rebuildIncompatiblePackages(); } else if (event.target === this.refs.reloadButton) { atom.reload(); } else if (event.target.classList.contains('view-settings')) { atom.workspace.open( `atom://config/packages/${event.target.package.name}` ); } }); } update() {} render() { if (!this.loaded) { return
    Loading...
    ; } return (
    {this.renderHeading()} {this.renderIncompatiblePackageList()}
    ); } renderHeading() { if (this.incompatiblePackages.length > 0) { if (this.rebuiltPackageCount > 0) { let alertClass = this.rebuiltPackageCount === this.incompatiblePackages.length ? 'alert-success icon-check' : 'alert-warning icon-bug'; return (
    {this.rebuiltPackageCount} of {this.incompatiblePackages.length}{' '} packages were rebuilt successfully. Reload Atom to activate them.
    ); } else { return (
    Some installed packages could not be loaded because they contain native modules that were compiled for an earlier version of Atom.
    ); } } else { return (
    None of your packages contain incompatible native modules.
    ); } } renderIncompatiblePackageList() { return (
    {this.incompatiblePackages.map( this.renderIncompatiblePackage.bind(this) )}
    ); } renderIncompatiblePackage(pack) { let rebuildStatus = this.rebuildStatuses.get(pack); return (
    {this.renderRebuildStatusIndicator(rebuildStatus)}

    {pack.name} {pack.metadata.version}

    {rebuildStatus ? this.renderRebuildOutput(pack) : this.renderIncompatibleModules(pack)}
    ); } renderRebuildStatusIndicator(rebuildStatus) { if (rebuildStatus === REBUILDING) { return (
    Rebuilding
    ); } else if (rebuildStatus === REBUILD_SUCCEEDED) { return (
    Rebuild Succeeded
    ); } else if (rebuildStatus === REBUILD_FAILED) { return (
    Rebuild Failed
    ); } else { return ''; } } renderRebuildOutput(pack) { if (this.rebuildStatuses.get(pack) === REBUILD_FAILED) { return
    {this.rebuildFailureOutputs.get(pack)}
    ; } else { return ''; } } renderIncompatibleModules(pack) { return (
      {pack.incompatibleModules.map(nativeModule => (
    • {nativeModule.name}@{nativeModule.version || 'unknown'} –{' '} {nativeModule.error}
    • ))}
    ); } populateIncompatiblePackages() { this.incompatiblePackages = this.packageManager .getLoadedPackages() .filter(pack => !pack.isCompatible()); for (let pack of this.incompatiblePackages) { let buildFailureOutput = pack.getBuildFailureOutput(); if (buildFailureOutput) { this.setPackageStatus(pack, REBUILD_FAILED); this.setRebuildFailureOutput(pack, buildFailureOutput); } } this.loaded = true; etch.update(this); } async rebuildIncompatiblePackages() { this.rebuildInProgress = true; let rebuiltPackageCount = 0; for (let pack of this.incompatiblePackages) { this.setPackageStatus(pack, REBUILDING); let { code, stderr } = await pack.rebuild(); if (code === 0) { this.setPackageStatus(pack, REBUILD_SUCCEEDED); rebuiltPackageCount++; } else { this.setRebuildFailureOutput(pack, stderr); this.setPackageStatus(pack, REBUILD_FAILED); } } this.rebuildInProgress = false; this.rebuiltPackageCount = rebuiltPackageCount; etch.update(this); } setPackageStatus(pack, status) { this.rebuildStatuses.set(pack, status); etch.update(this); } setRebuildFailureOutput(pack, output) { this.rebuildFailureOutputs.set(pack, output); etch.update(this); } getTitle() { return 'Incompatible Packages'; } getURI() { return VIEW_URI; } getIconName() { return 'package'; } serialize() { return { deserializer: 'IncompatiblePackagesComponent' }; } } ================================================ FILE: packages/incompatible-packages/lib/main.js ================================================ /** @babel */ import { Disposable, CompositeDisposable } from 'atom'; import VIEW_URI from './view-uri'; let disposables = null; export function activate() { disposables = new CompositeDisposable(); disposables.add( atom.workspace.addOpener(uri => { if (uri === VIEW_URI) { return deserializeIncompatiblePackagesComponent(); } }) ); disposables.add( atom.commands.add('atom-workspace', { 'incompatible-packages:view': () => { atom.workspace.open(VIEW_URI); } }) ); } export function deactivate() { disposables.dispose(); } export function consumeStatusBar(statusBar) { let incompatibleCount = 0; for (let pack of atom.packages.getLoadedPackages()) { if (!pack.isCompatible()) incompatibleCount++; } if (incompatibleCount > 0) { let icon = createIcon(incompatibleCount); let tile = statusBar.addRightTile({ item: icon, priority: 200 }); icon.element.addEventListener('click', () => { atom.commands.dispatch(icon.element, 'incompatible-packages:view'); }); disposables.add(new Disposable(() => tile.destroy())); } } export function deserializeIncompatiblePackagesComponent() { const IncompatiblePackagesComponent = require('./incompatible-packages-component'); return new IncompatiblePackagesComponent(atom.packages); } function createIcon(count) { const StatusIconComponent = require('./status-icon-component'); return new StatusIconComponent({ count }); } ================================================ FILE: packages/incompatible-packages/lib/status-icon-component.js ================================================ /** @babel */ /** @jsx etch.dom */ import etch from 'etch'; export default class StatusIconComponent { constructor({ count }) { this.count = count; etch.initialize(this); } update() {} render() { return (
    {this.count}
    ); } } ================================================ FILE: packages/incompatible-packages/lib/view-uri.js ================================================ /** @babel */ export default 'atom://incompatible-packages'; ================================================ FILE: packages/incompatible-packages/package.json ================================================ { "name": "incompatible-packages", "main": "./lib/main", "version": "0.27.3", "description": "Show incompatible packages", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.50.0" }, "dependencies": { "etch": "^0.12.2" }, "consumedServices": { "status-bar": { "versions": { "^1.0.0": "consumeStatusBar" } } }, "deserializers": { "IncompatiblePackagesComponent": "deserializeIncompatiblePackagesComponent" } } ================================================ FILE: packages/incompatible-packages/spec/fixtures/incompatible-package/bad.js ================================================ ================================================ FILE: packages/incompatible-packages/spec/fixtures/incompatible-package/package.json ================================================ { "name": "incompatible-package", "version": "1.0.0", "main": "./bad.js" } ================================================ FILE: packages/incompatible-packages/spec/incompatible-packages-component-spec.js ================================================ /** @babel */ import etch from 'etch'; import IncompatiblePackagesComponent from '../lib/incompatible-packages-component'; describe('IncompatiblePackagesComponent', () => { let packages, etchScheduler; beforeEach(() => { etchScheduler = etch.getScheduler(); packages = [ { name: 'incompatible-1', isCompatible() { return false; }, rebuild: function() { return new Promise(resolve => (this.resolveRebuild = resolve)); }, getBuildFailureOutput() { return null; }, path: '/Users/joe/.atom/packages/incompatible-1', metadata: { repository: 'https://github.com/atom/incompatible-1', version: '1.0.0' }, incompatibleModules: [ { name: 'x', version: '1.0.0', error: 'Expected version X, got Y' }, { name: 'y', version: '1.0.0', error: 'Expected version X, got Z' } ] }, { name: 'incompatible-2', isCompatible() { return false; }, rebuild() { return new Promise(resolve => (this.resolveRebuild = resolve)); }, getBuildFailureOutput() { return null; }, path: '/Users/joe/.atom/packages/incompatible-2', metadata: { repository: 'https://github.com/atom/incompatible-2', version: '1.0.0' }, incompatibleModules: [ { name: 'z', version: '1.0.0', error: 'Expected version X, got Y' } ] }, { name: 'compatible', isCompatible() { return true; }, rebuild() { throw new Error('Should not rebuild a compatible package'); }, getBuildFailureOutput() { return null; }, path: '/Users/joe/.atom/packages/b', metadata: { repository: 'https://github.com/atom/b', version: '1.0.0' }, incompatibleModules: [] } ]; }); describe('when packages have not finished loading', () => { it('delays rendering incompatible packages until the end of the tick', () => { waitsForPromise(async () => { let component = new IncompatiblePackagesComponent({ getActivePackages: () => [], getLoadedPackages: () => packages }); let { element } = component; expect( element.querySelectorAll('.incompatible-package').length ).toEqual(0); await etchScheduler.getNextUpdatePromise(); expect( element.querySelectorAll('.incompatible-package').length ).toBeGreaterThan(0); }); }); }); describe('when there are no incompatible packages', () => { it('does not render incompatible packages or the rebuild button', () => { waitsForPromise(async () => { expect(packages[2].isCompatible()).toBe(true); let compatiblePackages = [packages[2]]; let component = new IncompatiblePackagesComponent({ getActivePackages: () => compatiblePackages, getLoadedPackages: () => compatiblePackages }); let { element } = component; await etchScheduler.getNextUpdatePromise(); expect(element.querySelectorAll('.incompatible-package').length).toBe( 0 ); expect(element.querySelector('button')).toBeNull(); }); }); }); describe('when some packages previously failed to rebuild', () => { it('renders them with failed build status and error output', () => { waitsForPromise(async () => { packages[1].getBuildFailureOutput = function() { return 'The build failed'; }; let component = new IncompatiblePackagesComponent({ getActivePackages: () => packages, getLoadedPackages: () => packages }); let { element } = component; await etchScheduler.getNextUpdatePromise(); let packageElement = element.querySelector( '.incompatible-package:nth-child(2)' ); expect(packageElement.querySelector('.badge').textContent).toBe( 'Rebuild Failed' ); expect(packageElement.querySelector('pre').textContent).toBe( 'The build failed' ); }); }); }); describe('when there are incompatible packages', () => { it('renders incompatible packages and the rebuild button', () => { waitsForPromise(async () => { let component = new IncompatiblePackagesComponent({ getActivePackages: () => packages, getLoadedPackages: () => packages }); let { element } = component; await etchScheduler.getNextUpdatePromise(); expect( element.querySelectorAll('.incompatible-package').length ).toEqual(2); expect(element.querySelector('button')).not.toBeNull(); }); }); describe('when the "Rebuild All" button is clicked', () => { it("rebuilds every incompatible package, updating each package's view with status", () => { waitsForPromise(async () => { let component = new IncompatiblePackagesComponent({ getActivePackages: () => packages, getLoadedPackages: () => packages }); let { element } = component; jasmine.attachToDOM(element); await etchScheduler.getNextUpdatePromise(); component.refs.rebuildButton.dispatchEvent( new CustomEvent('click', { bubbles: true }) ); await etchScheduler.getNextUpdatePromise(); // view update expect(component.refs.rebuildButton.disabled).toBe(true); expect(packages[0].resolveRebuild).toBeDefined(); expect( element.querySelector('.incompatible-package:nth-child(1) .badge') .textContent ).toBe('Rebuilding'); expect( element.querySelector('.incompatible-package:nth-child(2) .badge') ).toBeNull(); packages[0].resolveRebuild({ code: 0 }); // simulate rebuild success await etchScheduler.getNextUpdatePromise(); // view update expect(packages[1].resolveRebuild).toBeDefined(); expect( element.querySelector('.incompatible-package:nth-child(1) .badge') .textContent ).toBe('Rebuild Succeeded'); expect( element.querySelector('.incompatible-package:nth-child(2) .badge') .textContent ).toBe('Rebuilding'); packages[1].resolveRebuild({ code: 12, stderr: 'This is an error from the test!' }); // simulate rebuild failure await etchScheduler.getNextUpdatePromise(); // view update expect( element.querySelector('.incompatible-package:nth-child(1) .badge') .textContent ).toBe('Rebuild Succeeded'); expect( element.querySelector('.incompatible-package:nth-child(2) .badge') .textContent ).toBe('Rebuild Failed'); expect( element.querySelector('.incompatible-package:nth-child(2) pre') .textContent ).toBe('This is an error from the test!'); }); }); it('displays a prompt to reload Atom when the packages finish rebuilding', () => { waitsForPromise(async () => { let component = new IncompatiblePackagesComponent({ getActivePackages: () => packages, getLoadedPackages: () => packages }); let { element } = component; jasmine.attachToDOM(element); await etchScheduler.getNextUpdatePromise(); // view update component.refs.rebuildButton.dispatchEvent( new CustomEvent('click', { bubbles: true }) ); expect(packages[0].resolveRebuild({ code: 0 })); await new Promise(global.setImmediate); expect(packages[1].resolveRebuild({ code: 0 })); await etchScheduler.getNextUpdatePromise(); // view update expect(component.refs.reloadButton).toBeDefined(); expect(element.querySelector('.alert').textContent).toMatch(/2 of 2/); spyOn(atom, 'reload'); component.refs.reloadButton.dispatchEvent( new CustomEvent('click', { bubbles: true }) ); expect(atom.reload).toHaveBeenCalled(); }); }); }); describe('when the "Package Settings" button is clicked', () => { it('opens the settings panel for the package', () => { waitsForPromise(async () => { let component = new IncompatiblePackagesComponent({ getActivePackages: () => packages, getLoadedPackages: () => packages }); let { element } = component; jasmine.attachToDOM(element); await etchScheduler.getNextUpdatePromise(); spyOn(atom.workspace, 'open'); element .querySelector('.incompatible-package:nth-child(2) button') .dispatchEvent(new CustomEvent('click', { bubbles: true })); expect(atom.workspace.open).toHaveBeenCalledWith( 'atom://config/packages/incompatible-2' ); }); }); }); }); }); ================================================ FILE: packages/incompatible-packages/spec/incompatible-packages-spec.js ================================================ /** @babel */ import path from 'path'; import IncompatiblePackagesComponent from '../lib/incompatible-packages-component'; import StatusIconComponent from '../lib/status-icon-component'; // This exists only so that CI passes on both Atom 1.6 and Atom 1.8+. function findStatusBar() { if (typeof atom.workspace.getFooterPanels === 'function') { const footerPanels = atom.workspace.getFooterPanels(); if (footerPanels.length > 0) { return footerPanels[0].getItem(); } } return atom.workspace.getBottomPanels()[0].getItem(); } describe('Incompatible packages', () => { let statusBar; beforeEach(() => { atom.views.getView(atom.workspace); waitsForPromise(() => atom.packages.activatePackage('status-bar')); runs(() => { statusBar = findStatusBar(); }); }); describe('when there are packages with incompatible native modules', () => { beforeEach(() => { let incompatiblePackage = atom.packages.loadPackage( path.join(__dirname, 'fixtures', 'incompatible-package') ); spyOn(incompatiblePackage, 'isCompatible').andReturn(false); incompatiblePackage.incompatibleModules = []; waitsForPromise(() => atom.packages.activatePackage('incompatible-packages') ); waits(1); }); it('adds an icon to the status bar', () => { let statusBarIcon = statusBar.getRightTiles()[0].getItem(); expect(statusBarIcon.constructor).toBe(StatusIconComponent); }); describe('clicking the icon', () => { it('displays the incompatible packages view in a pane', () => { let statusBarIcon = statusBar.getRightTiles()[0].getItem(); statusBarIcon.element.dispatchEvent(new MouseEvent('click')); let activePaneItem; waitsFor(() => (activePaneItem = atom.workspace.getActivePaneItem())); runs(() => { expect(activePaneItem.constructor).toBe( IncompatiblePackagesComponent ); }); }); }); }); describe('when there are no packages with incompatible native modules', () => { beforeEach(() => { waitsForPromise(() => atom.packages.activatePackage('incompatible-packages') ); }); it('does not add an icon to the status bar', () => { let statusBarItemClasses = statusBar .getRightTiles() .map(tile => tile.getItem().className); expect(statusBarItemClasses).not.toContain('incompatible-packages'); }); }); }); ================================================ FILE: packages/incompatible-packages/styles/incompatible-packages.less ================================================ @import "ui-variables"; .incompatible-packages { background-color: @pane-item-background-color; overflow-y: scroll; .incompatible-package { padding: 15px; margin-bottom: 10px; border-radius: 6px; border: 1px solid @base-border-color; background-color: lighten(@tool-panel-background-color, 8%); overflow: hidden; .badge { margin-left: 1em; } .heading { margin-top: 0px; } ul { padding-left: 1em; } li { list-style-type: none; } pre { margin-top: 2em; max-height: 25em; overflow: scroll; color: @text-color-error; } } } .incompatible-packages-status { padding-left: 2px; } ================================================ FILE: packages/language-rust-bundled/README.md ================================================ # language-rust-bundled This package provides Rust syntax highlighting in Atom based on syntax trees provided by [tree-sitter-rust](https://github.com/tree-sitter/tree-sitter-rust). ================================================ FILE: packages/language-rust-bundled/grammars/tree-sitter-rust.cson ================================================ name: 'Rust' scopeName: 'source.rust' type: 'tree-sitter' parser: 'tree-sitter-rust' injectionRegex: 'rust' fileTypes: [ 'rs' ] comments: start: '// ' folds: [ { type: 'block_comment' } { start: {index: 0, type: '{'} end: {index: -1, type: '}'} } { start: {index: 0, type: '['} end: {index: -1, type: ']'} } { start: {index: 0, type: '('} end: {index: -1, type: ')'} } { start: {index: 0, type: '<'} end: {index: -1, type: '>'} } ] scopes: 'type_identifier': 'support.type' 'primitive_type': 'support.type' 'field_identifier': 'variable.other.member' 'line_comment': 'comment.block' 'block_comment': 'comment.block' 'identifier': [ {match: '^[A-Z\\d_]+$', scopes: 'constant.other'} ] ''' identifier, call_expression > identifier, call_expression > field_expression > field_identifier, call_expression > scoped_identifier > identifier:nth-child(2) ''': [ {match: '^[A-Z]', scopes: 'entity.name.class'} ] ''' macro_invocation > identifier, macro_invocation > "!", macro_definition > identifier, call_expression > identifier, call_expression > field_expression > field_identifier, call_expression > scoped_identifier > identifier:nth-child(2), generic_function > identifier, generic_function > field_expression > field_identifier, generic_function > scoped_identifier > identifier, function_item > identifier, function_signature_item > identifier, ''': 'entity.name.function' ''' use_list > self, scoped_use_list > self, scoped_identifier > self, crate, super ''': 'keyword.control' 'self': 'variable.self' ''' use_wildcard > identifier:nth-child(0), use_wildcard > scoped_identifier > identifier:nth-child(2), scoped_type_identifier > identifier:nth-child(0), scoped_type_identifier > scoped_identifier:nth-child(0) > identifier, scoped_identifier > identifier:nth-child(0), scoped_identifier > scoped_identifier:nth-child(0) > identifier, use_declaration > identifier, use_declaration > scoped_identifier > identifier, use_list > identifier, use_list > scoped_identifier > identifier, meta_item > identifier ''': [ {match: '^[A-Z]', scopes: 'support.type'} ] 'lifetime > identifier': 'constant.variable' '"let"': 'storage.modifier' '"const"': 'storage.modifier' '"static"': 'storage.modifier' '"extern"': 'storage.modifier' '"fn"': 'storage.modifier' '"type"': 'storage.modifier' '"impl"': 'storage.modifier' '"dyn"': 'storage.modifier' '"trait"': 'storage.modifier' '"mod"': 'storage.modifier' '"pub"': 'storage.modifier' '"crate"': 'storage.modifier' '"default"': 'storage.modifier' '"struct"': 'storage.modifier' '"enum"': 'storage.modifier' '"union"': 'storage.modifier' 'mutable_specifier': 'storage.modifier' '"unsafe"': 'keyword.control' '"use"': 'keyword.control' '"match"': 'keyword.control' '"if"': 'keyword.control' '"in"': 'keyword.control' '"else"': 'keyword.control' '"move"': 'keyword.control' '"while"': 'keyword.control' '"loop"': 'keyword.control' '"for"': 'keyword.control' '"let"': 'keyword.control' '"return"': 'keyword.control' '"continue"': 'keyword.control' '"break"': 'keyword.control' '"where"': 'keyword.control' '"ref"': 'keyword.control' '"macro_rules!"': 'keyword.control' '"async"': 'keyword.control' '"await"': 'keyword.control' '"as"': 'keyword.operator' 'char_literal': 'string.quoted.single' 'string_literal': 'string.quoted.double' 'raw_string_literal': 'string.quoted.other' 'boolean_literal': 'constant.language.boolean' 'integer_literal': 'constant.numeric.decimal' 'float_literal': 'constant.numeric.decimal' 'escape_sequence': 'constant.character.escape' 'attribute_item, inner_attribute_item': 'entity.other.attribute-name' ''' "as", "*", "&", ''': 'keyword.operator' ================================================ FILE: packages/language-rust-bundled/lib/main.js ================================================ exports.activate = function() { for (const nodeType of ['macro_invocation', 'macro_rule']) { atom.grammars.addInjectionPoint('source.rust', { type: nodeType, language() { return 'rust'; }, content(node) { return node.lastChild; }, includeChildren: true }); } }; ================================================ FILE: packages/language-rust-bundled/package.json ================================================ { "name": "language-rust-bundled", "version": "0.1.0", "description": "Rust support for Atom", "keywords": [ "language", "grammar", "rust" ], "main": "lib/main.js", "repository": "https://github.com/atom/atom", "license": "MIT", "dependencies": { "tree-sitter-rust": "^0.17.0" }, "engines": { "atom": ">=1.0.0 <2.0.0" } } ================================================ FILE: packages/language-rust-bundled/settings/rust.cson ================================================ '.source.rust': 'editor': 'commentStart': '// ' 'increaseIndentPattern': '(?x) ^ .* \\{ [^}"\']* $ |^ .* \\( [^\\)"\']* $ |^ \\s* \\{ \\} $ ' 'decreaseIndentPattern': '(?x) ^ \\s* (\\s* /[*] .* [*]/ \\s*)* \\} |^ \\s* (\\s* /[*] .* [*]/ \\s*)* \\) ' 'tabLength': 4 'softTabs': true 'preferredLineLength': 99 'bracket-matcher': autocompleteCharacters: [ '()' '[]' '{}' '<>' '""' '``' ] ================================================ FILE: packages/line-ending-selector/.gitattributes ================================================ # Do not autoconvert line endings for these test files, they are not supposed # to be native to your platform. spec/fixtures/*.md -text ================================================ FILE: packages/line-ending-selector/.gitignore ================================================ node_modules ================================================ FILE: packages/line-ending-selector/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/line-ending-selector/README.md ================================================ # Line Ending Selector package ![status bar tile](https://cloud.githubusercontent.com/assets/1305617/9274149/6b317568-4293-11e5-83ba-614a6c0d9890.png) This is an [Atom](https://atom.io) package that displays the current line ending type of a file: `CRLF` (Windows), `LF` (Unix), or `Mixed` (both). It also lets you change the line ending of a file. ## To Use When the package is activated it will show the current line ending of the file in the right side of the status-bar. If a new file is created the line ending will start with the system default: `CRLF` for Windows, `LF` for Mac and Linux, and `CR` for old-style Mac files. If a file contains multiple line-ending types it will display `Mixed`. ### Changing a File's Line Ending You can click the line ending in the status-bar to open a modal with the line ending options. Selecting a different line ending will change each line of the file in the active editor. ![modal](https://cloud.githubusercontent.com/assets/1305617/9273907/2be5c136-4291-11e5-94af-65ece408eb12.png) **Line Endings** - `LF` is "\n" - `CRLF` is "\r\n" **Note:** Because the `CR` line ending style is not used in any modern operating system, this package only supports converting *from* `CR` line endings not to it. ### Atom Commands You can also change a file's line endings by using or cmd-shift-P searching for these commands: ```text line-ending-selector:convert-to-LF line-ending-selector:convert-to-CRLF ``` ================================================ FILE: packages/line-ending-selector/lib/helpers.js ================================================ 'use babel'; export default { getProcessPlatform() { return process.platform; } }; ================================================ FILE: packages/line-ending-selector/lib/main.js ================================================ 'use babel'; import _ from 'underscore-plus'; import { CompositeDisposable, Disposable } from 'atom'; import { Selector } from './selector'; import StatusBarItem from './status-bar-item'; import helpers from './helpers'; const LineEndingRegExp = /\r\n|\n/g; // the following regular expression is executed natively via the `substring` package, // where `\A` corresponds to the beginning of the string. // More info: https://github.com/atom/line-ending-selector/pull/56 // eslint-disable-next-line no-useless-escape const LFRegExp = /(\A|[^\r])\n/g; const CRLFRegExp = /\r\n/g; let disposables = null; export function activate() { disposables = new CompositeDisposable(); let selectorDisposable; let selector; disposables.add( atom.commands.add('atom-text-editor', { 'line-ending-selector:show': () => { // Initiating Selector object - called only once when `line-ending-selector:show` is called if (!selectorDisposable) { // make a Selector object selector = new Selector([ { name: 'LF', value: '\n' }, { name: 'CRLF', value: '\r\n' } ]); // Add disposable for selector selectorDisposable = new Disposable(() => selector.dispose()); disposables.add(selectorDisposable); } selector.show(); }, 'line-ending-selector:convert-to-LF': event => { const editorElement = event.target.closest('atom-text-editor'); setLineEnding(editorElement.getModel(), '\n'); }, 'line-ending-selector:convert-to-CRLF': event => { const editorElement = event.target.closest('atom-text-editor'); setLineEnding(editorElement.getModel(), '\r\n'); } }) ); } export function deactivate() { disposables.dispose(); } export function consumeStatusBar(statusBar) { let statusBarItem = new StatusBarItem(); let currentBufferDisposable = null; let tooltipDisposable = null; const updateTile = _.debounce(buffer => { getLineEndings(buffer).then(lineEndings => { if (lineEndings.size === 0) { let defaultLineEnding = getDefaultLineEnding(); buffer.setPreferredLineEnding(defaultLineEnding); lineEndings = new Set().add(defaultLineEnding); } statusBarItem.setLineEndings(lineEndings); }); }, 0); disposables.add( atom.workspace.observeActiveTextEditor(editor => { if (currentBufferDisposable) currentBufferDisposable.dispose(); if (editor && editor.getBuffer) { let buffer = editor.getBuffer(); updateTile(buffer); currentBufferDisposable = buffer.onDidChange(({ oldText, newText }) => { if (!statusBarItem.hasLineEnding('\n')) { if (newText.indexOf('\n') >= 0) { updateTile(buffer); } } else if (!statusBarItem.hasLineEnding('\r\n')) { if (newText.indexOf('\r\n') >= 0) { updateTile(buffer); } } else if (oldText.indexOf('\n')) { updateTile(buffer); } }); } else { statusBarItem.setLineEndings(new Set()); currentBufferDisposable = null; } if (tooltipDisposable) { disposables.remove(tooltipDisposable); tooltipDisposable.dispose(); } tooltipDisposable = atom.tooltips.add(statusBarItem.element, { title() { return `File uses ${statusBarItem.description()} line endings`; } }); disposables.add(tooltipDisposable); }) ); disposables.add( new Disposable(() => { if (currentBufferDisposable) currentBufferDisposable.dispose(); }) ); statusBarItem.onClick(() => { const editor = atom.workspace.getActiveTextEditor(); atom.commands.dispatch( atom.views.getView(editor), 'line-ending-selector:show' ); }); let tile = statusBar.addRightTile({ item: statusBarItem, priority: 200 }); disposables.add(new Disposable(() => tile.destroy())); } function getDefaultLineEnding() { switch (atom.config.get('line-ending-selector.defaultLineEnding')) { case 'LF': return '\n'; case 'CRLF': return '\r\n'; case 'OS Default': default: return helpers.getProcessPlatform() === 'win32' ? '\r\n' : '\n'; } } function getLineEndings(buffer) { if (typeof buffer.find === 'function') { return Promise.all([buffer.find(LFRegExp), buffer.find(CRLFRegExp)]).then( ([hasLF, hasCRLF]) => { const result = new Set(); if (hasLF) result.add('\n'); if (hasCRLF) result.add('\r\n'); return result; } ); } else { return new Promise(resolve => { const result = new Set(); for (let i = 0; i < buffer.getLineCount() - 1; i++) { result.add(buffer.lineEndingForRow(i)); } resolve(result); }); } } export function setLineEnding(item, lineEnding) { if (item && item.getBuffer) { let buffer = item.getBuffer(); buffer.setPreferredLineEnding(lineEnding); buffer.setText(buffer.getText().replace(LineEndingRegExp, lineEnding)); } } ================================================ FILE: packages/line-ending-selector/lib/selector.js ================================================ 'use babel'; import SelectListView from 'atom-select-list'; import { TextEditor } from 'atom'; import { setLineEnding } from './main'; export class Selector { lineEndingListView; modalPanel; previousActivePane; // Make a selector object (should be called once) constructor(selectorItems) { // Defining a SelectListView with methods - https://github.com/atom/atom-select-list this.lineEndingListView = new SelectListView({ // an array containing the objects you want to show in the select list items: selectorItems, // called whenever an item needs to be displayed. elementForItem: lineEnding => { const element = document.createElement('li'); element.textContent = lineEnding.name; return element; }, // called to retrieve a string property on each item and that will be used to filter them. filterKeyForItem: lineEnding => { return lineEnding.name; }, // called when the user clicks or presses Enter on an item. // use `=>` for `this` didConfirmSelection: lineEnding => { const editor = atom.workspace.getActiveTextEditor(); if (editor instanceof TextEditor) { setLineEnding(editor, lineEnding.value); } this.hide(); }, // called when the user presses Esc or the list loses focus. // use `=>` for `this` didCancelSelection: () => { this.hide(); } }); // Adding SelectListView to panel this.modalPanel = atom.workspace.addModalPanel({ item: this.lineEndingListView }); } // Show a selector object show() { this.previousActivePane = atom.workspace.getActivePane(); // Show selector this.lineEndingListView.reset(); this.modalPanel.show(); this.lineEndingListView.focus(); } // Hide a selector hide() { // hide modal panel this.modalPanel.hide(); // focus on the previous active pane this.previousActivePane.activate(); } // Dispose selector dispose() { this.lineEndingListView.destroy(); this.modalPanel.destroy(); this.modalPanel = null; } } ================================================ FILE: packages/line-ending-selector/lib/status-bar-item.js ================================================ const { Emitter } = require('atom'); module.exports = class StatusBarItem { constructor() { this.element = document.createElement('a'); this.element.className = 'line-ending-tile inline-block'; this.emitter = new Emitter(); this.setLineEndings(new Set()); } setLineEndings(lineEndings) { this.lineEndings = lineEndings; this.element.textContent = lineEndingName(lineEndings); this.emitter.emit('did-change'); } onDidChange(callback) { return this.emitter.on('did-change', callback); } hasLineEnding(lineEnding) { return this.lineEndings.has(lineEnding); } description() { return lineEndingDescription(this.lineEndings); } onClick(callback) { this.element.addEventListener('click', callback); } }; function lineEndingName(lineEndings) { if (lineEndings.size > 1) { return 'Mixed'; } else if (lineEndings.has('\n')) { return 'LF'; } else if (lineEndings.has('\r\n')) { return 'CRLF'; } else { return ''; } } function lineEndingDescription(lineEndings) { switch (lineEndingName(lineEndings)) { case 'Mixed': return 'mixed'; case 'LF': return 'LF (Unix)'; case 'CRLF': return 'CRLF (Windows)'; default: return 'unknown'; } } ================================================ FILE: packages/line-ending-selector/package.json ================================================ { "name": "line-ending-selector", "version": "0.7.7", "main": "./lib/main", "description": "Select the line ending to use for the current editor.", "license": "MIT", "repository": "https://github.com/atom/atom", "engines": { "atom": "^1.0.0" }, "dependencies": { "atom-select-list": "^0.7.0", "underscore-plus": "^1.7.0" }, "consumedServices": { "status-bar": { "versions": { "^1.0.0": "consumeStatusBar" } } }, "devDependencies": { "standard": "^5.1.0" }, "configSchema": { "defaultLineEnding": { "title": "Default line ending", "description": "Line ending to use for new files", "type": "string", "default": "OS Default", "enum": [ "OS Default", "LF", "CRLF" ] } }, "standard": { "globals": [ "advanceClock", "atom", "beforeEach", "expect", "describe", "it", "jasmine", "MouseEvent", "runs", "spyOn", "waits", "waitsFor", "waitsForPromise" ] } } ================================================ FILE: packages/line-ending-selector/spec/fixtures/mixed-endings.md ================================================ Hello Goodbye Mixed ================================================ FILE: packages/line-ending-selector/spec/fixtures/unix-endings.md ================================================ Hello Goodbye Unix ================================================ FILE: packages/line-ending-selector/spec/fixtures/windows-endings.md ================================================ Hello Goodbye Windows ================================================ FILE: packages/line-ending-selector/spec/line-ending-selector-spec.js ================================================ const helpers = require('../lib/helpers'); const { TextEditor } = require('atom'); describe('line ending selector', () => { let lineEndingTile; beforeEach(() => { jasmine.useRealClock(); waitsForPromise(() => { return atom.packages.activatePackage('status-bar'); }); waitsForPromise(() => { return atom.packages.activatePackage('line-ending-selector'); }); waits(1); runs(() => { const statusBar = atom.workspace.getFooterPanels()[0].getItem(); lineEndingTile = statusBar.getRightTiles()[0].getItem(); expect(lineEndingTile.element.className).toMatch(/line-ending-tile/); expect(lineEndingTile.element.textContent).toBe(''); }); }); describe('Commands', () => { let editor, editorElement; beforeEach(() => { waitsForPromise(() => { return atom.workspace.open('mixed-endings.md').then(e => { editor = e; editorElement = atom.views.getView(editor); jasmine.attachToDOM(editorElement); }); }); }); describe('When "line-ending-selector:convert-to-LF" is run', () => { it('converts the file to LF line endings', () => { editorElement.focus(); atom.commands.dispatch( document.activeElement, 'line-ending-selector:convert-to-LF' ); expect(editor.getText()).toBe('Hello\nGoodbye\nMixed\n'); }); }); describe('When "line-ending-selector:convert-to-LF" is run', () => { it('converts the file to CRLF line endings', () => { editorElement.focus(); atom.commands.dispatch( document.activeElement, 'line-ending-selector:convert-to-CRLF' ); expect(editor.getText()).toBe('Hello\r\nGoodbye\r\nMixed\r\n'); }); }); }); describe('Status bar tile', () => { describe('when an empty file is opened', () => { it('uses the default line endings for the platform', () => { waitsFor(done => { spyOn(helpers, 'getProcessPlatform').andReturn('win32'); atom.workspace.open('').then(editor => { const subscription = lineEndingTile.onDidChange(() => { subscription.dispose(); expect(lineEndingTile.element.textContent).toBe('CRLF'); expect(editor.getBuffer().getPreferredLineEnding()).toBe('\r\n'); expect(getTooltipText(lineEndingTile.element)).toBe( 'File uses CRLF (Windows) line endings' ); done(); }); }); }); waitsFor(done => { helpers.getProcessPlatform.andReturn('darwin'); atom.workspace.open('').then(editor => { const subscription = lineEndingTile.onDidChange(() => { subscription.dispose(); expect(lineEndingTile.element.textContent).toBe('LF'); expect(editor.getBuffer().getPreferredLineEnding()).toBe('\n'); expect(getTooltipText(lineEndingTile.element)).toBe( 'File uses LF (Unix) line endings' ); done(); }); }); }); }); describe('when the "defaultLineEnding" setting is set to "LF"', () => { beforeEach(() => { atom.config.set('line-ending-selector.defaultLineEnding', 'LF'); }); it('uses LF line endings, regardless of the platform', () => { waitsFor(done => { spyOn(helpers, 'getProcessPlatform').andReturn('win32'); atom.workspace.open('').then(editor => { lineEndingTile.onDidChange(() => { expect(lineEndingTile.element.textContent).toBe('LF'); expect(editor.getBuffer().getPreferredLineEnding()).toBe('\n'); done(); }); }); }); }); }); describe('when the "defaultLineEnding" setting is set to "CRLF"', () => { beforeEach(() => { atom.config.set('line-ending-selector.defaultLineEnding', 'CRLF'); }); it('uses CRLF line endings, regardless of the platform', () => { waitsFor(done => { atom.workspace.open('').then(editor => { lineEndingTile.onDidChange(() => { expect(lineEndingTile.element.textContent).toBe('CRLF'); expect(editor.getBuffer().getPreferredLineEnding()).toBe( '\r\n' ); done(); }); }); }); }); }); }); describe('when a file is opened that contains only CRLF line endings', () => { it('displays "CRLF" as the line ending', () => { waitsFor(done => { atom.workspace.open('windows-endings.md').then(() => { lineEndingTile.onDidChange(() => { expect(lineEndingTile.element.textContent).toBe('CRLF'); done(); }); }); }); }); }); describe('when a file is opened that contains only LF line endings', () => { it('displays "LF" as the line ending', () => { waitsFor(done => { atom.workspace.open('unix-endings.md').then(editor => { lineEndingTile.onDidChange(() => { expect(lineEndingTile.element.textContent).toBe('LF'); expect(editor.getBuffer().getPreferredLineEnding()).toBe(null); done(); }); }); }); }); }); describe('when a file is opened that contains mixed line endings', () => { it('displays "Mixed" as the line ending', () => { waitsFor(done => { atom.workspace.open('mixed-endings.md').then(() => { lineEndingTile.onDidChange(() => { expect(lineEndingTile.element.textContent).toBe('Mixed'); done(); }); }); }); }); }); describe('clicking the tile', () => { let lineEndingModal, lineEndingSelector; beforeEach(() => { jasmine.attachToDOM(atom.views.getView(atom.workspace)); waitsFor(done => atom.workspace .open('unix-endings.md') .then(() => lineEndingTile.onDidChange(done)) ); }); describe('when the text editor has focus', () => { it('opens the line ending selector modal for the text editor', () => { atom.workspace.getCenter().activate(); const item = atom.workspace.getActivePaneItem(); expect(item.getFileName && item.getFileName()).toBe( 'unix-endings.md' ); lineEndingTile.element.dispatchEvent(new MouseEvent('click', {})); lineEndingModal = atom.workspace.getModalPanels()[0]; lineEndingSelector = lineEndingModal.getItem(); expect(lineEndingModal.isVisible()).toBe(true); expect( lineEndingSelector.element.contains(document.activeElement) ).toBe(true); let listItems = lineEndingSelector.element.querySelectorAll('li'); expect(listItems[0].textContent).toBe('LF'); expect(listItems[1].textContent).toBe('CRLF'); }); }); describe('when the text editor does not have focus', () => { it('opens the line ending selector modal for the active text editor', () => { atom.workspace.getLeftDock().activate(); const item = atom.workspace.getActivePaneItem(); expect(item instanceof TextEditor).toBe(false); lineEndingTile.element.dispatchEvent(new MouseEvent('click', {})); lineEndingModal = atom.workspace.getModalPanels()[0]; lineEndingSelector = lineEndingModal.getItem(); expect(lineEndingModal.isVisible()).toBe(true); expect( lineEndingSelector.element.contains(document.activeElement) ).toBe(true); let listItems = lineEndingSelector.element.querySelectorAll('li'); expect(listItems[0].textContent).toBe('LF'); expect(listItems[1].textContent).toBe('CRLF'); }); }); describe('when selecting a different line ending for the file', () => { it('changes the line endings in the buffer', () => { lineEndingTile.element.dispatchEvent(new MouseEvent('click', {})); lineEndingModal = atom.workspace.getModalPanels()[0]; lineEndingSelector = lineEndingModal.getItem(); const lineEndingChangedPromise = new Promise(resolve => { lineEndingTile.onDidChange(() => { expect(lineEndingTile.element.textContent).toBe('CRLF'); const editor = atom.workspace.getActiveTextEditor(); expect(editor.getText()).toBe('Hello\r\nGoodbye\r\nUnix\r\n'); expect(editor.getBuffer().getPreferredLineEnding()).toBe('\r\n'); resolve(); }); }); lineEndingSelector.refs.queryEditor.setText('CR'); lineEndingSelector.confirmSelection(); expect(lineEndingModal.isVisible()).toBe(false); waitsForPromise(() => lineEndingChangedPromise); }); }); describe('when modal is exited', () => { it('leaves the tile selection as-is', () => { lineEndingTile.element.dispatchEvent(new MouseEvent('click', {})); lineEndingModal = atom.workspace.getModalPanels()[0]; lineEndingSelector = lineEndingModal.getItem(); lineEndingSelector.cancelSelection(); expect(lineEndingTile.element.textContent).toBe('LF'); }); }); }); describe('closing the last text editor', () => { it('displays no line ending in the status bar', () => { waitsForPromise(() => { return atom.workspace.open('unix-endings.md').then(() => { atom.workspace.getActivePane().destroy(); expect(lineEndingTile.element.textContent).toBe(''); }); }); }); }); describe("when the buffer's line endings change", () => { let editor; beforeEach(() => { waitsFor(done => { atom.workspace.open('unix-endings.md').then(e => { editor = e; lineEndingTile.onDidChange(done); }); }); }); it('updates the line ending text in the tile', () => { let tileText = lineEndingTile.element.textContent; let tileUpdateCount = 0; Object.defineProperty(lineEndingTile.element, 'textContent', { get() { return tileText; }, set(text) { tileUpdateCount++; tileText = text; } }); expect(lineEndingTile.element.textContent).toBe('LF'); expect(getTooltipText(lineEndingTile.element)).toBe( 'File uses LF (Unix) line endings' ); waitsFor(done => { editor.setTextInBufferRange([[0, 0], [0, 0]], '... '); editor.setTextInBufferRange([[0, Infinity], [1, 0]], '\r\n', { normalizeLineEndings: false }); lineEndingTile.onDidChange(done); }); runs(() => { expect(tileUpdateCount).toBe(1); expect(lineEndingTile.element.textContent).toBe('Mixed'); expect(getTooltipText(lineEndingTile.element)).toBe( 'File uses mixed line endings' ); }); waitsFor(done => { atom.commands.dispatch( editor.getElement(), 'line-ending-selector:convert-to-CRLF' ); lineEndingTile.onDidChange(done); }); runs(() => { expect(tileUpdateCount).toBe(2); expect(lineEndingTile.element.textContent).toBe('CRLF'); expect(getTooltipText(lineEndingTile.element)).toBe( 'File uses CRLF (Windows) line endings' ); }); waitsFor(done => { atom.commands.dispatch( editor.getElement(), 'line-ending-selector:convert-to-LF' ); lineEndingTile.onDidChange(done); }); runs(() => { expect(tileUpdateCount).toBe(3); expect(lineEndingTile.element.textContent).toBe('LF'); }); runs(() => { editor.setTextInBufferRange([[0, 0], [0, 0]], '\n'); }); waits(100); runs(() => { expect(tileUpdateCount).toBe(3); }); }); }); }); }); function getTooltipText(element) { const [tooltip] = atom.tooltips.findTooltips(element); return tooltip.getTitle(); } ================================================ FILE: packages/link/.gitignore ================================================ node_modules npm-debug.log ================================================ FILE: packages/link/.npmignore ================================================ npm-debug.log ================================================ FILE: packages/link/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/link/README.md ================================================ # Link package Opens http(s) links under the cursor. ### Commands and Keybindings |Command|Selector|Description|Keybinding (Linux)|Keybinding (macOS)|Keybinding (Windows)| |-------|--------|-----------|------------------|------------------|--------------------| |`link:open`|`atom-text-editor`|Opens the http(s) link under the cursor||ctrl-shift-o|| Custom keybindings can be added by referencing the above commands. To learn more, visit the [Using Atom: Basic Customization](http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings) or [Behind Atom: Keymaps In-Depth](http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth) sections of the Atom Flight Manual. ================================================ FILE: packages/link/keymaps/links.cson ================================================ '.platform-darwin atom-text-editor': 'ctrl-shift-o': 'link:open' ================================================ FILE: packages/link/lib/link.js ================================================ const url = require('url'); const { shell } = require('electron'); const _ = require('underscore-plus'); const LINK_SCOPE_REGEX = /markup\.underline\.link/; module.exports = { activate() { this.commandDisposable = atom.commands.add( 'atom-text-editor', 'link:open', () => this.openLink() ); }, deactivate() { this.commandDisposable.dispose(); }, openLink() { const editor = atom.workspace.getActiveTextEditor(); if (editor == null) return; let link = this.linkUnderCursor(editor); if (link == null) return; if (editor.getGrammar().scopeName === 'source.gfm') { link = this.linkForName(editor, link); } const { protocol } = url.parse(link); if (protocol === 'http:' || protocol === 'https:' || protocol === 'atom:') { shell.openExternal(link); } }, // Get the link under the cursor in the editor // // Returns a {String} link or undefined if no link found. linkUnderCursor(editor) { const cursorPosition = editor.getCursorBufferPosition(); const link = this.linkAtPosition(editor, cursorPosition); if (link != null) return link; // Look for a link to the left of the cursor if (cursorPosition.column > 0) { return this.linkAtPosition(editor, cursorPosition.translate([0, -1])); } }, // Get the link at the buffer position in the editor. // // Returns a {String} link or undefined if no link found. linkAtPosition(editor, bufferPosition) { const token = editor.tokenForBufferPosition(bufferPosition); if ( token && token.value && token.scopes.some(scope => LINK_SCOPE_REGEX.test(scope)) ) { return token.value; } }, // Get the link for the given name. // // This is for Markdown links of the style: // // ``` // [label][name] // // [name]: https://github.com // ``` // // Returns a {String} link linkForName(editor, linkName) { let link = linkName; const regex = new RegExp( `^\\s*\\[${_.escapeRegExp(linkName)}\\]\\s*:\\s*(.+)$`, 'g' ); editor.backwardsScanInBufferRange( regex, [[0, 0], [Infinity, Infinity]], ({ match, stop }) => { link = match[1]; stop(); } ); return link; } }; ================================================ FILE: packages/link/menus/link.cson ================================================ 'context-menu': 'atom-text-editor .syntax--markup.syntax--underline.syntax--link': [ {label: 'Open link', command: 'link:open'} ] ================================================ FILE: packages/link/package.json ================================================ { "name": "link", "version": "0.31.6", "main": "./lib/link", "description": "Opens http(s) links under the cursor", "license": "MIT", "repository": "https://github.com/atom/atom", "engines": { "atom": "*" }, "activationCommands": { "atom-workspace": [ "link:open" ] }, "dependencies": { "underscore-plus": "^1.7.0" }, "devDependencies": { "standard": "^10.0.3" }, "standard": { "env": { "atomtest": true, "browser": true, "jasmine": true, "node": true }, "globals": [ "atom" ] } } ================================================ FILE: packages/link/spec/link-spec.js ================================================ const { shell } = require('electron'); describe('link package', () => { beforeEach(async () => { await atom.packages.activatePackage('language-gfm'); await atom.packages.activatePackage('language-hyperlink'); const activationPromise = atom.packages.activatePackage('link'); atom.commands.dispatch(atom.views.getView(atom.workspace), 'link:open'); await activationPromise; }); describe('when the cursor is on a link', () => { it("opens the link using the 'open' command", async () => { await atom.workspace.open('sample.md'); const editor = atom.workspace.getActiveTextEditor(); editor.setText('// "http://github.com"'); spyOn(shell, 'openExternal'); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).not.toHaveBeenCalled(); editor.setCursorBufferPosition([0, 4]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).toHaveBeenCalled(); expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com'); shell.openExternal.reset(); editor.setCursorBufferPosition([0, 8]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).toHaveBeenCalled(); expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com'); shell.openExternal.reset(); editor.setCursorBufferPosition([0, 21]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).toHaveBeenCalled(); expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com'); }); // only works in Atom >= 1.33.0 // https://github.com/atom/link/pull/33#issuecomment-419643655 const atomVersion = atom.getVersion().split('.'); console.error('atomVersion', atomVersion); if (+atomVersion[0] > 1 || +atomVersion[1] >= 33) { it("opens an 'atom:' link", async () => { await atom.workspace.open('sample.md'); const editor = atom.workspace.getActiveTextEditor(); editor.setText( '// "atom://core/open/file?filename=sample.js&line=1&column=2"' ); spyOn(shell, 'openExternal'); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).not.toHaveBeenCalled(); editor.setCursorBufferPosition([0, 4]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).toHaveBeenCalled(); expect(shell.openExternal.argsForCall[0][0]).toBe( 'atom://core/open/file?filename=sample.js&line=1&column=2' ); shell.openExternal.reset(); editor.setCursorBufferPosition([0, 8]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).toHaveBeenCalled(); expect(shell.openExternal.argsForCall[0][0]).toBe( 'atom://core/open/file?filename=sample.js&line=1&column=2' ); shell.openExternal.reset(); editor.setCursorBufferPosition([0, 60]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).toHaveBeenCalled(); expect(shell.openExternal.argsForCall[0][0]).toBe( 'atom://core/open/file?filename=sample.js&line=1&column=2' ); }); } describe('when the cursor is on a [name][url-name] style markdown link', () => it('opens the named url', async () => { await atom.workspace.open('README.md'); const editor = atom.workspace.getActiveTextEditor(); editor.setText(`\ you should [click][here] you should not [click][her] [here]: http://github.com\ `); spyOn(shell, 'openExternal'); editor.setCursorBufferPosition([0, 0]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).not.toHaveBeenCalled(); editor.setCursorBufferPosition([0, 20]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).toHaveBeenCalled(); expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com'); shell.openExternal.reset(); editor.setCursorBufferPosition([1, 24]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).not.toHaveBeenCalled(); })); it('does not open non http/https/atom links', async () => { await atom.workspace.open('sample.md'); const editor = atom.workspace.getActiveTextEditor(); editor.setText('// ftp://github.com\n'); spyOn(shell, 'openExternal'); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).not.toHaveBeenCalled(); editor.setCursorBufferPosition([0, 5]); atom.commands.dispatch(atom.views.getView(editor), 'link:open'); expect(shell.openExternal).not.toHaveBeenCalled(); }); }); }); ================================================ FILE: packages/one-dark-syntax/LICENSE.md ================================================ Copyright (c) 2016 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/one-dark-syntax/README.md ================================================ ## One Dark Syntax theme ![one-dark-syntax](https://user-images.githubusercontent.com/238929/40553597-5f741518-6000-11e8-9068-70dfc5008b54.png) > The font used in the screenshot is [Fira Mono](https://github.com/mozilla/Fira). There is also a matching [UI theme](https://atom.io/themes/one-dark-ui). ### Install This theme is installed by default with Atom and can be activated by going to the __Settings > Themes__ section and selecting it from the __Syntax Themes__ drop-down menu. ================================================ FILE: packages/one-dark-syntax/index.less ================================================ // Atom Syntax Theme: One @import "styles/syntax-variables.less"; @import "styles/editor.less"; @import "styles/syntax-legacy/_base.less"; // @import "styles/syntax-legacy/c.less"; // @import "styles/syntax-legacy/cpp.less"; @import "styles/syntax-legacy/cs.less"; @import "styles/syntax-legacy/css.less"; @import "styles/syntax-legacy/elixir.less"; @import "styles/syntax-legacy/gfm.less"; // @import "styles/syntax-legacy/go.less"; @import "styles/syntax-legacy/ini.less"; @import "styles/syntax-legacy/java.less"; // @import "styles/syntax-legacy/javascript.less"; @import "styles/syntax-legacy/typescript.less"; @import "styles/syntax-legacy/json.less"; @import "styles/syntax-legacy/ng.less"; // @import "styles/syntax-legacy/ruby.less"; @import "styles/syntax-legacy/php.less"; // @import "styles/syntax-legacy/python.less"; @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; ================================================ FILE: packages/one-dark-syntax/package.json ================================================ { "name": "one-dark-syntax", "theme": "syntax", "version": "1.8.4", "description": "A dark syntax theme", "keywords": [ "dark", "blue", "syntax" ], "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.50.0" } } ================================================ FILE: packages/one-dark-syntax/styles/colors.less ================================================ // Config ----------------------------------- @syntax-hue: 220; @syntax-saturation: 13%; @syntax-brightness: 18%; // Monochrome ----------------------------------- @mono-1: hsl(@syntax-hue, 14%, 71%); // default text @mono-2: hsl(@syntax-hue, 9%, 55%); @mono-3: hsl(@syntax-hue, 10%, 40%); // Colors ----------------------------------- @hue-1: hsl(187, 47%, 55%); // <-cyan @hue-2: hsl(207, 82%, 66%); // <-blue @hue-3: hsl(286, 60%, 67%); // <-purple @hue-4: hsl( 95, 38%, 62%); // <-green @hue-5: hsl(355, 65%, 65%); // <-red 1 @hue-5-2: hsl( 5, 48%, 51%); // <-red 2 @hue-6: hsl( 29, 54%, 61%); // <-orange 1 @hue-6-2: hsl( 39, 67%, 69%); // <-orange 2 // Base colors ----------------------------------- @syntax-fg: @mono-1; @syntax-bg: hsl(@syntax-hue, @syntax-saturation, @syntax-brightness); @syntax-gutter: darken(@syntax-fg, 26%); @syntax-guide: fade(@syntax-fg, 15%); @syntax-accent: hsl(@syntax-hue, 100%, 66% ); ================================================ FILE: packages/one-dark-syntax/styles/editor.less ================================================ // Editor styles (background, gutter, guides) atom-text-editor { background-color: @syntax-background-color; color: @syntax-text-color; .line.cursor-line { background-color: @syntax-cursor-line; } .invisible { color: @syntax-text-color; } .cursor { border-left: 2px solid @syntax-cursor-color; } .selection .region { background-color: @syntax-selection-color; } .bracket-matcher .region { border-bottom: 1px solid @syntax-cursor-color; box-sizing: border-box; } .invisible-character { color: @syntax-invisible-character-color; } .indent-guide { color: @syntax-indent-guide-color; } .wrap-guide { background-color: @syntax-wrap-guide-color; } // find + replace .find-result .region.region.region, .current-result .region.region.region { border-radius: 2px; background-color: @syntax-result-marker-color; transition: border-color .4s; } .find-result .region.region.region { border: 2px solid transparent; } .current-result .region.region.region { border: 2px solid @syntax-result-marker-color-selected; transition-duration: .1s; } .gutter { .line-number { color: @syntax-gutter-text-color; -webkit-font-smoothing: antialiased; &.cursor-line { color: @syntax-gutter-text-color-selected; background-color: @syntax-gutter-background-color-selected; } &.cursor-line-no-selection { background-color: transparent; } .icon-right { color: @syntax-text-color; } } &:not(.git-diff-icon) .line-number.git-line-removed { &.git-line-removed::before { bottom: -3px; } &::after { content: ""; position: absolute; left: 0px; bottom: 0px; width: 25px; border-bottom: 1px dotted fade(@syntax-color-removed, 50%); pointer-events: none; } } } .gutter .line-number.folded, .gutter .line-number:after, .fold-marker:after { color: @syntax-gutter-text-color-selected; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax/base.less ================================================ /* This defines styling rules for syntax classes. See the naming conventions for a list of syntax classes: https://flight-manual.atom.io/hacking-atom/sections/syntax-naming-conventions When styling rules conflict: - The last rule overrides previous rules. - The rule with most classes and pseudo-classes overrides the last rule. */ // if for and del let = && .syntax--keyword { color: @hue-3; // int char float &.syntax--type { color: @hue-1; } // super &.syntax--function { color: @hue-5; } // this self &.syntax--variable { color: @hue-5; } } // identifier .syntax--entity { color: @mono-1; // function(parameter) &.syntax--parameter { color: @mono-1; } // self cls iota &.syntax--support { color: @hue-5; } // @entity.decorator &.syntax--decorator:last-child { color: @hue-2; } // label: &.syntax--label { text-decoration: underline; } // function method &.syntax--function { color: @hue-2; } // add &.syntax--operator { color: @hue-2; // %>% <=> &.syntax--symbolic { color: @mono-1; } } // String Class int rune list &.syntax--type { color: @hue-1; } // div span &.syntax--tag { color: @hue-5; } // href src alt &.syntax--attribute { color: @hue-6; } } // () [] {} => @ .syntax--punctuation { color: @mono-1; &.syntax--accessor { color: @mono-1; // . -> :: &.syntax--member, &.syntax--scope { color: @hue-3; } } // { } ~~~ &.syntax--embedded { color: @hue-3; } } // "string" .syntax--string { color: @hue-4; // :immutable &.syntax--immutable { color: @hue-4; } // {placeholder} %().2f &.syntax--part { color: @hue-1; } // ${ } &.syntax--interpolation { color: @hue-3; } // /^reg[ex]?p/ &.syntax--regexp { color: @hue-4; // ^ $ \b ? + i &.syntax--language { color: @hue-3; } // \1 &.syntax--variable { color: @hue-2; } // ( ) [^ ] (?= ) | &.syntax--punctuation { color: @hue-3; } } } // literal 4 1.3 true nil .syntax--constant { color: @hue-6; // < 'a' &.syntax--character { color: @hue-4; // \" \' \g \. &.syntax--escape { color: @hue-4; } // \u2661 \n \t \W . &.syntax--code { color: @hue-1; } } } // text .syntax--text { color: @mono-1; } // __formatted__ .syntax--markup { // # Heading &.syntax--heading { color: @hue-5; } // 1. * - &.syntax--list.syntax--punctuation { color: @hue-5; } // > quote &.syntax--quote { color: @mono-3; font-style: italic; } // **bold** &.syntax--bold { color: @hue-6; font-weight: bold; } // *italic* &.syntax--italic { color: @hue-3; font-style: italic; } // __underline__ &.syntax--underline { color: @hue-1; text-decoration: underline; } // ~~strike~~ &.syntax--strike { color: @hue-5; } // `raw` &.syntax--raw { color: @hue-4; } // url.com (path) &.syntax--link { color: @hue-1; } // [alt] ![alt] &.syntax--alt { color: @hue-2; } // {++ inserted ++} &.syntax--inserted { color: @hue-4; .syntax--punctuation { color: @hue-4; } } // {== highlighted ==} &.syntax--highlighted { color: @hue-4; .syntax--punctuation { color: @hue-4; } } // {-- deleted --} &.syntax--deleted { color: @hue-5; .syntax--punctuation { color: @hue-5; } } // {~~ from~>to ~~} &.syntax--changed { color: @hue-3; .syntax--punctuation { color: @hue-3; } } // {>> commented <<} &.syntax--commented { color: @mono-3; .syntax--punctuation { color: @mono-3; } } } // /* comment */ .syntax--comment { color: @mono-3; font-style: italic; // @param TODO NOTE &.syntax--caption { color: lighten(@mono-3, 6); font-weight: bold; } // variable function type &.syntax--term { color: lighten(@mono-3, 9); } // { } / . &.syntax--punctuation { color: @mono-3; font-weight: normal; } } // 0invalid .syntax--invalid:not(.syntax--punctuation) { // §illegal &.syntax--illegal { color: @syntax-illegal-fg !important; background-color: @syntax-illegal-bg !important; } // obsolete() &.syntax--deprecated { color: @syntax-deprecated-fg !important; background-color: @syntax-deprecated-bg !important; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax/css.less ================================================ .syntax--source.syntax--css { .syntax--entity { // function() &.syntax--function { color: @mono-2; // url rgb &.syntax--support { color: @hue-1; } } // .class :pseudo-class attribute &.syntax--selector { color: @hue-6; // div span &.syntax--tag { color: @hue-5; } // #id &.syntax--id { color: @hue-2; } } // property: constant &.syntax--property { color: @mono-2; // height position border &.syntax--support { color: @mono-1; } } // --variable &.syntax--variable { color: @hue-5; } } // property: constant .syntax--constant { color: @mono-2; // flex solid bold &.syntax--support { color: @mono-1; } // 3px 4em &.syntax--numeric { color: @hue-6; } // screen print &.syntax--media { color: @hue-6; } // #b294bb blue red &.syntax--color { color: @hue-6; } // from to &.syntax--offset { color: @mono-1; } // [attribute=attribute-value] &.syntax--attribute-value { color: @hue-4; } } .syntax--punctuation { // . : :: &.syntax--selector { color: @hue-6; // * &.syntax--wildcard { color: @hue-5; } // # &.syntax--id { color: @hue-2; } // [] &.syntax--attribute { color: @mono-1; } } } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/_base.less ================================================ // Language syntax highlighting .syntax--comment { color: @mono-3; font-style: italic; .syntax--markup.syntax--link { color: @mono-3; } } .syntax--entity { &.syntax--name.syntax--type { color: @hue-6-2; } &.syntax--other.syntax--inherited-class { color: @hue-6-2; } } .syntax--keyword { color: @hue-3; &.syntax--control { color: @hue-3; } &.syntax--operator { color: @hue-3; } &.syntax--other.syntax--special-method { color: @hue-2; } &.syntax--other.syntax--unit { color: @hue-6; } } .syntax--storage { color: @hue-3; &.syntax--type { &.syntax--annotation, &.syntax--primitive { color: @hue-3; } } &.syntax--modifier { &.syntax--package, &.syntax--import { color: @mono-1; } } } .syntax--constant { color: @hue-6; &.syntax--variable { color: @hue-6; } &.syntax--character.syntax--escape { color: @hue-1; } &.syntax--numeric { color: @hue-6; } &.syntax--other.syntax--color { color: @hue-1; } &.syntax--other.syntax--symbol { color: @hue-1; } } .syntax--variable { color: @hue-5; &.syntax--interpolation { color: @hue-5-2; } &.syntax--parameter { color: @mono-1; } } .syntax--string { color: @hue-4; > .syntax--source, .syntax--embedded { color: @mono-1; } &.syntax--regexp { color: @hue-1; .syntax--source.syntax--ruby.syntax--embedded { color: @hue-6-2; } } &.syntax--other.syntax--link { color: @hue-5; } } .syntax--punctuation { &.syntax--definition { &.syntax--comment { color: @mono-3; } &.syntax--method-parameters, &.syntax--function-parameters, &.syntax--parameters, &.syntax--separator, &.syntax--seperator, &.syntax--array { color: @mono-1; } &.syntax--heading, &.syntax--identity { color: @hue-2; } &.syntax--bold { color: @hue-6-2; font-weight: bold; } &.syntax--italic { color: @hue-3; font-style: italic; } } &.syntax--section { &.syntax--embedded { color: @hue-5-2; } &.syntax--method, &.syntax--class, &.syntax--inner-class { color: @mono-1; } } } .syntax--support { &.syntax--class { color: @hue-6-2; } &.syntax--type { color: @hue-1; } &.syntax--function { color: @hue-1; &.syntax--any-method { color: @hue-2; } } } .syntax--entity { &.syntax--name.syntax--function { color: @hue-2; } &.syntax--name.syntax--class, &.syntax--name.syntax--type.syntax--class { color: @hue-6-2; } &.syntax--name.syntax--section { color: @hue-2; } &.syntax--name.syntax--tag { color: @hue-5; } &.syntax--other.syntax--attribute-name { color: @hue-6; &.syntax--id { color: @hue-2; } } } .syntax--meta { &.syntax--class { color: @hue-6-2; &.syntax--body { color: @mono-1; } } &.syntax--method-call, &.syntax--method { color: @mono-1; } &.syntax--definition { &.syntax--variable { color: @hue-5; } } &.syntax--link { color: @hue-6; } &.syntax--require { color: @hue-2; } &.syntax--selector { color: @hue-3; } &.syntax--separator { color: @mono-1; } &.syntax--tag { color: @mono-1; } } .syntax--underline { text-decoration: underline; } .syntax--none { color: @mono-1; } .syntax--invalid { &.syntax--deprecated { color: @syntax-deprecated-fg !important; background-color: @syntax-deprecated-bg !important; } &.syntax--illegal { color: @syntax-illegal-fg !important; background-color: @syntax-illegal-bg !important; } } // Languages ------------------------------------------------- .syntax--markup { &.syntax--bold { color: @hue-6; font-weight: bold; } &.syntax--changed { color: @hue-3; } &.syntax--deleted { color: @hue-5; } &.syntax--italic { color: @hue-3; font-style: italic; } &.syntax--heading { color: @hue-5; .syntax--punctuation.syntax--definition.syntax--heading { color: @hue-2; } } &.syntax--link { color: @hue-1; } &.syntax--inserted { color: @hue-4; } &.syntax--quote { color: @hue-6; } &.syntax--raw { color: @hue-4; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/c.less ================================================ .syntax--source.syntax--c { .syntax--keyword.syntax--operator { color: @hue-3; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/cpp.less ================================================ .syntax--source.syntax--cpp { .syntax--keyword.syntax--operator { color: @hue-3; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/cs.less ================================================ .syntax--source.syntax--cs { .syntax--keyword.syntax--operator { color: @hue-3; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/css.less ================================================ .syntax--source.syntax--css { // highlight properties/values if they are supported .syntax--property-name, .syntax--property-value { color: @mono-2; &.syntax--support { color: @mono-1; } } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/elixir.less ================================================ .syntax--source.syntax--elixir { .syntax--source.syntax--embedded.syntax--source { color: @mono-1; } .syntax--constant.syntax--language, .syntax--constant.syntax--numeric, .syntax--constant.syntax--definition { color: @hue-2; } .syntax--variable.syntax--definition, .syntax--variable.syntax--anonymous{ color: @hue-3; } .syntax--parameter.syntax--variable.syntax--function { color: @hue-6; font-style: italic; } .syntax--quoted{ color: @hue-4; } .syntax--keyword.syntax--special-method, .syntax--embedded.syntax--section, .syntax--embedded.syntax--source.syntax--empty, { color: @hue-5; } .syntax--readwrite.syntax--module { .syntax--punctuation { color: @hue-5; } } .syntax--regexp.syntax--section, .syntax--regexp.syntax--string { color: @hue-5-2; } .syntax--separator, .syntax--keyword.syntax--operator { color: @hue-6; } .syntax--variable.syntax--constant { color: @hue-6-2; } .syntax--array, .syntax--scope, .syntax--section { color: @mono-2; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/gfm.less ================================================ .syntax--source.syntax--gfm { .syntax--markup { -webkit-font-smoothing: auto; } .syntax--link .syntax--entity { color: @hue-2; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/go.less ================================================ .syntax--source.syntax--go { .syntax--storage.syntax--type.syntax--string { color: @hue-3; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/ini.less ================================================ .syntax--source.syntax--ini { .syntax--keyword.syntax--other.syntax--definition.syntax--ini { color: @hue-5; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/java.less ================================================ .syntax--source.syntax--java { .syntax--storage { &.syntax--modifier.syntax--import { color: @hue-6-2; } &.syntax--type { color: @hue-6-2; } } .syntax--keyword.syntax--operator.syntax--instanceof { color: @hue-3; } } .syntax--source.syntax--java-properties { .syntax--meta.syntax--key-pair { color: @hue-5; & > .syntax--punctuation { color: @mono-1; } } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/javascript.less ================================================ .syntax--source.syntax--js { .syntax--keyword.syntax--operator { color: @hue-1; // keywords are definded in https://github.com/atom/language-javascript/blob/master/grammars/javascript.cson // search "instanceof" for location &.syntax--delete, &.syntax--in, &.syntax--of, &.syntax--instanceof, &.syntax--new, &.syntax--typeof, &.syntax--void { color: @hue-3; } } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/json.less ================================================ .syntax--source.syntax--json { .syntax--meta.syntax--structure.syntax--dictionary.syntax--json { & > .syntax--string.syntax--quoted.syntax--json { & > .syntax--punctuation.syntax--string { color: @hue-5; } color: @hue-5; } } .syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json { & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json, & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation { color: @hue-4; } & > .syntax--constant.syntax--language.syntax--json { color: @hue-1; } } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/ng.less ================================================ .syntax--ng { &.syntax--interpolation { color: @hue-5; &.syntax--begin, &.syntax--end { color: @hue-2; } .syntax--function { color: @hue-5; &.syntax--begin, &.syntax--end { color: @hue-2; } } .syntax--bool { color: @hue-6; } .syntax--bracket { color: @mono-1; } } &.syntax--pipe, &.syntax--operator { color: @mono-1; } &.syntax--tag { color: @hue-1; } &.syntax--attribute-with-value { .syntax--attribute-name { color: @hue-6-2; } .syntax--string { color: @hue-3; &.syntax--begin, &.syntax--end { color: @mono-1; } } } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/php.less ================================================ .syntax--source.syntax--php { .syntax--class.syntax--bracket { color: @mono-1; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/python.less ================================================ .syntax--source.syntax--python { .syntax--keyword.syntax--operator.syntax--logical.syntax--python { color: @hue-3; } .syntax--variable.syntax--parameter { color: @hue-6; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/ruby.less ================================================ .syntax--source.syntax--ruby { .syntax--constant.syntax--other.syntax--symbol > .syntax--punctuation { color: inherit; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-legacy/typescript.less ================================================ .syntax--source.syntax--ts { .syntax--keyword.syntax--operator { color: @hue-1; } } .syntax--source.syntax--flow { .syntax--keyword.syntax--operator { color: @hue-1; } } ================================================ FILE: packages/one-dark-syntax/styles/syntax-variables.less ================================================ @import "colors.less"; // Official Syntax Variables ----------------------------------- // General colors @syntax-text-color: @syntax-fg; @syntax-cursor-color: @syntax-accent; @syntax-selection-color: lighten(@syntax-background-color, 10%); @syntax-selection-flash-color: @syntax-accent; @syntax-background-color: @syntax-bg; // Guide colors @syntax-wrap-guide-color: @syntax-guide; @syntax-indent-guide-color: @syntax-guide; @syntax-invisible-character-color: @syntax-guide; // For find and replace markers @syntax-result-marker-color: fade(@syntax-accent, 24%); @syntax-result-marker-color-selected: @syntax-accent; // Gutter colors @syntax-gutter-text-color: @syntax-gutter; @syntax-gutter-text-color-selected: @syntax-fg; @syntax-gutter-background-color: @syntax-bg; // unused @syntax-gutter-background-color-selected: lighten(@syntax-bg, 8%); // Git colors - For git diff info. i.e. in the gutter @syntax-color-renamed: hsl(208, 100%, 60%); @syntax-color-added: hsl(150, 60%, 54%); @syntax-color-modified: hsl(40, 60%, 70%); @syntax-color-removed: hsl(0, 70%, 60%); // For language entity colors @syntax-color-variable: @hue-5; @syntax-color-constant: @hue-6; @syntax-color-property: @syntax-fg; @syntax-color-value: @syntax-fg; @syntax-color-function: @hue-2; @syntax-color-method: @hue-2; @syntax-color-class: @hue-6-2; @syntax-color-keyword: @hue-3; @syntax-color-tag: @hue-5; @syntax-color-attribute: @hue-6; @syntax-color-import: @hue-3; @syntax-color-snippet: @hue-4; // Custom Syntax Variables ----------------------------------- // Don't use in packages @syntax-cursor-line: hsla(@syntax-hue, 100%, 80%, .04); // needs to be semi-transparent to show search results @syntax-deprecated-fg: darken(@syntax-color-modified, 50%); @syntax-deprecated-bg: @syntax-color-modified; @syntax-illegal-fg: white; @syntax-illegal-bg: @syntax-color-removed; ================================================ FILE: packages/one-dark-ui/.gitignore ================================================ node_modules ================================================ FILE: packages/one-dark-ui/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/one-dark-ui/README.md ================================================ ## One Dark UI theme [![Build Status](https://travis-ci.org/atom/one-dark-ui.svg?branch=master)](https://travis-ci.org/atom/one-dark-ui) A dark UI theme that adapts to most syntax themes. ![One dark UI](https://cloud.githubusercontent.com/assets/378023/26246818/08255b76-3cd6-11e7-9f6d-6ae3e16a89a9.png) > The font used in the screenshot is [Fira Mono](https://github.com/mozilla/Fira). ### Install This theme comes bundled with Atom and can be activated by going to the __Settings > Themes__ section and selecting "One Dark" from the __UI Themes__ drop-down menu. ### Settings In the theme settings you can: - Change the __Font Size__ to scale the whole UI up or down. - Choose between 3 __Tab Sizing__ modes. - Hide the __dock buttons__. To make changes, go to `Settings > Themes > One Dark UI > Settings` or the cog icon next to the theme picker. ### Customize It's also possible to resize only certain areas by adding the following to your `styles.less` (Use DevTools to find the right selectors): ```css .theme-one-dark-ui { .tab-bar { font-size: 18px; } .tree-view { font-size: 14px; } .status-bar { font-size: 12px; } } ``` ### FAQ __Why do the colors change when I switch Syntax themes?__ This UI theme uses the same background color as the chosen syntax theme. If that syntax theme has a light background color, it only uses its hue, but otherwise stays dark. This lets you use dark-light combos. ================================================ FILE: packages/one-dark-ui/index.less ================================================ // Atom UI Theme: One @import "styles/ui-variables.less"; @import "styles/ui-mixins.less"; @import "octicon-mixins.less"; // https://github.com/atom/atom/blob/master/static/variables/octicon-mixins.less @import "styles/atom.less"; @import "styles/badges.less"; @import "styles/buttons.less"; @import "styles/docks.less"; @import "styles/editor.less"; @import "styles/git.less"; @import "styles/inputs.less"; @import "styles/lists.less"; @import "styles/messages.less"; @import "styles/nav.less"; @import "styles/notifications.less"; @import "styles/modal.less"; @import "styles/panels.less"; @import "styles/panes.less"; @import "styles/progress.less"; @import "styles/tabs.less"; @import "styles/text.less"; @import "styles/title-bar.less"; @import "styles/tooltips.less"; @import "styles/tree-view.less"; @import "styles/status-bar.less"; @import "styles/key-binding.less"; @import "styles/sites.less"; @import "styles/settings.less"; @import "styles/packages.less"; @import "styles/core.less"; @import "styles/config.less"; ================================================ FILE: packages/one-dark-ui/lib/main.js ================================================ const root = document.documentElement; const themeName = 'one-dark-ui'; module.exports = { activate(state) { atom.config.observe(`${themeName}.fontSize`, setFontSize); atom.config.observe(`${themeName}.tabSizing`, setTabSizing); atom.config.observe(`${themeName}.tabCloseButton`, setTabCloseButton); atom.config.observe(`${themeName}.hideDockButtons`, setHideDockButtons); atom.config.observe(`${themeName}.stickyHeaders`, setStickyHeaders); }, deactivate() { unsetFontSize(); unsetTabSizing(); unsetTabCloseButton(); unsetHideDockButtons(); unsetStickyHeaders(); } }; // Font Size ----------------------- function setFontSize(currentFontSize) { root.style.fontSize = `${currentFontSize}px`; } function unsetFontSize() { root.style.fontSize = ''; } // Tab Sizing ----------------------- function setTabSizing(tabSizing) { root.setAttribute(`theme-${themeName}-tabsizing`, tabSizing.toLowerCase()); } function unsetTabSizing() { root.removeAttribute(`theme-${themeName}-tabsizing`); } // Tab Close Button ----------------------- function setTabCloseButton(tabCloseButton) { if (tabCloseButton === 'Left') { root.setAttribute(`theme-${themeName}-tab-close-button`, 'left'); } else { unsetTabCloseButton(); } } function unsetTabCloseButton() { root.removeAttribute(`theme-${themeName}-tab-close-button`); } // Dock Buttons ----------------------- function setHideDockButtons(hideDockButtons) { if (hideDockButtons) { root.setAttribute(`theme-${themeName}-dock-buttons`, 'hidden'); } else { unsetHideDockButtons(); } } function unsetHideDockButtons() { root.removeAttribute(`theme-${themeName}-dock-buttons`); } // Sticky Headers ----------------------- function setStickyHeaders(stickyHeaders) { if (stickyHeaders) { root.setAttribute(`theme-${themeName}-sticky-headers`, 'sticky'); } else { unsetStickyHeaders(); } } function unsetStickyHeaders() { root.removeAttribute(`theme-${themeName}-sticky-headers`); } ================================================ FILE: packages/one-dark-ui/package.json ================================================ { "name": "one-dark-ui", "theme": "ui", "version": "1.12.5", "description": "Atom One dark UI theme", "keywords": [ "dark", "adaptive", "ui" ], "license": "MIT", "repository": "https://github.com/atom/atom", "main": "lib/main", "engines": { "atom": ">0.40.0" }, "devDependencies": { "standard": "^11.0.0" }, "configSchema": { "fontSize": { "title": "Font Size", "description": "Change the font size for the UI.", "type": "integer", "default": 12, "enum": [ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ], "order": 1 }, "tabSizing": { "title": "Tab Sizing", "description": "In Even mode all tabs will be the same size. Great for quickly closing many tabs. In Maximum mode the tabs will expand to take up the full width. In Minimum mode the tabs will only take as little space as needed and also show longer file names.", "type": "string", "default": "Even", "enum": [ "Even", "Maximum", "Minimum" ], "order": 2 }, "tabCloseButton": { "title": "Tab Close Button", "description": "Choose the position of the close button shown in tabs.", "type": "string", "default": "Right", "enum": [ "Left", "Right" ], "order": 3 }, "hideDockButtons": { "title": "Hide dock toggle buttons", "description": "Note: When hiding the toggle buttons, opening a dock needs to be done by using the keyboard or other alternatives.", "type": "boolean", "default": "false", "order": 4 }, "stickyHeaders": { "title": "Make tree-view project headers sticky", "type": "boolean", "default": "false", "order": 5 } } } ================================================ FILE: packages/one-dark-ui/spec/theme-spec.js ================================================ const themeName = 'one-dark-ui'; describe(`${themeName} theme`, () => { beforeEach(() => { waitsForPromise(() => atom.packages.activatePackage(themeName)); }); it('allows the font size to be set via config', () => { expect(document.documentElement.style.fontSize).toBe('12px'); atom.config.set(`${themeName}.fontSize`, '10'); expect(document.documentElement.style.fontSize).toBe('10px'); }); it('allows the tab sizing to be set via config', () => { atom.config.set(`${themeName}.tabSizing`, 'Maximum'); expect( document.documentElement.getAttribute(`theme-${themeName}-tabsizing`) ).toBe('maximum'); }); it('allows the tab sizing to be set via config', () => { atom.config.set(`${themeName}.tabSizing`, 'Minimum'); expect( document.documentElement.getAttribute(`theme-${themeName}-tabsizing`) ).toBe('minimum'); }); it('allows the tab close button to be shown on the left via config', () => { atom.config.set(`${themeName}.tabCloseButton`, 'Left'); expect( document.documentElement.getAttribute( `theme-${themeName}-tab-close-button` ) ).toBe('left'); }); it('allows the dock toggle buttons to be hidden via config', () => { atom.config.set(`${themeName}.hideDockButtons`, true); expect( document.documentElement.getAttribute(`theme-${themeName}-dock-buttons`) ).toBe('hidden'); }); it('allows the tree-view headers to be sticky via config', () => { atom.config.set(`${themeName}.stickyHeaders`, true); expect( document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`) ).toBe('sticky'); }); it('allows the tree-view headers to not be sticky via config', () => { atom.config.set(`${themeName}.stickyHeaders`, false); expect( document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`) ).toBe(null); }); }); ================================================ FILE: packages/one-dark-ui/styles/atom.less ================================================ * { box-sizing: border-box; } html { font-size: @font-size; } atom-workspace { background-color: @app-background-color; } // Scrollbars ------------------------------------ .scrollbars-visible-always { ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: @scrollbar-background-color; } ::-webkit-scrollbar-thumb { border-radius: 5px; border: 3px solid @scrollbar-background-color; background: @scrollbar-color; background-clip: content-box; } ::-webkit-scrollbar-corner { background: @scrollbar-background-color; } ::-webkit-scrollbar-thumb:vertical:active { border-radius: 0; border-left-width: 0; border-right-width: 0; } ::-webkit-scrollbar-thumb:horizontal:active { border-radius: 0; border-top-width: 0; border-bottom-width: 0; } atom-text-editor { ::-webkit-scrollbar-track { background: @scrollbar-background-color-editor; } ::-webkit-scrollbar-corner { background: @scrollbar-background-color-editor; } ::-webkit-scrollbar-thumb { border-color: @scrollbar-background-color-editor; background: @scrollbar-color-editor; } } } // TODO: Move to a better place, not sure where it gets used .caret { border-top: 5px solid #fff; margin-top: -1px; } ================================================ FILE: packages/one-dark-ui/styles/badges.less ================================================ .badge { padding: @ui-padding/4 @ui-padding/2.5; min-width: @ui-padding*1.25; .text(highlight); border-radius: @ui-size*2; background-color: @badge-background-color; // Icon ---------------------- &.icon { font-size: @ui-size; padding: @ui-padding-icon @ui-padding-icon*1.5; } } ================================================ FILE: packages/one-dark-ui/styles/buttons.less ================================================ @btn-border: 1px solid @button-border-color; @btn-padding: 0 @ui-size/1.25; // Mixins ----------------------- .btn-default (@color, @hover-color, @selected-color, @text-color) { color: @text-color; text-shadow: none; border: @btn-border; background-color: @color; background-image: linear-gradient(lighten(@color, 2%), @color); &:hover { color: @text-color-highlight; background-image: linear-gradient(lighten(@hover-color, 2%), @hover-color); } &:active { background: darken(@color, 4%); box-shadow: none; } &.selected { background: @selected-color; } &.selected:focus, &.selected:hover { background: lighten(@selected-color, 2%); } &:focus { .focus(); // unfortunately :focus styles stay even after releasing mouse. } } .btn-variant (@color) { @_text-color: contrast(@color, white, hsl(0,0%,20%), 33% ); .btn-default( @color, lighten(@color, 3%), saturate(darken(@color, 12%), 20%), @text-color-highlight ); color: @_text-color; & when (@ui-lightness > 50%) { border-color: transparent; // hide border on light backgrounds } &:hover, &:focus { color: @_text-color; } &:focus { border-color: transparent; background-clip: padding-box; box-shadow: inset 0 0 0 1px fade(@base-border-color, 50%), 0 0 0 1px @color; } &.icon:before { color: @_text-color; } } // Buttons ----------------------- .btn { height: initial; padding: @btn-padding; font-size: @ui-size; line-height: @ui-line-height; } .btn, .btn.btn-default { .btn-default(@button-background-color, @button-background-color-hover, @button-background-color-selected, @text-color); } .btn.btn-primary { .btn-variant(@accent-bg-color); } .btn.btn-info { .btn-variant(@background-color-info); } .btn.btn-success { .btn-variant(@background-color-success); } .btn.btn-warning { .btn-variant(@background-color-warning); } .btn.btn-error { .btn-variant(@background-color-error); } // Button Sizes ----------------------- .btn.btn-xs, .btn-group-xs > .btn { font-size: @ui-size*.8; line-height: @ui-line-height; padding: @btn-padding; } .btn.btn-sm, .btn-group-sm > .btn { font-size: @ui-size*.9; line-height: @ui-line-height; padding: @btn-padding; } .btn.btn-lg, .btn-group-lg > .btn { font-size: @ui-size * 1.5; line-height: @ui-line-height; padding: @btn-padding; } // Button Group ----------------------- .btn-group > .btn { z-index: 0; &:hover { z-index: 0; } &.btn:focus { z-index: 1; .focus(); } &:first-child { border-left: @btn-border; } &:last-child, &.selected:last-child { border-right: @btn-border; } // hide border on light backgrounds & when (@ui-lightness > 50%) { &.btn-primary:first-child, &.btn-info:first-child, &.btn-success:first-child, &.btn-warning:first-child, &.btn-error:first-child { border-left-color: transparent; } &.btn-primary:last-child, &.btn-info:last-child, &.btn-success:last-child, &.btn-warning:last-child, &.btn-error:last-child { border-right-color: transparent; } } &.selected, &.selected:first-child, &.selected:last-child { color: @button-text-color-selected; border-color: @button-border-color-selected; } & when (@ui-lightness > 50%) { &.selected + .btn { border-left-color: @button-border-color-selected; } &.selected + .selected { border-left-color: mix(@button-border-color, @button-border-color-selected); } } &.selected:focus { border-color: @button-background-color-selected; box-shadow: inset 0 0 0 1px fade(@base-border-color, 50%), 0 0 0 1px @button-background-color-selected; } } // Button Icons ----------------------- .btn.icon:before { width: auto; height: auto; font-size: 1.333333em; vertical-align: -.1em; } ================================================ FILE: packages/one-dark-ui/styles/config.less ================================================ // Theme config // This gets changed from the theme settings @theme-tabsizing: ~'theme-@{ui-theme-name}-tabsizing'; @theme-dockButtons: ~'theme-@{ui-theme-name}-dock-buttons'; @theme-stickyHeaders: ~'theme-@{ui-theme-name}-sticky-headers'; @theme-closeButton: ~'theme-@{ui-theme-name}-tab-close-button'; // Tabs ---------------------------------------------- @tab-min-width: 7em; // ~ icon + 6 characters // Even (default) .tab-bar { .tab, .tab.active { flex: 1 1 0; max-width: 22em; min-width: @tab-min-width; } atom-dock & { .tab, .tab.active { max-width: none; } } // TODO: Turn this into a config // Truncates the beginning instead // .title.title.title { // direction: rtl; // change direction // } } // Maximum (full width) [@{theme-tabsizing}="maximum"] .tab-bar { .tab, .tab.active { max-width: none; } } // Minimum (show long paths) [@{theme-tabsizing}="minimum"] .tab-bar { .tab, .tab.active { flex: 0 0 auto; min-width: 2.75em; max-width: @tab-min-width * 3.3; } atom-dock { .tab, .tab.active { max-width: @tab-min-width * 2; } } } // Tabs: close button position ------------------------------ [@{theme-closeButton}="left"] { .tab-bar .tab { .close-icon { right: auto; left: @icon-padding-right; } } } // Hide docks toggle buttons ------------------------------ [@{theme-dockButtons}="hidden"] { // Hide docks when not open .atom-dock-inner:not(.atom-dock-open) { display: none; } // Hide toggle buttons .atom-dock-toggle-button { display: none; } } // Sticky Projects ------------------------------ [@{theme-stickyHeaders}="sticky"] { .tree-view { .project-root-header { position: sticky; top: 0; z-index: 3; padding-left: 5px; padding-right: 10px; border-bottom: 1px solid @base-border-color; background-color: @tree-view-background-color; } .project-root.project-root { margin-left: -5px; margin-right: -10px; // Disable selection &::before { display: none; } // Add selection back &.selected .project-root-header { background-color: @background-color-selected; } } &:focus .selected .project-root-header.project-root-header { background: @button-background-color-selected; } // Fix sticky header from covering auto-revealed files .entry.file.selected { padding-top: @ui-tab-height; margin-top: -@ui-tab-height; } // Fix sticky header from covering auto-revealed directories when using up/down keys // for directories, scroll test moves to .header, see https://github.com/atom/tree-view/blob/d2857ad4d7eeb7dad5cf94b33257a8740211480e/lib/tree-view.coffee#L839 .entry.directory.selected:not(.project-root) { & > .header { padding-top: @ui-tab-height; margin-top: -@ui-tab-height; } &::before { margin-top: @ui-tab-height; } } // Fix above directory is not being clickable .entry.directory:not(.project-root) > .header { z-index: 2; } .entry.directory.selected:not(.project-root) > .header { z-index: 1; } } } ================================================ FILE: packages/one-dark-ui/styles/core.less ================================================ // Misc .preview-pane .results-view .path-match-number { // show number also on selected item color: inherit; opacity: .6; } .tool-panel.incompatible-packages { // incompatible-packages isn't really a tool-panel and more a whole pane .text(normal); background-color: @level-2-color; } // Styleguide ---------------------------------------------- .styleguide { // Modal atom-panel.modal:after { position: absolute; // prevent overlay backdrop from leaking outside left: -@ui-padding; right: -@ui-padding; bottom: -@ui-padding; } } ================================================ FILE: packages/one-dark-ui/styles/docks.less ================================================ // Docks ------------------------------ // Make handles not take up any space when dock is open .atom-dock-resize-handle { position: absolute; z-index: 11; // same as toggle buttons &.left { top: 0; right: 0; bottom: 0; } &.right { top: 0; left: 0; bottom: 0; } &.bottom { top: 0; left: 0; right: 0; } } // Add borders .atom-dock-inner.atom-dock-open.left { border-right: 1px solid @base-border-color; } .atom-dock-inner.atom-dock-open.right { border-left: 1px solid @base-border-color; } // Make toggle buttons cover ^ border .atom-dock-toggle-button.left { margin-left: -2px; } .atom-dock-toggle-button.right { margin-right: -2px; } .atom-dock-inner:not(.atom-dock-open) .atom-dock-toggle-button.bottom { margin-bottom: -1px; } ================================================ FILE: packages/one-dark-ui/styles/dropdowns.less ================================================ .dropdown-menu { background-color: @overlay-background-color; border-radius: @component-border-radius; border: 1px solid @base-border-color; padding: 0; > li > a { .text(normal); } > li > a:hover { .text(highlight); background-color: @background-color-highlight; } } ================================================ FILE: packages/one-dark-ui/styles/editor.less ================================================ // Editor in a panel // TODO: Find a better selector, maybe a new class like atom-text-editor[medium] atom-panel-container atom-text-editor.is-focused { .focus(); } // Mini // Usually just single line inputs atom-text-editor[mini] { overflow: auto; font-size: @ui-input-size; line-height: @ui-line-height; max-height: @ui-line-height * 5; // rows padding-left: @ui-padding/3; border-radius: @component-border-radius; color: @text-color-highlight; border: 1px solid @input-border-color; background-color: @input-background-color; .placeholder-text { color: @text-color-subtle; } .selection .region { background-color: @input-selection-color; } .cursor { border-color: @accent-color; border-width: 2px; } &.is-focused { .focus(); background-color: @input-background-color-focus; .selection .region { background-color: @input-selection-color-focus; } } } ================================================ FILE: packages/one-dark-ui/styles/git.less ================================================ .status { .text(normal); } .status-added { .text(success); } // green .status-ignored { .text(subtle); } // faded .status-modified { .text(warning); } // orange .status-removed { .text(error); } // red .status-renamed { .text(info); } // blue ================================================ FILE: packages/one-dark-ui/styles/inputs.less ================================================ // // Checkbox // ------------------------- .input-checkbox { &:active { background-color: @accent-color; } &:before, &:after { background-color: @accent-text-color; } &:checked { background-color: @accent-color; } &:indeterminate { background-color: @accent-color; } } // // Radio // ------------------------- .input-radio { &:before { background-color: @accent-text-color; } &:active { background-color: @accent-color; } &:checked { background-color: @accent-color; } } // // Range (Slider) // ------------------------- .input-range { &::-webkit-slider-thumb { background-color: @accent-color; } } // // Toggle // ------------------------- .input-toggle { &:checked { background-color: @accent-color; } &:before { background-color: @accent-text-color; } } // States ------------------------- .input-checkbox, .input-text, .input-search, .input-number, .input-textarea, .input-select, .input-color { &:focus { .focus(); } } .input-text, .input-search, .input-number, .input-textarea { &:invalid { .invalid(); } } ================================================ FILE: packages/one-dark-ui/styles/key-binding.less ================================================ .key-binding { display: inline-block; margin-left: @ui-padding-icon; padding: 0 @ui-padding/4; line-height: 2; font-family: inherit; font-size: max(1em, @ui-size*.85); letter-spacing: @ui-size/10; border-radius: @component-border-radius; color: @accent-bg-text-color; background-color: @accent-bg-color; } ================================================ FILE: packages/one-dark-ui/styles/lists.less ================================================ .list-group, .list-tree { li:not(.list-nested-item), li.list-nested-item > .list-item { .text(normal); } .generate-list-item-text-color(@class) { li:not(.list-nested-item).text-@{class}, li.list-nested-item.text-@{class} > .list-item { .text(@class); } } .generate-list-item-text-color(subtle); .generate-list-item-text-color(info); .generate-list-item-text-color(success); .generate-list-item-text-color(warning); .generate-list-item-text-color(error); .generate-list-item-text-color(selected); .generate-list-item-status-color(@color, @status) { li:not(.list-nested-item).status-@{status}, li.list-nested-item.status-@{status} > .list-item { color: @color; } li:not(.list-nested-item).selected.status-@{status}, li.list-nested-item.selected.status-@{status} > .list-item { color: @color; } } .generate-list-item-status-color(@text-color-added, added); .generate-list-item-status-color(@text-color-ignored, ignored); .generate-list-item-status-color(@text-color-modified, modified); .generate-list-item-status-color(@text-color-removed, removed); .generate-list-item-status-color(@text-color-renamed, renamed); li:not(.list-nested-item).selected, li.list-nested-item.selected > .list-item { .text(selected); } .no-icon { padding-left: calc(@ui-padding-icon ~"+" @component-icon-size); } } .list-tree.has-collapsable-children .list-nested-item > .list-item::before { text-align: center; } .select-list ol.list-group, &.select-list ol.list-group { li.two-lines { .secondary-line { color: @text-color-subtle; } &.selected .secondary-line { color: fade(@text-color-highlight, 50%); text-shadow: none; } } // Reset icon to allow nesting .icon { display: initial; height: initial; } // We want to highlight the background of the list items because we dont // know their size. li.selected { background-color: @background-color-selected; &:before{ display: none; } } &.mark-active { @active-icon-size: 14px; // pad in front of the text where the icon would be We'll pad the non- // active items with a 'fake' icon so other classes can pad the item // without worrying about the icon padding. li:before { content: ''; background-color: transparent; position: static; display: inline-block; left: auto; right: auto; height: @active-icon-size; width: @active-icon-size; font-size: @active-icon-size; } > li:not(.active):before { margin-right: @ui-padding-icon; } li.active { .octicon(check, @active-icon-size); &:before { margin-right: @ui-padding-icon; color: @text-color-success; } } } } .select-list.popover-list { @popover-list-padding: @ui-padding/4; background-color: @overlay-background-color; box-shadow: 0 2px 8px 1px rgba(0, 0, 0, 0.3); padding: @popover-list-padding; border-radius: @component-border-radius; atom-text-editor[mini] { margin-bottom: @popover-list-padding; } ol.list-group { margin-top: @popover-list-padding; } .list-group li { padding-left: @popover-list-padding; } // Un-reset icon in popover lists .icon.icon { display: inline-block; height: inherit; } } .ui-sortable { li { line-height: 2.5; } // For sortable lists in the settings view li.ui-sortable-placeholder { visibility: visible !important; background-color: darken(@pane-item-background-color, 10%); } } li.ui-draggable-dragging, li.ui-sortable-helper { line-height: @ui-line-height; height: @ui-line-height; border: 0; border-radius: 0; list-style: none; padding: 0 @ui-padding; background: @background-color-highlight; box-shadow: 0 0 1px @base-border-color; } ================================================ FILE: packages/one-dark-ui/styles/messages.less ================================================ background-tips ul.background-message { font-weight: 500; font-size: 2em; color: @text-color-faded; .message { padding: 0 @component-padding * 10; .keystroke { white-space: nowrap; vertical-align: middle; line-height: 1; padding: .1em .4em; } } } ================================================ FILE: packages/one-dark-ui/styles/modal.less ================================================ @modal-padding: @ui-padding/2 @ui-padding/1.5; @modal-width: @ui-size * 50; atom-panel-container.modal { position: absolute; top: 0; left: 0; right: 0; } atom-panel.modal { position: relative; width: 100%; max-width: @modal-width; margin: 0 auto; left: initial; color: @text-color; background-color: transparent; padding: @ui-padding/2; &.from-top { top: @component-padding * 5; } atom-text-editor[mini] { margin-bottom: @ui-padding/2; } .select-list ol.list-group, &.select-list ol.list-group { border: 1px solid @overlay-border-color; background-color: lighten(@overlay-background-color, 2%); &:empty { border: none; margin-top: 0; } li { padding: @modal-padding; line-height: @ui-line-height; border-bottom: 1px solid @overlay-border-color; &:last-of-type { border-bottom: none; } .icon::before { margin-left: 1px; } .icon.status { float: right; margin-left: @ui-padding-icon; &:before { margin-left: 0; margin-right: 0; } } &.selected { .status.icon { color: @text-color-selected; } } } } .select-list .key-binding { margin-top: -1px; margin-left: @ui-padding/2; margin-right: calc( -@ui-padding/3 ~"+" 1px); } .select-list .primary-line { display: block; } & > * { position: relative; // fixes stacking order } .command-palette { padding: 1px; // prevents the box-shadow of the input from being cut off background-color: @overlay-background-color; } // Container &:before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0; background-color: @overlay-background-color; border-radius: @component-border-radius*2; box-shadow: 0 6px 12px -2px hsla(0,0%,0%,.4); } // Backdrop // TODO: Add extra wrapper to translate individually or easier positioning &:after { content: ""; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; background: @overlay-backdrop-color; opacity: @overlay-backdrop-opacity; backface-visibility: hidden; // fixes scrollbar on retina screens -webkit-animation: overlay-fade .24s cubic-bezier(0.215, 0.61, 0.355, 1); } @-webkit-keyframes overlay-fade { 0% { opacity: 0; } 100% { opacity: @overlay-backdrop-opacity; } } } ================================================ FILE: packages/one-dark-ui/styles/nav.less ================================================ .nav-tabs { border-bottom: 1px solid @base-border-color; li { a, &.active a { border: none; margin-right: 0px; margin-bottom: 1px; } a:hover, &.active a, &.active a:hover { background-color: @background-color-highlight; border: none; color: @text-color-selected; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } &.active a { background-color: @tab-background-color-active; } } } ================================================ FILE: packages/one-dark-ui/styles/notifications.less ================================================ atom-notifications { font-size: @ui-size * 1.2; atom-notification { width: 32em; &.has-detail { width: 32em; } &:first-child.has-close .message { padding-right: 9em; } &:only-child.has-close .message, &.has-close .message { padding-right: 2.5em; } .item { padding: @ui-padding/2; } .detail, .description { font-size: .85em; } &.icon:before { padding-top: .85em; } .close { width: 2.5em; height: 3em; line-height: 3em; font-size: inherit; } .close-all.btn { top: .5em; right: 2.5em; } .btn-copy-report { line-height: 2em; margin-left: .5em; } } } ================================================ FILE: packages/one-dark-ui/styles/packages.less ================================================ // Overrides packages // find-and-replace + project-find --------------------------- .find-and-replace, .project-find { padding: @ui-padding/4; .input-block-item { padding: @ui-padding/4; } } // find-and-replace .find-and-replace { .header, .input-block { min-width: @ui-size*22; } .input-block-item { flex: 1 1 @ui-size*22; } .input-block-item--flex { flex: 100 1 @ui-size*22; } .btn, .btn-group-options .btn { font-size: @ui-size*1.1; padding: 0; } .btn-group-options .btn, .btn-group-options .btn.option-selection, .btn-group-options .btn.option-whole-word { padding: 0; font-size: @ui-input-size; // keep same as text input } .find-container atom-text-editor { padding-right: @ui-size*5; // leave some room for the results count } .find-meta-container { top: 0; font-size: @ui-size; line-height: @ui-size*2.5; } } // project-find .project-find { .header, .input-block { min-width: @ui-size*15; } .input-block-item { flex: 1 1 @ui-size*14; } .input-block-item--flex { flex: 100 1 @ui-size*20; } .btn { font-size: @ui-size*1.1; padding: 0; } .btn-group-options .btn { padding: 0; font-size: @ui-input-size; // keep same as text input } } // Colorize find-and-replace based on results & when (@ui-hue >= 190) and (@ui-hue <= 340) { .find-and-replace { &.has-no-results .find-container atom-text-editor[mini].is-focused { .invalid(); .selection .region { background-color: mix(@text-color-error, @input-background-color, 50%); } .cursor { border-color: @text-color-error; } } &.has-results .find-container atom-text-editor[mini].is-focused { .valid(); .selection .region { background-color: mix(@text-color-success, @input-background-color, 50%); } .cursor { border-color: @text-color-success; } } &.has-results .find-container .result-counter { color: @text-color-success; } &.has-no-results .find-container .result-counter { color: @text-color-error; } } } // Timecop --------------------------- .timecop { .timecop-panel { padding: @component-padding/2; background-color: @level-2-color; } .tool-panel { padding: @component-padding/2; background-color: @level-2-color; } .inset-panel { border: 1px solid @base-border-color; } .panel-heading { .text(highlight); border-color: @base-border-color; background-color: @level-1-color; } .list-item .inline-block { line-height: 1.5; } } // Command Palette + Fuzzy Finder --------------------------- .command-palette .list-group .character-match, .fuzzy-finder .list-group .character-match { color: @accent-only-text-color; } // Deprecation Cop --------------------------- .deprecation-cop { .deprecation-overview { background-color: @level-2-color; border-bottom: 1px solid @base-border-color; } } // Tool Bar --------------------------- .tool-bar { // Make it look the same as other panels background-color: @level-3-color; border: none; // just a single border + more spacing &.tool-bar-horizontal .tool-bar-spacer { border-left: 0 none; margin-left: .5em; margin-right: .5em; } &.tool-bar-vertical .tool-bar-spacer { border-bottom: 0 none; margin-top: .5em; margin-bottom: .5em; } // only show button styles on hover button.tool-bar-btn { background-color: @level-3-color; background-image: none; border-color: @level-3-color; } } // GitHub package --------------------------------------------------- .github { // Fix focus styles // Since it's not possible to add a padding to // a pseudo element is used to add the border when focused. &-CommitView-editor atom-text-editor.is-focused { box-shadow: none; &:before { content: ""; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border: 2px solid; border-color: inherit; border-radius: @component-border-radius; } } // Add focus styles since :focus doesn't work &-CommitView-coAuthorEditor { &.is-focused { .focus(); } &.is-open { border-top-left-radius: 0; border-top-right-radius: 0; } .Select-option { &.is-focused { border-bottom-left-radius: 0; border-bottom-right-radius: 0; color: @accent-text-color; background-color: @accent-color; } } .Select-menu-outer { left: -2px; right: -2px; bottom: 100%; border: 2px solid @accent-color; background-color: @overlay-background-color; } } } ================================================ FILE: packages/one-dark-ui/styles/panels.less ================================================ // Panels atom-panel { .text(normal); position: relative; border-bottom: 1px solid @base-border-color; &.top { border-right: 1px solid @base-border-color; } &.left { border-right: 1px solid @base-border-color; } &.right { border-left: 1px solid @base-border-color; } &.bottom { border-right: 1px solid @base-border-color; } &.footer:last-child { border-bottom: none; } &.tool-panel:empty { border: none; } } .panel { &.bordered { border: 1px solid @base-border-color; border-radius: @component-border-radius; } } .inset-panel { position: relative; background-color: @inset-panel-background-color; border-radius: @component-border-radius; &.bordered { border: 1px solid @base-border-color; border-radius: @component-border-radius; } & .panel-heading { border-color: @inset-panel-border-color; } } .panel-heading { .text(normal); border-bottom: 1px solid @panel-heading-border-color; background-color: @panel-heading-background-color; .btn { padding-left: 8px; padding-right: 8px; .btn-default( lighten(@button-background-color, 10%), lighten(@button-background-color-hover, 10%), lighten(@button-background-color-selected, 10%), lighten(@text-color, 10%) ); } } ================================================ FILE: packages/one-dark-ui/styles/panes.less ================================================ atom-pane-container { atom-pane { position: relative; border-right: 1px solid @base-border-color; border-bottom: 1px solid @base-border-color; .item-views { // prevent atom-text-editor from leaking ouside might improve performance overflow: hidden; } } } // Hide right-most border atom-pane:only-child, atom-pane-axis.pane-row > atom-pane:last-child, atom-pane-axis.pane-column:last-child > atom-pane { border-right: none; } ================================================ FILE: packages/one-dark-ui/styles/progress.less ================================================ // Spinner ---------------------- @spinner-duration: 1.2s; .loading-spinner(@size) { position: relative; display: block; width: 1em; height: 1em; font-size: @size; background: radial-gradient(@accent-color .1em, transparent .11em); &::before, &::after { content: ""; position: absolute; z-index: 10; // prevent sibling elements from getting their own layers top: 0; left: 0; border-radius: 1em; width: inherit; height: inherit; border-radius: 1em; border: 2px solid; -webkit-animation: spinner-animation @spinner-duration infinite; -webkit-animation-fill-mode: backwards; } &::before { border-color: @accent-color transparent transparent transparent; } &::after { border-color: transparent lighten(@accent-color, 15%) transparent transparent; -webkit-animation-delay: @spinner-duration/2; } &.inline-block { display: inline-block; } } @-webkit-keyframes spinner-animation { 0% { transform: rotateZ( 0deg); -webkit-animation-timing-function: cubic-bezier(0, 0, .8, .2); } 50% { transform: rotateZ(180deg); -webkit-animation-timing-function: cubic-bezier(.2, .8, 1, 1); } 100% { transform: rotateZ(360deg); } } // Spinner sizes .loading-spinner-tiny { .loading-spinner(16px); &::before, &::after { border-width: 1px; } } .loading-spinner-small { .loading-spinner(32px); } .loading-spinner-medium { .loading-spinner(48px); } .loading-spinner-large { .loading-spinner(64px); } // Progress Bar ---------------------- @progress-height: 8px; @progress-buffer-color: fade(@progress-background-color, 20%); progress { -webkit-appearance: none; height: @progress-height; border-radius: @component-border-radius; background-color: @input-background-color; box-shadow: inset 0 0 0 1px @input-border-color; &::-webkit-progress-bar { background-color: transparent; } &::-webkit-progress-value { border-radius: @component-border-radius; background-color: @progress-background-color; } // Is buffering (when no value is set) &:indeterminate { background-image: linear-gradient(-45deg, transparent 33%, @progress-buffer-color 33%, @progress-buffer-color 66%, transparent 66%); background-size: 25px @progress-height, 100% 100%, 100% 100%; // Plays animation for 1min (12runs) at normal speed, // then slows down frame-rate for 9mins (108runs) to limit CPU usage -webkit-animation: progress-buffering 5s linear 12, progress-buffering 5s 60s steps(10) 108; } } @-webkit-keyframes progress-buffering { 100% { background-position: -100px 0px; } } ================================================ FILE: packages/one-dark-ui/styles/settings.less ================================================ // Settings // Modular Scale (1.125): http://www.modularscale.com/?1&em&1.125&web&table @ms-6: @ui-size * 2.027; @ms-5: @ui-size * 1.802; @ms-4: @ui-size * 1.602; @ms-3: @ui-size * 1.424; @ms-2: @ui-size * 1.266; @ms-1: @ui-size * 1.125; @ms-0: @ui-size * 1; @ms_1: @ui-size * 0.889; @ms_2: @ui-size * 0.790; .settings-view { // Menu ------------------------------ .config-menu { position: relative; min-width: @ui-size * 15; max-width: @ui-size * 20; border-width: 0 1px 0 0; border-image: linear-gradient(@level-2-color 10px, @base-border-color 200px) 0 1 0 0 stretch; background: @level-2-color; .btn { white-space: initial; font-size: @ms_1; line-height: 1; padding: @ui-padding/3 @ui-padding/2; &::before { vertical-align: middle; } } } .nav { & > li > a { padding: @ui-padding/2 @ui-padding; line-height: @ui-line-height; } } // Sections ------------------------------ & > .panels { background-color: @level-2-color; } .section-container { max-width: @ui-size*60; } .section, .section:first-child, .section:last-child { padding: @ui-padding*3; } .themes-panel .control-group { margin-top: @ui-padding*2; } // Titles ------------------------------ .section .section-heading { margin-bottom: @ui-padding/1.5; } .sub-section-heading.icon:before, .section-heading.icon:before { margin-right: @ui-padding-icon; } // Cards ------------------------------ .sub-section:not(.collapsed) .package-container { padding-bottom: @component-padding*3; } .package-card { padding: @ui-padding; .meta-controls .status-indicator { width: @ui-padding/4; &:before { content: "\00a0"; // fixes 0 height } } } // Components ------------------------------ .icon::before { color: @text-color-subtle; } .editor-container { margin: @ui-padding 0; } .form-control { font-size: @ui-size*1.25; height: @ui-line-height; padding-top: 0; padding-bottom: 0; } .update-all-button { font-size: .75em; } .install-button { .btn-variant(@accent-bg-color); } input[type="checkbox"] { background-color: @background-color-selected; &:active, &:checked { background-color: @accent-color; } &:before, &:after { background-color: @accent-text-color; } } .search-container .btn { font-size: @ui-input-size; } } ================================================ FILE: packages/one-dark-ui/styles/sites.less ================================================ // Site Colors .ui-site(@num, @color) { .ui-site-@{num} { background-color: @color; } } .ui-site(1, @ui-site-color-1); .ui-site(2, @ui-site-color-2); .ui-site(3, @ui-site-color-3); .ui-site(4, @ui-site-color-4); .ui-site(5, @ui-site-color-5); ================================================ FILE: packages/one-dark-ui/styles/status-bar.less ================================================ @status-bar-height: @ui-tab-height; // same as tabs @status-bar-padding: @ui-padding; .status-bar { font-size: @ui-size; height: @status-bar-height; line-height: @status-bar-height; background-color: @level-3-color; .flexbox-repaint-hack { padding: 0; // override default } // underlines should only be used for external links a:hover, a:focus { text-decoration: none; cursor: default; } .inline-block { margin: 0; // override default padding: 0 @status-bar-padding/2; vertical-align: top; &:hover { text-decoration: none; background-color: @level-3-color-hover; } &:active { background-color: @level-3-color-active; } // reset on child inline-block .inline-block { margin: 0; padding: 0; } } .status-bar-right { .inline-block { margin-left: 0; // override default } } .icon { vertical-align: middle; } .icon::before { font-size: 1.33333em; // should be 16px with a default of 12px width: auto; // use natural width line-height: 1; height: 1em; // same as line-height margin-right: .25em; top: auto; } } // Package overrides ------------------------------- .status-bar.status-bar { // Read-only -> Remove hover effect .is-read-only, // <- use this class in packages status-bar-launch-mode, busy-signal { &:hover, &:active, .inline-block:hover, .inline-block:active { background-color: transparent; } } // Remove underline .package-updates-status-view, .github-ChangedFilesCount { &:hover, &:focus { text-decoration: none; cursor: default; } } // Remove margin for icon without text status-bar-launch-mode::before, // Launch mode .about-release-notes::before, // New release squirrel .PortalStatusBarIndicator .icon::before, // Teletype .icon.is-icon-only::before { margin-right: 0; } .github-PushPull-label.is-push:empty { // GitHub package when nothing to push margin-right: -.25em; } } ================================================ FILE: packages/one-dark-ui/styles/tabs.less ================================================ // Tabs @tab-border: 1px solid @tab-border-color; @title-padding: .66em; @icon-padding-top: .5em; // 2.5 (total) - 1.5 (text) / 2 @icon-padding-right: .5em; .tab-bar { position: relative; height: @ui-tab-height; box-shadow: inset 0 -1px 0 @tab-border-color; background: @tab-bar-background-color; overflow-x: auto; overflow-y: hidden; border-radius: 0; &::-webkit-scrollbar { display: none; } &:empty { display: none; } // Tab ---------------------- .tab { position: relative; top: 0; padding: 0; margin: 0; height: inherit; font-size: inherit; line-height: @ui-tab-height; color: @tab-text-color; background-color: @tab-background-color; box-shadow: inherit; border-left: @tab-border; &.active { color: @tab-text-color-active; background-color: @tab-background-color-active; box-shadow: none; } &:first-of-type { border-left-color: transparent; } &:last-of-type { // use box-shadow to not take up any space box-shadow: inset 0 -1px 0 @tab-border-color, 1px 0 0 @base-border-color; } &.active:last-of-type { box-shadow: 1px 0 0 @base-border-color; } // Title ---------------------- .title { text-align: center; margin: 0 @title-padding; } // VCS coloring ---------------------- &:not(.active) .status-added { color: @tab-inactive-status-added; } &:not(.active) .status-modified { color: @tab-inactive-status-modified; } // Icons ---------------------- .title.title:before { margin-right: .3em; width: auto; height: auto; line-height: 1; font-size: 1.125em; vertical-align: -.0625em; // Adjust center for the 0.1em font-size increase } // Close icon ---------------------- .close-icon { top: @icon-padding-top; right: @icon-padding-right; z-index: 2; font-size: 1em; width: 1.5em; height: 1.5em; line-height: 1.5; text-align: center; border-radius: @component-border-radius; background-color: inherit; overflow: hidden; transform: scale(0); transition: transform .08s; &:hover { color: @accent-text-color; background-color: @accent-color; } &:active { background-color: fade(@accent-color, 50%); } &::before { z-index: 1; font-size: 1.1em; vertical-align: -.05em; // Adjust center for the 0.1em font-size increase width: auto; height: auto; pointer-events: none; } } &:hover .close-icon { transform: scale(1); transition-duration: .16s; } } // Modified icon ---------------------- .tab.modified { &:hover .close-icon { color: @accent-color; &:hover { color: @accent-bg-text-color; } } &:not(:hover) .close-icon { top: @icon-padding-top; right: @icon-padding-right; width: 1.5em; height: 1.5em; line-height: 1.5; color: @accent-color; border-radius: @component-border-radius; border: none; transform: scale(1); &::before { content: "\f052"; display: inline-block; } } } // Tabs in the docks ---------------------- atom-dock & { .tab.active { background-color: @tool-panel-background-color; } } // Dragging ---------------------- .tab.is-dragging { opacity: .5; .close-icon, &:before { visibility: hidden; } } .placeholder { position: relative; pointer-events: none; // bar &:before { z-index: 1; margin: 0; width: 2px; height: @ui-tab-height; background-color: @accent-color; } // arrow &:after { z-index: 0; top: @ui-tab-height/2; margin: -4px 0 0 -3px; border-radius: 0; border: 4px solid @accent-color; transform: rotate(45deg); background: transparent; } &:last-child { &:before { margin-left: -2px; } &:after { transform: none; margin-left: -10px; border-color: transparent @accent-color transparent transparent; } } } // Overrides ---------------------- // keep tabs same size when active .tab, .tab.active { padding-right: 0; .title { padding: 0; } } } // Active/focused pane marker -------------- atom-pane-axis > atom-pane.active, atom-pane-container > atom-pane.pane { .tab.active:before { content: ""; position: absolute; pointer-events: none; z-index: 2; top: 0; left: -1px; // cover left border bottom: 0; width: 2px; background: mix(@text-color, @tab-background-color-editor, 33%); } } .pane:focus-within { .tab.active:before { background: @accent-color; } } // hide marker in docks atom-dock .tab-bar .tab::before { display: none; } // Custom tabs -------------- .tab-bar .tab.active { &[data-type$="Editor"], &[data-type$="AboutView"], &[data-type$="TimecopView"], &[data-type$="StyleguideView"], &[data-type="MarkdownPreviewView"] { color: @tab-text-color-editor; background-color: @tab-background-color-editor; // Match syntax background color } } ================================================ FILE: packages/one-dark-ui/styles/text.less ================================================ h1, h2, h3 { line-height: 1em; margin-bottom: 15px } h1 { font-size: 2em; } h2 { font-size: 1.5em; } h3 { font-size: 1.2em; } p { line-height: 1.6; margin-bottom: 15px; } label { font-weight: normal; } pre { box-shadow: none; color: @text-color; background: @inset-panel-background-color; border-radius: @component-border-radius; border: none; margin: 0; } code { .text(highlight); background: @background-color-highlight; border-radius: @component-border-radius; } .selected { .text(highlight); } .text-smaller { font-size: 0.9em; } .text-subtle { .text(subtle); } .text-highlight { .text(highlight); } .text-error { .text(error); } .text-info { .text(info); &:hover { color: @text-color-info; } } .text-warning { .text(warning); &:hover { color: @text-color-warning; } } .text-success { .text(success); &:hover { color: @text-color-success; } } .highlight-mixin { padding: 1px 4px; border-radius: 2px; } .highlight { .highlight-mixin(); font-weight: 700; color: @text-color-highlight; background-color: @background-color-highlight; } .highlight-color(@name, @background-color) { .highlight-@{name} { .highlight-mixin(); font-weight: 500; color: white; text-shadow: 0 1px 0px hsla(0,0%,0%,.2); background-color: @background-color; } } .highlight-color( info, @background-color-info); .highlight-color(warning, @background-color-warning); .highlight-color( error, @background-color-error); .highlight-color(success, @background-color-success); .results-view .path-details.list-item { color: darken(@text-color-highlight, 18%); } ================================================ FILE: packages/one-dark-ui/styles/title-bar.less ================================================ .title-bar { height: 22px; // remove 1px since there is no border border-bottom: none; } ================================================ FILE: packages/one-dark-ui/styles/tooltips.less ================================================ .tooltip { white-space: nowrap; font-size: @ui-size*1.15; &.in { opacity: 1; transition: opacity .12s ease-out; } .tooltip-inner { line-height: 1; padding: @ui-padding*.5 @ui-padding*.65; border-radius: @component-border-radius; background-color: @tooltip-background-color; color: @tooltip-text-color; white-space: nowrap; max-width: none; } .keystroke { font-size: max(1em, @ui-size*.85); padding: .1em .4em; margin: 0 @ui-padding*-.35 0 @ui-padding*.25; border-radius: max(2px, @component-border-radius / 2); color: @tooltip-text-key-color; background: @tooltip-background-key-color; } &.top .tooltip-arrow { border-top-color: @tooltip-background-color; } &.top-left .tooltip-arrow { border-top-color: @tooltip-background-color; } &.top-right .tooltip-arrow { border-top-color: @tooltip-background-color; } &.right .tooltip-arrow { border-right-color: @tooltip-background-color; } &.left .tooltip-arrow { border-left-color: @tooltip-background-color; } &.bottom .tooltip-arrow { border-bottom-color: @tooltip-background-color; } &.bottom-left .tooltip-arrow { border-bottom-color: @tooltip-background-color; } &.bottom-right .tooltip-arrow { border-bottom-color: @tooltip-background-color; } } ================================================ FILE: packages/one-dark-ui/styles/tree-view.less ================================================ @tree-view-height: @ui-line-height; .tree-view { font-size: @ui-size; background: @tree-view-background-color; .project-root.project-root { &:before { height: @ui-tab-height; background-clip: padding-box; } & > .header .name { line-height: @ui-tab-height; } } // Selected state .selected:before { background: @background-color-selected; } // Focus + selected state &:focus { .selected.list-item > .name, // files .selected.list-nested-item > .list-item > .name, // folders .selected.list-nested-item > .header:before { // arrow icon color: contrast(@button-background-color-selected); } .selected:before { background: @button-background-color-selected; } } } .theme-one-dark-ui .tree-view .project-root.project-root::before { border-top: 1px solid transparent; background-clip: padding-box; } .tree-view-resizer { .tree-view-resize-handle { width: 8px; } } // Variable height, based on ems .list-group li:not(.list-nested-item), .list-tree li:not(.list-nested-item), .list-group li.list-nested-item > .list-item, .list-tree li.list-nested-item > .list-item { line-height: @tree-view-height; } .list-group .selected::before, .list-tree .selected::before { height: @tree-view-height; } // icon .list-group .icon, .list-tree .icon { display: inline-block; height: inherit; &::before { top: initial; line-height: inherit; height: inherit; vertical-align: top; } } // Arrow icon .list-group, .list-tree { .header.header.header.header::before { top: initial; line-height: inherit; height: inherit; vertical-align: top; font-size: inherit; } } .tree-view .project-root-header.project-root-header.project-root-header.project-root-header::before { line-height: @ui-tab-height; } ================================================ FILE: packages/one-dark-ui/styles/ui-mixins.less ================================================ // Pattern matching; ish is cray. // http://lesscss.org/#-pattern-matching-and-guard-expressions .text(normal) { font-weight: normal; color: @text-color; } .text(subtle) { font-weight: normal; color: @text-color-subtle; } .text(highlight) { font-weight: normal; color: @text-color-highlight; } .text(selected) { .text(highlight) } .text(info) { color: @text-color-info; } .text(success) { color: @text-color-success; } .text(warning) { color: @text-color-warning; } .text(error) { color: @text-color-error; } .focus() { outline: none; border-color: @accent-color; box-shadow: 0 0 0 1px @accent-color; } .valid() { border-color: @text-color-success; box-shadow: 0 0 0 1px @text-color-success; background-color: mix(@text-color-success, @input-background-color, 10%); } .invalid() { border-color: @text-color-error; box-shadow: 0 0 0 1px @text-color-error; background-color: mix(@text-color-error, @input-background-color, 10%); } ================================================ FILE: packages/one-dark-ui/styles/ui-variables-custom.less ================================================ // ONE dark UI colors // ---------------------------------------------- @import "syntax-variables"; .ui-syntax-color() { @syntax-background-color: hsl(220,24%,20%); } .ui-syntax-color(); // fallback color @ui-syntax-color: @syntax-background-color; // Color guards ----------------- @ui-s-h: hue(@ui-syntax-color); @ui-s-s: saturation(@ui-syntax-color); @ui-s-l: lightness(@ui-syntax-color); @ui-inv: 10%; // inverse lightness if below .ui-hue() when (@ui-s-s = 0) { @ui-hue: 220; } // Use blue hue when no saturation .ui-hue() when (@ui-s-s > 0) { @ui-hue: @ui-s-h; } .ui-hue(); .ui-saturation() when (@ui-s-h <= 80) { @ui-saturation: min(@ui-s-s, 5%); } // minimize saturation for brown .ui-saturation() when (@ui-s-h > 80) and (@ui-s-h < 160) { @ui-saturation: min(@ui-s-s, 12%); } // reduce saturation for green .ui-saturation() when (@ui-s-h >= 160) and (@ui-s-l < @ui-inv) { @ui-saturation: min(@ui-s-s, 48%); } // limit max saturation for very dark backgrounds .ui-saturation() when (@ui-s-h >= 160) and (@ui-s-l >= @ui-inv) { @ui-saturation: @ui-s-s; } .ui-saturation(); .ui-lightness() when (@ui-s-l < @ui-inv) { @ui-lightness: @ui-s-l + 8%; // increase lightness when too dark @ui-lightness-border: @ui-lightness*.3; } .ui-lightness() when (@ui-s-l >= @ui-inv) { @ui-lightness: min(@ui-s-l, 20%); // limit max lightness (for light syntax themes) @ui-lightness-border: @ui-lightness*.6; } .ui-lightness(); // Main colors ----------------- @ui-fg: hsl(@ui-hue, min(@ui-saturation, 18%), max(@ui-lightness*3, 66%) ); @ui-bg: hsl(@ui-hue, @ui-saturation, @ui-lightness); // normalized @syntax-background-color @ui-border: hsl(@ui-hue, @ui-saturation, @ui-lightness-border); // Custom variables // These variables are only used in this theme // ---------------------------------------------- @ui-theme-name: one-dark-ui; // Text (Custom) ----------------- @text-color-faded: fade(@text-color, 20%); @text-color-added: @text-color-success; // green @text-color-ignored: @text-color-subtle; // faded @text-color-modified: @text-color-warning; // orange @text-color-removed: @text-color-error; // red @text-color-renamed: @text-color-info; // blue // Background (Custom) ----------------- @level-1-color: lighten(@base-background-color, 6%); @level-2-color: @base-background-color; @level-3-color: darken(@base-background-color, 3%); @level-3-color-hover: lighten(@level-3-color, 6%); @level-3-color-active: lighten(@level-3-color, 3%); // Accent (Custom) ----------------- @accent-luma: luma( hsl(@ui-hue, 50%, 50%) ); // get lightness of current hue // used for marker, inputs (smaller things) @accent-color: mix( hsv( @ui-hue, 100%, 66%), hsl( @ui-hue, 100%, 70%), @accent-luma ); // mix hsv + hsl (favor mostly hsl) @accent-text-color: contrast(@accent-color, hsl(@ui-hue,100%,10%), #fff, 25% ); // used for button, tooltip (larger things) @accent-bg-color: mix( hsv( @ui-hue, 66%, 66%), hsl( @ui-hue, 66%, 60%), @accent-luma * 2 ); // mix hsv + hsl (favor hsl for dark, hsv for light colors) @accent-bg-text-color: contrast(@accent-bg-color, hsl(@ui-hue,100%,10%), #fff, 30% ); // used for text only @accent-only-text-color: mix( hsv( @ui-hue, 100%, 66%), hsl( @ui-hue, 100%, 77%), @accent-luma ); // mix hsv + hsl (favor mostly hsl) // Components (Custom) ----------------- @badge-background-color: lighten(@background-color-highlight, 6%); @button-text-color-selected: @accent-bg-text-color; @button-border-color-selected: @base-border-color; @checkbox-background-color: fade(@accent-bg-color, 33%); @input-background-color-focus: mix(@accent-bg-color, @input-background-color, 10%); @input-selection-color: mix(@accent-color, @input-background-color, 25%); @input-selection-color-focus: mix(@accent-color, @input-background-color, 50%); @overlay-backdrop-color: hsl(@ui-hue, @ui-saturation, @ui-lightness*0.2); @overlay-backdrop-opacity: .75; @progress-background-color: @accent-color; @scrollbar-color: lighten(@ui-syntax-color, 16%); @scrollbar-background-color: @level-3-color; // replaced `transparent` with a solid color to test https://github.com/atom/one-light-ui/issues/4 @scrollbar-color-editor: lighten(@ui-syntax-color, 16%); @scrollbar-background-color-editor: @ui-syntax-color; @tab-text-color: @text-color-subtle; @tab-text-color-active: @text-color-highlight; @tab-text-color-editor: contrast(@ui-syntax-color, darken(@ui-syntax-color, 50%), @text-color-highlight ); @tab-background-color-editor: @ui-syntax-color; @tab-inactive-status-added: fade(@text-color-success, 55%); @tab-inactive-status-modified: fade(@text-color-warning, 55%); @tooltip-background-color: @accent-bg-color; @tooltip-text-color: @accent-bg-text-color; @tooltip-text-key-color: @tooltip-background-color; @tooltip-background-key-color: @tooltip-text-color; // Sizes (Custom) ----------------- @ui-size: 1em; @ui-input-size: @ui-size*1.15; @ui-padding: @ui-size*1.5; @ui-padding-pane: @ui-size*.5; @ui-padding-icon: @ui-padding/3.3; @ui-line-height: @ui-size*2; @ui-tab-height: @ui-size*2.5; // Packages variables // These variables are used to override packages // ---------------------------------------------- @settings-list-background-color: darken(@level-2-color, 1.5%); @theme-config-box-shadow: inset 0 0 3px hsla(0, 0%, 100%, .4), 0 1px 3px hsla(0, 0%, 0%, .2); @theme-config-box-shadow-selected: inset 0 1px 3px hsla(0, 0%, 0%, .1); @theme-config-border-selected: hsla(0, 0%, 100%, .75); // Debug // Output variables to the top of the UI // ------------------------------------- // html:before { // content: "@{variable}"; // } ================================================ FILE: packages/one-dark-ui/styles/ui-variables.less ================================================ @import "ui-variables-custom.less"; // import colors and custom variables // ONE dark UI variables // ---------------------------------------------- // Official variables // These variables must be defined in every theme // Source: https://github.com/atom/atom/blob/master/static/variables/ui-variables.less // ---------------------------------------------- // Text ----------------- @text-color: @ui-fg; @text-color-subtle: fadeout(@text-color, 40%); @text-color-highlight: lighten(@text-color, 20%); @text-color-selected: white; @text-color-info: hsl(219, 79%, 66%); @text-color-success: hsl(140, 44%, 62%); @text-color-warning: hsl( 36, 60%, 72%); @text-color-error: hsl( 9, 100%, 64%); // Background ----------------- @background-color-info: hsl(208, 88%, 48%); @background-color-success: hsl(132, 58%, 40%); @background-color-warning: hsl( 42, 88%, 36%); @background-color-error: hsl( 5, 64%, 50%); @background-color-highlight: lighten(@base-background-color, 4%); @background-color-selected: lighten(@base-background-color, 8%); @app-background-color: @level-3-color; // Base ----------------- @base-background-color: @ui-bg; @base-border-color: @ui-border; // Components ----------------- @pane-item-background-color: @base-background-color; @pane-item-border-color: @base-border-color; @input-background-color: darken(@base-background-color, 6%); @input-border-color: @base-border-color; @tool-panel-background-color: @level-3-color; @tool-panel-border-color: @base-border-color; @inset-panel-background-color: lighten(@level-2-color, 4%); @inset-panel-border-color: fadeout(@base-border-color, 15%); @panel-heading-background-color: @level-2-color; @panel-heading-border-color: @base-border-color; @overlay-background-color: mix(@level-2-color, @level-3-color); @overlay-border-color: @base-border-color; @button-background-color: @level-1-color; @button-background-color-hover: lighten(@button-background-color, 2%); @button-background-color-selected: @accent-bg-color; @button-border-color: @base-border-color; @tab-bar-background-color: @level-3-color; @tab-bar-border-color: @base-border-color; @tab-background-color: @level-3-color; @tab-background-color-active: @level-2-color; @tab-border-color: @base-border-color; @tree-view-background-color: @level-3-color; @tree-view-border-color: @base-border-color; @ui-site-color-1: hsl(208, 100%, 50%); // blue @ui-site-color-2: hsl(160, 70%, 42%); // green @ui-site-color-3: hsl(32, 60%, 50%); // orange @ui-site-color-4: #D831B0; // pink @ui-site-color-5: #EBDD5B; // yellow // Sizes ----------------- @font-size: 12px; @input-font-size: 14px; @disclosure-arrow-size: 12px; @component-padding: 10px; @component-icon-padding: 5px; @component-icon-size: 16px; // needs to stay 16px to look sharpest @component-line-height: 25px; @component-border-radius: 3px; @tab-height: 30px; // Font ----------------- @font-family: system-ui; ================================================ FILE: packages/one-light-syntax/LICENSE.md ================================================ Copyright (c) 2016 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/one-light-syntax/README.md ================================================ ## One Light Syntax theme ![one-syntax-light](https://cloud.githubusercontent.com/assets/378023/7783214/c146b4e6-0174-11e5-8377-a57cf0274d5d.png) > The font used in the screenshot is [Fira Mono](https://github.com/mozilla/Fira). There is also a matching [UI theme](../one-light-ui). ### Install This theme is installed by default with Atom and can be activated by going to the __Settings > Themes__ section and selecting it from the __Syntax Themes__ drop-down menu. ================================================ FILE: packages/one-light-syntax/index.less ================================================ // Atom Syntax Theme: One @import "styles/syntax-variables.less"; @import "styles/editor.less"; @import "styles/syntax-legacy/_base.less"; // @import "styles/syntax-legacy/c.less"; // @import "styles/syntax-legacy/cpp.less"; @import "styles/syntax-legacy/cs.less"; @import "styles/syntax-legacy/css.less"; @import "styles/syntax-legacy/elixir.less"; @import "styles/syntax-legacy/gfm.less"; // @import "styles/syntax-legacy/go.less"; @import "styles/syntax-legacy/ini.less"; @import "styles/syntax-legacy/java.less"; // @import "styles/syntax-legacy/javascript.less"; @import "styles/syntax-legacy/typescript.less"; @import "styles/syntax-legacy/json.less"; @import "styles/syntax-legacy/ng.less"; // @import "styles/syntax-legacy/ruby.less"; @import "styles/syntax-legacy/php.less"; // @import "styles/syntax-legacy/python.less"; @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; ================================================ FILE: packages/one-light-syntax/package.json ================================================ { "name": "one-light-syntax", "theme": "syntax", "version": "1.8.4", "description": "Atom One light syntax theme", "keywords": [ "light", "syntax" ], "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.40.0" } } ================================================ FILE: packages/one-light-syntax/styles/colors.less ================================================ // Config ----------------------------------- @syntax-hue: 230; @syntax-saturation: 1%; @syntax-brightness: 98%; // Monochrome ----------------------------------- @mono-1: hsl(@syntax-hue, 8%, 24%); @mono-2: hsl(@syntax-hue, 6%, 44%); @mono-3: hsl(@syntax-hue, 4%, 64%); // Colors ----------------------------------- @hue-1: hsl(198, 99%, 37%); // <-cyan @hue-2: hsl(221, 87%, 60%); // <-blue @hue-3: hsl(301, 63%, 40%); // <-purple @hue-4: hsl(119, 34%, 47%); // <-green @hue-5: hsl( 5, 74%, 59%); // <-red 1 @hue-5-2: hsl(344, 84%, 43%); // <-red 2 @hue-6: hsl(35, 99%, 36%); // <-orange 1 @hue-6-2: hsl(35, 99%, 40%); // <-orange 2 // Base colors ----------------------------------- @syntax-fg: @mono-1; @syntax-bg: hsl(@syntax-hue, @syntax-saturation, @syntax-brightness); @syntax-gutter: darken(@syntax-bg, 36%); @syntax-guide: fade(@syntax-fg, 20%); @syntax-accent: hsl(@syntax-hue, 100%, 66% ); ================================================ FILE: packages/one-light-syntax/styles/editor.less ================================================ // Editor styles (background, gutter, guides) atom-text-editor { background-color: @syntax-background-color; color: @syntax-text-color; .line.cursor-line { background-color: @syntax-cursor-line; } .invisible { color: @syntax-text-color; } .cursor { border-left: 2px solid @syntax-cursor-color; } .selection .region { background-color: @syntax-selection-color; } .bracket-matcher .region { border-bottom: 1px solid @syntax-cursor-color; box-sizing: border-box; } .invisible-character { color: @syntax-invisible-character-color; } .indent-guide { color: @syntax-indent-guide-color; } .wrap-guide { background-color: @syntax-wrap-guide-color; } // find + replace .find-result .region.region.region, .current-result .region.region.region { border-radius: 2px; background-color: @syntax-result-marker-color; transition: border-color .4s; } .find-result .region.region.region { border: 2px solid transparent; } .current-result .region.region.region { border: 2px solid @syntax-result-marker-color-selected; transition-duration: .1s; } .gutter { .line-number { color: @syntax-gutter-text-color; -webkit-font-smoothing: antialiased; &.cursor-line { color: @syntax-gutter-text-color-selected; background-color: @syntax-gutter-background-color-selected; } &.cursor-line-no-selection { background-color: transparent; } .icon-right { color: @syntax-text-color; } } &:not(.git-diff-icon) .line-number.git-line-removed { &.git-line-removed::before { bottom: -3px; } &::after { content: ""; position: absolute; left: 0px; bottom: 0px; width: 25px; border-bottom: 1px dotted fade(@syntax-color-removed, 50%); pointer-events: none; } } } .gutter .line-number.folded, .gutter .line-number:after, .fold-marker:after { color: @syntax-gutter-text-color-selected; } } ================================================ FILE: packages/one-light-syntax/styles/syntax/base.less ================================================ /* This defines styling rules for syntax classes. See the naming conventions for a list of syntax classes: https://flight-manual.atom.io/hacking-atom/sections/syntax-naming-conventions When styling rules conflict: - The last rule overrides previous rules. - The rule with most classes and pseudo-classes overrides the last rule. */ // if for return global let .syntax--keyword { color: @hue-3; // int char float &.syntax--type { color: @hue-1; } // and or new del &.syntax--operator { color: @hue-3; } // super &.syntax--function { color: @hue-5; } // this self &.syntax--variable { color: @hue-5; } // = + && | << ? &.syntax--symbolic { color: @mono-1; } } // identifier .syntax--entity { color: @mono-1; // function(parameter) &.syntax--parameter { color: @mono-1; } // self cls iota &.syntax--support { color: @hue-5; } // @entity.decorator &.syntax--decorator:last-child { color: @hue-2; } // label: &.syntax--label { text-decoration: underline; } // function method &.syntax--function { color: @hue-2; } // add &.syntax--operator { color: @hue-2; // %>% <=> &.syntax--symbolic { color: @mono-1; } } // String Class int rune list &.syntax--type { color: @hue-1; } // div span &.syntax--tag { color: @hue-5; } // href src alt &.syntax--attribute { color: @hue-6; } } // () [] {} => @ .syntax--punctuation { color: @mono-1; // . -> :: [] &.syntax--accessor { color: @mono-1; } } // "string" .syntax--string { color: @hue-4; // :immutable &.syntax--immutable { color: @hue-4; } // {placeholder} %().2f &.syntax--part { color: @hue-1; } // ${ } &.syntax--interpolation { color: @mono-1; } // /^reg[ex]?p/ &.syntax--regexp { color: @hue-4; // ^ $ \b ? + i &.syntax--language { color: @hue-3; } // \1 &.syntax--variable { color: @hue-2; } // ( ) [^ ] (?= ) | &.syntax--punctuation { color: @hue-3; } } } // literal 4 1.3 true nil .syntax--constant { color: @hue-6; // < 'a' &.syntax--character { color: @hue-4; // \" \' \g \. &.syntax--escape { color: @hue-4; } // \u2661 \n \t \W . &.syntax--code { color: @hue-1; } } } // text .syntax--text { color: @mono-1; } // __formatted__ .syntax--markup { // # Heading &.syntax--heading { color: @hue-5; } // 1. * - &.syntax--list.syntax--punctuation { color: @hue-5; } // > quote &.syntax--quote { color: @mono-3; font-style: italic; } // **bold** &.syntax--bold { color: @hue-6; font-weight: bold; } // *italic* &.syntax--italic { color: @hue-3; font-style: italic; } // __underline__ &.syntax--underline { color: @hue-1; text-decoration: underline; } // ~~strike~~ &.syntax--strike { color: @hue-5; } // `raw` &.syntax--raw { color: @hue-4; } // url.com (path) &.syntax--link { color: @hue-1; } // [alt] ![alt] &.syntax--alt { color: @hue-2; } // {++ inserted ++} &.syntax--inserted { color: @hue-4; .syntax--punctuation { color: @hue-4; } } // {== highlighted ==} &.syntax--highlighted { color: @hue-4; .syntax--punctuation { color: @hue-4; } } // {-- deleted --} &.syntax--deleted { color: @hue-5; .syntax--punctuation { color: @hue-5; } } // {~~ from~>to ~~} &.syntax--changed { color: @hue-3; .syntax--punctuation { color: @hue-3; } } // {>> commented <<} &.syntax--commented { color: @mono-3; .syntax--punctuation { color: @mono-3; } } } // /* comment */ .syntax--comment { color: @mono-3; font-style: italic; // @param TODO NOTE &.syntax--caption { color: lighten(@mono-3, 6); font-weight: bold; } // variable function type &.syntax--term { color: lighten(@mono-3, 9); } // { } / . &.syntax--punctuation { color: @mono-3; font-weight: normal; } } // 0invalid .syntax--invalid:not(.syntax--punctuation) { // §illegal &.syntax--illegal { color: @syntax-illegal-fg !important; background-color: @syntax-illegal-bg !important; } // obsolete() &.syntax--deprecated { color: @syntax-deprecated-fg !important; background-color: @syntax-deprecated-bg !important; } } ================================================ FILE: packages/one-light-syntax/styles/syntax/css.less ================================================ .syntax--source.syntax--css { .syntax--entity { // function() &.syntax--function { color: @mono-2; // url rgb &.syntax--support { color: @hue-1; } } // .class :pseudo-class attribute &.syntax--selector { color: @hue-6; // div span &.syntax--tag { color: @hue-5; } // #id &.syntax--id { color: @hue-2; } } // property: constant &.syntax--property { color: @mono-2; // height position border &.syntax--support { color: @mono-1; } } // --variable &.syntax--variable { color: @hue-5; } } // property: constant .syntax--constant { color: @mono-2; // flex solid bold &.syntax--support { color: @mono-1; } // 3px 4em &.syntax--numeric { color: @hue-6; } // screen print &.syntax--media { color: @hue-6; } // #b294bb blue red &.syntax--color { color: @hue-6; } // from to &.syntax--offset { color: @mono-1; } // [attribute=attribute-value] &.syntax--attribute-value { color: @hue-4; } } .syntax--punctuation { // . : :: &.syntax--selector { color: @hue-6; // * &.syntax--wildcard { color: @hue-5; } // # &.syntax--id { color: @hue-2; } // [] &.syntax--attribute { color: @mono-1; } } } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/_base.less ================================================ // Language syntax highlighting .syntax--comment { color: @mono-3; font-style: italic; .syntax--markup.syntax--link { color: @mono-3; } } .syntax--entity { &.syntax--name.syntax--type { color: @hue-6-2; } &.syntax--other.syntax--inherited-class { color: @hue-6-2; } } .syntax--keyword { color: @hue-3; &.syntax--control { color: @hue-3; } &.syntax--operator { color: @mono-1; } &.syntax--other.syntax--special-method { color: @hue-2; } &.syntax--other.syntax--unit { color: @hue-6; } } .syntax--storage { color: @hue-3; &.syntax--type { &.syntax--annotation, &.syntax--primitive { color: @hue-3; } } &.syntax--modifier { &.syntax--package, &.syntax--import { color: @mono-1; } } } .syntax--constant { color: @hue-6; &.syntax--variable { color: @hue-6; } &.syntax--character.syntax--escape { color: @hue-1; } &.syntax--numeric { color: @hue-6; } &.syntax--other.syntax--color { color: @hue-1; } &.syntax--other.syntax--symbol { color: @hue-1; } } .syntax--variable { color: @hue-5; &.syntax--interpolation { color: @hue-5-2; } &.syntax--parameter { color: @mono-1; } } .syntax--string { color: @hue-4; > .syntax--source, .syntax--embedded { color: @mono-1; } &.syntax--regexp { color: @hue-1; .syntax--source.syntax--ruby.syntax--embedded { color: @hue-6-2; } } &.syntax--other.syntax--link { color: @hue-5; } } .syntax--punctuation { &.syntax--definition { &.syntax--comment { color: @mono-3; } &.syntax--method-parameters, &.syntax--function-parameters, &.syntax--parameters, &.syntax--separator, &.syntax--seperator, &.syntax--array { color: @mono-1; } &.syntax--heading, &.syntax--identity { color: @hue-2; } &.syntax--bold { color: @hue-6-2; font-weight: bold; } &.syntax--italic { color: @hue-3; font-style: italic; } } &.syntax--section { &.syntax--embedded { color: @hue-5-2; } &.syntax--method, &.syntax--class, &.syntax--inner-class { color: @mono-1; } } } .syntax--support { &.syntax--class { color: @hue-6-2; } &.syntax--type { color: @hue-1; } &.syntax--function { color: @hue-1; &.syntax--any-method { color: @hue-2; } } } .syntax--entity { &.syntax--name.syntax--function { color: @hue-2; } &.syntax--name.syntax--class, &.syntax--name.syntax--type.syntax--class { color: @hue-6-2; } &.syntax--name.syntax--section { color: @hue-2; } &.syntax--name.syntax--tag { color: @hue-5; } &.syntax--other.syntax--attribute-name { color: @hue-6; &.syntax--id { color: @hue-2; } } } .syntax--meta { &.syntax--class { color: @hue-6-2; &.syntax--body { color: @mono-1; } } &.syntax--method-call, &.syntax--method { color: @mono-1; } &.syntax--definition { &.syntax--variable { color: @hue-5; } } &.syntax--link { color: @hue-6; } &.syntax--require { color: @hue-2; } &.syntax--selector { color: @hue-3; } &.syntax--separator { color: @mono-1; } &.syntax--tag { color: @mono-1; } } .syntax--underline { text-decoration: underline; } .syntax--none { color: @mono-1; } .syntax--invalid { &.syntax--deprecated { color: @syntax-deprecated-fg !important; background-color: @syntax-deprecated-bg !important; } &.syntax--illegal { color: @syntax-illegal-fg !important; background-color: @syntax-illegal-bg !important; } } // Languages ------------------------------------------------- .syntax--markup { &.syntax--bold { color: @hue-6; font-weight: bold; } &.syntax--changed { color: @hue-3; } &.syntax--deleted { color: @hue-5; } &.syntax--italic { color: @hue-3; font-style: italic; } &.syntax--heading { color: @hue-5; .syntax--punctuation.syntax--definition.syntax--heading { color: @hue-2; } } &.syntax--link { color: @hue-1; } &.syntax--inserted { color: @hue-4; } &.syntax--quote { color: @hue-6; } &.syntax--raw { color: @hue-4; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/c.less ================================================ .syntax--source.syntax--c { .syntax--keyword.syntax--operator { color: @hue-3; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/cpp.less ================================================ .syntax--source.syntax--cpp { .syntax--keyword.syntax--operator { color: @hue-3; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/cs.less ================================================ .syntax--source.syntax--cs { .syntax--keyword.syntax--operator { color: @hue-3; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/css.less ================================================ .syntax--source.syntax--css { // highlight properties/values if they are supported .syntax--property-name, .syntax--property-value { color: @mono-2; &.syntax--support { color: @mono-1; } } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/elixir.less ================================================ .syntax--source.syntax--elixir { .syntax--source.syntax--embedded.syntax--source { color: @mono-1; } .syntax--constant.syntax--language, .syntax--constant.syntax--numeric, .syntax--constant.syntax--definition { color: @hue-2; } .syntax--variable.syntax--definition, .syntax--variable.syntax--anonymous{ color: @hue-3; } .syntax--parameter.syntax--variable.syntax--function { color: @hue-6; font-style: italic; } .syntax--quoted{ color: @hue-4; } .syntax--keyword.syntax--special-method, .syntax--embedded.syntax--section, .syntax--embedded.syntax--source.syntax--empty, { color: @hue-5; } .syntax--readwrite.syntax--module { .syntax--punctuation { color: @hue-5; } } .syntax--regexp.syntax--section, .syntax--regexp.syntax--string { color: @hue-5-2; } .syntax--separator, .syntax--keyword.syntax--operator { color: @hue-6; } .syntax--variable.syntax--constant { color: @hue-6-2; } .syntax--array, .syntax--scope, .syntax--section { color: @mono-2; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/gfm.less ================================================ .syntax--source.syntax--gfm { .syntax--markup { -webkit-font-smoothing: auto; } .syntax--link .syntax--entity { color: @hue-2; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/go.less ================================================ .syntax--source.syntax--go { .syntax--storage.syntax--type.syntax--string { color: @hue-3; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/ini.less ================================================ .syntax--source.syntax--ini { .syntax--keyword.syntax--other.syntax--definition.syntax--ini { color: @hue-5; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/java.less ================================================ .syntax--source.syntax--java { .syntax--storage { &.syntax--modifier.syntax--import { color: @hue-6-2; } &.syntax--type { color: @hue-6-2; } } .syntax--keyword.syntax--operator.syntax--instanceof { color: @hue-3; } } .syntax--source.syntax--java-properties { .syntax--meta.syntax--key-pair { color: @hue-5; & > .syntax--punctuation { color: @mono-1; } } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/javascript.less ================================================ .syntax--source.syntax--js { .syntax--keyword.syntax--operator { color: @hue-1; // keywords are definded in https://github.com/atom/language-javascript/blob/master/grammars/javascript.cson // search "instanceof" for location &.syntax--delete, &.syntax--in, &.syntax--of, &.syntax--instanceof, &.syntax--new, &.syntax--typeof, &.syntax--void { color: @hue-3; } } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/json.less ================================================ .syntax--source.syntax--json { .syntax--meta.syntax--structure.syntax--dictionary.syntax--json { & > .syntax--string.syntax--quoted.syntax--json { & > .syntax--punctuation.syntax--string { color: @hue-5; } color: @hue-5; } } .syntax--meta.syntax--structure.syntax--dictionary.syntax--json, .syntax--meta.syntax--structure.syntax--array.syntax--json { & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json, & > .syntax--value.syntax--json > .syntax--string.syntax--quoted.syntax--json > .syntax--punctuation { color: @hue-4; } & > .syntax--constant.syntax--language.syntax--json { color: @hue-1; } } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/ng.less ================================================ .syntax--ng { &.syntax--interpolation { color: @hue-5; &.syntax--begin, &.syntax--end { color: @hue-2; } .syntax--function { color: @hue-5; &.syntax--begin, &.syntax--end { color: @hue-2; } } .syntax--bool { color: @hue-6; } .syntax--bracket { color: @mono-1; } } &.syntax--pipe, &.syntax--operator { color: @mono-1; } &.syntax--tag { color: @hue-1; } &.syntax--attribute-with-value { .syntax--attribute-name { color: @hue-6-2; } .syntax--string { color: @hue-3; &.syntax--begin, &.syntax--end { color: @mono-1; } } } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/php.less ================================================ .syntax--source.syntax--php { .syntax--class.syntax--bracket { color: @mono-1; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/python.less ================================================ .syntax--source.syntax--python { .syntax--keyword.syntax--operator.syntax--logical.syntax--python { color: @hue-3; } .syntax--variable.syntax--parameter { color: @hue-6; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/ruby.less ================================================ .syntax--source.syntax--ruby { .syntax--constant.syntax--other.syntax--symbol > .syntax--punctuation { color: inherit; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-legacy/typescript.less ================================================ .syntax--source.syntax--ts { .syntax--keyword.syntax--operator { color: @hue-1; } } .syntax--source.syntax--flow { .syntax--keyword.syntax--operator { color: @hue-1; } } ================================================ FILE: packages/one-light-syntax/styles/syntax-variables.less ================================================ @import "colors.less"; // Official Syntax Variables ----------------------------------- // General colors @syntax-text-color: @syntax-fg; @syntax-cursor-color: @syntax-accent; @syntax-selection-color: darken(@syntax-bg, 8%); @syntax-selection-flash-color: @syntax-accent; @syntax-background-color: @syntax-bg; // Guide colors @syntax-wrap-guide-color: @syntax-guide; @syntax-indent-guide-color: @syntax-guide; @syntax-invisible-character-color: @syntax-guide; // For find and replace markers @syntax-result-marker-color: fade(@syntax-accent, 20%); @syntax-result-marker-color-selected: @syntax-accent; // Gutter colors ----------------------------------- @syntax-gutter-text-color: @syntax-gutter; @syntax-gutter-text-color-selected: @syntax-fg; @syntax-gutter-background-color: @syntax-bg; // unused @syntax-gutter-background-color-selected: darken(@syntax-bg, 8%); // Git colors - For git diff info. i.e. in the gutter @syntax-color-renamed: hsl(208, 100%, 66%); @syntax-color-added: hsl(132, 60%, 44%); @syntax-color-modified: hsl( 40, 90%, 50%); @syntax-color-removed: hsl( 0, 100%, 54%); // For language entity colors @syntax-color-variable: @hue-5; @syntax-color-constant: @hue-6; @syntax-color-property: @syntax-fg; @syntax-color-value: @syntax-fg; @syntax-color-function: @hue-2; @syntax-color-method: @hue-2; @syntax-color-class: @hue-6-2; @syntax-color-keyword: @hue-3; @syntax-color-tag: @hue-5; @syntax-color-attribute: @hue-6; @syntax-color-import: @hue-3; @syntax-color-snippet: @hue-4; // Custom Syntax Variables ----------------------------------- // Don't use in packages @syntax-cursor-line: fade(@syntax-fg, 5%); // needs to be semi-transparent to show search results @syntax-deprecated-fg: darken(@syntax-color-modified, 50%); @syntax-deprecated-bg: @syntax-color-modified; @syntax-illegal-fg: white; @syntax-illegal-bg: @syntax-color-removed; ================================================ FILE: packages/one-light-ui/.gitignore ================================================ node_modules ================================================ FILE: packages/one-light-ui/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/one-light-ui/README.md ================================================ ## One Light UI theme A light UI theme that adapts to most syntax themes. ![One light UI](https://cloud.githubusercontent.com/assets/378023/26246819/0826f04e-3cd6-11e7-98eb-cd94bc48b090.png) > The font used in the screenshot is [Fira Mono](https://github.com/mozilla/Fira). ### Install This theme comes bundled with Atom and can be activated by going to the __Settings > Themes__ section and selecting "One Light" from the __UI Themes__ drop-down menu. ### Settings In the theme settings you can: - Change the __Font Size__ to scale the whole UI up or down. - Choose between 3 __Tab Sizing__ modes. - Hide the __dock buttons__. To make changes, go to `Settings > Themes > One Light UI > Settings` or the cog icon next to the theme picker. ### Customize It's also possible to resize only certain areas by adding the following to your `styles.less` (Use DevTools to find the right selectors): ```css .theme-one-light-ui { .tab-bar { font-size: 18px; } .tree-view { font-size: 14px; } .status-bar { font-size: 12px; } } ``` ### FAQ __Why do the colors change when I switch Syntax themes.__ This UI theme uses the same background color as the chosen syntax theme. If that syntax theme has a dark background color, it only uses its hue, but otherwise stays light. This lets you use light-dark combos. ================================================ FILE: packages/one-light-ui/index.less ================================================ // Atom UI Theme: One @import "styles/ui-variables.less"; @import "styles/ui-mixins.less"; @import "octicon-mixins.less"; // https://github.com/atom/atom/blob/master/static/variables/octicon-mixins.less @import "styles/atom.less"; @import "styles/badges.less"; @import "styles/buttons.less"; @import "styles/docks.less"; @import "styles/editor.less"; @import "styles/git.less"; @import "styles/inputs.less"; @import "styles/lists.less"; @import "styles/messages.less"; @import "styles/nav.less"; @import "styles/notifications.less"; @import "styles/modal.less"; @import "styles/panels.less"; @import "styles/panes.less"; @import "styles/progress.less"; @import "styles/tabs.less"; @import "styles/text.less"; @import "styles/title-bar.less"; @import "styles/tooltips.less"; @import "styles/tree-view.less"; @import "styles/status-bar.less"; @import "styles/key-binding.less"; @import "styles/sites.less"; @import "styles/settings.less"; @import "styles/packages.less"; @import "styles/core.less"; @import "styles/config.less"; ================================================ FILE: packages/one-light-ui/lib/main.js ================================================ const root = document.documentElement; const themeName = 'one-light-ui'; module.exports = { activate(state) { atom.config.observe(`${themeName}.fontSize`, setFontSize); atom.config.observe(`${themeName}.tabSizing`, setTabSizing); atom.config.observe(`${themeName}.tabCloseButton`, setTabCloseButton); atom.config.observe(`${themeName}.hideDockButtons`, setHideDockButtons); atom.config.observe(`${themeName}.stickyHeaders`, setStickyHeaders); }, deactivate() { unsetFontSize(); unsetTabSizing(); unsetTabCloseButton(); unsetHideDockButtons(); unsetStickyHeaders(); } }; // Font Size ----------------------- function setFontSize(currentFontSize) { root.style.fontSize = `${currentFontSize}px`; } function unsetFontSize() { root.style.fontSize = ''; } // Tab Sizing ----------------------- function setTabSizing(tabSizing) { root.setAttribute(`theme-${themeName}-tabsizing`, tabSizing.toLowerCase()); } function unsetTabSizing() { root.removeAttribute(`theme-${themeName}-tabsizing`); } // Tab Close Button ----------------------- function setTabCloseButton(tabCloseButton) { if (tabCloseButton === 'Left') { root.setAttribute(`theme-${themeName}-tab-close-button`, 'left'); } else { unsetTabCloseButton(); } } function unsetTabCloseButton() { root.removeAttribute(`theme-${themeName}-tab-close-button`); } // Dock Buttons ----------------------- function setHideDockButtons(hideDockButtons) { if (hideDockButtons) { root.setAttribute(`theme-${themeName}-dock-buttons`, 'hidden'); } else { unsetHideDockButtons(); } } function unsetHideDockButtons() { root.removeAttribute(`theme-${themeName}-dock-buttons`); } // Sticky Headers ----------------------- function setStickyHeaders(stickyHeaders) { if (stickyHeaders) { root.setAttribute(`theme-${themeName}-sticky-headers`, 'sticky'); } else { unsetStickyHeaders(); } } function unsetStickyHeaders() { root.removeAttribute(`theme-${themeName}-sticky-headers`); } ================================================ FILE: packages/one-light-ui/package.json ================================================ { "name": "one-light-ui", "theme": "ui", "version": "1.12.5", "description": "Atom One light UI theme", "keywords": [ "light", "adaptive", "ui" ], "license": "MIT", "repository": "https://github.com/atom/atom", "main": "lib/main", "engines": { "atom": ">0.40.0" }, "devDependencies": { "standard": "^11.0.0" }, "configSchema": { "fontSize": { "title": "Font Size", "description": "Change the font size for the UI.", "type": "integer", "default": 12, "enum": [ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ], "order": 1 }, "tabSizing": { "title": "Tab Sizing", "description": "In Even mode all tabs will be the same size. Great for quickly closing many tabs. In Maximum mode the tabs will expand to take up the full width. In Minimum mode the tabs will only take as little space as needed and also show longer file names.", "type": "string", "default": "Even", "enum": [ "Even", "Maximum", "Minimum" ], "order": 2 }, "tabCloseButton": { "title": "Tab Close Button", "description": "Choose the position of the close button shown in tabs.", "type": "string", "default": "Right", "enum": [ "Left", "Right" ], "order": 3 }, "hideDockButtons": { "title": "Hide dock toggle buttons", "description": "Note: When hiding the toggle buttons, opening a dock needs to be done by using the keyboard or other alternatives.", "type": "boolean", "default": "false", "order": 4 }, "stickyHeaders": { "title": "Make tree-view project headers sticky", "type": "boolean", "default": "false", "order": 5 } } } ================================================ FILE: packages/one-light-ui/spec/theme-spec.js ================================================ const themeName = 'one-light-ui'; describe(`${themeName} theme`, () => { beforeEach(() => { waitsForPromise(() => atom.packages.activatePackage(themeName)); }); it('allows the font size to be set via config', () => { expect(document.documentElement.style.fontSize).toBe('12px'); atom.config.set(`${themeName}.fontSize`, '10'); expect(document.documentElement.style.fontSize).toBe('10px'); }); it('allows the tab sizing to be set via config', () => { atom.config.set(`${themeName}.tabSizing`, 'Maximum'); expect( document.documentElement.getAttribute(`theme-${themeName}-tabsizing`) ).toBe('maximum'); }); it('allows the tab sizing to be set via config', () => { atom.config.set(`${themeName}.tabSizing`, 'Minimum'); expect( document.documentElement.getAttribute(`theme-${themeName}-tabsizing`) ).toBe('minimum'); }); it('allows the tab close button to be shown on the left via config', () => { atom.config.set(`${themeName}.tabCloseButton`, 'Left'); expect( document.documentElement.getAttribute( `theme-${themeName}-tab-close-button` ) ).toBe('left'); }); it('allows the dock toggle buttons to be hidden via config', () => { atom.config.set(`${themeName}.hideDockButtons`, true); expect( document.documentElement.getAttribute(`theme-${themeName}-dock-buttons`) ).toBe('hidden'); }); it('allows the tree-view headers to be sticky via config', () => { atom.config.set(`${themeName}.stickyHeaders`, true); expect( document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`) ).toBe('sticky'); }); it('allows the tree-view headers to not be sticky via config', () => { atom.config.set(`${themeName}.stickyHeaders`, false); expect( document.documentElement.getAttribute(`theme-${themeName}-sticky-headers`) ).toBe(null); }); }); ================================================ FILE: packages/one-light-ui/styles/atom.less ================================================ * { box-sizing: border-box; } html { font-size: @font-size; } atom-workspace { background-color: @app-background-color; } // Scrollbars ------------------------------------ .scrollbars-visible-always { ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: @scrollbar-background-color; } ::-webkit-scrollbar-thumb { border-radius: 5px; border: 3px solid @scrollbar-background-color; background: @scrollbar-color; background-clip: content-box; } ::-webkit-scrollbar-corner { background: @scrollbar-background-color; } ::-webkit-scrollbar-thumb:vertical:active { border-radius: 0; border-left-width: 0; border-right-width: 0; } ::-webkit-scrollbar-thumb:horizontal:active { border-radius: 0; border-top-width: 0; border-bottom-width: 0; } atom-text-editor { ::-webkit-scrollbar-track { background: @scrollbar-background-color-editor; } ::-webkit-scrollbar-corner { background: @scrollbar-background-color-editor; } ::-webkit-scrollbar-thumb { border-color: @scrollbar-background-color-editor; background: @scrollbar-color-editor; } } } // TODO: Move to a better place, not sure where it gets used .caret { border-top: 5px solid #fff; margin-top: -1px; } ================================================ FILE: packages/one-light-ui/styles/badges.less ================================================ .badge { padding: @ui-padding/4 @ui-padding/2.5; min-width: @ui-padding*1.25; .text(highlight); border-radius: @ui-size*2; background-color: @badge-background-color; // Icon ---------------------- &.icon { font-size: @ui-size; padding: @ui-padding-icon @ui-padding-icon*1.5; } } ================================================ FILE: packages/one-light-ui/styles/buttons.less ================================================ @btn-border: 1px solid @button-border-color; @btn-padding: 0 @ui-size/1.25; // Mixins ----------------------- .btn-default (@color, @hover-color, @selected-color, @text-color) { color: @text-color; text-shadow: none; border: @btn-border; background-color: @color; background-image: linear-gradient(lighten(@color, 2%), @color); &:hover { color: @text-color-highlight; background-image: linear-gradient(lighten(@hover-color, 2%), @hover-color); } &:active { background: darken(@color, 4%); box-shadow: none; } &.selected { background: @selected-color; } &.selected:focus, &.selected:hover { background: lighten(@selected-color, 2%); } &:focus { .focus(); // unfortunately :focus styles stay even after releasing mouse. } } .btn-variant (@color) { @_text-color: contrast(@color, white, hsl(0,0%,20%), 33% ); .btn-default( @color, lighten(@color, 3%), saturate(darken(@color, 12%), 20%), @text-color-highlight ); color: @_text-color; & when (@ui-lightness > 50%) { border-color: transparent; // hide border on light backgrounds } &:hover, &:focus { color: @_text-color; } &:focus { border-color: transparent; background-clip: padding-box; box-shadow: inset 0 0 0 1px fade(@base-border-color, 50%), 0 0 0 1px @color; } &.icon:before { color: @_text-color; } } // Buttons ----------------------- .btn { height: initial; padding: @btn-padding; font-size: @ui-size; line-height: @ui-line-height; } .btn, .btn.btn-default { .btn-default(@button-background-color, @button-background-color-hover, @button-background-color-selected, @text-color); } .btn.btn-primary { .btn-variant(@accent-bg-color); } .btn.btn-info { .btn-variant(@background-color-info); } .btn.btn-success { .btn-variant(@background-color-success); } .btn.btn-warning { .btn-variant(@background-color-warning); } .btn.btn-error { .btn-variant(@background-color-error); } // Button Sizes ----------------------- .btn.btn-xs, .btn-group-xs > .btn { font-size: @ui-size*.8; line-height: @ui-line-height; padding: @btn-padding; } .btn.btn-sm, .btn-group-sm > .btn { font-size: @ui-size*.9; line-height: @ui-line-height; padding: @btn-padding; } .btn.btn-lg, .btn-group-lg > .btn { font-size: @ui-size * 1.5; line-height: @ui-line-height; padding: @btn-padding; } // Button Group ----------------------- .btn-group > .btn { z-index: 0; &:hover { z-index: 0; } &.btn:focus { z-index: 1; .focus(); } &:first-child { border-left: @btn-border; } &:last-child, &.selected:last-child { border-right: @btn-border; } // hide border on light backgrounds & when (@ui-lightness > 50%) { &.btn-primary:first-child, &.btn-info:first-child, &.btn-success:first-child, &.btn-warning:first-child, &.btn-error:first-child { border-left-color: transparent; } &.btn-primary:last-child, &.btn-info:last-child, &.btn-success:last-child, &.btn-warning:last-child, &.btn-error:last-child { border-right-color: transparent; } } &.selected, &.selected:first-child, &.selected:last-child { color: @button-text-color-selected; border-color: @button-border-color-selected; } & when (@ui-lightness > 50%) { &.selected + .btn { border-left-color: @button-border-color-selected; } &.selected + .selected { border-left-color: mix(@button-border-color, @button-border-color-selected); } } &.selected:focus { border-color: @button-background-color-selected; box-shadow: inset 0 0 0 1px fade(@base-border-color, 50%), 0 0 0 1px @button-background-color-selected; } } // Button Icons ----------------------- .btn.icon:before { width: auto; height: auto; font-size: 1.333333em; vertical-align: -.1em; } ================================================ FILE: packages/one-light-ui/styles/config.less ================================================ // Theme config // This gets changed from the theme settings @theme-tabsizing: ~'theme-@{ui-theme-name}-tabsizing'; @theme-dockButtons: ~'theme-@{ui-theme-name}-dock-buttons'; @theme-stickyHeaders: ~'theme-@{ui-theme-name}-sticky-headers'; @theme-closeButton: ~'theme-@{ui-theme-name}-tab-close-button'; // Tabs ---------------------------------------------- @tab-min-width: 7em; // ~ icon + 6 characters // Even (default) .tab-bar { .tab, .tab.active { flex: 1 1 0; max-width: 22em; min-width: @tab-min-width; } atom-dock & { .tab, .tab.active { max-width: none; } } // TODO: Turn this into a config // Truncates the beginning instead // .title.title.title { // direction: rtl; // change direction // } } // Maximum (full width) [@{theme-tabsizing}="maximum"] .tab-bar { .tab, .tab.active { max-width: none; } } // Minimum (show long paths) [@{theme-tabsizing}="minimum"] .tab-bar { .tab, .tab.active { flex: 0 0 auto; min-width: 2.75em; max-width: @tab-min-width * 3.3; } atom-dock { .tab, .tab.active { max-width: @tab-min-width * 2; } } } // Tabs: close button position ------------------------------ [@{theme-closeButton}="left"] { .tab-bar .tab { .close-icon { right: auto; left: @icon-padding-right; } } } // Hide docks toggle buttons ------------------------------ [@{theme-dockButtons}="hidden"] { // Hide docks when not open .atom-dock-inner:not(.atom-dock-open) { display: none; } // Hide toggle buttons .atom-dock-toggle-button { display: none; } } // Sticky Projects ------------------------------ [@{theme-stickyHeaders}="sticky"] { .tree-view { .project-root-header { position: sticky; top: 0; z-index: 3; padding-left: 5px; padding-right: 10px; border-bottom: 1px solid @base-border-color; background-color: @tree-view-background-color; } .project-root.project-root { margin-left: -5px; margin-right: -10px; // Disable selection &::before { display: none; } // Add selection back &.selected .project-root-header { background-color: @background-color-selected; } } &:focus .selected .project-root-header.project-root-header { background: @button-background-color-selected; } // Fix sticky header from covering auto-revealed files .entry.file.selected { padding-top: @ui-tab-height; margin-top: -@ui-tab-height; } // Fix sticky header from covering auto-revealed directories when using up/down keys // for directories, scroll test moves to .header, see https://github.com/atom/tree-view/blob/d2857ad4d7eeb7dad5cf94b33257a8740211480e/lib/tree-view.coffee#L839 .entry.directory.selected:not(.project-root) { & > .header { padding-top: @ui-tab-height; margin-top: -@ui-tab-height; } &::before { margin-top: @ui-tab-height; } } // Fix above directory is not being clickable .entry.directory:not(.project-root) > .header { z-index: 2; } .entry.directory.selected:not(.project-root) > .header { z-index: 1; } } } ================================================ FILE: packages/one-light-ui/styles/core.less ================================================ // Misc .preview-pane .results-view .path-match-number { // show number also on selected item color: inherit; opacity: .6; } .tool-panel.incompatible-packages { // incompatible-packages isn't really a tool-panel and more a whole pane .text(normal); background-color: @level-2-color; } // Styleguide ---------------------------------------------- .styleguide { // Modal atom-panel.modal:after { position: absolute; // prevent overlay backdrop from leaking outside left: -@ui-padding; right: -@ui-padding; bottom: -@ui-padding; } } ================================================ FILE: packages/one-light-ui/styles/docks.less ================================================ // Docks ------------------------------ // Make handles not take up any space when dock is open .atom-dock-resize-handle { position: absolute; z-index: 11; // same as toggle buttons &.left { top: 0; right: 0; bottom: 0; } &.right { top: 0; left: 0; bottom: 0; } &.bottom { top: 0; left: 0; right: 0; } } // Add borders .atom-dock-inner.atom-dock-open.left { border-right: 1px solid @base-border-color; } .atom-dock-inner.atom-dock-open.right { border-left: 1px solid @base-border-color; } // Make toggle buttons cover ^ border .atom-dock-toggle-button.left { margin-left: -2px; } .atom-dock-toggle-button.right { margin-right: -2px; } .atom-dock-inner:not(.atom-dock-open) .atom-dock-toggle-button.bottom { margin-bottom: -1px; } ================================================ FILE: packages/one-light-ui/styles/dropdowns.less ================================================ .dropdown-menu { background-color: @overlay-background-color; border-radius: @component-border-radius; border: 1px solid @base-border-color; padding: 0; > li > a { .text(normal); } > li > a:hover { .text(highlight); background-color: @background-color-highlight; } } ================================================ FILE: packages/one-light-ui/styles/editor.less ================================================ // Editor in a panel // TODO: Find a better selector, maybe a new class like atom-text-editor[medium] atom-panel-container atom-text-editor.is-focused { .focus(); } // Mini // Usually just single line inputs atom-text-editor[mini] { overflow: auto; font-size: @ui-input-size; line-height: @ui-line-height; max-height: @ui-line-height * 5; // rows padding-left: @ui-padding/3; border-radius: @component-border-radius; color: @text-color-highlight; border: 1px solid @input-border-color; background-color: @input-background-color; .placeholder-text { color: @text-color-subtle; } .selection .region { background-color: @input-selection-color; } .cursor { border-color: @accent-color; border-width: 2px; } &.is-focused { .focus(); background-color: @input-background-color-focus; .selection .region { background-color: @input-selection-color-focus; } } } ================================================ FILE: packages/one-light-ui/styles/git.less ================================================ .status { .text(normal); } .status-added { .text(success); } // green .status-ignored { .text(subtle); } // faded .status-modified { .text(warning); } // orange .status-removed { .text(error); } // red .status-renamed { .text(info); } // blue ================================================ FILE: packages/one-light-ui/styles/inputs.less ================================================ // // Checkbox // ------------------------- .input-checkbox { &:active { background-color: @accent-color; } &:before, &:after { background-color: @accent-text-color; } &:checked { background-color: @accent-color; } &:indeterminate { background-color: @accent-color; } } // // Radio // ------------------------- .input-radio { &:before { background-color: @accent-text-color; } &:active { background-color: @accent-color; } &:checked { background-color: @accent-color; } } // // Range (Slider) // ------------------------- .input-range { &::-webkit-slider-thumb { background-color: @accent-color; } } // // Toggle // ------------------------- .input-toggle { &:checked { background-color: @accent-color; } &:before { background-color: @accent-text-color; } } // States ------------------------- .input-checkbox, .input-text, .input-search, .input-number, .input-textarea, .input-select, .input-color { &:focus { .focus(); } } .input-text, .input-search, .input-number, .input-textarea { &:invalid { .invalid(); } } ================================================ FILE: packages/one-light-ui/styles/key-binding.less ================================================ .key-binding { display: inline-block; margin-left: @ui-padding-icon; padding: 0 @ui-padding/4; line-height: 2; font-family: inherit; font-size: max(1em, @ui-size*.85); letter-spacing: @ui-size/10; border-radius: @component-border-radius; color: @accent-bg-text-color; background-color: @accent-bg-color; } ================================================ FILE: packages/one-light-ui/styles/lists.less ================================================ .list-group, .list-tree { li:not(.list-nested-item), li.list-nested-item > .list-item { .text(normal); } .generate-list-item-text-color(@class) { li:not(.list-nested-item).text-@{class}, li.list-nested-item.text-@{class} > .list-item { .text(@class); } } .generate-list-item-text-color(subtle); .generate-list-item-text-color(info); .generate-list-item-text-color(success); .generate-list-item-text-color(warning); .generate-list-item-text-color(error); .generate-list-item-text-color(selected); .generate-list-item-status-color(@color, @status) { li:not(.list-nested-item).status-@{status}, li.list-nested-item.status-@{status} > .list-item { color: @color; } li:not(.list-nested-item).selected.status-@{status}, li.list-nested-item.selected.status-@{status} > .list-item { color: @color; } } .generate-list-item-status-color(@text-color-added, added); .generate-list-item-status-color(@text-color-ignored, ignored); .generate-list-item-status-color(@text-color-modified, modified); .generate-list-item-status-color(@text-color-removed, removed); .generate-list-item-status-color(@text-color-renamed, renamed); li:not(.list-nested-item).selected, li.list-nested-item.selected > .list-item { .text(selected); } .no-icon { padding-left: calc(@ui-padding-icon ~"+" @component-icon-size); } } .list-tree.has-collapsable-children .list-nested-item > .list-item::before { text-align: center; } .select-list ol.list-group, &.select-list ol.list-group { li.two-lines { .secondary-line { color: @text-color-subtle; } &.selected .secondary-line { color: fade(@text-color-highlight, 50%); text-shadow: none; } } // Reset icon to allow nesting .icon { display: initial; height: initial; } // We want to highlight the background of the list items because we dont // know their size. li.selected { background-color: @background-color-selected; &:before{ display: none; } } &.mark-active { @active-icon-size: 14px; // pad in front of the text where the icon would be We'll pad the non- // active items with a 'fake' icon so other classes can pad the item // without worrying about the icon padding. li:before { content: ''; background-color: transparent; position: static; display: inline-block; left: auto; right: auto; height: @active-icon-size; width: @active-icon-size; font-size: @active-icon-size; } > li:not(.active):before { margin-right: @ui-padding-icon; } li.active { .octicon(check, @active-icon-size); &:before { margin-right: @ui-padding-icon; color: @text-color-success; } } } } .select-list.popover-list { @popover-list-padding: @ui-padding/4; background-color: @overlay-background-color; box-shadow: 0 2px 8px 1px rgba(0, 0, 0, 0.3); padding: @popover-list-padding; border-radius: @component-border-radius; atom-text-editor[mini] { margin-bottom: @popover-list-padding; } ol.list-group { margin-top: @popover-list-padding; } .list-group li { padding-left: @popover-list-padding; } // Un-reset icon in popover lists .icon.icon { display: inline-block; height: inherit; } } .ui-sortable { li { line-height: 2.5; } // For sortable lists in the settings view li.ui-sortable-placeholder { visibility: visible !important; background-color: darken(@pane-item-background-color, 10%); } } li.ui-draggable-dragging, li.ui-sortable-helper { line-height: @ui-line-height; height: @ui-line-height; border: 0; border-radius: 0; list-style: none; padding: 0 @ui-padding; background: @background-color-highlight; box-shadow: 0 0 1px @base-border-color; } ================================================ FILE: packages/one-light-ui/styles/messages.less ================================================ background-tips ul.background-message { font-weight: 500; font-size: 2em; color: @text-color-faded; .message { padding: 0 @component-padding * 10; .keystroke { white-space: nowrap; vertical-align: middle; line-height: 1; padding: .1em .4em; } } } ================================================ FILE: packages/one-light-ui/styles/modal.less ================================================ @modal-padding: @ui-padding/2 @ui-padding/1.5; @modal-width: @ui-size * 50; atom-panel-container.modal { position: absolute; top: 0; left: 0; right: 0; } atom-panel.modal { position: relative; width: 100%; max-width: @modal-width; margin: 0 auto; left: initial; color: @text-color; background-color: transparent; padding: @ui-padding/2; &.from-top { top: @component-padding * 5; } atom-text-editor[mini] { margin-bottom: @ui-padding/2; } .select-list ol.list-group, &.select-list ol.list-group { border: 1px solid @overlay-border-color; background-color: lighten(@overlay-background-color, 2%); &:empty { border: none; margin-top: 0; } li { padding: @modal-padding; line-height: @ui-line-height; border-bottom: 1px solid @overlay-border-color; &:last-of-type { border-bottom: none; } .icon::before { margin-left: 1px; } .icon.status { float: right; margin-left: @ui-padding-icon; &:before { margin-left: 0; margin-right: 0; } } &.selected { .status.icon { color: @text-color-selected; } } } } .select-list .key-binding { margin-top: -1px; margin-left: @ui-padding/2; margin-right: calc( -@ui-padding/3 ~"+" 1px); } .select-list .primary-line { display: block; } & > * { position: relative; // fixes stacking order } .command-palette { padding: 1px; // prevents the box-shadow of the input from being cut off background-color: @overlay-background-color; } // Container &:before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0; background-color: @overlay-background-color; border-radius: @component-border-radius*2; box-shadow: 0 6px 12px -2px hsla(0,0%,0%,.4); } // Backdrop // TODO: Add extra wrapper to translate individually or easier positioning &:after { content: ""; position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; background: @overlay-backdrop-color; opacity: @overlay-backdrop-opacity; backface-visibility: hidden; // fixes scrollbar on retina screens -webkit-animation: overlay-fade .24s cubic-bezier(0.215, 0.61, 0.355, 1); } @-webkit-keyframes overlay-fade { 0% { opacity: 0; } 100% { opacity: @overlay-backdrop-opacity; } } } ================================================ FILE: packages/one-light-ui/styles/nav.less ================================================ .nav-tabs { border-bottom: 1px solid @base-border-color; li { a, &.active a { border: none; margin-right: 0px; margin-bottom: 1px; } a:hover, &.active a, &.active a:hover { background-color: @background-color-highlight; border: none; color: @text-color-selected; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } &.active a { background-color: @tab-background-color-active; } } } ================================================ FILE: packages/one-light-ui/styles/notifications.less ================================================ atom-notifications { font-size: @ui-size * 1.2; atom-notification { width: 32em; &.has-detail { width: 32em; } &:first-child.has-close .message { padding-right: 9em; } &:only-child.has-close .message, &.has-close .message { padding-right: 2.5em; } .item { padding: @ui-padding/2; } .detail, .description { font-size: .85em; } &.icon:before { padding-top: .85em; } .close { width: 2.5em; height: 3em; line-height: 3em; font-size: inherit; } .close-all.btn { top: .5em; right: 2.5em; } .btn-copy-report { line-height: 2em; margin-left: .5em; } } } ================================================ FILE: packages/one-light-ui/styles/packages.less ================================================ // Overrides packages // find-and-replace + project-find --------------------------- .find-and-replace, .project-find { padding: @ui-padding/4; .input-block-item { padding: @ui-padding/4; } } // find-and-replace .find-and-replace { .header, .input-block { min-width: @ui-size*22; } .input-block-item { flex: 1 1 @ui-size*22; } .input-block-item--flex { flex: 100 1 @ui-size*22; } .btn, .btn-group-options .btn { font-size: @ui-size*1.1; padding: 0; } .btn-group-options .btn, .btn-group-options .btn.option-selection, .btn-group-options .btn.option-whole-word { padding: 0; font-size: @ui-input-size; // keep same as text input } .find-container atom-text-editor { padding-right: @ui-size*5; // leave some room for the results count } .find-meta-container { top: 0; font-size: @ui-size; line-height: @ui-size*2.5; } } // project-find .project-find { .header, .input-block { min-width: @ui-size*15; } .input-block-item { flex: 1 1 @ui-size*14; } .input-block-item--flex { flex: 100 1 @ui-size*20; } .btn { font-size: @ui-size*1.1; padding: 0; } .btn-group-options .btn { padding: 0; font-size: @ui-input-size; // keep same as text input } } // Colorize find-and-replace based on results & when (@ui-hue >= 190) and (@ui-hue <= 340) { .find-and-replace { &.has-no-results .find-container atom-text-editor[mini].is-focused { .invalid(); .selection .region { background-color: mix(@text-color-error, @input-background-color, 50%); } .cursor { border-color: @text-color-error; } } &.has-results .find-container atom-text-editor[mini].is-focused { .valid(); .selection .region { background-color: mix(@text-color-success, @input-background-color, 50%); } .cursor { border-color: @text-color-success; } } &.has-results .find-container .result-counter { color: @text-color-success; } &.has-no-results .find-container .result-counter { color: @text-color-error; } } } // Timecop --------------------------- .timecop { .timecop-panel { padding: @component-padding/2; background-color: @level-2-color; } .tool-panel { padding: @component-padding/2; background-color: @level-2-color; } .inset-panel { border: 1px solid @base-border-color; } .panel-heading { .text(highlight); border-color: @base-border-color; background-color: @level-1-color; } .list-item .inline-block { line-height: 1.5; } } // Command Palette + Fuzzy Finder --------------------------- .command-palette .list-group .character-match, .fuzzy-finder .list-group .character-match { color: @accent-only-text-color; } // Deprecation Cop --------------------------- .deprecation-cop { .deprecation-overview { background-color: @level-2-color; border-bottom: 1px solid @base-border-color; } } // Tool Bar --------------------------- .tool-bar { // Make it look the same as other panels background-color: @level-3-color; border: none; // just a single border + more spacing &.tool-bar-horizontal .tool-bar-spacer { border-left: 0 none; margin-left: .5em; margin-right: .5em; } &.tool-bar-vertical .tool-bar-spacer { border-bottom: 0 none; margin-top: .5em; margin-bottom: .5em; } // only show button styles on hover button.tool-bar-btn { background-color: @level-3-color; background-image: none; border-color: @level-3-color; } } // GitHub package --------------------------------------------------- .github { // Fix focus styles // Since it's not possible to add a padding to // a pseudo element is used to add the border when focused. &-CommitView-editor atom-text-editor.is-focused { box-shadow: none; &:before { content: ""; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border: 2px solid; border-color: inherit; border-radius: @component-border-radius; } } // Add focus styles since :focus doesn't work &-CommitView-coAuthorEditor { &.is-focused { .focus(); } &.is-open { border-top-left-radius: 0; border-top-right-radius: 0; } .Select-option { &.is-focused { border-bottom-left-radius: 0; border-bottom-right-radius: 0; color: @accent-text-color; background-color: @accent-color; } } .Select-menu-outer { left: -2px; right: -2px; bottom: 100%; border: 2px solid @accent-color; background-color: @overlay-background-color; } } } ================================================ FILE: packages/one-light-ui/styles/panels.less ================================================ // Panels atom-panel { .text(normal); position: relative; border-bottom: 1px solid @base-border-color; &.top { border-right: 1px solid @base-border-color; } &.left { border-right: 1px solid @base-border-color; } &.right { border-left: 1px solid @base-border-color; } &.bottom { border-right: 1px solid @base-border-color; } &.footer:last-child { border-bottom: none; } &.tool-panel:empty { border: none; } } .panel { &.bordered { border: 1px solid @base-border-color; border-radius: @component-border-radius; } } .inset-panel { position: relative; background-color: @inset-panel-background-color; border-radius: @component-border-radius; &.bordered { border: 1px solid @base-border-color; border-radius: @component-border-radius; } & .panel-heading { border-color: @inset-panel-border-color; } } .panel-heading { .text(normal); border-bottom: 1px solid @panel-heading-border-color; background-color: @panel-heading-background-color; .btn { padding-left: 8px; padding-right: 8px; .btn-default( lighten(@button-background-color, 10%), lighten(@button-background-color-hover, 10%), lighten(@button-background-color-selected, 10%), lighten(@text-color, 10%) ); } } ================================================ FILE: packages/one-light-ui/styles/panes.less ================================================ atom-pane-container { atom-pane { position: relative; border-right: 1px solid @base-border-color; border-bottom: 1px solid @base-border-color; .item-views { // prevent atom-text-editor from leaking ouside might improve performance overflow: hidden; } } } // Hide right-most border atom-pane:only-child, atom-pane-axis.pane-row > atom-pane:last-child, atom-pane-axis.pane-column:last-child > atom-pane { border-right: none; } ================================================ FILE: packages/one-light-ui/styles/progress.less ================================================ // Spinner ---------------------- @spinner-duration: 1.2s; .loading-spinner(@size) { position: relative; display: block; width: 1em; height: 1em; font-size: @size; background: radial-gradient(@accent-color .1em, transparent .11em); &::before, &::after { content: ""; position: absolute; z-index: 10; // prevent sibling elements from getting their own layers top: 0; left: 0; border-radius: 1em; width: inherit; height: inherit; border-radius: 1em; border: 2px solid; -webkit-animation: spinner-animation @spinner-duration infinite; -webkit-animation-fill-mode: backwards; } &::before { border-color: @accent-color transparent transparent transparent; } &::after { border-color: transparent lighten(@accent-color, 15%) transparent transparent; -webkit-animation-delay: @spinner-duration/2; } &.inline-block { display: inline-block; } } @-webkit-keyframes spinner-animation { 0% { transform: rotateZ( 0deg); -webkit-animation-timing-function: cubic-bezier(0, 0, .8, .2); } 50% { transform: rotateZ(180deg); -webkit-animation-timing-function: cubic-bezier(.2, .8, 1, 1); } 100% { transform: rotateZ(360deg); } } // Spinner sizes .loading-spinner-tiny { .loading-spinner(16px); &::before, &::after { border-width: 1px; } } .loading-spinner-small { .loading-spinner(32px); } .loading-spinner-medium { .loading-spinner(48px); } .loading-spinner-large { .loading-spinner(64px); } // Progress Bar ---------------------- @progress-height: 8px; @progress-buffer-color: fade(@progress-background-color, 20%); progress { -webkit-appearance: none; height: @progress-height; border-radius: @component-border-radius; background-color: @input-background-color; box-shadow: inset 0 0 0 1px @input-border-color; &::-webkit-progress-bar { background-color: transparent; } &::-webkit-progress-value { border-radius: @component-border-radius; background-color: @progress-background-color; } // Is buffering (when no value is set) &:indeterminate { background-image: linear-gradient(-45deg, transparent 33%, @progress-buffer-color 33%, @progress-buffer-color 66%, transparent 66%); background-size: 25px @progress-height, 100% 100%, 100% 100%; // Plays animation for 1min (12runs) at normal speed, // then slows down frame-rate for 9mins (108runs) to limit CPU usage -webkit-animation: progress-buffering 5s linear 12, progress-buffering 5s 60s steps(10) 108; } } @-webkit-keyframes progress-buffering { 100% { background-position: -100px 0px; } } ================================================ FILE: packages/one-light-ui/styles/settings.less ================================================ // Settings // Modular Scale (1.125): http://www.modularscale.com/?1&em&1.125&web&table @ms-6: @ui-size * 2.027; @ms-5: @ui-size * 1.802; @ms-4: @ui-size * 1.602; @ms-3: @ui-size * 1.424; @ms-2: @ui-size * 1.266; @ms-1: @ui-size * 1.125; @ms-0: @ui-size * 1; @ms_1: @ui-size * 0.889; @ms_2: @ui-size * 0.790; .settings-view { // Menu ------------------------------ .config-menu { position: relative; min-width: @ui-size * 15; max-width: @ui-size * 20; border-width: 0 1px 0 0; border-image: linear-gradient(@level-2-color 10px, @base-border-color 200px) 0 1 0 0 stretch; background: @level-2-color; .btn { white-space: initial; font-size: @ms_1; line-height: 1; padding: @ui-padding/3 @ui-padding/2; &::before { vertical-align: middle; } } } .nav { & > li > a { padding: @ui-padding/2 @ui-padding; line-height: @ui-line-height; } } // Sections ------------------------------ & > .panels { background-color: @level-2-color; } .section-container { max-width: @ui-size*60; } .section, .section:first-child, .section:last-child { padding: @ui-padding*3; } .themes-panel .control-group { margin-top: @ui-padding*2; } // Titles ------------------------------ .section .section-heading { margin-bottom: @ui-padding/1.5; } .sub-section-heading.icon:before, .section-heading.icon:before { margin-right: @ui-padding-icon; } // Cards ------------------------------ .sub-section:not(.collapsed) .package-container { padding-bottom: @component-padding*3; } .package-card { padding: @ui-padding; .meta-controls .status-indicator { width: @ui-padding/4; &:before { content: "\00a0"; // fixes 0 height } } } // Components ------------------------------ .icon::before { color: @text-color-subtle; } .editor-container { margin: @ui-padding 0; } .form-control { font-size: @ui-size*1.25; height: @ui-line-height; padding-top: 0; padding-bottom: 0; } .update-all-button { font-size: .75em; } .install-button { .btn-variant(@accent-bg-color); } input[type="checkbox"] { background-color: @background-color-selected; &:active, &:checked { background-color: @accent-color; } &:before, &:after { background-color: @accent-text-color; } } .search-container .btn { font-size: @ui-input-size; } } ================================================ FILE: packages/one-light-ui/styles/sites.less ================================================ // Site Colors .ui-site(@num, @color) { .ui-site-@{num} { background-color: @color; } } .ui-site(1, @ui-site-color-1); .ui-site(2, @ui-site-color-2); .ui-site(3, @ui-site-color-3); .ui-site(4, @ui-site-color-4); .ui-site(5, @ui-site-color-5); ================================================ FILE: packages/one-light-ui/styles/status-bar.less ================================================ @status-bar-height: @ui-tab-height; // same as tabs @status-bar-padding: @ui-padding; .status-bar { font-size: @ui-size; height: @status-bar-height; line-height: @status-bar-height; background-color: @level-3-color; .flexbox-repaint-hack { padding: 0; // override default } // underlines should only be used for external links a:hover, a:focus { text-decoration: none; cursor: default; } .inline-block { margin: 0; // override default padding: 0 @status-bar-padding/2; vertical-align: top; &:hover { text-decoration: none; background-color: @level-3-color-hover; } &:active { background-color: @level-3-color-active; } // reset on child inline-block .inline-block { margin: 0; padding: 0; } } .status-bar-right { .inline-block { margin-left: 0; // override default } } .icon { vertical-align: middle; } .icon::before { font-size: 1.33333em; // should be 16px with a default of 12px width: auto; // use natural width line-height: 1; height: 1em; // same as line-height margin-right: .25em; top: auto; } } // Package overrides ------------------------------- .status-bar.status-bar { // Read-only -> Remove hover effect .is-read-only, // <- use this class in packages status-bar-launch-mode, busy-signal { &:hover, &:active, .inline-block:hover, .inline-block:active { background-color: transparent; } } // Remove underline .package-updates-status-view, .github-ChangedFilesCount { &:hover, &:focus { text-decoration: none; cursor: default; } } // Remove margin for icon without text status-bar-launch-mode::before, // Launch mode .about-release-notes::before, // New release squirrel .PortalStatusBarIndicator .icon::before, // Teletype .icon.is-icon-only::before { margin-right: 0; } .github-PushPull-label.is-push:empty { // GitHub package when nothing to push margin-right: -.25em; } } ================================================ FILE: packages/one-light-ui/styles/tabs.less ================================================ // Tabs @tab-border: 1px solid @tab-border-color; @title-padding: .66em; @icon-padding-top: .5em; // 2.5 (total) - 1.5 (text) / 2 @icon-padding-right: .5em; .tab-bar { position: relative; height: @ui-tab-height; box-shadow: inset 0 -1px 0 @tab-border-color; background: @tab-bar-background-color; overflow-x: auto; overflow-y: hidden; border-radius: 0; &::-webkit-scrollbar { display: none; } &:empty { display: none; } // Tab ---------------------- .tab { position: relative; top: 0; padding: 0; margin: 0; height: inherit; font-size: inherit; line-height: @ui-tab-height; color: @tab-text-color; background-color: @tab-background-color; box-shadow: inherit; border-left: @tab-border; &.active { color: @tab-text-color-active; background-color: @tab-background-color-active; box-shadow: none; } &:first-of-type { border-left-color: transparent; } &:last-of-type { // use box-shadow to not take up any space box-shadow: inset 0 -1px 0 @tab-border-color, 1px 0 0 @base-border-color; } &.active:last-of-type { box-shadow: 1px 0 0 @base-border-color; } // Title ---------------------- .title { text-align: center; margin: 0 @title-padding; } // VCS coloring ---------------------- &:not(.active) .status-added { color: @tab-inactive-status-added; } &:not(.active) .status-modified { color: @tab-inactive-status-modified; } // Icons ---------------------- .title.title:before { margin-right: .3em; width: auto; height: auto; line-height: 1; font-size: 1.125em; vertical-align: -.0625em; // Adjust center for the 0.1em font-size increase } // Close icon ---------------------- .close-icon { top: @icon-padding-top; right: @icon-padding-right; z-index: 2; font-size: 1em; width: 1.5em; height: 1.5em; line-height: 1.5; text-align: center; border-radius: @component-border-radius; background-color: inherit; overflow: hidden; transform: scale(0); transition: transform .08s; &:hover { color: @accent-text-color; background-color: @accent-color; } &:active { background-color: fade(@accent-color, 50%); } &::before { z-index: 1; font-size: 1.1em; vertical-align: -.05em; // Adjust center for the 0.1em font-size increase width: auto; height: auto; pointer-events: none; } } &:hover .close-icon { transform: scale(1); transition-duration: .16s; } } // Modified icon ---------------------- .tab.modified { &:hover .close-icon { color: @accent-color; &:hover { color: @accent-bg-text-color; } } &:not(:hover) .close-icon { top: @icon-padding-top; right: @icon-padding-right; width: 1.5em; height: 1.5em; line-height: 1.5; color: @accent-color; border-radius: @component-border-radius; border: none; transform: scale(1); &::before { content: "\f052"; display: inline-block; } } } // Tabs in the docks ---------------------- atom-dock & { .tab.active { background-color: @tool-panel-background-color; } } // Dragging ---------------------- .tab.is-dragging { opacity: .5; .close-icon, &:before { visibility: hidden; } } .placeholder { position: relative; pointer-events: none; // bar &:before { z-index: 1; margin: 0; width: 2px; height: @ui-tab-height; background-color: @accent-color; } // arrow &:after { z-index: 0; top: @ui-tab-height/2; margin: -4px 0 0 -3px; border-radius: 0; border: 4px solid @accent-color; transform: rotate(45deg); background: transparent; } &:last-child { &:before { margin-left: -2px; } &:after { transform: none; margin-left: -10px; border-color: transparent @accent-color transparent transparent; } } } // Overrides ---------------------- // keep tabs same size when active .tab, .tab.active { padding-right: 0; .title { padding: 0; } } } // Active/focused pane marker -------------- atom-pane-axis > atom-pane.active, atom-pane-container > atom-pane.pane { .tab.active:before { content: ""; position: absolute; pointer-events: none; z-index: 2; top: 0; left: -1px; // cover left border bottom: 0; width: 2px; background: mix(@text-color, @tab-background-color-editor, 33%); } } .pane:focus-within { .tab.active:before { background: @accent-color; } } // hide marker in docks atom-dock .tab-bar .tab::before { display: none; } // Custom tabs -------------- .tab-bar .tab.active { &[data-type$="Editor"], &[data-type$="AboutView"], &[data-type$="TimecopView"], &[data-type$="StyleguideView"], &[data-type="MarkdownPreviewView"] { color: @tab-text-color-editor; background-color: @tab-background-color-editor; // Match syntax background color } } ================================================ FILE: packages/one-light-ui/styles/text.less ================================================ h1, h2, h3 { line-height: 1em; margin-bottom: 15px } h1 { font-size: 2em; } h2 { font-size: 1.5em; } h3 { font-size: 1.2em; } p { line-height: 1.6; margin-bottom: 15px; } label { font-weight: normal; } pre { box-shadow: none; color: @text-color; background: @inset-panel-background-color; border-radius: @component-border-radius; border: none; margin: 0; } code { .text(highlight); background: @background-color-highlight; border-radius: @component-border-radius; } .selected { .text(highlight); } .text-smaller { font-size: 0.9em; } .text-subtle { .text(subtle); } .text-highlight { .text(highlight); } .text-error { .text(error); } .text-info { .text(info); &:hover { color: @text-color-info; } } .text-warning { .text(warning); &:hover { color: @text-color-warning; } } .text-success { .text(success); &:hover { color: @text-color-success; } } .highlight-mixin { padding: 1px 4px; border-radius: 2px; } .highlight { .highlight-mixin(); font-weight: 700; color: @text-color-highlight; background-color: @background-color-highlight; } .highlight-color(@name, @background-color) { .highlight-@{name} { .highlight-mixin(); font-weight: 500; color: white; text-shadow: 0 1px 0px hsla(0,0%,0%,.2); background-color: @background-color; } } .highlight-color( info, @background-color-info); .highlight-color(warning, @background-color-warning); .highlight-color( error, @background-color-error); .highlight-color(success, @background-color-success); .results-view .path-details.list-item { color: darken(@text-color-highlight, 18%); } ================================================ FILE: packages/one-light-ui/styles/title-bar.less ================================================ .title-bar { height: 22px; // remove 1px since there is no border border-bottom: none; } ================================================ FILE: packages/one-light-ui/styles/tooltips.less ================================================ .tooltip { white-space: nowrap; font-size: @ui-size*1.15; &.in { opacity: 1; transition: opacity .12s ease-out; } .tooltip-inner { line-height: 1; padding: @ui-padding*.5 @ui-padding*.65; border-radius: @component-border-radius; background-color: @tooltip-background-color; color: @tooltip-text-color; white-space: nowrap; max-width: none; } .keystroke { font-size: max(1em, @ui-size*.85); padding: .1em .4em; margin: 0 @ui-padding*-.35 0 @ui-padding*.25; border-radius: max(2px, @component-border-radius / 2); color: @tooltip-text-key-color; background: @tooltip-background-key-color; } &.top .tooltip-arrow { border-top-color: @tooltip-background-color; } &.top-left .tooltip-arrow { border-top-color: @tooltip-background-color; } &.top-right .tooltip-arrow { border-top-color: @tooltip-background-color; } &.right .tooltip-arrow { border-right-color: @tooltip-background-color; } &.left .tooltip-arrow { border-left-color: @tooltip-background-color; } &.bottom .tooltip-arrow { border-bottom-color: @tooltip-background-color; } &.bottom-left .tooltip-arrow { border-bottom-color: @tooltip-background-color; } &.bottom-right .tooltip-arrow { border-bottom-color: @tooltip-background-color; } } ================================================ FILE: packages/one-light-ui/styles/tree-view.less ================================================ @tree-view-height: @ui-line-height; .tree-view { font-size: @ui-size; background: @tree-view-background-color; .project-root.project-root { &:before { height: @ui-tab-height; background-clip: padding-box; } & > .header .name { line-height: @ui-tab-height; } } // Selected state .selected:before { background: @background-color-selected; } // Focus + selected state &:focus { .selected.list-item > .name, // files .selected.list-nested-item > .list-item > .name, // folders .selected.list-nested-item > .header:before { // arrow icon color: contrast(@button-background-color-selected); } .selected:before { background: @button-background-color-selected; } } } .theme-one-dark-ui .tree-view .project-root.project-root::before { border-top: 1px solid transparent; background-clip: padding-box; } .tree-view-resizer { .tree-view-resize-handle { width: 8px; } } // Variable height, based on ems .list-group li:not(.list-nested-item), .list-tree li:not(.list-nested-item), .list-group li.list-nested-item > .list-item, .list-tree li.list-nested-item > .list-item { line-height: @tree-view-height; } .list-group .selected::before, .list-tree .selected::before { height: @tree-view-height; } // icon .list-group .icon, .list-tree .icon { display: inline-block; height: inherit; &::before { top: initial; line-height: inherit; height: inherit; vertical-align: top; } } // Arrow icon .list-group, .list-tree { .header.header.header.header::before { top: initial; line-height: inherit; height: inherit; vertical-align: top; font-size: inherit; } } .tree-view .project-root-header.project-root-header.project-root-header.project-root-header::before { line-height: @ui-tab-height; } ================================================ FILE: packages/one-light-ui/styles/ui-mixins.less ================================================ // Pattern matching; ish is cray. // http://lesscss.org/#-pattern-matching-and-guard-expressions .text(normal) { font-weight: normal; color: @text-color; } .text(subtle) { font-weight: normal; color: @text-color-subtle; } .text(highlight) { font-weight: normal; color: @text-color-highlight; } .text(selected) { .text(highlight) } .text(info) { color: @text-color-info; } .text(success) { color: @text-color-success; } .text(warning) { color: @text-color-warning; } .text(error) { color: @text-color-error; } .focus() { outline: none; border-color: @accent-color; box-shadow: 0 0 0 1px @accent-color; } .valid() { border-color: @text-color-success; box-shadow: 0 0 0 1px @text-color-success; background-color: mix(@text-color-success, @input-background-color, 10%); } .invalid() { border-color: @text-color-error; box-shadow: 0 0 0 1px @text-color-error; background-color: mix(@text-color-error, @input-background-color, 10%); } ================================================ FILE: packages/one-light-ui/styles/ui-variables-custom.less ================================================ // ONE light UI variables // ---------------------------------------------- @import "syntax-variables"; .ui-syntax-color() { @syntax-background-color: hsl(220,1%,98%); } .ui-syntax-color(); // fallback color @ui-syntax-color: @syntax-background-color; // Color guards ----------------- @ui-s-h: hue(@ui-syntax-color); .ui-hue() when (@ui-s-h = 0) { @ui-hue: 220; } // Use blue hue when no saturation .ui-hue() when (@ui-s-h > 0) { @ui-hue: @ui-s-h; } .ui-hue(); @ui-saturation: min( saturation(@ui-syntax-color), 24%); // max saturation @ui-lightness: max( lightness(@ui-syntax-color), 92%); // min lightness // Main colors ----------------- @ui-fg: hsl(@ui-hue, @ui-saturation, @ui-lightness - 72%); @ui-bg: hsl(@ui-hue, @ui-saturation, @ui-lightness); // normalized @syntax-background-color @ui-border: darken(@level-3-color, 6%); // Custom variables // These variables are only used in this theme // ---------------------------------------------- @ui-theme-name: one-light-ui; // Text (Custom) ----------------- @text-color-faded: fade(@text-color, 30%); @text-color-added: @text-color-success; // green @text-color-ignored: @text-color-subtle; // faded @text-color-modified: @text-color-warning; // orange @text-color-removed: @text-color-error; // red @text-color-renamed: @text-color-info; // blue // Background (Custom) ----------------- @level-1-color: lighten(@base-background-color, 4%); @level-2-color: @base-background-color; @level-3-color: darken(@base-background-color, 6%); @level-3-color-hover: darken(@level-3-color, 6%); @level-3-color-active: darken(@level-3-color, 3%); // Accent (Custom) ----------------- @accent-luma: luma( hsl(@ui-hue, 50%, 50%) ); // get lightness of current hue // used for marker, inputs (smaller things) @accent-color: mix( hsv( @ui-hue, 60%, 60%), hsl( @ui-hue, 100%, 68%), @accent-luma * 2 ); // mix hsv + hsl (favor hsl for dark, hsv for light colors) @accent-text-color: contrast(@accent-color, hsl(@ui-hue,100%,16%), #fff, 40% ); // used for button, tooltip (larger things) @accent-bg-color: mix( hsv( @ui-hue, 40%, 72%), hsl( @ui-hue, 100%, 66%), @accent-luma * 2 ); // mix hsv + hsl (favor hsl for dark, hsv for light colors) @accent-bg-text-color: contrast(@accent-bg-color, hsl(@ui-hue,100%,10%), #fff, 40% ); // used for text only @accent-only-text-color: mix( hsv( @ui-hue, 70%, 50%), hsl( @ui-hue, 100%, 60%), @accent-luma * 2 ); // mix hsv + hsl (favor hsl for dark, hsv for light colors) // Components (Custom) ----------------- @badge-background-color: @background-color-selected; @button-text-color-selected: @accent-bg-text-color; @button-border-color-selected: @accent-color; @checkbox-background-color: fade(@accent-bg-color, 33%); @input-background-color-focus: hsl(@ui-hue, 100%, 96%); @input-selection-color: mix( hsv( @ui-hue, 33%, 95%), hsl( @ui-hue, 100%, 98%), @accent-luma * 2 ); // mix hsv + hsl (favor hsl for dark, hsv for light colors) @input-selection-color-focus: mix( hsv( @ui-hue, 44%, 90%), hsl( @ui-hue, 100%, 94%), @accent-luma * 2 ); // mix hsv + hsl (favor hsl for dark, hsv for light colors) @overlay-backdrop-color: hsl(@ui-hue, @ui-saturation*0.4, @ui-lightness*0.8); @overlay-backdrop-opacity: .66; @progress-background-color: @accent-color; @scrollbar-color: darken(@level-3-color, 14%); @scrollbar-background-color: @level-3-color; // replaced `transparent` with a solid color to test https://github.com/atom/one-light-ui/issues/4 @scrollbar-color-editor: contrast(@ui-syntax-color, darken(@ui-syntax-color, 14%), lighten(@ui-syntax-color, 9%) ); @scrollbar-background-color-editor: @ui-syntax-color; @tab-text-color: @text-color-subtle; @tab-text-color-active: @text-color-highlight; @tab-text-color-editor: contrast(@ui-syntax-color, lighten(@ui-syntax-color, 70%), @text-color-highlight ); @tab-background-color-editor: @ui-syntax-color; @tab-inactive-status-added: fade(@text-color-success, 77%); @tab-inactive-status-modified: fade(@text-color-warning, 77%); @tooltip-background-color: @accent-bg-color; @tooltip-text-color: @accent-bg-text-color; @tooltip-text-key-color: @tooltip-background-color; @tooltip-background-key-color: @tooltip-text-color; // Sizes (Custom) ----------------- @ui-size: 1em; @ui-input-size: @ui-size*1.15; @ui-padding: @ui-size*1.5; @ui-padding-pane: @ui-size*.5; @ui-padding-icon: @ui-padding/3.3; @ui-line-height: @ui-size*2; @ui-tab-height: @ui-size*2.5; // Packages variables // These variables are used to override packages // ---------------------------------------------- @settings-list-background-color: darken(@level-2-color, 3%); @theme-config-box-shadow: inset 0 1px 2px hsla(0, 0%, 0%, .2), 0 1px 0 hsla(0, 0%, 100%, .3); @theme-config-box-shadow-selected: inset 0 1px 3px hsla(0, 0%, 0%, .2); @theme-config-border-selected: hsla(0, 0%, 0%, .5); // Debug // Output variables to the top of the UI // ------------------------------------- // html:before { // content: "@{variable}"; // } ================================================ FILE: packages/one-light-ui/styles/ui-variables.less ================================================ @import "ui-variables-custom.less"; // import colors and custom variables // ONE light UI variables // ---------------------------------------------- // Official variables // These variables must be defined in every theme // Source: https://github.com/atom/atom/blob/master/static/variables/ui-variables.less // ---------------------------------------------- // Text ----------------- @text-color: @ui-fg; @text-color-subtle: lighten(@text-color, 30%); @text-color-highlight: darken(@text-color, 12%); @text-color-selected: darken(@text-color-highlight, 12%); @text-color-info: hsl(208, 100%, 54%); @text-color-success: hsl(132, 60%, 44%); @text-color-warning: hsl( 37, 90%, 44%); @text-color-error: hsl( 0, 90%, 56%); // Background ----------------- @background-color-info: hsl(208, 100%, 56%); @background-color-success: hsl(132, 52%, 48%); @background-color-warning: hsl( 40, 60%, 48%); @background-color-error: hsl( 5, 72%, 56%); @background-color-highlight: darken(@level-3-color, 2%); @background-color-selected: darken(@level-3-color, 6%); @app-background-color: @level-3-color; // Base ----------------- @base-background-color: @ui-bg; @base-border-color: @ui-border; // Components ----------------- @pane-item-background-color: @base-background-color; @pane-item-border-color: @base-border-color; @input-background-color: @level-1-color; @input-border-color: @base-border-color; @tool-panel-background-color: @level-3-color; @tool-panel-border-color: @base-border-color; @inset-panel-background-color: lighten(@level-2-color, 4%); @inset-panel-border-color: fadeout(@base-border-color, 15%); @panel-heading-background-color: @level-2-color; @panel-heading-border-color: @base-border-color; @overlay-background-color: mix(@level-2-color, @level-3-color); @overlay-border-color: @base-border-color; @button-background-color: @level-1-color; @button-background-color-hover: darken(@button-background-color, 4%); @button-background-color-selected: @accent-bg-color; @button-border-color: @base-border-color; @tab-bar-background-color: @level-3-color; @tab-bar-border-color: @base-border-color; @tab-background-color: @level-3-color; @tab-background-color-active: @level-2-color; @tab-border-color: @base-border-color; @tree-view-background-color: @level-3-color; @tree-view-border-color: @base-border-color; @ui-site-color-1: hsl(208, 100%, 56%); // blue @ui-site-color-2: hsl(132, 48%, 48%); // green @ui-site-color-3: hsl( 40, 60%, 52%); // orange @ui-site-color-4: #D831B0; // pink @ui-site-color-5: #EBDD5B; // yellow // Sizes ----------------- @font-size: 12px; @input-font-size: 14px; @disclosure-arrow-size: 12px; @component-padding: 10px; @component-icon-padding: 5px; @component-icon-size: 16px; // needs to stay 16px to look sharpest @component-line-height: 25px; @component-border-radius: 3px; @tab-height: 30px; // Font ----------------- @font-family: system-ui; ================================================ FILE: packages/solarized-dark-syntax/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/solarized-dark-syntax/README.md ================================================ # Solarized Dark Syntax theme Atom theme using the ever popular dark [solarized](http://ethanschoonover.com/solarized) colors. screenshot This theme is installed by default with Atom and can be activated by going to the _Themes_ section in the Settings view (`cmd-,`) and selecting it from the _Syntax Themes_ dropdown menu. ================================================ FILE: packages/solarized-dark-syntax/index.less ================================================ // Solarized Syntax Theme @import "styles/syntax-variables.less"; // Editor @import "styles/editor.less"; // Languages @import "styles/syntax-legacy/_base.less"; // @import "styles/syntax-legacy/c.less"; @import "styles/syntax-legacy/coffee.less"; @import "styles/syntax-legacy/css.less"; // @import "styles/syntax-legacy/go.less"; @import "styles/syntax-legacy/java.less"; // @import "styles/syntax-legacy/javascript.less"; @import "styles/syntax-legacy/markdown.less"; @import "styles/syntax-legacy/markup.less"; @import "styles/syntax-legacy/php.less"; // @import "styles/syntax-legacy/python.less"; // @import "styles/syntax-legacy/ruby.less"; @import "styles/syntax-legacy/scala.less"; @import "styles/syntax-legacy/typescript.less"; @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; @import "styles/syntax/html.less"; @import "styles/syntax/js.less"; ================================================ FILE: packages/solarized-dark-syntax/package.json ================================================ { "name": "solarized-dark-syntax", "theme": "syntax", "version": "1.3.0", "description": "A dark syntax theme using the solarized colors", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.50.0" } } ================================================ FILE: packages/solarized-dark-syntax/styles/colors.less ================================================ // Solarized color scheme // http://ethanschoonover.com/solarized#the-values // Background/Foreground Tones @base03: #002b36; @base02: #073642; // Content Tones @base01: #586e75; @base00: #657b83; @base0: #839496; @base1: #93a1a1; // Background/Foreground Tones @base2: #eee8d5; @base3: #fdf6e3; // Accent Colors @yellow: #b58900; @orange: #cb4b16; @red: #dc322f; @magenta: #d33682; @violet: #6c71c4; @blue: #268bd2; @cyan: #2aa198; @green: #859900; ================================================ FILE: packages/solarized-dark-syntax/styles/editor.less ================================================ atom-text-editor { color: @syntax-text-color; background-color: @syntax-background-color; .gutter { color: @syntax-gutter-text-color; background-color: @syntax-gutter-background-color; .line-number { &.cursor-line { background-color: @syntax-gutter-background-color-selected; } } } .invisible-character { color: @syntax-invisible-character-color; } .indent-guide { color: @syntax-indent-guide-color; } .cursor { border-color: @syntax-cursor-color; } .cursor-line { background-color: @syntax-cursor-line; } .selection .region { background-color: @syntax-selection-color; } .fold-marker:after, .gutter .line-number.folded { color: @magenta; } .bracket-matcher .region { border-color: @magenta; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax/base.less ================================================ /* This defines styling rules for syntax classes. See the naming conventions for a list of syntax classes: https://flight-manual.atom.io/hacking-atom/sections/syntax-naming-conventions When styling rules conflict: - The last rule overrides previous rules. - The rule with most classes and pseudo-classes overrides the last rule. */ // if for return global let and int .syntax--keyword { color: @green; // super &.syntax--function { color: @yellow; } // this self &.syntax--variable { color: @yellow; } // = + && | << ? &.syntax--symbolic { color: @syntax-text-color; } } // identifier .syntax--entity { color: @syntax-text-color; // self cls iota &.syntax--support { color: @yellow; } // @entity.decorator &.syntax--decorator:last-child { color: @blue; } // label: &.syntax--label { text-decoration: underline; } // function method &.syntax--function { color: @blue; } // add &.syntax--operator { color: @blue; // %>% <=> &.syntax--symbolic { color: @syntax-text-color; } } // String Enum Class &.syntax--type { color: @blue; // int dict char map &.syntax--fundamental { color: @green; } } // div span &.syntax--tag { color: @blue; } // href src alt &.syntax--attribute { color: @yellow; } } // () [] {} => @ .syntax--punctuation { color: @syntax-text-color; } // "string" .syntax--string { color: @cyan; // {placeholder} %().2f &.syntax--part { color: @violet; } // ${ } &.syntax--interpolation { color: @syntax-text-color; } // /^reg[ex]?p/ &.syntax--regexp { color: @cyan; // ^ $ \b ? + i &.syntax--language { color: @violet; } // \1 &.syntax--variable { color: @violet; } // ( ) [^ ] (?= ) | r" / &.syntax--punctuation { color: @violet; } } } // literal 4 1.3 0x29 .syntax--constant { color: @magenta; // < 'a' &.syntax--character { color: @cyan; // \" \' \g \. &.syntax--escape { color: @blue; } // \u2661 \n \t \W . &.syntax--code { color: @blue; } } // true false nil &.syntax--language { color: @magenta; } } // text .syntax--text { color: @syntax-text-color; } // __formatted__ .syntax--markup { // # Heading &.syntax--heading { color: @blue; } // > &.syntax--quote.syntax--punctuation { color: @violet; } &.syntax--list.syntax--punctuation { // 1. &.syntax--ordered { color: @green; } // * - &.syntax--unordered { color: @yellow; } } // **bold** &.syntax--bold { font-weight: bold; } // *italic* &.syntax--italic { font-style: italic; } // `raw` &.syntax--raw { font-style: italic; } // url.com (path) &.syntax--link { color: @cyan; } // [alt] ![alt] &.syntax--alt { color: @violet; } // {++ inserted ++} &.syntax--inserted { color: @cyan; .syntax--punctuation { color: @cyan; } } // {== highlighted ==} &.syntax--highlighted { color: @cyan; .syntax--punctuation { color: @cyan; } } // {-- deleted --} &.syntax--deleted { color: @red; .syntax--punctuation { color: @red; } } // {~~ from~>to ~~} &.syntax--changed { color: @yellow; .syntax--punctuation { color: @yellow; } } // {>> commented <<} &.syntax--commented { color: @syntax-comment-color; .syntax--punctuation { color: @syntax-comment-color; } } } // /* comment */ .syntax--comment { color: @syntax-comment-color; font-style: italic; // @param TODO NOTE &.syntax--caption { color: @syntax-subtle-color; font-weight: bold; } // variable function type &.syntax--term { color: @syntax-subtle-color; } // { } / . &.syntax--punctuation { color: @syntax-comment-color; font-weight: normal; } } // 0invalid .syntax--invalid:not(.syntax--punctuation) { // §illegal &.syntax--illegal { color: @red !important; text-decoration: underline !important; } // obsolete() &.syntax--deprecated { color: @yellow !important; text-decoration: underline !important; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax/css.less ================================================ .syntax--source.syntax--css { // @media and or .syntax--keyword { color: @green; // + = *= &.syntax--symbolic { color: @syntax-text-color; } // !important &.syntax--important { color: @green; } } .syntax--entity { // function() &.syntax--function { color: @cyan; // url rgb &.syntax--support { color: @blue; } } // #id .class &.syntax--selector { color: @magenta; // div span &.syntax--tag { color: @blue; } // :pseudo-class ::pseudo-element &.syntax--pseudo-class, &.syntax--pseudo-element { color: @yellow; } } // href src alt &.syntax--attribute { color: @yellow; } // property: constant &.syntax--property { color: @syntax-subtle-color; // height position border &.syntax--support { color: @syntax-text-color; } } // --variable &.syntax--variable { color: @syntax-text-color; } // @keyframes keyframe &.syntax--keyframe { color: @syntax-text-color; } } // property: constant .syntax--constant { color: @cyan; // tv tty &.syntax--media { color: @yellow; // print screen &.syntax--support { color: @yellow; } } // from to 50% &.syntax--offset { color: @syntax-text-color; // % &.syntax--unit { color: @syntax-text-color; } } } .syntax--punctuation { &.syntax--selector { // * &.syntax--wildcard { color: @blue; } // # &.syntax--id { color: @magenta; } // . &.syntax--class { color: @magenta; } // : :: &.syntax--pseudo-class, &.syntax--pseudo-element { color: @yellow; } } // () &.syntax--arguments { color: @syntax-text-color; } } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax/html.less ================================================ .syntax--source.syntax--html { .syntax--punctuation { // < /> &.syntax--tag { color: @syntax-comment-color; } // = &.syntax--pair.syntax--attribute-value { color: @syntax-comment-color; } } .syntax--meta { // &.syntax--doctype { color: @syntax-comment-color; } } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax/js.less ================================================ .syntax--source.syntax--js { .syntax--jsx { color: @syntax-text-color; // Component &.syntax--entity.syntax--type { color: @blue; } // "string" &.syntax--string { color: @cyan; } //
    text
    &.syntax--text { color: @cyan; } } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/_base.less ================================================ .syntax--comment { color: @syntax-comment-color; font-style: italic; .syntax--markup.syntax--link { color: @syntax-comment-color; } } .syntax--string { color: @cyan; &.syntax--regexp { color: @red; } } .syntax--constant { &.syntax--numeric { color: @magenta; } &.syntax--language { color: @yellow; } &.syntax--character, &.syntax--other, &.syntax--support { color: @orange; } } .syntax--variable { color: @blue; } .syntax--keyword { color: @green; } .syntax--storage { color: @green; } .syntax--meta.syntax--class { color: @blue; } .syntax--entity { &.syntax--name { &.syntax--class, &.syntax--function, &.syntax--section, &.syntax--type { color: @blue; } } &.syntax--other.syntax--attribute-name { color: @syntax-subtle-color; } } .syntax--support { &.syntax--function { color: @blue; &.syntax--builtin { color: @green; } } &.syntax--type, &.syntax--class { color: @green; } } .syntax--tag { &.syntax--entity.syntax--name { color: @blue; } &.syntax--punctuation.syntax--definition { &.syntax--html, &.syntax--begin, &.syntax--end { color: @syntax-comment-color; } } } .syntax--invalid { &.syntax--deprecated { color: @yellow; text-decoration: underline; } &.syntax--illegal { color: @red; text-decoration: underline; } } .syntax--none { color: @syntax-text-color; } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/c.less ================================================ .syntax--source.syntax--c, .syntax--source.syntax--cpp { .syntax--meta.syntax--preprocessor { color: @red; } .syntax--keyword.syntax--control.syntax--directive { color: @orange; } .syntax--punctuation.syntax--string { color: @cyan; } .syntax--constant { color: @orange; &.syntax--numeric, &.syntax--language.syntax--c { color: @cyan; } } .syntax--storage { color: @yellow; } .syntax--entity { color: @syntax-text-color; &.syntax--name.syntax--function.syntax--preprocessor { color: @red; } } .syntax--support.syntax--type { color: @yellow; &.syntax--posix-reserved { color: @syntax-text-color; } } .syntax--variable { &.syntax--other.syntax--dot-access { color: @syntax-text-color; } &.syntax--parameter.syntax--preprocessor { color: @red; } } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/coffee.less ================================================ .syntax--source.syntax--coffee { .syntax--support.syntax--class { color: @green; } .syntax--variable, .syntax--entity.syntax--name.syntax--function, .syntax--entity.syntax--name.syntax--class { color: @blue; } .syntax--variable.syntax--parameter.syntax--function { color: @syntax-text-color; } .syntax--variable.syntax--other.syntax--readwrite { color: @green; } .syntax--storage.syntax--type.syntax--function { color: @green; } .syntax--entity.syntax--name { color: @syntax-text-color; } .syntax--meta.syntax--brace { &.syntax--round, &.syntax--square { color: @syntax-text-color; } } .syntax--meta.syntax--delimiter { color: @syntax-text-color; } .syntax--storage.syntax--type.syntax--class { color: @green; } .syntax--punctuation.syntax--terminator { color: @syntax-text-color; } .syntax--punctuation.syntax--section.syntax--embedded { color: @red; } .syntax--embedded.syntax--source { color: @syntax-text-color; } .syntax--constant.syntax--numeric { color: @magenta; } .syntax--constant.syntax--language.syntax--boolean { color: @yellow; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/css.less ================================================ .syntax--source.syntax--css { .syntax--punctuation { &.syntax--separator, &.syntax--terminator { color: @syntax-text-color; } &.syntax--property-list.syntax--begin, &.syntax--property-list.syntax--end { color: @red; } &.syntax--section.syntax--function { color: @cyan; } } .syntax--entity.syntax--name { color: @green; } .syntax--attribute-name.syntax--class, .syntax--id { color: @blue; } .syntax--pseudo-element, .syntax--pseudo-class { color: @orange; } .syntax--property-value { color: @cyan; } .syntax--constant.syntax--numeric { color: @cyan; .syntax--unit { color: @cyan; } } .syntax--rgb-value { color: @cyan; } .syntax--support.syntax--constant { color: @cyan; &.syntax--media { color: @red; } } .syntax--keyword.syntax--important { color: @red; } } // Less/Sass should have their own files, // but for just a single override, here should be fine too .syntax--source.syntax--less, .syntax--source.syntax--scss { .syntax--keyword.syntax--unit { color: @cyan; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/go.less ================================================ .syntax--source.syntax--go { .syntax--operator { color: @syntax-text-color; &.syntax--assignment { color: @green; } } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/java.less ================================================ .syntax--source.syntax--java { .syntax--keyword.syntax--operator{ color:@green; } .syntax--keyword.syntax--import{ color: @orange; } .syntax--storage.syntax--modifier.syntax--import{ color: @syntax-comment-color; } .syntax--meta.syntax--class{ .syntax--storage.syntax--modifier{ color: @yellow; } .syntax--meta.syntax--class.syntax--identifier{ .syntax--entity.syntax--name.syntax--type.syntax--class{ color: @blue; } } } .syntax--storage.syntax--type.syntax--primitive.syntax--array{ color:@green; } .syntax--constant.syntax--numeric{ color:@magenta; } .syntax--constant.syntax--other{ color:@orange; } .syntax--storage.syntax--type{ color:@green; } .syntax--meta.syntax--method-call{ //@ibocon: method parameter's color color:@red; //@ibocon: method and variable use different hightlight .syntax--meta.syntax--method{ color:@violet; } .syntax--punctuation.syntax--definition.syntax--seperator.syntax--parameter{ color:@green; } } .syntax--punctuation.syntax--definition.syntax--method-parameters{ color: @syntax-emphasized-color; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/javascript.less ================================================ .syntax--source.syntax--js { .syntax--comma { color: @syntax-text-color; } .syntax--support.syntax--class { color: @green; } .syntax--entity { &.syntax--name.syntax--type { color: @yellow; } &.syntax--name { color: @syntax-text-color; } &.syntax--name.syntax--tag { color: @blue; } &.syntax--other.syntax--attribute-name { color: @yellow; } } .syntax--meta.syntax--brace { color: @syntax-text-color; } .syntax--keyword { color: @syntax-text-color; } .syntax--keyword.syntax--operator.syntax--new { color: @green; } .syntax--keyword.syntax--control { color: @green; } .syntax--keyword.syntax--control.syntax--regexp { color: @cyan; } .syntax--variable { color: @syntax-text-color; } .syntax--variable.syntax--dom { color: @green; } .syntax--delimiter + .syntax--dom { color: @syntax-text-color; } .syntax--name { color: @syntax-text-color; } .syntax--variable.syntax--language { color: @blue; } .syntax--variable.syntax--parameter { color: @syntax-text-color; } .syntax--regexp { color: @cyan; } .syntax--support.syntax--function { color: @syntax-text-color; } .syntax--support.syntax--constant { color: @syntax-text-color; } .syntax--constant.syntax--numeric { color: @syntax-text-color; } .syntax--storage { color: @blue; } .syntax--storage.syntax--modifier { color: @yellow; } .syntax--punctuation.syntax--terminator.syntax--statement { color: @syntax-text-color; } .syntax--meta.syntax--delimiter.syntax--method.syntax--period { color: @syntax-text-color; } .syntax--meta.syntax--brace.syntax--square { color: @blue; } .syntax--meta.syntax--brace.syntax--curly { color: @blue; } .syntax--string.syntax--quoted.syntax--template { .syntax--embedded.syntax--source { color: @syntax-text-color; & > .syntax--embedded.syntax--punctuation { color: @red; } } } &.syntax--embedded .syntax--entity.syntax--name.syntax--tag { color: @blue; } .syntax--import { .syntax--control { color: @yellow; } } } // JavaScript (Rails) language-ruby-on-rails .syntax--source.syntax--js.syntax--rails { .syntax--instance { color: @blue; } .syntax--class { color: @yellow; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/markdown.less ================================================ .syntax--md, .syntax--gfm { .syntax--link .syntax--entity { color: @violet; } .syntax--list { &.syntax--ordered { color: @green; } &.syntax--unordered { color: @yellow; } } .syntax--raw { font-style: italic; } &.syntax--support { color:@syntax-comment-color; &.syntax--quote { color: @violet; } } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/markup.less ================================================ .syntax--markup { &.syntax--bold { font-weight: bold; } &.syntax--italic { font-style: italic; } &.syntax--heading { color: @blue; } &.syntax--link { color: @cyan; } &.syntax--deleted { color: @red; } &.syntax--changed { color: @yellow; } &.syntax--inserted { color: @cyan; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/php.less ================================================ .syntax--source.syntax--php { .syntax--storage { &.syntax--type { &.syntax--class { color: @yellow; } &.syntax--function { color: @orange; } } &.syntax--modifier { color: @yellow; } } .syntax--entity { &.syntax--name { &.syntax--type.syntax--class { color: @syntax-text-color; } &.syntax--function { color: @syntax-text-color; } } &.syntax--other { color: @syntax-text-color; } } .syntax--variable { color: @blue; } .syntax--punctuation.syntax--definition { color: @syntax-text-color; &.syntax--comment { color: @syntax-comment-color; } &.syntax--array { color: @red; } &.syntax--string { color: @syntax-text-color; } &.syntax--variable { color: @green; } } .syntax--support.syntax--function { &.syntax--construct { color: @yellow; } &.syntax--array { color: @green; } } .syntax--keyword { &.syntax--operator { &.syntax--class { color: @yellow; } &.syntax--assignment { color: @green; } } &.syntax--other { color: @red; } } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/python.less ================================================ .syntax--source.syntax--python { .syntax--entity { color: @syntax-text-color; &.syntax--name { color: @blue; } &.syntax--other { color: @blue; } } .syntax--function { color: @blue; &.syntax--magic { color: @blue; } } .syntax--punctuation.syntax--string { color: @cyan; } .syntax--keyword { &.syntax--operator { color: @syntax-text-color; &.syntax--quantifier { color: @cyan; } &.syntax--logical { color: @green; } } &.syntax--control.syntax--import { color: @orange; } &.syntax--other { color: @green; } } .syntax--constant { &.syntax--language { color: @blue; } &.syntax--character { color: @cyan; } &.syntax--other { color: @red; } } .syntax--entity.syntax--name.syntax--type.syntax--class { color: @blue; } .syntax--variable { color: @syntax-text-color; } .syntax--support { &.syntax--function.syntax--builtin { color: @blue; } &.syntax--type { &.syntax--exception.syntax--python { color: @yellow; } &.syntax--python { color: @blue; } } } .syntax--storage.syntax--type.syntax--string { color: @cyan; } .syntax--storage.syntax--type.syntax--class { color: @green; &.syntax--todo { color: @magenta; } } .syntax--storage.syntax--type.syntax--function { color: @green; } .syntax--punctuation.syntax--definition.syntax--parameters { color: @syntax-text-color; } .syntax--punctuation.syntax--section.syntax--function.syntax--begin { color: @syntax-text-color; } .syntax--punctuation.syntax--separator.syntax--parameters { color: @syntax-text-color; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/ruby.less ================================================ .syntax--source.syntax--ruby { .syntax--meta.syntax--embedded { .syntax--punctuation.syntax--section { color: @red; } } .syntax--punctuation.syntax--definition { color: @syntax-text-color; &.syntax--string { color: @red; } } .syntax--punctuation.syntax--definition.syntax--comment { color: @syntax-comment-color; } .syntax--entity.syntax--inherited-class { color: @yellow; } .syntax--variable { &.syntax--parameter { color: @syntax-text-color; } } .syntax--variable.syntax--constant { color: @yellow; } .syntax--constant.syntax--boolean { color: @cyan; } .syntax--instance { .syntax--punctuation.syntax--definition { color: @blue; } } .syntax--class { color: @yellow; &.syntax--control { color: @syntax-text-color; } } .syntax--module { color: @yellow; } .syntax--require { .syntax--keyword.syntax--other.syntax--special-method { color: @orange; } } .syntax--keyword.syntax--other.syntax--special-method { color: @orange; } .syntax--keyword.syntax--other { color: @green; } .syntax--keyword.syntax--control { color: @green; } .syntax--keyword.syntax--operator { color: @syntax-text-color; } .syntax--special-method { color: @blue; } .syntax--symbol { color: @cyan; .syntax--punctuation.syntax--definition { color: @cyan; } } .syntax--hashkey { color: @red; .syntax--punctuation.syntax--definition { color: @red; } } .syntax--string.syntax--regexp { color: @red; } .syntax--todo { color: @magenta; } .syntax--variable.syntax--ruby.syntax--global { color: @blue; .syntax--punctuation { color: @blue; } } .syntax--variable.syntax--block { color: @blue; } .syntax--variable.syntax--self { color: @cyan; } .syntax--punctuation.syntax--separator { color: @syntax-text-color; } .syntax--numeric { color: @cyan; } .syntax--punctuation.syntax--section.syntax--regexp { color: @red; } .syntax--string.syntax--interpolated { color: @cyan; } .syntax--string.syntax--interpolated { .syntax--embedded.syntax--line.syntax--ruby { .syntax--punctuation { .syntax--source.syntax--ruby { color: @red; } } .syntax--source.syntax--ruby { .syntax--punctuation.syntax--array, .syntax--punctuation.syntax--function { color: @syntax-text-color; } color: @syntax-text-color; } } } .syntax--support.syntax--function { color: @syntax-text-color; } .syntax--support.syntax--function.syntax--kernel { color: @green; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/scala.less ================================================ .syntax--source.syntax--scala { .syntax--variable { color: @syntax-emphasized-color; } .syntax--declaration { color: @syntax-emphasized-color; font-weight: bold; } .syntax--comparison { color: @syntax-emphasized-color; } .syntax--class, .syntax--type { color: @yellow; } .syntax--val { font-weight: normal; } .syntax--variable { font-weight: bold; } .syntax--variable.syntax--parameter { color: @violet; font-weight: normal; } .syntax--control.syntax--flow { color: @syntax-emphasized-color; font-weight: bold; } .syntax--constant.syntax--language { color: @syntax-emphasized-color; font-weight: bold; } .syntax--function.syntax--declaration { color: @violet; } .syntax--modifier.syntax--other { font-weight: bold; } .syntax--package { color: @syntax-emphasized-color; } .syntax--variable.syntax--import { font-weight: normal; } .syntax--type { .syntax--bounds, .syntax--class { color: @violet; } } .syntax--documentation { :not(.syntax--embedded) { // out of scope ? // https://github.syntax--com/atom/link &.syntax--link.syntax--entity { color: @blue; text-decoration: underline; } .syntax--class, .syntax--parameter { color: @syntax-emphasized-color; } .syntax--description { color: @syntax-comment-color; } } } .syntax--embedded { color: darken(@syntax-emphasized-color, 15%); // so we dont confused it with normal expressions font-style: italic; .syntax--margin, .syntax--delimiters { font-style: normal; } } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-legacy/typescript.less ================================================ .syntax--source.syntax--ts, .syntax--source.syntax--tsx { .syntax--import { .syntax--control { color: @orange; } } .syntax--entity { &.syntax--name.syntax--type { color: @yellow; } &.syntax--inherited-class { color: @yellow; } } .syntax--support.syntax--type { color: @yellow; } } ================================================ FILE: packages/solarized-dark-syntax/styles/syntax-variables.less ================================================ @import "colors.less"; // This defines all syntax variables that syntax themes must implement when they // include a syntax-variables.less file. // General colors @syntax-text-color: @base0; @syntax-cursor-color: @base3; @syntax-selection-color: lighten(@base02, 1%); @syntax-selection-flash-color: @base1; @syntax-background-color: @base03; // Guide colors @syntax-wrap-guide-color: lighten(@base02, 6%); @syntax-indent-guide-color: lighten(@base02, 6%); @syntax-invisible-character-color: lighten(@base02, 6%); // For find and replace markers @syntax-result-marker-color: @cyan; @syntax-result-marker-color-selected: @base3; // Gutter colors @syntax-gutter-text-color: @base0; @syntax-gutter-text-color-selected: @base2; @syntax-gutter-background-color: @base02; @syntax-gutter-background-color-selected: lighten(@base02, 6%); // For git diff info. i.e. in the gutter @syntax-color-added: @green; @syntax-color-renamed: @blue; @syntax-color-modified: @yellow; @syntax-color-removed: @red; // For language entity colors @syntax-color-variable: @blue; @syntax-color-constant: @yellow; @syntax-color-property: @yellow; @syntax-color-value: @cyan; @syntax-color-function: @blue; @syntax-color-method: @blue; @syntax-color-class: @blue; @syntax-color-keyword: @green; @syntax-color-tag: @blue; @syntax-color-attribute: @syntax-comment-color; @syntax-color-import: @red; @syntax-color-snippet: @syntax-color-keyword; // Custom variables // Warning: Don't use in packages @syntax-comment-color: @base01; @syntax-subtle-color: @base00; @syntax-emphasized-color: @base1; @syntax-cursor-line: fade(lighten(@syntax-background-color, 30%), 8%); // needs to be semi-transparent ================================================ FILE: packages/solarized-light-syntax/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/solarized-light-syntax/README.md ================================================ # Solarized Light Syntax theme Atom theme using the ever popular light [solarized](http://ethanschoonover.com/solarized) colors. screenshot This theme is installed by default with Atom and can be activated by going to the _Themes_ section in the Settings view (`cmd-,`) and selecting it from the _Syntax Themes_ dropdown menu. ================================================ FILE: packages/solarized-light-syntax/index.less ================================================ // Solarized Syntax Theme @import "styles/syntax-variables.less"; // Editor @import "styles/editor.less"; // Languages @import "styles/syntax-legacy/_base.less"; // @import "styles/syntax-legacy/c.less"; @import "styles/syntax-legacy/coffee.less"; @import "styles/syntax-legacy/css.less"; // @import "styles/syntax-legacy/go.less"; @import "styles/syntax-legacy/java.less"; // @import "styles/syntax-legacy/javascript.less"; @import "styles/syntax-legacy/markdown.less"; @import "styles/syntax-legacy/markup.less"; @import "styles/syntax-legacy/php.less"; // @import "styles/syntax-legacy/python.less"; // @import "styles/syntax-legacy/ruby.less"; @import "styles/syntax-legacy/scala.less"; @import "styles/syntax-legacy/typescript.less"; @import "styles/syntax/base.less"; @import "styles/syntax/css.less"; @import "styles/syntax/html.less"; @import "styles/syntax/js.less"; ================================================ FILE: packages/solarized-light-syntax/package.json ================================================ { "name": "solarized-light-syntax", "theme": "syntax", "version": "1.3.0", "description": "A light syntax theme using the solarized colors", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.50.0" } } ================================================ FILE: packages/solarized-light-syntax/styles/colors.less ================================================ // Solarized color scheme // http://ethanschoonover.com/solarized#the-values // Background/Foreground Tones @base03: #002b36; @base02: #073642; // Content Tones @base01: #586e75; @base00: #657b83; @base0: #839496; @base1: #93a1a1; // Background/Foreground Tones @base2: #eee8d5; @base3: #fdf6e3; // Accent Colors @yellow: #b58900; @orange: #cb4b16; @red: #dc322f; @magenta: #d33682; @violet: #6c71c4; @blue: #268bd2; @cyan: #2aa198; @green: #859900; ================================================ FILE: packages/solarized-light-syntax/styles/editor.less ================================================ atom-text-editor { color: @syntax-text-color; background-color: @syntax-background-color; .gutter { color: @syntax-gutter-text-color; background-color: @syntax-gutter-background-color; .line-number { &.cursor-line { background-color: @syntax-gutter-background-color-selected; } } } .invisible-character { color: @syntax-invisible-character-color; } .indent-guide { color: @syntax-indent-guide-color; } .cursor { border-color: @syntax-cursor-color; } .cursor-line { background-color: @syntax-cursor-line; } .selection .region { background-color: @syntax-selection-color; } .fold-marker:after, .gutter .line-number.folded { color: @magenta; } .bracket-matcher .region { border-color: @magenta; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax/base.less ================================================ /* This defines styling rules for syntax classes. See the naming conventions for a list of syntax classes: https://flight-manual.atom.io/hacking-atom/sections/syntax-naming-conventions When styling rules conflict: - The last rule overrides previous rules. - The rule with most classes and pseudo-classes overrides the last rule. */ // if for return global let and int .syntax--keyword { color: @green; // super &.syntax--function { color: @yellow; } // this self &.syntax--variable { color: @yellow; } // = + && | << ? &.syntax--symbolic { color: @syntax-text-color; } } // identifier .syntax--entity { color: @syntax-text-color; // self cls iota &.syntax--support { color: @yellow; } // @entity.decorator &.syntax--decorator:last-child { color: @blue; } // label: &.syntax--label { text-decoration: underline; } // function method &.syntax--function { color: @blue; } // add &.syntax--operator { color: @blue; // %>% <=> &.syntax--symbolic { color: @syntax-text-color; } } // String Enum Class &.syntax--type { color: @blue; // int dict char map &.syntax--fundamental { color: @green; } } // div span &.syntax--tag { color: @blue; } // href src alt &.syntax--attribute { color: @yellow; } } // () [] {} => @ .syntax--punctuation { color: @syntax-text-color; } // "string" .syntax--string { color: @cyan; // ${variable} %().2f &.syntax--part { color: @orange; } // /^reg[ex]?p/ &.syntax--regexp { color: @cyan; // ^ $ \b ? + i &.syntax--language { color: @orange; } // \1 &.syntax--variable { color: @orange; } // ( ) [^ ] (?= ) | r" / &.syntax--punctuation { color: @orange; } } } // literal 4 1.3 0x29 .syntax--constant { color: @magenta; // < 'a' &.syntax--character { color: @cyan; // \" \' \g \. &.syntax--escape { color: @blue; } // \u2661 \n \t \W . &.syntax--code { color: @blue; } } // true false nil &.syntax--language { color: @magenta; } } // text .syntax--text { color: @syntax-text-color; } // __formatted__ .syntax--markup { // # Heading &.syntax--heading { color: @blue; } // > &.syntax--quote.syntax--punctuation { color: @violet; } &.syntax--list.syntax--punctuation { // 1. &.syntax--ordered { color: @green; } // * - &.syntax--unordered { color: @yellow; } } // **bold** &.syntax--bold { font-weight: bold; } // *italic* &.syntax--italic { font-style: italic; } // `raw` &.syntax--raw { font-style: italic; } // url.com (path) &.syntax--link { color: @cyan; } // [alt] ![alt] &.syntax--alt { color: @violet; } // {++ inserted ++} &.syntax--inserted { color: @cyan; .syntax--punctuation { color: @cyan; } } // {== highlighted ==} &.syntax--highlighted { color: @cyan; .syntax--punctuation { color: @cyan; } } // {-- deleted --} &.syntax--deleted { color: @red; .syntax--punctuation { color: @red; } } // {~~ from~>to ~~} &.syntax--changed { color: @yellow; .syntax--punctuation { color: @yellow; } } // {>> commented <<} &.syntax--commented { color: @syntax-comment-color; .syntax--punctuation { color: @syntax-comment-color; } } } // /* comment */ .syntax--comment { color: @syntax-comment-color; font-style: italic; // @param TODO NOTE &.syntax--caption { color: @syntax-subtle-color; font-weight: bold; } // variable function type &.syntax--term { color: @syntax-subtle-color; } // { } / . &.syntax--punctuation { color: @syntax-comment-color; font-weight: normal; } } // 0invalid .syntax--invalid:not(.syntax--punctuation) { // §illegal &.syntax--illegal { color: @red !important; text-decoration: underline !important; } // obsolete() &.syntax--deprecated { color: @yellow !important; text-decoration: underline !important; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax/css.less ================================================ .syntax--source.syntax--css { // @media and or .syntax--keyword { color: @green; // + = *= &.syntax--symbolic { color: @syntax-text-color; } // !important &.syntax--important { color: @green; } } .syntax--entity { // function() &.syntax--function { color: @cyan; // url rgb &.syntax--support { color: @blue; } } // #id .class &.syntax--selector { color: @magenta; // div span &.syntax--tag { color: @blue; } // :pseudo-class ::pseudo-element &.syntax--pseudo-class, &.syntax--pseudo-element { color: @yellow; } } // href src alt &.syntax--attribute { color: @yellow; } // property: constant &.syntax--property { color: @syntax-subtle-color; // height position border &.syntax--support { color: @syntax-text-color; } } // --variable &.syntax--variable { color: @syntax-text-color; } // @keyframes keyframe &.syntax--keyframe { color: @syntax-text-color; } } // property: constant .syntax--constant { color: @cyan; // tv tty &.syntax--media { color: @yellow; // print screen &.syntax--support { color: @yellow; } } // from to 50% &.syntax--offset { color: @syntax-text-color; // % &.syntax--unit { color: @syntax-text-color; } } } .syntax--punctuation { &.syntax--selector { // * &.syntax--wildcard { color: @blue; } // # &.syntax--id { color: @magenta; } // . &.syntax--class { color: @magenta; } // : :: &.syntax--pseudo-class, &.syntax--pseudo-element { color: @yellow; } } // () &.syntax--arguments { color: @syntax-text-color; } } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax/html.less ================================================ .syntax--source.syntax--html { .syntax--punctuation { // < /> &.syntax--tag { color: @syntax-comment-color; } // = &.syntax--pair.syntax--attribute-value { color: @syntax-comment-color; } } .syntax--meta { // &.syntax--doctype { color: @syntax-comment-color; } } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax/js.less ================================================ .syntax--source.syntax--js { .syntax--jsx { color: @syntax-text-color; // Component &.syntax--entity.syntax--type { color: @blue; } // "string" &.syntax--string { color: @cyan; } //
    text
    &.syntax--text { color: @cyan; } } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/_base.less ================================================ .syntax--comment { color: @syntax-comment-color; font-style: italic; .syntax--markup.syntax--link { color: @syntax-comment-color; } } .syntax--string { color: @cyan; &.syntax--regexp { color: @red; } } .syntax--constant { &.syntax--numeric { color: @magenta; } &.syntax--language { color: @yellow; } &.syntax--character, &.syntax--other, &.syntax--support { color: @orange; } } .syntax--variable { color: @blue; } .syntax--keyword { color: @green; } .syntax--storage { color: @green; } .syntax--meta.syntax--class { color: @blue; } .syntax--entity { &.syntax--name { &.syntax--class, &.syntax--function, &.syntax--section, &.syntax--type { color: @blue; } } &.syntax--other.syntax--attribute-name { color: @syntax-subtle-color; } } .syntax--support { &.syntax--function { color: @blue; &.syntax--builtin { color: @green; } } &.syntax--type, &.syntax--class { color: @green; } } .syntax--tag { &.syntax--entity.syntax--name { color: @blue; } &.syntax--punctuation.syntax--definition { &.syntax--html, &.syntax--begin, &.syntax--end { color: @syntax-comment-color; } } } .syntax--invalid { &.syntax--deprecated { color: @yellow; text-decoration: underline; } &.syntax--illegal { color: @red; text-decoration: underline; } } .syntax--none { color: @syntax-text-color; } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/c.less ================================================ .syntax--source.syntax--c, .syntax--source.syntax--cpp { .syntax--meta.syntax--preprocessor { color: @red; } .syntax--keyword.syntax--control.syntax--directive { color: @orange; } .syntax--punctuation.syntax--string { color: @cyan; } .syntax--constant { color: @orange; &.syntax--numeric, &.syntax--language.syntax--c { color: @cyan; } } .syntax--storage { color: @yellow; } .syntax--entity { color: @syntax-text-color; &.syntax--name.syntax--function.syntax--preprocessor { color: @red; } } .syntax--support.syntax--type { color: @yellow; &.syntax--posix-reserved { color: @syntax-text-color; } } .syntax--variable { &.syntax--other.syntax--dot-access { color: @syntax-text-color; } &.syntax--parameter.syntax--preprocessor { color: @red; } } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/coffee.less ================================================ .syntax--source.syntax--coffee { .syntax--support.syntax--class { color: @green; } .syntax--variable, .syntax--entity.syntax--name.syntax--function, .syntax--entity.syntax--name.syntax--class { color: @blue; } .syntax--variable.syntax--parameter.syntax--function { color: @syntax-text-color; } .syntax--variable.syntax--other.syntax--readwrite { color: @green; } .syntax--storage.syntax--type.syntax--function { color: @green; } .syntax--entity.syntax--name { color: @syntax-text-color; } .syntax--meta.syntax--brace { &.syntax--round, &.syntax--square { color: @syntax-text-color; } } .syntax--meta.syntax--delimiter { color: @syntax-text-color; } .syntax--storage.syntax--type.syntax--class { color: @green; } .syntax--punctuation.syntax--terminator { color: @syntax-text-color; } .syntax--punctuation.syntax--section.syntax--embedded { color: @red; } .syntax--embedded.syntax--source { color: @syntax-text-color; } .syntax--constant.syntax--numeric { color: @magenta; } .syntax--constant.syntax--language.syntax--boolean { color: @yellow; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/css.less ================================================ .syntax--source.syntax--css { .syntax--punctuation { &.syntax--separator, &.syntax--terminator { color: @syntax-text-color; } &.syntax--property-list.syntax--begin, &.syntax--property-list.syntax--end { color: @red; } &.syntax--section.syntax--function { color: @cyan; } } .syntax--entity.syntax--name { color: @green; } .syntax--attribute-name.syntax--class, .syntax--id { color: @blue; } .syntax--pseudo-element, .syntax--pseudo-class { color: @orange; } .syntax--property-value { color: @cyan; } .syntax--constant.syntax--numeric { color: @cyan; .syntax--unit { color: @cyan; } } .syntax--rgb-value { color: @cyan; } .syntax--support.syntax--constant { color: @cyan; &.syntax--media { color: @red; } } .syntax--keyword.syntax--important { color: @red; } } // Less/Sass should have their own files, // but for just a single override, here should be fine too .syntax--source.syntax--less, .syntax--source.syntax--scss { .syntax--keyword.syntax--unit { color: @cyan; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/go.less ================================================ .syntax--source.syntax--go { .syntax--operator { color: @syntax-text-color; &.syntax--assignment { color: @green; } } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/java.less ================================================ .syntax--source.syntax--java { .syntax--keyword.syntax--operator{ color:@green; } .syntax--keyword.syntax--import{ color: @orange; } .syntax--storage.syntax--modifier.syntax--import{ color: @syntax-comment-color; } .syntax--meta.syntax--class{ .syntax--storage.syntax--modifier{ color: @yellow; } .syntax--meta.syntax--class.syntax--identifier{ .syntax--entity.syntax--name.syntax--type.syntax--class{ color: @blue; } } } .syntax--storage.syntax--type.syntax--primitive.syntax--array{ color:@green; } .syntax--constant.syntax--numeric{ color:@magenta; } .syntax--constant.syntax--other{ color:@orange; } .syntax--storage.syntax--type{ color:@green; } .syntax--meta.syntax--method-call{ //@ibocon: method parameter's color color:@red; //@ibocon: method and variable use different hightlight .syntax--meta.syntax--method{ color:@violet; } .syntax--punctuation.syntax--definition.syntax--seperator.syntax--parameter{ color:@green; } } .syntax--punctuation.syntax--definition.syntax--method-parameters{ color: @syntax-emphasized-color; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/javascript.less ================================================ .syntax--source.syntax--js { .syntax--comma { color: @syntax-text-color; } .syntax--support.syntax--class { color: @green; } .syntax--entity { &.syntax--name.syntax--type { color: @yellow; } &.syntax--name { color: @syntax-text-color; &.syntax--function { color: @blue; } } &.syntax--name.syntax--tag { color: @blue; } &.syntax--other.syntax--attribute-name { color: @yellow; } } .syntax--meta.syntax--brace { color: @syntax-text-color; } .syntax--keyword { color: @syntax-text-color; } .syntax--keyword.syntax--operator.syntax--new { color: @green; } .syntax--keyword.syntax--control { color: @orange; } .syntax--keyword.syntax--control.syntax--regexp { color: @cyan; } .syntax--variable { color: @syntax-text-color; } .syntax--variable.syntax--dom { color: @green; } .syntax--delimiter + .syntax--dom { color: @syntax-text-color; } .syntax--name { color: @syntax-text-color; } .syntax--variable.syntax--language { color: @blue; } .syntax--variable.syntax--parameter { color: @syntax-text-color; } .syntax--regexp { color: @cyan; } .syntax--support.syntax--function { color: @syntax-text-color; } .syntax--support.syntax--constant { color: @syntax-text-color; } .syntax--storage.syntax--modifier { color: @yellow; } .syntax--punctuation.syntax--terminator.syntax--statement { color: @syntax-text-color; } .syntax--meta.syntax--delimiter.syntax--method.syntax--period { color: @syntax-text-color; } .syntax--meta.syntax--brace.syntax--square { color: @blue; } .syntax--meta.syntax--brace.syntax--curly { color: @blue; } .syntax--string.syntax--quoted.syntax--template { .syntax--embedded.syntax--source { color: @syntax-text-color; & > .syntax--embedded.syntax--punctuation { color: @red; } } } &.syntax--embedded .syntax--entity.syntax--name.syntax--tag { color: @blue; } .syntax--import { .syntax--control { color: @orange; } } } // JavaScript (Rails) language-ruby-on-rails .syntax--source.syntax--js.syntax--rails { .syntax--instance { color: @blue; } .syntax--class { color: @yellow; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/markdown.less ================================================ .syntax--md, .syntax--gfm { .syntax--link .syntax--entity { color: @violet; } .syntax--list { &.syntax--ordered { color: @green; } &.syntax--unordered { color: @yellow; } } .syntax--raw { font-style: italic; } &.syntax--support { color:@syntax-comment-color; &.syntax--quote { color: @violet; } } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/markup.less ================================================ .syntax--markup { &.syntax--bold { font-weight: bold; } &.syntax--italic { font-style: italic; } &.syntax--heading { color: @blue; } &.syntax--link { color: @cyan; } &.syntax--deleted { color: @red; } &.syntax--changed { color: @yellow; } &.syntax--inserted { color: @cyan; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/php.less ================================================ .syntax--source.syntax--php { .syntax--storage { &.syntax--type { &.syntax--class { color: @yellow; } &.syntax--function { color: @orange; } } &.syntax--modifier { color: @yellow; } } .syntax--entity { &.syntax--name { &.syntax--type.syntax--class { color: @syntax-text-color; } &.syntax--function { color: @syntax-text-color; } } &.syntax--other { color: @syntax-text-color; } } .syntax--variable { color: @blue; } .syntax--punctuation.syntax--definition { color: @syntax-text-color; &.syntax--comment { color: @syntax-comment-color; } &.syntax--array { color: @red; } &.syntax--string { color: @syntax-text-color; } &.syntax--variable { color: @green; } } .syntax--support.syntax--function { &.syntax--construct { color: @yellow; } &.syntax--array { color: @green; } } .syntax--keyword { &.syntax--operator { &.syntax--class { color: @yellow; } &.syntax--assignment { color: @green; } } &.syntax--other { color: @red; } } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/python.less ================================================ .syntax--source.syntax--python { .syntax--entity { color: @syntax-text-color; &.syntax--name { color: @blue; } &.syntax--other { color: @blue; } } .syntax--function { color: @blue; &.syntax--magic { color: @blue; } } .syntax--punctuation.syntax--string { color: @cyan; } .syntax--keyword { &.syntax--operator { color: @syntax-text-color; &.syntax--quantifier { color: @cyan; } &.syntax--logical { color: @green; } } &.syntax--control.syntax--import { color: @orange; } &.syntax--other { color: @green; } } .syntax--constant { &.syntax--language { color: @blue; } &.syntax--character { color: @cyan; } &.syntax--other { color: @red; } } .syntax--entity.syntax--name.syntax--type.syntax--class { color: @blue; } .syntax--variable { color: @syntax-text-color; } .syntax--support { &.syntax--function.syntax--builtin { color: @blue; } &.syntax--type { &.syntax--exception.syntax--python { color: @yellow; } &.syntax--python { color: @blue; } } } .syntax--storage.syntax--type.syntax--string { color: @cyan; } .syntax--storage.syntax--type.syntax--class { color: @green; &.syntax--todo { color: @magenta; } } .syntax--storage.syntax--type.syntax--function { color: @green; } .syntax--punctuation.syntax--definition.syntax--parameters { color: @syntax-text-color; } .syntax--punctuation.syntax--section.syntax--function.syntax--begin { color: @syntax-text-color; } .syntax--punctuation.syntax--separator.syntax--parameters { color: @syntax-text-color; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/ruby.less ================================================ .syntax--source.syntax--ruby { .syntax--meta.syntax--embedded { .syntax--punctuation.syntax--section { color: @red; } } .syntax--punctuation.syntax--definition { color: @syntax-text-color; &.syntax--string { color: @red; } } .syntax--punctuation.syntax--definition.syntax--comment { color: @syntax-comment-color; } .syntax--entity.syntax--inherited-class { color: @yellow; } .syntax--variable { &.syntax--parameter { color: @syntax-text-color; } } .syntax--variable.syntax--constant { color: @yellow; } .syntax--constant.syntax--boolean { color: @cyan; } .syntax--instance { .syntax--punctuation.syntax--definition { color: @blue; } } .syntax--class { color: @yellow; &.syntax--control { color: @syntax-text-color; } } .syntax--module { color: @yellow; } .syntax--require { .syntax--keyword.syntax--other.syntax--special-method { color: @orange; } } .syntax--keyword.syntax--other.syntax--special-method { color: @orange; } .syntax--keyword.syntax--other { color: @green; } .syntax--keyword.syntax--control { color: @green; } .syntax--keyword.syntax--operator { color: @syntax-text-color; } .syntax--special-method { color: @blue; } .syntax--symbol { color: @cyan; .syntax--punctuation.syntax--definition { color: @cyan; } } .syntax--hashkey { color: @red; .syntax--punctuation.syntax--definition { color: @red; } } .syntax--string.syntax--regexp { color: @red; } .syntax--todo { color: @magenta; } .syntax--variable.syntax--ruby.syntax--global { color: @blue; .syntax--punctuation { color: @blue; } } .syntax--variable.syntax--block { color: @blue; } .syntax--variable.syntax--self { color: @cyan; } .syntax--punctuation.syntax--separator { color: @syntax-text-color; } .syntax--numeric { color: @cyan; } .syntax--punctuation.syntax--section.syntax--regexp { color: @red; } .syntax--string.syntax--interpolated { color: @cyan; } .syntax--string.syntax--interpolated { .syntax--embedded.syntax--line.syntax--ruby { .syntax--punctuation { .syntax--source.syntax--ruby { color: @red; } } .syntax--source.syntax--ruby { .syntax--punctuation.syntax--array, .syntax--punctuation.syntax--function { color: @syntax-text-color; } color: @syntax-text-color; } } } .syntax--support.syntax--function { color: @syntax-text-color; } .syntax--support.syntax--function.syntax--kernel { color: @green; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/scala.less ================================================ .syntax--source.syntax--scala { .syntax--variable { color: @syntax-emphasized-color; } .syntax--declaration { color: @syntax-emphasized-color; font-weight: bold; } .syntax--comparison { color: @syntax-emphasized-color; } .syntax--class, .syntax--type { color: @yellow; } .syntax--val { font-weight: normal; } .syntax--variable { font-weight: bold; } .syntax--variable.syntax--parameter { color: @violet; font-weight: normal; } .syntax--control.syntax--flow { color: @syntax-emphasized-color; font-weight: bold; } .syntax--constant.syntax--language { color: @syntax-emphasized-color; font-weight: bold; } .syntax--function.syntax--declaration { color: @violet; } .syntax--modifier.syntax--other { font-weight: bold; } .syntax--package { color: @syntax-emphasized-color; } .syntax--variable.syntax--import { font-weight: normal; } .syntax--type { .syntax--bounds, .syntax--class { color: @violet; } } .syntax--documentation { :not(.syntax--embedded) { // out of scope ? // https://github.syntax--com/atom/link &.syntax--link.syntax--entity { color: @blue; text-decoration: underline; } .syntax--class, .syntax--parameter { color: @syntax-emphasized-color; } .syntax--description { color: @syntax-comment-color; } } } .syntax--embedded { color: darken(@syntax-emphasized-color, 15%); // so we dont confused it with normal expressions font-style: italic; .syntax--margin, .syntax--delimiters { font-style: normal; } } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-legacy/typescript.less ================================================ .syntax--source.syntax--ts, .syntax--source.syntax--tsx { .syntax--import { .syntax--control { color: @orange; } } .syntax--entity { &.syntax--name.syntax--type { color: @yellow; } &.syntax--inherited-class { color: @yellow; } } .syntax--support.syntax--type { color: @yellow; } } ================================================ FILE: packages/solarized-light-syntax/styles/syntax-variables.less ================================================ @import "colors.less"; // This defines all syntax variables that syntax themes must implement when they // include a syntax-variables.less file. // General colors @syntax-text-color: @base00; @syntax-cursor-color: @base03; @syntax-selection-color: @base2; @syntax-selection-flash-color: @base0; @syntax-background-color: @base3; // Guide colors @syntax-wrap-guide-color: darken(@base2, 12%); @syntax-indent-guide-color: darken(@base2, 12%); @syntax-invisible-character-color: darken(@base2, 12%); // For find and replace markers @syntax-result-marker-color: @base1; @syntax-result-marker-color-selected: @base03; // Gutter colors @syntax-gutter-text-color: @base00; @syntax-gutter-text-color-selected: @base03; @syntax-gutter-background-color: @base2; @syntax-gutter-background-color-selected: darken(@syntax-gutter-background-color, 10%); // For git diff info. i.e. in the gutter @syntax-color-added: @green; @syntax-color-renamed: @blue; @syntax-color-modified: @yellow; @syntax-color-removed: @red; // For language entity colors @syntax-color-variable: @blue; @syntax-color-constant: @yellow; @syntax-color-property: @yellow; @syntax-color-value: @cyan; @syntax-color-function: @blue; @syntax-color-method: @blue; @syntax-color-class: @blue; @syntax-color-keyword: @green; @syntax-color-tag: @blue; @syntax-color-attribute: @syntax-comment-color; @syntax-color-import: @red; @syntax-color-snippet: @syntax-color-keyword; // Custom variables // Warning: Don't use in packages @syntax-comment-color: @base1; @syntax-subtle-color: @base00; @syntax-emphasized-color: @base01; @syntax-cursor-line: fade(darken(@syntax-background-color, 30%), 15%); // needs to be semi-transparent ================================================ FILE: packages/update-package-dependencies/.gitignore ================================================ .DS_Store npm-debug.log node_modules ================================================ FILE: packages/update-package-dependencies/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/update-package-dependencies/README.md ================================================ ## Update Package Dependencies package Runs `apm install` from the current project's directory. This will install all dependencies referenced in the `package.json` file to the `node_modules` folder. This should only be used in projects that are Atom packages. ================================================ FILE: packages/update-package-dependencies/lib/update-package-dependencies-status-view.js ================================================ module.exports = class UpdatePackageDependenciesStatusView { constructor(statusBar) { this.statusBar = statusBar; this.element = document.createElement('update-package-dependencies-status'); this.element.classList.add( 'update-package-dependencies-status', 'inline-block', 'is-read-only' ); this.spinner = document.createElement('span'); this.spinner.classList.add( 'loading', 'loading-spinner-tiny', 'inline-block' ); this.element.appendChild(this.spinner); } attach() { this.tile = this.statusBar.addRightTile({ item: this.element }); this.tooltip = atom.tooltips.add(this.element, { title: 'Updating package dependencies\u2026' }); } detach() { if (this.tile) this.tile.destroy(); if (this.tooltip) this.tooltip.dispose(); } }; ================================================ FILE: packages/update-package-dependencies/lib/update-package-dependencies.js ================================================ const { BufferedProcess } = require('atom'); const UpdatePackageDependenciesStatusView = require('./update-package-dependencies-status-view'); module.exports = { activate() { this.subscription = atom.commands.add( 'atom-workspace', 'update-package-dependencies:update', () => this.update() ); }, deactivate() { this.subscription.dispose(); if (this.updatePackageDependenciesStatusView) { this.updatePackageDependenciesStatusView.detach(); this.updatePackageDependenciesStatusView = null; } }, consumeStatusBar(statusBar) { this.updatePackageDependenciesStatusView = new UpdatePackageDependenciesStatusView( statusBar ); }, update() { if (this.process) return; // Do not allow multiple apm processes to run if (this.updatePackageDependenciesStatusView) this.updatePackageDependenciesStatusView.attach(); let errorOutput = ''; const command = atom.packages.getApmPath(); const args = ['install', '--no-color']; const stderr = output => { errorOutput += output; }; const options = { cwd: this.getActiveProjectPath(), env: Object.assign({}, process.env, { NODE_ENV: 'development' }) }; const exit = code => { this.process = null; if (this.updatePackageDependenciesStatusView) this.updatePackageDependenciesStatusView.detach(); if (code === 0) { atom.notifications.addSuccess('Package dependencies updated'); } else { atom.notifications.addError('Failed to update package dependencies', { detail: errorOutput, dismissable: true }); } }; this.process = this.runBufferedProcess({ command, args, stderr, exit, options }); }, // This function exists so that it can be spied on by tests runBufferedProcess(params) { return new BufferedProcess(params); }, getActiveProjectPath() { const activeItem = atom.workspace.getActivePaneItem(); if (activeItem && typeof activeItem.getPath === 'function') { return atom.project.relativizePath(activeItem.getPath())[0]; } else { return atom.project.getPaths()[0]; } } }; ================================================ FILE: packages/update-package-dependencies/package.json ================================================ { "name": "update-package-dependencies", "main": "./lib/update-package-dependencies", "version": "0.13.1", "private": true, "description": "Runs `apm install` for the current project", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.39.0" }, "activationCommands": { "atom-workspace": [ "update-package-dependencies:update" ] }, "consumedServices": { "status-bar": { "versions": { "^1.1.0": "consumeStatusBar" } } }, "dependencies": {}, "devDependencies": { "standard": "^10.0.3" }, "standard": { "env": { "atomtest": true, "browser": true, "jasmine": true, "node": true }, "globals": [ "atom" ] } } ================================================ FILE: packages/update-package-dependencies/spec/update-package-dependencies-spec.js ================================================ const os = require('os'); const path = require('path'); const updatePackageDependencies = require('../lib/update-package-dependencies'); describe('Update Package Dependencies', () => { let projectPath = null; beforeEach(() => { projectPath = __dirname; atom.project.setPaths([projectPath]); }); describe('updating package dependencies', () => { let { command, args, stderr, exit, options } = {}; beforeEach(() => { spyOn(updatePackageDependencies, 'runBufferedProcess').andCallFake( params => { ({ command, args, stderr, exit, options } = params); return true; // so that this.process isn't null } ); }); afterEach(() => { if (updatePackageDependencies.process) exit(0); }); it('runs the `apm install` command', () => { updatePackageDependencies.update(); expect(updatePackageDependencies.runBufferedProcess).toHaveBeenCalled(); if (process.platform !== 'win32') { expect(command.endsWith('/apm')).toBe(true); } else { expect(command.endsWith('\\apm.cmd')).toBe(true); } expect(args).toEqual(['install', '--no-color']); expect(options.cwd).toEqual(projectPath); }); it('only allows one apm process to be spawned at a time', () => { updatePackageDependencies.update(); expect(updatePackageDependencies.runBufferedProcess.callCount).toBe(1); updatePackageDependencies.update(); updatePackageDependencies.update(); expect(updatePackageDependencies.runBufferedProcess.callCount).toBe(1); exit(0); updatePackageDependencies.update(); expect(updatePackageDependencies.runBufferedProcess.callCount).toBe(2); }); it('sets NODE_ENV to development in order to install devDependencies', () => { updatePackageDependencies.update(); expect(options.env.NODE_ENV).toEqual('development'); }); it('adds a status bar tile', async () => { const statusBar = await atom.packages.activatePackage('status-bar'); const activationPromise = atom.packages.activatePackage( 'update-package-dependencies' ); atom.commands.dispatch( atom.views.getView(atom.workspace), 'update-package-dependencies:update' ); const { mainModule } = await activationPromise; mainModule.update(); let tile = statusBar.mainModule.statusBar .getRightTiles() .find(tile => tile.item.matches('update-package-dependencies-status')); expect( tile.item.classList.contains('update-package-dependencies-status') ).toBe(true); expect(tile.item.firstChild.classList.contains('loading')).toBe(true); exit(0); tile = statusBar.mainModule.statusBar .getRightTiles() .find(tile => tile.item.matches('update-package-dependencies-status')); expect(tile).toBeUndefined(); }); describe('when there are multiple project paths', () => { beforeEach(() => atom.project.setPaths([os.tmpdir(), projectPath])); it('uses the currently active one', async () => { await atom.workspace.open(path.join(projectPath, 'package.json')); updatePackageDependencies.update(); expect(options.cwd).toEqual(projectPath); }); }); describe('when the update succeeds', () => { beforeEach(() => { updatePackageDependencies.update(); exit(0); }); it('shows a success notification message', () => { const notification = atom.notifications.getNotifications()[0]; expect(notification.getType()).toEqual('success'); expect(notification.getMessage()).toEqual( 'Package dependencies updated' ); }); }); describe('when the update fails', () => { beforeEach(() => { updatePackageDependencies.update(); stderr('oh bother'); exit(127); }); it('shows a failure notification', () => { const notification = atom.notifications.getNotifications()[0]; expect(notification.getType()).toEqual('error'); expect(notification.getMessage()).toEqual( 'Failed to update package dependencies' ); expect(notification.getDetail()).toEqual('oh bother'); expect(notification.isDismissable()).toBe(true); }); }); }); describe('the `update-package-dependencies:update` command', () => { beforeEach(() => spyOn(updatePackageDependencies, 'update')); it('activates the package and updates package dependencies', async () => { const activationPromise = atom.packages.activatePackage( 'update-package-dependencies' ); atom.commands.dispatch( atom.views.getView(atom.workspace), 'update-package-dependencies:update' ); const { mainModule } = await activationPromise; expect(mainModule.update).toHaveBeenCalled(); }); }); }); ================================================ FILE: packages/update-package-dependencies/styles/update-package-dependencies.less ================================================ .update-package-dependencies-status .loading.inline-block { vertical-align: text-bottom; } ================================================ FILE: packages/welcome/.gitignore ================================================ node_modules ================================================ FILE: packages/welcome/LICENSE.md ================================================ Copyright (c) 2014 GitHub Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/welcome/README.md ================================================ ## Welcome package Opens a welcome editor with helpful information the very first time Atom is opened and the usage statistics opt-in. ================================================ FILE: packages/welcome/docs/events.md ================================================ # Events specification This document specifies all the data (along with the format) which gets sent from the Welcome package to the GitHub analytics pipeline. This document follows the same format and nomenclature as the [Atom Core Events spec](https://github.com/atom/metrics/blob/master/docs/events.md). ## Counters Currently the Welcome package does not log any counter events. ## Timing events Currently the Welcome package does not log any timing events. ## Standard events #### Welcome package shown * **eventType**: `welcome-v1` * **metadata** | field | value | |-------|-------| | `ea` | `show-on-initial-load` #### Click on links * **eventType**: `welcome-v1` * **metadata** | field | value | |-------|-------| | `ea` | link that was clicked (There are many potential values for the `ea` param, e.g: `clicked-welcome-atom-docs-link`,`clicked-welcome-atom-org-link`, `clicked-project-cta`, `clicked-init-script-cta`, ...). ================================================ FILE: packages/welcome/lib/consent-view.js ================================================ 'use babel'; /** @jsx etch.dom */ import etch from 'etch'; export default class ConsentView { constructor() { etch.initialize(this); } render() { return (

    Help improve Atom by sending your anonymous{' '} usage data to the Atom team. The resulting data plays a key role in deciding what we focus on next.

    Including exception and crash reports. See the{' '} atom/metrics package {' '} for more details.

    Note: We only register anonymously that you opted-out.

    with by

    ); } update() { return etch.update(this); } consent() { atom.config.set('core.telemetryConsent', 'limited'); atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); } decline() { atom.config.set('core.telemetryConsent', 'no'); atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); } openMetricsPackage() { atom.workspace.open('atom://config/packages/metrics'); } getTitle() { return 'Telemetry Consent'; } async destroy() { await etch.destroy(this); } } ================================================ FILE: packages/welcome/lib/guide-view.js ================================================ /** @babel */ /** @jsx etch.dom **/ import etch from 'etch'; export default class GuideView { constructor(props) { this.props = props; this.didClickProjectButton = this.didClickProjectButton.bind(this); this.didClickGitButton = this.didClickGitButton.bind(this); this.didClickGitHubButton = this.didClickGitHubButton.bind(this); this.didClickTeletypeButton = this.didClickTeletypeButton.bind(this); this.didClickPackagesButton = this.didClickPackagesButton.bind(this); this.didClickThemesButton = this.didClickThemesButton.bind(this); this.didClickStylingButton = this.didClickStylingButton.bind(this); this.didClickInitScriptButton = this.didClickInitScriptButton.bind(this); this.didClickSnippetsButton = this.didClickSnippetsButton.bind(this); this.didExpandOrCollapseSection = this.didExpandOrCollapseSection.bind( this ); etch.initialize(this); } update() {} render() { return (

    Get to know Atom!

    Open a Project

    In Atom you can open individual files or a whole folder as a project. Opening a folder will add a tree view to the editor where you can browse all the files.

    Next time: You can also open projects from the menu, keyboard shortcut or by dragging a folder onto the Atom dock icon.

    Version control with{' '} Git and GitHub

    Track changes to your code as you work. Branch, commit, push, and pull without leaving the comfort of your editor. Collaborate with other developers on GitHub.

    Next time: You can toggle the Git tab by clicking on the button in your status bar.

    Collaborate in real time with{' '} Teletype

    Share your workspace with team members and collaborate on code in real time.

    Install a Package

    One of the best things about Atom is the package ecosystem. Installing packages adds new features and functionality you can use to make the editor suit your needs. Let's install one.

    Next time: You can install new packages from the settings.

    Choose a Theme

    Atom comes with preinstalled themes. Let's try a few.

    You can also install themes created by the Atom community. To install new themes, click on "+ Install" and switch the toggle to "themes".

    Next time: You can switch themes from the settings.

    Customize the Styling

    You can customize almost anything by adding your own CSS/LESS.

    Now uncomment some of the examples or try your own

    Next time: You can open your stylesheet from Menu {this.getApplicationMenuName()}.

    Hack on the Init Script

    The init script is a bit of JavaScript or CoffeeScript run at startup. You can use it to quickly change the behaviour of Atom.

    Uncomment some of the examples or try out your own.

    Next time: You can open your init script from Menu > {this.getApplicationMenuName()}.

    Add a Snippet

    Atom snippets allow you to enter a simple prefix in the editor and hit tab to expand the prefix into a larger code block with templated values.

    In your snippets file, type snip then hit{' '} tab. The snip snippet will expand to create a snippet!

    Next time: You can open your snippets in Menu > {this.getApplicationMenuName()}.

    Learn Keyboard Shortcuts

    If you only remember one keyboard shortcut make it{' '} {this.getCommandPaletteKeyBinding()} . This keystroke toggles the command palette, which lists every Atom command. It's a good way to learn more shortcuts. Yes, you can try it now!

    If you want to use these guides again use the command palette{' '} {this.getCommandPaletteKeyBinding()} {' '} and search for Welcome .

    ); } getSectionProps(sectionName) { const props = { dataset: { section: sectionName }, onclick: this.didExpandOrCollapseSection }; if ( this.props.openSections && this.props.openSections.indexOf(sectionName) !== -1 ) { props.open = true; } return props; } getCommandPaletteKeyBinding() { if (process.platform === 'darwin') { return 'cmd-shift-p'; } else { return 'ctrl-shift-p'; } } getApplicationMenuName() { if (process.platform === 'darwin') { return 'Atom'; } else if (process.platform === 'linux') { return 'Edit'; } else { return 'File'; } } serialize() { return { deserializer: this.constructor.name, openSections: this.getOpenSections(), uri: this.getURI() }; } getURI() { return this.props.uri; } getTitle() { return 'Welcome Guide'; } isEqual(other) { return other instanceof GuideView; } getOpenSections() { return Array.from(this.element.querySelectorAll('details[open]')).map( sectionElement => sectionElement.dataset.section ); } didClickProjectButton() { this.props.reporterProxy.sendEvent('clicked-project-cta'); atom.commands.dispatch( atom.views.getView(atom.workspace), 'application:open' ); } didClickGitButton() { this.props.reporterProxy.sendEvent('clicked-git-cta'); atom.commands.dispatch( atom.views.getView(atom.workspace), 'github:toggle-git-tab' ); } didClickGitHubButton() { this.props.reporterProxy.sendEvent('clicked-github-cta'); atom.commands.dispatch( atom.views.getView(atom.workspace), 'github:toggle-github-tab' ); } didClickPackagesButton() { this.props.reporterProxy.sendEvent('clicked-packages-cta'); atom.workspace.open('atom://config/install', { split: 'left' }); } didClickThemesButton() { this.props.reporterProxy.sendEvent('clicked-themes-cta'); atom.workspace.open('atom://config/themes', { split: 'left' }); } didClickStylingButton() { this.props.reporterProxy.sendEvent('clicked-styling-cta'); atom.workspace.open('atom://.atom/stylesheet', { split: 'left' }); } didClickInitScriptButton() { this.props.reporterProxy.sendEvent('clicked-init-script-cta'); atom.workspace.open('atom://.atom/init-script', { split: 'left' }); } didClickSnippetsButton() { this.props.reporterProxy.sendEvent('clicked-snippets-cta'); atom.workspace.open('atom://.atom/snippets', { split: 'left' }); } didClickTeletypeButton() { this.props.reporterProxy.sendEvent('clicked-teletype-cta'); atom.workspace.open('atom://config/packages/teletype', { split: 'left' }); } didExpandOrCollapseSection(event) { const sectionName = event.currentTarget.closest('details').dataset.section; const action = event.currentTarget.hasAttribute('open') ? 'collapse' : 'expand'; this.props.reporterProxy.sendEvent(`${action}-${sectionName}-section`); } } ================================================ FILE: packages/welcome/lib/main.js ================================================ /** @babel */ import WelcomePackage from './welcome-package'; export default new WelcomePackage(); ================================================ FILE: packages/welcome/lib/reporter-proxy.js ================================================ /** @babel */ export default class ReporterProxy { constructor() { this.reporter = null; this.queue = []; this.eventType = 'welcome-v1'; } setReporter(reporter) { this.reporter = reporter; let customEvent; while ((customEvent = this.queue.shift())) { this.reporter.addCustomEvent(this.eventType, customEvent); } } sendEvent(action, label, value) { const event = { ea: action, el: label, ev: value }; if (this.reporter) { this.reporter.addCustomEvent(this.eventType, event); } else { this.queue.push(event); } } } ================================================ FILE: packages/welcome/lib/welcome-package.js ================================================ /** @babel */ import { CompositeDisposable } from 'atom'; import ReporterProxy from './reporter-proxy'; let WelcomeView, GuideView, ConsentView; const WELCOME_URI = 'atom://welcome/welcome'; const GUIDE_URI = 'atom://welcome/guide'; const CONSENT_URI = 'atom://welcome/consent'; export default class WelcomePackage { constructor() { this.reporterProxy = new ReporterProxy(); } async activate() { this.subscriptions = new CompositeDisposable(); this.subscriptions.add( atom.workspace.addOpener(filePath => { if (filePath === WELCOME_URI) { return this.createWelcomeView({ uri: WELCOME_URI }); } }) ); this.subscriptions.add( atom.workspace.addOpener(filePath => { if (filePath === GUIDE_URI) { return this.createGuideView({ uri: GUIDE_URI }); } }) ); this.subscriptions.add( atom.workspace.addOpener(filePath => { if (filePath === CONSENT_URI) { return this.createConsentView({ uri: CONSENT_URI }); } }) ); this.subscriptions.add( atom.commands.add('atom-workspace', 'welcome:show', () => this.show()) ); if (atom.config.get('core.telemetryConsent') === 'undecided') { await atom.workspace.open(CONSENT_URI); } if (atom.config.get('welcome.showOnStartup')) { await this.show(); this.reporterProxy.sendEvent('show-on-initial-load'); } } show() { return Promise.all([ atom.workspace.open(WELCOME_URI, { split: 'left' }), atom.workspace.open(GUIDE_URI, { split: 'right' }) ]); } consumeReporter(reporter) { return this.reporterProxy.setReporter(reporter); } deactivate() { this.subscriptions.dispose(); } createWelcomeView(state) { if (WelcomeView == null) WelcomeView = require('./welcome-view'); return new WelcomeView({ reporterProxy: this.reporterProxy, ...state }); } createGuideView(state) { if (GuideView == null) GuideView = require('./guide-view'); return new GuideView({ reporterProxy: this.reporterProxy, ...state }); } createConsentView(state) { if (ConsentView == null) ConsentView = require('./consent-view'); return new ConsentView({ reporterProxy: this.reporterProxy, ...state }); } } ================================================ FILE: packages/welcome/lib/welcome-view.js ================================================ /** @babel */ /** @jsx etch.dom **/ import etch from 'etch'; export default class WelcomeView { constructor(props) { this.props = props; etch.initialize(this); this.element.addEventListener('click', event => { const link = event.target.closest('a'); if (link && link.dataset.event) { this.props.reporterProxy.sendEvent( `clicked-welcome-${link.dataset.event}-link` ); } }); } didChangeShowOnStartup() { atom.config.set('welcome.showOnStartup', this.checked); } update() {} serialize() { return { deserializer: 'WelcomeView', uri: this.props.uri }; } render() { return (

    A hackable text editor for the 21st Century

    For help, please visit

    ); } getURI() { return this.props.uri; } getTitle() { return 'Welcome'; } isEqual(other) { return other instanceof WelcomeView; } } ================================================ FILE: packages/welcome/menus/welcome.cson ================================================ 'menu': [ 'label': 'Help' 'submenu': [ {'label': 'Welcome Guide', 'command': 'welcome:show'} ] ] ================================================ FILE: packages/welcome/package.json ================================================ { "name": "welcome", "version": "0.36.9", "description": "Welcome users to Atom with useful information", "main": "./lib/main", "atomTestRunner": "atom-mocha-test-runner", "repository": "https://github.com/atom/atom", "license": "MIT", "engines": { "atom": ">0.50.0" }, "scripts": { "lint": "standard", "test": "atom --test test/*.test.js" }, "consumedServices": { "metrics-reporter": { "versions": { "^1.1.0": "consumeReporter" } } }, "configSchema": { "showOnStartup": { "type": "boolean", "default": true, "description": "Show welcome panes with useful information when opening a new Atom window." } }, "deserializers": { "WelcomeView": "createWelcomeView", "GuideView": "createGuideView", "ConsentView": "createConsentView" }, "dependencies": { "etch": "0.9.0" }, "devDependencies": { "atom-mocha-test-runner": "^1.0.0", "standard": "^8.6.0" }, "standard": { "globals": [ "atom" ] } } ================================================ FILE: packages/welcome/styles/welcome.less ================================================ @import "octicon-mixins"; @import "ui-variables"; .welcome { display: flex; font-size: 1.25em; line-height: 1.4; color: @text-color; background-color: @base-background-color; overflow-x: auto; // Overrides ---------------------- p { line-height: inherit; } a { color: @text-color-info; } ul { padding-left: 2em; } label { margin-top: 1em; font-weight: normal; } .input-checkbox { margin-top: -.2em; margin-right: .5em; } // Components ---------------------- &-container { width: 100%; max-width: 580px; min-width: 300px; margin: auto; padding: 3em 2em; } &-header { margin: 0 0 3em 0; } &-logo { display: block; width: 100%; max-width: 280px; margin: 0 auto 2em auto; color: @text-color-highlight; } &-title { font-size: 1.4em; text-align: center; line-height: 1.3; margin: 1em 0; } &-panel { } &-card { margin: 1em 0; border-radius: @component-border-radius*2; border: 1px solid @base-border-color; background-color: lighten(@base-background-color, 3%); } &-summary { padding: 1em 1.5em; font-size: 1.1em; font-weight: 300; line-height: 1.4; cursor: pointer; &:hover { color: lighten(@text-color, 4%); } &::-webkit-details-marker { display: none } &:before { width: 20px; color: @text-color-subtle; details[open] & { color: @text-color-highlight; } } details[open] & { color: @text-color-highlight; } } &-highlight { color: @text-color-highlight; } &-detail { border-top: 1px solid @base-border-color; padding: 1.5em; .welcome-note { margin-bottom: 0; } .btn { margin-top: @component-padding/3; margin-bottom: @component-padding/3; } } &-img { display: block; max-width: 100%; border-radius: 3px; margin: 0 0 1em 0; border: 1px solid @base-border-color; } &-consent { margin-top: 3em; &-choices { display: flex; flex-direction: row; flex-wrap: wrap; margin: 1em -1em; button { width: 100% } div { margin: .5em 1em; flex: 1; } .welcome-note { margin-top: 1em; } } } &-key { display: inline-block; padding: .3em .4em; font-size: .8em; line-height: 1; border-radius: 3px; color: @text-color-highlight; border: 1px solid @text-color-subtle; background: hsla(0,0%,0%,.1); } &-love { color: @text-color-subtle; a { color: @text-color-subtle; } .icon::before { // Make these octicons look good inlined with text position: relative; top: 4px; width: auto; margin-right: 0; font-size: 1.5em; } .icon-logo-github::before { top: 2px; font-size: 3.6em; vertical-align: top; } } &-note { font-size: .86em; color: @text-color-subtle; .icon { margin-left: 5px; } .icon::before { margin-right: 0; } } &-footer { margin-top: 1em; text-align: center; } } ================================================ FILE: packages/welcome/test/helpers.js ================================================ /** @babel */ export function conditionPromise(predicate) { return new Promise(resolve => { setInterval(() => { if (predicate()) resolve(); }, 100); }); } ================================================ FILE: packages/welcome/test/welcome.test.js ================================================ /** @babel */ /* global beforeEach, afterEach, describe, it */ import WelcomePackage from '../lib/welcome-package'; import assert from 'assert'; import { conditionPromise } from './helpers'; describe('Welcome', () => { let welcomePackage; beforeEach(() => { welcomePackage = new WelcomePackage(); atom.config.set('welcome.showOnStartup', true); }); afterEach(() => { atom.reset(); }); describe("when `core.telemetryConsent` is 'undecided'", () => { beforeEach(async () => { atom.config.set('core.telemetryConsent', 'undecided'); await welcomePackage.activate(); }); it('opens the telemetry consent pane and the welcome panes', () => { const panes = atom.workspace.getCenter().getPanes(); assert.equal(panes.length, 2); assert.equal(panes[0].getItems()[0].getTitle(), 'Telemetry Consent'); assert.equal(panes[0].getItems()[1].getTitle(), 'Welcome'); assert.equal(panes[1].getItems()[0].getTitle(), 'Welcome Guide'); }); }); describe('when `core.telemetryConsent` is not `undecided`', () => { beforeEach(async () => { atom.config.set('core.telemetryConsent', 'no'); await welcomePackage.activate(); }); describe('when activated for the first time', () => it('shows the welcome panes', () => { const panes = atom.workspace.getCenter().getPanes(); assert.equal(panes.length, 2); assert.equal(panes[0].getItems()[0].getTitle(), 'Welcome'); assert.equal(panes[1].getItems()[0].getTitle(), 'Welcome Guide'); })); describe('the welcome:show command', () => { it('shows the welcome buffer', async () => { atom.workspace .getCenter() .getPanes() .map(pane => pane.destroy()); assert(!atom.workspace.getActivePaneItem()); const workspaceElement = atom.views.getView(atom.workspace); atom.commands.dispatch(workspaceElement, 'welcome:show'); await conditionPromise(() => atom.workspace.getActivePaneItem()); const panes = atom.workspace.getCenter().getPanes(); assert.equal(panes.length, 2); assert.equal(panes[0].getItems()[0].getTitle(), 'Welcome'); }); }); describe('deserializing the pane items', () => { describe('when GuideView is deserialized', () => { it('remembers open sections', () => { const panes = atom.workspace.getCenter().getPanes(); const guideView = panes[1].getItems()[0]; guideView.element .querySelector('details[data-section="snippets"]') .setAttribute('open', 'open'); guideView.element .querySelector('details[data-section="init-script"]') .setAttribute('open', 'open'); const state = guideView.serialize(); assert.deepEqual(state.openSections, ['init-script', 'snippets']); const newGuideView = welcomePackage.createGuideView(state); assert( !newGuideView.element .querySelector('details[data-section="packages"]') .hasAttribute('open') ); assert( newGuideView.element .querySelector('details[data-section="snippets"]') .hasAttribute('open') ); assert( newGuideView.element .querySelector('details[data-section="init-script"]') .hasAttribute('open') ); }); }); }); describe('reporting events', () => { let panes, guideView, reportedEvents; beforeEach(() => { panes = atom.workspace.getCenter().getPanes(); guideView = panes[1].getItems()[0]; reportedEvents = []; welcomePackage.reporterProxy.sendEvent = (...event) => { reportedEvents.push(event); }; }); describe('GuideView events', () => { it('captures expand and collapse events', () => { guideView.element .querySelector('details[data-section="packages"] summary') .click(); assert.deepEqual(reportedEvents, [['expand-packages-section']]); guideView.element .querySelector('details[data-section="packages"]') .setAttribute('open', 'open'); guideView.element .querySelector('details[data-section="packages"] summary') .click(); assert.deepEqual(reportedEvents, [ ['expand-packages-section'], ['collapse-packages-section'] ]); }); it('captures button events', () => { for (const detailElement of Array.from( guideView.element.querySelector('details') )) { reportedEvents.length = 0; const sectionName = detailElement.dataset.section; const eventName = `clicked-${sectionName}-cta`; const primaryButton = detailElement.querySelector('.btn-primary'); if (primaryButton) { primaryButton.click(); assert.deepEqual(reportedEvents, [[eventName]]); } } }); }); }); describe('when the reporter changes', () => it('sends all queued events', () => { welcomePackage.reporterProxy.queue.length = 0; const reporter1 = { addCustomEvent(category, event) { this.reportedEvents.push({ category, ...event }); }, reportedEvents: [] }; const reporter2 = { addCustomEvent(category, event) { this.reportedEvents.push({ category, ...event }); }, reportedEvents: [] }; welcomePackage.reporterProxy.sendEvent('foo', 'bar', 10); welcomePackage.reporterProxy.sendEvent('foo2', 'bar2', 60); welcomePackage.reporterProxy.setReporter(reporter1); assert.deepEqual(reporter1.reportedEvents, [ { category: 'welcome-v1', ea: 'foo', el: 'bar', ev: 10 }, { category: 'welcome-v1', ea: 'foo2', el: 'bar2', ev: 60 } ]); welcomePackage.consumeReporter(reporter2); assert.deepEqual(reporter2.reportedEvents, []); })); }); }); ================================================ FILE: resources/linux/atom.desktop.in ================================================ [Desktop Entry] Name=<%= appName %> Comment=<%= description %> GenericName=Text Editor Exec=env ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT=false <%= installDir %>/bin/<%= appFileName %> %F Icon=<%= iconPath %> Type=Application StartupNotify=true Categories=GTK;Utility;TextEditor;Development; MimeType=application/javascript;application/json;application/x-httpd-eruby;application/x-httpd-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/x-ruby;application/x-bash;application/x-csh;application/x-sh;application/x-zsh;application/x-shellscript;application/x-sql;application/x-tcl;application/xhtml+xml;application/xml;application/xml-dtd;application/xslt+xml;text/coffeescript;text/css;text/html;text/plain;text/xml;text/xml-dtd;text/x-bash;text/x-c++;text/x-c++hdr;text/x-c++src;text/x-c;text/x-chdr;text/x-csh;text/x-csrc;text/x-dsrc;text/x-diff;text/x-go;text/x-java;text/x-java-source;text/x-makefile;text/x-markdown;text/x-objc;text/x-perl;text/x-php;text/x-python;text/x-ruby;text/x-sh;text/x-zsh;text/yaml;inode/directory StartupWMClass=atom ================================================ FILE: resources/linux/atom.policy ================================================ Atom Admin privileges required Please enter your password to save this file /bin/dd true auth_admin_keep auth_admin_keep auth_admin_keep ================================================ FILE: resources/linux/debian/control.in ================================================ Package: <%= appFileName %> Version: <%= version %> Depends: git, libgcrypt20, libgtk-3-0 (>= 3.9.10), libnotify4, libnss3 (>= 2:3.22), libglib2.0-bin | kde-cli-tools | kde-runtime, xdg-utils, libasound2 (>= 1.0.16), libgbm1, libx11-xcb1, libxcb-dri3-0, libxss1, libxtst6, libxkbfile1, libcurl3 | libcurl4 Recommends: policykit-1, libsecret-1-0, gnome-keyring Suggests: lsb-release Section: devel Priority: optional Architecture: <%= arch %> Installed-Size: <%= installedSize %> Maintainer: GitHub Description: <%= description %> Atom is a free and open source text editor that is modern, approachable, and hackable to the core. ================================================ FILE: resources/linux/desktopenviroment/cinnamon/atom.nemo_action ================================================ [Nemo Action] Active=true Name=Open in Atom Comment=Open in Atom #%U is the current selected file, this will also work on current directory Exec=atom -n %U Icon-Name=atom Selection=any Extensions=any ================================================ FILE: resources/linux/redhat/atom.spec.in ================================================ Name: <%= appFileName %> Version: <%= version %> Release: 0.1%{?dist} Summary: <%= description %> License: MIT URL: https://atom.io/ AutoReqProv: no # Avoid libchromiumcontent.so missing dependency Prefix: <%= installDir %> %ifarch i386 i486 i586 i686 Requires: alsa-lib, git-core, (glib2 or kde-cli-tools or xdg-utils), lsb-core-noarch, (libcurl.so.3 or libcurl.so.4), libgbm.so.1, libgcrypt.so.20, libnotify, libnss3.so, libX11-xcb.so.1, libxcb-dri3.so.0, libxkbfile.so.1, libXss.so.1, libsecret-1.so.0, gtk3, polkit %else Requires: alsa-lib, git-core, (glib2 or kde-cli-tools or xdg-utils), lsb-core-noarch, (libcurl.so.3()(64bit) or libcurl.so.4()(64bit)), libgbm.so.1()(64bit), libgcrypt.so.20()(64bit), libnotify, libnss3.so()(64bit), libX11-xcb.so.1()(64bit), libxcb-dri3.so.0()(64bit), libxkbfile.so.1()(64bit), libXss.so.1()(64bit), libsecret-1.so.0()(64bit), gtk3, polkit %endif # Disable Fedora's shebang mangling script, # which errors out on any file with versionless `python` in its shebang # See: https://github.com/atom/atom/issues/21937 %undefine __brp_mangle_shebangs %description <%= description %> %install mkdir -p "%{buildroot}/<%= installDir %>/share/<%= appFileName %>/" cp -r "<%= appName %>"/* "%{buildroot}/<%= installDir %>/share/<%= appFileName %>/" mkdir -p "%{buildroot}/<%= installDir %>/bin/" ln -sf "../share/<%= appFileName %>/resources/app/apm/node_modules/.bin/apm" "%{buildroot}/<%= installDir %>/bin/<%= apmFileName %>" cp atom.sh "%{buildroot}/<%= installDir %>/bin/<%= appFileName %>" chmod 755 "%{buildroot}/<%= installDir %>/bin/<%= appFileName %>" mkdir -p "%{buildroot}/<%= installDir %>/share/applications/" cp "<%= appFileName %>.desktop" "%{buildroot}/<%= installDir %>/share/applications/" mkdir -p "%{buildroot}/<%= installDir %>/share/polkit-1/actions/" cp "<%= policyFileName %>" "%{buildroot}/<%= installDir %>/share/polkit-1/actions/<%= policyFileName %>" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/1024x1024/apps" cp "icons/1024.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/1024x1024/apps/<%= appFileName %>.png" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/512x512/apps" cp "icons/512.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/512x512/apps/<%= appFileName %>.png" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/256x256/apps" cp "icons/256.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/256x256/apps/<%= appFileName %>.png" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/128x128/apps" cp "icons/128.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/128x128/apps/<%= appFileName %>.png" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/64x64/apps" cp "icons/64.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/64x64/apps/<%= appFileName %>.png" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/48x48/apps" cp "icons/48.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/48x48/apps/<%= appFileName %>.png" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/32x32/apps" cp "icons/32.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/32x32/apps/<%= appFileName %>.png" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/24x24/apps" cp "icons/24.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/24x24/apps/<%= appFileName %>.png" mkdir -p "%{buildroot}/<%= installDir %>/share/icons/hicolor/16x16/apps" cp "icons/16.png" "%{buildroot}/<%= installDir %>/share/icons/hicolor/16x16/apps/<%= appFileName %>.png" %files <%= installDir %>/bin/<%= appFileName %> <%= installDir %>/bin/<%= apmFileName %> <%= installDir %>/share/<%= appFileName %>/ <%= installDir %>/share/applications/<%= appFileName %>.desktop <%= installDir %>/share/polkit-1/actions/<%= policyFileName %> <%= installDir %>/share/icons/hicolor/ %attr(4755, root, root) <%= installDir %>/share/<%= appFileName %>/chrome-sandbox ================================================ FILE: resources/mac/atom-Info.plist ================================================ CFBundleExecutable Atom CFBundleIconFile atom.icns CFBundleIdentifier com.github.atom CFBundleInfoDictionaryVersion 6.0 CFBundleName Atom CFBundlePackageType APPL CFBundleDevelopmentRegion English CFBundleShortVersionString 0 CFBundleSignature ???? CFBundleVersion 0 LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion 10.8 NSAppleScriptEnabled YES NSMainNibFile MainMenu NSPrincipalClass AtomApplication NSSupportsAutomaticGraphicsSwitching NSRequiresAquaSystemAppearance NO SUScheduledCheckInterval 3600 CFBundleURLTypes CFBundleURLSchemes atom CFBundleURLName Atom Shared Session Protocol CFBundleDocumentTypes CFBundleTypeExtensions adb ads CFBundleTypeIconFile file.icns CFBundleTypeName ADA source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions scpt CFBundleTypeIconFile file.icns CFBundleTypeName Compiled AppleScript CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions applescript CFBundleTypeIconFile file.icns CFBundleTypeName AppleScript source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions as CFBundleTypeIconFile file.icns CFBundleTypeName ActionScript source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions asp asa CFBundleTypeIconFile file.icns CFBundleTypeName ASP document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions aspx ascx asmx ashx CFBundleTypeIconFile file.icns CFBundleTypeName ASP.NET document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions bib CFBundleTypeIconFile file.icns CFBundleTypeName BibTeX bibliography CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions c CFBundleTypeIconFile file.icns CFBundleTypeName C source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions cc cp cpp cxx c++ CFBundleTypeIconFile file.icns CFBundleTypeName C++ source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions cs CFBundleTypeIconFile file.icns CFBundleTypeName C# source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions coffee CFBundleTypeIconFile file.icns CFBundleTypeName CoffeeScript source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions COMMIT_EDITMSG CFBundleTypeIconFile file.icns CFBundleTypeName Commit message CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions cfdg CFBundleTypeIconFile file.icns CFBundleTypeName Context Free Design Grammar CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions clj cljs CFBundleTypeIconFile file.icns CFBundleTypeName Clojure source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions csv CFBundleTypeIconFile file.icns CFBundleTypeName Comma separated values CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions tsv CFBundleTypeIconFile file.icns CFBundleTypeName Tab separated values CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions cgi fcgi CFBundleTypeIconFile file.icns CFBundleTypeName CGI script CFBundleTypeRole Editor CFBundleTypeExtensions cfg conf config htaccess CFBundleTypeIconFile file.icns CFBundleTypeName Configuration file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions css CFBundleTypeIconFile file.icns CFBundleTypeName Cascading style sheet CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions diff CFBundleTypeIconFile file.icns CFBundleTypeName Differences file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions dtd CFBundleTypeIconFile file.icns CFBundleTypeName Document Type Definition CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions dylan CFBundleTypeIconFile file.icns CFBundleTypeName Dylan source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions erl hrl CFBundleTypeIconFile file.icns CFBundleTypeName Erlang source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions fscript CFBundleTypeIconFile file.icns CFBundleTypeName F-Script source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions f for fpp f77 f90 f95 CFBundleTypeIconFile file.icns CFBundleTypeName Fortran source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions h pch CFBundleTypeIconFile file.icns CFBundleTypeName Header CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions hh hpp hxx h++ CFBundleTypeIconFile file.icns CFBundleTypeName C++ header CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions go CFBundleTypeIconFile file.icns CFBundleTypeName Go source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions gtd gtdlog CFBundleTypeIconFile file.icns CFBundleTypeName GTD document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions hs lhs CFBundleTypeIconFile file.icns CFBundleTypeName Haskell source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions htm html phtml shtml CFBundleTypeIconFile file.icns CFBundleTypeName HTML document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions inc CFBundleTypeIconFile file.icns CFBundleTypeName Include file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions ics CFBundleTypeIconFile file.icns CFBundleTypeName iCalendar schedule CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions ini CFBundleTypeIconFile file.icns CFBundleTypeName MS Windows initialization file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions io CFBundleTypeIconFile file.icns CFBundleTypeName Io source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions java CFBundleTypeIconFile file.icns CFBundleTypeName Java source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions bsh CFBundleTypeIconFile file.icns CFBundleTypeName BeanShell script CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions properties CFBundleTypeIconFile file.icns CFBundleTypeName Java properties file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions js htc CFBundleTypeIconFile file.icns CFBundleTypeName JavaScript source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions jsp CFBundleTypeIconFile file.icns CFBundleTypeName Java Server Page CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions json CFBundleTypeIconFile file.icns CFBundleTypeName JSON file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions ldif CFBundleTypeIconFile file.icns CFBundleTypeName LDAP Data Interchange Format CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions less CFBundleTypeIconFile file.icns CFBundleTypeName Less source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions lisp cl l lsp mud el CFBundleTypeIconFile file.icns CFBundleTypeName Lisp source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions log CFBundleTypeIconFile file.icns CFBundleTypeName Log file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions logo CFBundleTypeIconFile file.icns CFBundleTypeName Logo source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions lua CFBundleTypeIconFile file.icns CFBundleTypeName Lua source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions markdown mdown markdn md CFBundleTypeIconFile file.icns CFBundleTypeName Markdown document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions mk CFBundleTypeIconFile file.icns CFBundleTypeName Makefile source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions wiki wikipedia mediawiki CFBundleTypeIconFile file.icns CFBundleTypeName Mediawiki document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions s mips spim asm CFBundleTypeIconFile file.icns CFBundleTypeName MIPS assembler source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions m3 cm3 CFBundleTypeIconFile file.icns CFBundleTypeName Modula-3 source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions moinmoin CFBundleTypeIconFile file.icns CFBundleTypeName MoinMoin document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions m CFBundleTypeIconFile file.icns CFBundleTypeName Objective-C source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions mm CFBundleTypeIconFile file.icns CFBundleTypeName Objective-C++ source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions ml mli mll mly CFBundleTypeIconFile file.icns CFBundleTypeName OCaml source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions mustache hbs CFBundleTypeIconFile file.icns CFBundleTypeName Mustache document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions pas p CFBundleTypeIconFile file.icns CFBundleTypeName Pascal source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions patch CFBundleTypeIconFile file.icns CFBundleTypeName Patch file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions pl pod perl CFBundleTypeIconFile file.icns CFBundleTypeName Perl source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions pm CFBundleTypeIconFile file.icns CFBundleTypeName Perl module CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions php php3 php4 php5 CFBundleTypeIconFile file.icns CFBundleTypeName PHP source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions ps eps CFBundleTypeIconFile file.icns CFBundleTypeName PostScript source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions dict plist scriptSuite scriptTerminology CFBundleTypeIconFile file.icns CFBundleTypeName Property list CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions py rpy cpy python CFBundleTypeIconFile file.icns CFBundleTypeName Python source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions r s CFBundleTypeIconFile file.icns CFBundleTypeName R source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions rl ragel CFBundleTypeIconFile file.icns CFBundleTypeName Ragel source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions rem remind CFBundleTypeIconFile file.icns CFBundleTypeName Remind document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions rst rest CFBundleTypeIconFile file.icns CFBundleTypeName reStructuredText document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions rhtml erb CFBundleTypeIconFile file.icns CFBundleTypeName HTML with embedded Ruby CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions erbsql CFBundleTypeIconFile file.icns CFBundleTypeName SQL with embedded Ruby CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions rb rbx rjs rxml CFBundleTypeIconFile file.icns CFBundleTypeName Ruby source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions sass scss CFBundleTypeIconFile file.icns CFBundleTypeName Sass source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions scm sch CFBundleTypeIconFile file.icns CFBundleTypeName Scheme source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions ext CFBundleTypeIconFile file.icns CFBundleTypeName Setext document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions sh ss bashrc bash_profile bash_login profile bash_logout CFBundleTypeIconFile file.icns CFBundleTypeName Shell script CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions slate CFBundleTypeIconFile file.icns CFBundleTypeName Slate source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions sql CFBundleTypeIconFile file.icns CFBundleTypeName SQL source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions sml CFBundleTypeIconFile file.icns CFBundleTypeName Standard ML source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions strings CFBundleTypeIconFile file.icns CFBundleTypeName Strings document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions svg CFBundleTypeIconFile file.icns CFBundleTypeName Scalable vector graphics CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions i swg CFBundleTypeIconFile file.icns CFBundleTypeName SWIG source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions tcl CFBundleTypeIconFile file.icns CFBundleTypeName Tcl source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions tex sty cls CFBundleTypeIconFile file.icns CFBundleTypeName TeX document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions text txt utf8 CFBundleTypeIconFile file.icns CFBundleTypeMIMETypes text/plain CFBundleTypeName Plain text document CFBundleTypeOSTypes TEXT sEXT ttro CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions textile CFBundleTypeIconFile file.icns CFBundleTypeName Textile document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions toml CFBundleTypeIconFile file.icns CFBundleTypeName TOML file CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions xhtml CFBundleTypeIconFile file.icns CFBundleTypeName XHTML document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions xml xsd xib rss tld pt cpt dtml CFBundleTypeIconFile file.icns CFBundleTypeName XML document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions xsl xslt CFBundleTypeIconFile file.icns CFBundleTypeName XSL stylesheet CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions vcf vcard CFBundleTypeIconFile file.icns CFBundleTypeName Electronic business card CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions vb CFBundleTypeIconFile file.icns CFBundleTypeName Visual Basic source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions yaml yml CFBundleTypeIconFile file.icns CFBundleTypeName YAML document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions nfo CFBundleTypeIconFile file.icns CFBundleTypeName Text document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions g vss d e gri inf mel build re textmate fxscript lgt CFBundleTypeIconFile file.icns CFBundleTypeName Source CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeExtensions cfm cfml dbm dbml dist dot ics ifb dwt g in l m4 mp mtml orig pde rej servlet s5 tmp tpl tt xql yy * CFBundleTypeIconFile file.icns CFBundleTypeName Document CFBundleTypeRole Editor LSHandlerRank Alternate CFBundleTypeIconFile file.icns CFBundleTypeName Document CFBundleTypeOSTypes **** CFBundleTypeRole Editor LSHandlerRank Alternate LSItemContentTypes public.data CFBundleTypeRole Editor LSHandlerRank Alternate LSItemContentTypes public.directory com.apple.bundle com.apple.resolvable ================================================ FILE: resources/mac/entitlements.plist ================================================ com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-library-validation ================================================ FILE: resources/mac/helper-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName Atom Helper CFBundleExecutable Atom Helper CFBundleIdentifier com.github.atom.helper CFBundleInfoDictionaryVersion 6.0 CFBundleName Atom Helper CFBundlePackageType APPL CFBundleShortVersionString ${VERSION} CFBundleSignature ???? CFBundleVersion ${VERSION} LSMinimumSystemVersion 10.7.0 LSUIElement 1 NSSupportsAutomaticGraphicsSwitching ================================================ FILE: resources/win/apm.cmd ================================================ @echo off "%~dp0\..\app\apm\bin\apm.cmd" %* ================================================ FILE: resources/win/apm.sh ================================================ #!/bin/sh "$(dirname "$0")/../app/apm/bin/apm" "$@" ================================================ FILE: resources/win/atom.cmd ================================================ @echo off SET EXPECT_OUTPUT= SET WAIT= SET PSARGS=%* SET ELECTRON_ENABLE_LOGGING= SET ATOM_ADD= SET ATOM_NEW_WINDOW= FOR %%a IN (%*) DO ( IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES IF /I "%%a"=="--foreground" SET EXPECT_OUTPUT=YES IF /I "%%a"=="-h" SET EXPECT_OUTPUT=YES IF /I "%%a"=="--help" SET EXPECT_OUTPUT=YES IF /I "%%a"=="-t" SET EXPECT_OUTPUT=YES IF /I "%%a"=="--test" SET EXPECT_OUTPUT=YES IF /I "%%a"=="--benchmark" SET EXPECT_OUTPUT=YES IF /I "%%a"=="--benchmark-test" SET EXPECT_OUTPUT=YES IF /I "%%a"=="-v" SET EXPECT_OUTPUT=YES IF /I "%%a"=="--version" SET EXPECT_OUTPUT=YES IF /I "%%a"=="--enable-electron-logging" SET ELECTRON_ENABLE_LOGGING=YES IF /I "%%a"=="-a" SET ATOM_ADD=YES IF /I "%%a"=="--add" SET ATOM_ADD=YES IF /I "%%a"=="-n" SET ATOM_NEW_WINDOW=YES IF /I "%%a"=="--new-window" SET ATOM_NEW_WINDOW=YES IF /I "%%a"=="-w" ( SET EXPECT_OUTPUT=YES SET WAIT=YES ) IF /I "%%a"=="--wait" ( SET EXPECT_OUTPUT=YES SET WAIT=YES ) ) IF "%ATOM_ADD%"=="YES" ( IF "%ATOM_NEW_WINDOW%"=="YES" ( SET EXPECT_OUTPUT=YES ) ) IF "%EXPECT_OUTPUT%"=="YES" ( IF "%WAIT%"=="YES" ( powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\<%= atomExeName %>\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event" exit 0 ) ELSE ( "%~dp0\..\..\<%= atomExeName %>" %* ) ) ELSE ( "%~dp0\..\app\apm\bin\node.exe" "%~dp0\atom.js" "<%= atomExeName %>" %* ) ================================================ FILE: resources/win/atom.js ================================================ var path = require('path'); var spawn = require('child_process').spawn; var atomCommandPath = path.resolve(__dirname, '..', '..', process.argv[2]); var args = process.argv.slice(3); args.unshift('--executed-from', process.cwd()); var options = { detached: true, stdio: 'ignore' }; spawn(atomCommandPath, args, options); process.exit(0); ================================================ FILE: resources/win/atom.sh ================================================ #!/bin/bash # Get current path in Windows format if command -v "cygpath" > /dev/null; then # We have cygpath to do the conversion ATOMCMD=$(cygpath "$(dirname "$0")/atom.cmd" -a -w) ARGS=( $(cygpath -a -w "$@" | tr '\n' ' ') ) else ARGS=$@ pushd "$(dirname "$0")" > /dev/null if [[ $(uname -r) =~ (M|m)icrosoft ]]; then # We are in Windows Subsystem for Linux, map /mnt/drive root="/mnt/" # If different root mount point defined in /etc/wsl.conf, use that instead eval $(grep "^root" /etc/wsl.conf | sed -e "s/ //g") root="$(echo $root | sed 's|/|\\/|g')" ATOMCMD="$(echo $PWD | sed 's/\/mnt\/\([a-z]*\)\(.*\)/\1:\2/')/atom.cmd" else # We don't have cygpath or WSL so try pwd -W ATOMCMD="$(pwd -W)/atom.cmd" fi popd > /dev/null fi if [ "$(uname -o)" == "Msys" ] || [[ $(uname -r) == *-Microsoft ]]; then cmd.exe //C "$ATOMCMD" "$@" # Msys amd WSL think /C is a Windows path... else cmd.exe /C "$ATOMCMD" "${ARGS[@]}" # Cygwin does not fi ================================================ FILE: resources/win/atom.visualElementsManifest.xml ================================================ ================================================ FILE: script/bootstrap ================================================ #!/usr/bin/env node 'use strict' const path = require('path') const CONFIG = require('./config') const childProcess = require('child_process') const cleanDependencies = require('./lib/clean-dependencies') const deleteMsbuildFromPath = require('./lib/delete-msbuild-from-path') const dependenciesFingerprint = require('./lib/dependencies-fingerprint') const installApm = require('./lib/install-apm') const runApmInstall = require('./lib/run-apm-install') const installScriptDependencies = require('./lib/install-script-dependencies') const verifyMachineRequirements = require('./lib/verify-machine-requirements') process.on('unhandledRejection', function (e) { console.error(e.stack || e) process.exit(1) }) // We can't use yargs until installScriptDependencies() is executed, so... let ci = process.argv.indexOf('--ci') !== -1 if (!ci && process.env.CI === 'true' && process.argv.indexOf('--no-ci') === -1) { console.log('Automatically enabling --ci because CI is set in the environment') ci = true } verifyMachineRequirements(ci) if (dependenciesFingerprint.isOutdated()) { cleanDependencies() } if (process.platform === 'win32') deleteMsbuildFromPath() installScriptDependencies(ci) installApm(ci) const apmVersionEnv = Object.assign({}, process.env); // Set resource path so that apm can load Atom's version. apmVersionEnv.ATOM_RESOURCE_PATH = CONFIG.repositoryRootPath; childProcess.execFileSync( CONFIG.getApmBinPath(), ['--version'], {stdio: 'inherit', env: apmVersionEnv} ) runApmInstall(CONFIG.repositoryRootPath, ci) dependenciesFingerprint.write() ================================================ FILE: script/bootstrap.cmd ================================================ @IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\bootstrap" %* ) ELSE ( node "%~dp0\bootstrap" %* ) ================================================ FILE: script/build ================================================ #!/usr/bin/env node 'use strict' if (process.argv.includes('--no-bootstrap')) { console.log('Skipping bootstrap') } else { // Bootstrap first to ensure all the dependencies used later in this script // are installed. require('./bootstrap') } // Required to load CS files in this build script, such as those in `donna` require('coffee-script/register') require('colors') const path = require('path') const yargs = require('yargs') const argv = yargs .usage('Usage: $0 [options]') .help('help') .describe('existing-binaries', 'Use existing Atom binaries (skip clean/transpile/cache)') .describe('code-sign', 'Code-sign executables (macOS and Windows only)') .describe('test-sign', 'Test-sign executables (macOS only)') .describe('create-windows-installer', 'Create installer (Windows only)') .describe('create-debian-package', 'Create .deb package (Linux only)') .describe('create-rpm-package', 'Create .rpm package (Linux only)') .describe('compress-artifacts', 'Compress Atom binaries (and symbols on macOS)') .describe('generate-api-docs', 'Only build the API documentation') .describe('install', 'Install Atom') .string('install') .describe('ci', 'Install dependencies quickly (package-lock.json files must be up to date)') .wrap(yargs.terminalWidth()) .argv const checkChromedriverVersion = require('./lib/check-chromedriver-version') const cleanOutputDirectory = require('./lib/clean-output-directory') const codeSignOnMac = require('./lib/code-sign-on-mac') const codeSignOnWindows = require('./lib/code-sign-on-windows') const compressArtifacts = require('./lib/compress-artifacts') const copyAssets = require('./lib/copy-assets') const createDebianPackage = require('./lib/create-debian-package') const createRpmPackage = require('./lib/create-rpm-package') const createWindowsInstaller = require('./lib/create-windows-installer') const dumpSymbols = require('./lib/dump-symbols') const generateAPIDocs = require('./lib/generate-api-docs') const generateMetadata = require('./lib/generate-metadata') const generateModuleCache = require('./lib/generate-module-cache') const generateStartupSnapshot = require('./lib/generate-startup-snapshot') const installApplication = require('./lib/install-application') const notarizeOnMac = require('./lib/notarize-on-mac') const packageApplication = require('./lib/package-application') const prebuildLessCache = require('./lib/prebuild-less-cache') const testSignOnMac = require('./lib/test-sign-on-mac') const transpileBabelPaths = require('./lib/transpile-babel-paths') const transpileCoffeeScriptPaths = require('./lib/transpile-coffee-script-paths') const transpileCsonPaths = require('./lib/transpile-cson-paths') const transpilePegJsPaths = require('./lib/transpile-peg-js-paths') const transpilePackagesWithCustomTranspilerPaths = require('./lib/transpile-packages-with-custom-transpiler-paths.js') process.on('unhandledRejection', function (e) { console.error(e.stack || e) process.exit(1) }) const CONFIG = require('./config') // Used by the 'github' package for Babel configuration process.env.ELECTRON_VERSION = CONFIG.appMetadata.electronVersion let binariesPromise = Promise.resolve() if (!argv.existingBinaries) { checkChromedriverVersion() cleanOutputDirectory() copyAssets() transpilePackagesWithCustomTranspilerPaths() transpileBabelPaths() transpileCoffeeScriptPaths() transpileCsonPaths() transpilePegJsPaths() generateModuleCache() prebuildLessCache() generateMetadata() generateAPIDocs() if (!argv.generateApiDocs) { binariesPromise = dumpSymbols() } } if (!argv.generateApiDocs) { binariesPromise .then(packageApplication) .then(packagedAppPath => generateStartupSnapshot(packagedAppPath).then(() => packagedAppPath)) .then(async packagedAppPath => { switch (process.platform) { case 'darwin': { if (argv.codeSign) { await codeSignOnMac(packagedAppPath) await notarizeOnMac(packagedAppPath) } else if (argv.testSign) { testSignOnMac(packagedAppPath) } else { console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray) } break } case 'win32': { if (argv.testSign) { console.log('Test signing is not supported on Windows, skipping.'.gray) } if (argv.codeSign) { const executablesToSign = [ path.join(packagedAppPath, CONFIG.executableName) ] if (argv.createWindowsInstaller) { executablesToSign.push(path.join(__dirname, 'node_modules', '@atom', 'electron-winstaller', 'vendor', 'Squirrel.exe')) } codeSignOnWindows(executablesToSign) } else { console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray) } if (argv.createWindowsInstaller) { return createWindowsInstaller(packagedAppPath) .then((installerPath) => { argv.codeSign && codeSignOnWindows([installerPath]) return packagedAppPath }) } else { console.log('Skipping creating installer. Specify the --create-windows-installer option to create a Squirrel-based Windows installer.'.gray) } break } case 'linux': { if (argv.createDebianPackage) { createDebianPackage(packagedAppPath) } else { console.log('Skipping creating debian package. Specify the --create-debian-package option to create it.'.gray) } if (argv.createRpmPackage) { createRpmPackage(packagedAppPath) } else { console.log('Skipping creating rpm package. Specify the --create-rpm-package option to create it.'.gray) } break } } return Promise.resolve(packagedAppPath) }).then(packagedAppPath => { if (argv.compressArtifacts) { compressArtifacts(packagedAppPath) } else { console.log('Skipping artifacts compression. Specify the --compress-artifacts option to compress Atom binaries (and symbols on macOS)'.gray) } if (argv.install != null) { installApplication(packagedAppPath, argv.install) } else { console.log('Skipping installation. Specify the --install option to install Atom'.gray) } }) } ================================================ FILE: script/build.cmd ================================================ @IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\build" %* ) ELSE ( node "%~dp0\build" %* ) ================================================ FILE: script/cibuild ================================================ echo "Builds for this version of Atom no longer run on Janky." echo "See https://github.com/atom/atom/pull/12410 for more information." ================================================ FILE: script/clean ================================================ #!/usr/bin/env node 'use strict' const cleanCaches = require('./lib/clean-caches') const cleanDependencies = require('./lib/clean-dependencies') const cleanOutputDirectory = require('./lib/clean-output-directory') const killRunningAtomInstances = require('./lib/kill-running-atom-instances') killRunningAtomInstances() cleanDependencies() cleanCaches() cleanOutputDirectory() ================================================ FILE: script/clean.cmd ================================================ @IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\clean" %* ) ELSE ( node "%~dp0\clean" %* ) ================================================ FILE: script/config.js ================================================ // This module exports paths, names, and other metadata that is referenced // throughout the build. 'use strict'; const path = require('path'); const spawnSync = require('./lib/spawn-sync'); const repositoryRootPath = path.resolve(__dirname, '..'); const apmRootPath = path.join(repositoryRootPath, 'apm'); const scriptRootPath = path.join(repositoryRootPath, 'script'); const buildOutputPath = path.join(repositoryRootPath, 'out'); const docsOutputPath = path.join(repositoryRootPath, 'docs', 'output'); const intermediateAppPath = path.join(buildOutputPath, 'app'); const symbolsPath = path.join(buildOutputPath, 'symbols'); const electronDownloadPath = path.join(repositoryRootPath, 'electron'); const homeDirPath = process.env.HOME || process.env.USERPROFILE; const atomHomeDirPath = process.env.ATOM_HOME || path.join(homeDirPath, '.atom'); const appMetadata = require(path.join(repositoryRootPath, 'package.json')); const apmMetadata = require(path.join(apmRootPath, 'package.json')); const computedAppVersion = computeAppVersion( process.env.ATOM_RELEASE_VERSION || appMetadata.version ); const channel = getChannel(computedAppVersion); const appName = getAppName(channel); const executableName = getExecutableName(channel, appName); const channelName = getChannelName(channel); // Sets the installation jobs to run maximally in parallel if the user has // not already configured this. This is applied just by requiring this file. if (process.env.npm_config_jobs === undefined) { process.env.npm_config_jobs = 'max'; } module.exports = { appMetadata, apmMetadata, channel, channelName, appName, executableName, computedAppVersion, repositoryRootPath, apmRootPath, scriptRootPath, buildOutputPath, docsOutputPath, intermediateAppPath, symbolsPath, electronDownloadPath, atomHomeDirPath, homeDirPath, getApmBinPath, getNpmBinPath, snapshotAuxiliaryData: {} }; function getChannelName(channel) { return channel === 'stable' ? 'atom' : `atom-${channel}`; } function getChannel(version) { const match = version.match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/); if (!match) { throw new Error(`Found incorrectly formatted Atom version ${version}`); } else if (match[2]) { return match[2]; } return 'stable'; } function getAppName(channel) { return channel === 'stable' ? 'Atom' : `Atom ${process.env.ATOM_CHANNEL_DISPLAY_NAME || channel.charAt(0).toUpperCase() + channel.slice(1)}`; } function getExecutableName(channel, appName) { if (process.platform === 'darwin') { return appName; } else if (process.platform === 'win32') { return channel === 'stable' ? 'atom.exe' : `atom-${channel}.exe`; } else { return 'atom'; } } function computeAppVersion(version) { if (version.match(/-dev$/)) { const result = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: repositoryRootPath }); const commitHash = result.stdout.toString().trim(); version += '-' + commitHash; } return version; } function getApmBinPath() { const apmBinName = process.platform === 'win32' ? 'apm.cmd' : 'apm'; return path.join( apmRootPath, 'node_modules', 'atom-package-manager', 'bin', apmBinName ); } function getNpmBinPath(external = false) { const npmBinName = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const localNpmBinPath = path.resolve( repositoryRootPath, 'script', 'node_modules', '.bin', npmBinName ); return localNpmBinPath; } ================================================ FILE: script/deprecated-packages.json ================================================ { "advanced-new-file": { "version": "<=0.4.1", "hasDeprecations": true, "latestHasDeprecations": false }, "angularjs-helper": { "version": "<=0.9.2", "hasDeprecations": true, "latestHasDeprecations": true }, "apex-ui-personalize": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "api-blueprint-preview": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "asciidoc-preview": { "version": "<=0.5.0", "hasDeprecations": true, "latestHasDeprecations": true }, "ask-stack": { "version": "<=1.1.0", "hasDeprecations": true, "latestHasDeprecations": false }, "assign-align": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "asteroids": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-2048": { "version": "<=1.2.3", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-angularjs": { "hasAlternative": true, "alternative": "angularjs" }, "atom-beautifier": { "version": "<=0.5.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-beautify": { "version": "<=0.27.6", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-browser-webview": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-charcode": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-cli-diff": { "version": "<=0.11.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-compile-coffee": { "version": "<=1.4.0", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-ctags": { "version": "<=3.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-eslint": { "hasAlternative": true, "alternative": "linter" }, "atom-faker": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-flake8": { "hasAlternative": true, "alternative": "linter" }, "atom-go-format": { "hasAlternative": true, "alternative": "go-plus" }, "atom-grunt-configs": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-html-preview": { "version": "<=0.1.6", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-html5-boilerplate": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-htmlizer": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-jsfmt": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-jshint": { "version": "<=1.5.0", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-lint": { "hasAlternative": true, "alternative": "linter" }, "atom-pair": { "version": "<=1.1.5", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-prettify": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-processing": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-python-debugger": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-rails": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-raml-preview": { "version": "<=0.0.1", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-runner": { "version": "<=2.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-semicolons": { "version": "<=0.1.5", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-spotify": { "version": "<=1.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-terminal-panel": { "version": "<=4.3.1", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-typescript": { "version": "<=4.1.0", "hasDeprecations": true, "latestHasDeprecations": false }, "atom-ungit": { "version": "<=0.4.3", "hasDeprecations": true, "latestHasDeprecations": true }, "atom-yeoman": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "atomatigit": { "version": "<=1.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "atomic-emacs": { "version": "<=0.5.1", "hasDeprecations": true, "latestHasDeprecations": false }, "atomic-rest": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": true }, "auto-detect-indentation": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "auto-indent": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "auto-replace-in-selection": { "version": "<=2.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "auto-update-packages": { "version": "<=0.2.2", "hasDeprecations": true, "latestHasDeprecations": true }, "autoclose-html": { "version": "<=0.15.0", "hasDeprecations": true, "latestHasDeprecations": false }, "autocomplete-haskell": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": false }, "autocomplete-jedi": { "hasAlternative": true, "alternative": "autocomplete-python" }, "autocomplete-paths": { "version": "<=1.0.1", "hasDeprecations": true, "latestHasDeprecations": false }, "autocomplete-phpunit": { "version": "<=1.0.0", "hasDeprecations": true, "latestHasDeprecations": false }, "autocomplete-plus-async": { "hasAlternative": true, "message": "`autocomplete-plus-async` has been replaced by `autocomplete-plus` which is bundled in core", "alternative": "core" }, "autocomplete-plus-jedi": { "version": "<=0.0.9", "hasDeprecations": true, "latestHasDeprecations": true }, "autocomplete-plus-python-jedi": { "hasAlternative": true, "alternative": "autocomplete-python" }, "autocomplete-snippets": { "version": "<=1.0.0", "hasDeprecations": true, "latestHasDeprecations": false }, "bezier-curve-editor": { "version": "<=0.6.6", "hasDeprecations": true, "latestHasDeprecations": false }, "big-cursor": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "block-comment": { "version": "<=0.4.1", "hasDeprecations": true, "latestHasDeprecations": false }, "browser-refresh": { "version": "<=0.8.3", "hasDeprecations": true, "latestHasDeprecations": true }, "cabal": { "version": "<=0.0.13", "hasDeprecations": true, "latestHasDeprecations": false }, "change-case": { "version": "<=0.5.1", "hasDeprecations": true, "latestHasDeprecations": true }, "circle-ci": { "version": "<=0.9.1", "hasDeprecations": true, "latestHasDeprecations": false }, "clang-format": { "version": "<=1.8.0", "hasDeprecations": true, "latestHasDeprecations": false }, "clipboard-history": { "version": "<=0.6.5", "hasDeprecations": true, "latestHasDeprecations": true }, "clone-cursor": { "version": "<=1.0.0", "hasDeprecations": true, "latestHasDeprecations": true }, "closure-linter": { "version": "<=0.2.5", "hasDeprecations": true, "latestHasDeprecations": true }, "code-links": { "version": "<=0.3.8", "hasDeprecations": true, "latestHasDeprecations": false }, "codeship-status": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": true }, "coffee-compile": { "version": "<=0.5.0", "hasDeprecations": true, "latestHasDeprecations": false }, "coffee-lint": { "hasAlternative": true, "alternative": "linter" }, "coffee-trace": { "version": "<=0.2.2", "hasDeprecations": true, "latestHasDeprecations": true }, "coffeescript-preview": { "hasAlternative": true, "alternative": "preview" }, "color": { "version": "<=0.5.0", "hasDeprecations": true, "latestHasDeprecations": false }, "color-picker": { "version": "<=1.7.0", "hasDeprecations": true, "latestHasDeprecations": false }, "command-logger": { "version": "<=0.20.0", "hasDeprecations": true, "latestHasDeprecations": false }, "comment": { "version": "<=0.2.7", "hasDeprecations": true, "latestHasDeprecations": true }, "compass": { "version": "<=0.8.0", "hasDeprecations": true, "latestHasDeprecations": false }, "composer": { "version": "<=0.3.1", "hasDeprecations": true, "latestHasDeprecations": true }, "convert-to-utf8": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "coverage": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": true }, "csscomb": { "version": "<=0.1.2", "hasDeprecations": true, "latestHasDeprecations": true }, "ctags-status": { "version": "<=1.2.3", "hasDeprecations": true, "latestHasDeprecations": false }, "cucumber-runner": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": true }, "cucumber-step": { "version": "<=0.1.2", "hasDeprecations": true, "latestHasDeprecations": true }, "custom-title": { "version": "<=0.7.1", "hasDeprecations": true, "latestHasDeprecations": true }, "cut-line": { "hasAlternative": true, "alternative": "core" }, "dash": { "version": "<=1.0.3", "hasDeprecations": true, "latestHasDeprecations": false }, "data-atom": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": false }, "devdocs": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "django-templates": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "docblockr": { "version": "<=0.6.3", "hasDeprecations": true, "latestHasDeprecations": false }, "easy-motion": { "version": "<=1.1.4", "hasDeprecations": true, "latestHasDeprecations": true }, "editor-stats": { "version": "<=0.16.0", "hasDeprecations": true, "latestHasDeprecations": false }, "editorconfig": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "elixir-cmd": { "version": "<=0.2.6", "hasDeprecations": true, "latestHasDeprecations": true }, "emacs-mode": { "version": "<=0.0.29", "hasDeprecations": true, "latestHasDeprecations": true }, "ember-cli-helper": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": false }, "emmet": { "version": "<=2.3.7", "hasDeprecations": true, "latestHasDeprecations": false }, "emp-debugger": { "version": "<=0.6.13", "hasDeprecations": true, "latestHasDeprecations": false }, "emp-template-management": { "version": "<=0.1.13", "hasDeprecations": true, "latestHasDeprecations": true }, "enhanced-tabs": { "version": "<=1.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "erb-snippets": { "version": "<=0.5.0", "hasDeprecations": true, "latestHasDeprecations": true }, "error-status": { "version": "<=0.3.3", "hasDeprecations": true, "latestHasDeprecations": true }, "eslint": { "version": "<=0.15.0", "hasDeprecations": true, "latestHasDeprecations": true }, "eval": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "ex-mode": { "version": "<=0.4.1", "hasDeprecations": true, "latestHasDeprecations": false }, "execute-as-ruby": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": true }, "expand-selection": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": true }, "explicit-reload": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "fancy-new-file": { "version": "<=0.7.0", "hasDeprecations": true, "latestHasDeprecations": true }, "file-icon-supplement": { "version": "<=0.7.3", "hasDeprecations": true, "latestHasDeprecations": false }, "file-icons": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "file-types": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "filetype-color": { "version": "<=0.1.4", "hasDeprecations": true, "latestHasDeprecations": true }, "firepad": { "version": "<=0.3.1", "hasDeprecations": true, "latestHasDeprecations": true }, "flake8": { "hasAlternative": true, "alternative": "linter" }, "floobits": { "version": "<=0.4.2", "hasDeprecations": true, "latestHasDeprecations": true }, "function-name-in-status-bar": { "version": "<=0.2.6", "hasDeprecations": true, "latestHasDeprecations": true }, "fuzzy-finder": { "version": "<=0.60.0", "hasDeprecations": true, "latestHasDeprecations": false }, "get-routes": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "gist-it": { "version": "<=0.6.10", "hasDeprecations": true, "latestHasDeprecations": false }, "git-blame": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": false }, "git-control": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "git-diff": { "version": "<=0.43.0", "hasDeprecations": true, "latestHasDeprecations": false }, "git-diff-details": { "version": "<=0.8.0", "hasDeprecations": true, "latestHasDeprecations": false }, "git-log": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "git-plus": { "version": "<=4.5.0", "hasDeprecations": true, "latestHasDeprecations": false }, "git-review": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": true }, "git-tab-status": { "version": "<=1.5.3", "hasDeprecations": true, "latestHasDeprecations": false }, "github-issues": { "version": "<=0.5.0", "hasDeprecations": true, "latestHasDeprecations": true }, "gitignore-snippets": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": true }, "gitter": { "version": "<=0.6.2", "hasDeprecations": true, "latestHasDeprecations": true }, "go-oracle": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "go-plus": { "version": "<=2.0.8", "hasDeprecations": true, "latestHasDeprecations": false }, "go-to-view": { "version": "<=0.1.2", "hasDeprecations": true, "latestHasDeprecations": true }, "gocode": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": true }, "gradle-ci": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": true }, "grunt-runner": { "version": "<=0.8.2", "hasDeprecations": true, "latestHasDeprecations": false }, "gulp-helper": { "version": "<=4.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "gutter-shadow": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": false }, "hiera-eyaml": { "version": "<=0.4.7", "hasDeprecations": true, "latestHasDeprecations": false }, "highlight-column": { "version": "<=0.3.2", "hasDeprecations": true, "latestHasDeprecations": false }, "highlight-cov": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "highlight-css-color": { "hasAlternative": true, "alternative": "pigments" }, "highlight-line": { "version": "<=0.9.3", "hasDeprecations": true, "latestHasDeprecations": false }, "highlight-selected": { "version": "<=0.7.0", "hasDeprecations": true, "latestHasDeprecations": false }, "hipster-ipsum": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "html-entities": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "html-helper": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": true }, "html-img": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": true }, "html2haml": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": false }, "html2jade": { "version": "<=0.7.0", "hasDeprecations": true, "latestHasDeprecations": true }, "htmlhint": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": false }, "icon-font-picker": { "version": "<=0.0.2", "hasDeprecations": true, "latestHasDeprecations": true }, "ide-flow": { "version": "<=0.7.0", "hasDeprecations": true, "latestHasDeprecations": false }, "ide-haskell": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "import": { "version": "<=1.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "inc-dec-value": { "version": "<=0.0.7", "hasDeprecations": true, "latestHasDeprecations": true }, "increment-number": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "indent-helper": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": false }, "indentation-jumper": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": true }, "inline-autocomplete": { "version": "<=1.0.4", "hasDeprecations": true, "latestHasDeprecations": false }, "ionic-atom": { "version": "<=0.3.1", "hasDeprecations": true, "latestHasDeprecations": true }, "japanese-zen-han-convert": { "version": "<=0.3.2", "hasDeprecations": true, "latestHasDeprecations": false }, "jsdoc": { "version": "<=0.9.0", "hasDeprecations": true, "latestHasDeprecations": true }, "jsformat": { "version": "<=0.8.1", "hasDeprecations": true, "latestHasDeprecations": false }, "jslint": { "version": "<=1.2.1", "hasDeprecations": true, "latestHasDeprecations": false }, "jsonlint": { "version": "<=1.0.2", "hasDeprecations": true, "latestHasDeprecations": false }, "jsonpp": { "version": "<=0.0.6", "hasDeprecations": true, "latestHasDeprecations": false }, "keycodes": { "version": "<=0.1.2", "hasDeprecations": true, "latestHasDeprecations": true }, "kinetic": { "version": "<=0.2.5", "hasDeprecations": true, "latestHasDeprecations": true }, "language-javascript-semantic": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "language-jsoniq": { "version": "<=1.4.0", "hasDeprecations": true, "latestHasDeprecations": false }, "language-jxa": { "hasDeprecations": true, "latestHasDeprecations": true }, "language-nlf": { "hasAlternative": true, "alternative": "language-nsis" }, "language-rspec": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": false }, "laravel-facades": { "version": "<=1.0.0", "hasDeprecations": true, "latestHasDeprecations": true }, "last-cursor-position": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": false }, "layout-manager": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": true }, "less-autocompile": { "version": "<=0.3.3", "hasDeprecations": true, "latestHasDeprecations": false }, "letter-spacing": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "line-count": { "version": "<=0.3.3", "hasDeprecations": true, "latestHasDeprecations": true }, "line-jumper": { "version": "<=0.13.0", "hasDeprecations": true, "latestHasDeprecations": false }, "linter": { "version": "<=0.11.1", "hasDeprecations": true, "latestHasDeprecations": false }, "linter-flow": { "version": "<=0.1.4", "hasDeprecations": true, "latestHasDeprecations": false }, "livereload": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "local-history": { "version": "<=3.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "local-server": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "local-server-express": { "version": "<=0.2.2", "hasDeprecations": true, "latestHasDeprecations": false }, "local-settings": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "localization": { "version": "<=1.16.1", "hasDeprecations": true, "latestHasDeprecations": true }, "log-console": { "version": "<=0.1.2", "hasDeprecations": true, "latestHasDeprecations": false }, "lorem-ipsum": { "version": "<=0.5.0", "hasDeprecations": true, "latestHasDeprecations": true }, "mark-ring": { "version": "<=3.0.0", "hasDeprecations": true, "latestHasDeprecations": true }, "markdown-format": { "version": "<=2.5.0", "hasDeprecations": true, "latestHasDeprecations": false }, "markdown-helpers": { "version": "<=0.2.2", "hasDeprecations": true, "latestHasDeprecations": true }, "markdown-pdf": { "version": "<=1.3.6", "hasDeprecations": true, "latestHasDeprecations": false }, "markdown-preview-plus": { "version": "<=1.4.0", "hasDeprecations": true, "latestHasDeprecations": false }, "markdown-stream": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": true }, "markdown-writer": { "version": "<=1.3.2", "hasDeprecations": true, "latestHasDeprecations": true }, "marked": { "version": "<=0.1.8", "hasDeprecations": true, "latestHasDeprecations": true }, "mate-subword-navigation": { "version": "<=3.0.1", "hasDeprecations": true, "latestHasDeprecations": false }, "MavensMate-Atom": { "version": "<=0.0.20", "hasDeprecations": true, "latestHasDeprecations": true }, "max-tabs": { "hasAlternative": true, "alternative": "tidy-tabs" }, "maximize-panes": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "mdurl": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "mechanical-keyboard": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "minifier": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "minimap": { "version": "<=3.5.6", "hasDeprecations": true, "latestHasDeprecations": false }, "minimap-color-highlight": { "version": "<=4.1.3", "hasDeprecations": true, "latestHasDeprecations": false }, "minimap-git-diff": { "version": "<=3.0.4", "hasDeprecations": true, "latestHasDeprecations": false }, "mocha": { "version": "<=0.0.5", "hasDeprecations": true, "latestHasDeprecations": true }, "mocha-ui": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": false }, "nbsp-detect": { "hasAlternative": true, "alternative": "core" }, "node-debugger": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": false }, "npm-autocomplete": { "version": "<=0.1.2", "hasDeprecations": true, "latestHasDeprecations": true }, "omni-ruler": { "version": "<=0.3.1", "hasDeprecations": true, "latestHasDeprecations": true }, "omnisharp-atom": { "version": "<=0.4.9", "hasDeprecations": true, "latestHasDeprecations": false }, "open-git-modified-files": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "open-in-github-app": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": true }, "open-in-gitx": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": true }, "open-in-sourcetree": { "version": "<=0.1.3", "hasDeprecations": true, "latestHasDeprecations": true }, "open-last-project": { "hasAlternative": true, "alternative": "core" }, "open-recent": { "version": "<=2.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "package-cop": { "version": "<=0.2.5", "hasDeprecations": true, "latestHasDeprecations": false }, "package-list-downloader": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": true }, "pair-programming": { "version": "<=0.7.0", "hasDeprecations": true, "latestHasDeprecations": true }, "pane-layout-switcher": { "version": "<=0.0.3", "hasDeprecations": true, "latestHasDeprecations": true }, "paredit": { "version": "<=1.0.0", "hasDeprecations": true, "latestHasDeprecations": true }, "party-hard": { "version": "<=0.3.3", "hasDeprecations": true, "latestHasDeprecations": true }, "path": { "version": "<=0.4.1", "hasDeprecations": true, "latestHasDeprecations": true }, "pep8": { "hasAlternative": true, "alternative": "linter" }, "pepper-autocomplete": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": true }, "permute": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "php-documentation-online": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": true }, "php-getters-setters": { "version": "<=0.5.0", "hasDeprecations": true, "latestHasDeprecations": true }, "php-server": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": true }, "phpunit": { "version": "<=1.0.9", "hasDeprecations": true, "latestHasDeprecations": false }, "playlist": { "version": "<=0.1.7", "hasDeprecations": true, "latestHasDeprecations": true }, "pretty-json": { "version": "<=0.3.2", "hasDeprecations": true, "latestHasDeprecations": false }, "preview": { "version": "<=0.14.0", "hasDeprecations": true, "latestHasDeprecations": false }, "preview-plus": { "version": "<=1.1.42", "hasDeprecations": true, "latestHasDeprecations": true }, "project-colorize": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": false }, "project-manager": { "version": "<=1.11.1", "hasDeprecations": true, "latestHasDeprecations": false }, "project-palette-finder": { "version": "<=2.4.7", "hasDeprecations": true, "latestHasDeprecations": false }, "project-ring": { "version": "<=0.20.5", "hasDeprecations": true, "latestHasDeprecations": true }, "python": { "hasAlternative": true, "alternative": "script" }, "python-coverage": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "python-isort": { "version": "<=0.0.6", "hasDeprecations": true, "latestHasDeprecations": false }, "python-jedi": { "version": "<=0.1.7", "hasDeprecations": true, "latestHasDeprecations": false }, "quick-move-file": { "version": "<=0.7.0", "hasDeprecations": true, "latestHasDeprecations": true }, "r-exec": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "rails-navigation": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": true }, "react": { "version": "<=0.5.3", "hasDeprecations": true, "latestHasDeprecations": false }, "recent-projects": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "regex-railroad-diagram": { "version": "<=0.7.1", "hasDeprecations": true, "latestHasDeprecations": true }, "related-files": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "remember-session": { "hasAlternative": true, "alternative": "core" }, "remote-atom": { "version": "<=1.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "remote-edit": { "version": "<=1.6.4", "hasDeprecations": true, "latestHasDeprecations": false }, "remote-sync": { "version": "<=3.1.1", "hasDeprecations": true, "latestHasDeprecations": false }, "resize-panes": { "hasAlternative": true, "alternative": "core" }, "rest-client": { "version": "<=0.3.3", "hasDeprecations": true, "latestHasDeprecations": true }, "revert-buffer": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": false }, "rsense": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": true }, "rspec": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "rst-preview-pandoc": { "version": "<=0.1.6", "hasDeprecations": true, "latestHasDeprecations": false }, "ruby-define-method": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "ruby-hash-rocket": { "version": "<=1.1.2", "hasDeprecations": true, "latestHasDeprecations": true }, "ruby-strftime-reference": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "ruby-test": { "version": "<=0.9.5", "hasDeprecations": true, "latestHasDeprecations": false }, "ruler": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": true }, "run-command": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": false }, "run-file": { "version": "<=0.9.0", "hasDeprecations": true, "latestHasDeprecations": true }, "run-in-browser": { "version": "<=0.1.1", "hasDeprecations": true, "latestHasDeprecations": false }, "runcoderun": { "version": "<=0.5.1", "hasDeprecations": true, "latestHasDeprecations": true }, "sass-autocompile": { "version": "<=0.6.1", "hasDeprecations": true, "latestHasDeprecations": false }, "sassbeautify": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "save-commands": { "version": "<=0.6.1", "hasDeprecations": true, "latestHasDeprecations": true }, "save-session": { "version": "<=0.15.0", "hasDeprecations": true, "latestHasDeprecations": false }, "scope-inspector": { "version": "<=0.2.1", "hasDeprecations": true, "latestHasDeprecations": false }, "script": { "version": "<=2.20.0", "hasDeprecations": true, "latestHasDeprecations": false }, "script-runner": { "version": "<=1.6.0", "hasDeprecations": true, "latestHasDeprecations": false }, "select-scope": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "selection-count": { "hasAlternative": true, "alternative": "core" }, "slash-closer": { "version": "<=0.7.1", "hasDeprecations": true, "latestHasDeprecations": true }, "sloc": { "version": "<=0.1.3", "hasDeprecations": true, "latestHasDeprecations": true }, "smarter-delete-line": { "version": "<=1.0.0", "hasDeprecations": true, "latestHasDeprecations": false }, "space-block-jumper": { "version": "<=0.4.3", "hasDeprecations": true, "latestHasDeprecations": true }, "space-tab": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": false }, "spark-dfu-util": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": true }, "status-tab-spacing": { "version": "<=0.3.1", "hasDeprecations": true, "latestHasDeprecations": true }, "sublime-tabs": { "hasAlternative": true, "message": "`sublime-tabs` has been replaced by the 'Use Preview Tabs' option in the `tabs` package settings.", "alternative": "core" }, "supercollider": { "version": "<=0.4.2", "hasDeprecations": true, "latestHasDeprecations": false }, "supercopair": { "version": "<=0.9.34", "hasDeprecations": true, "latestHasDeprecations": true }, "support-gbk": { "version": "<=1.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "swift-playground": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "symbol-gen": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "synced-sidebar": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": true }, "tab-history": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": true }, "tab-switcher": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": true }, "tabs-to-spaces": { "version": "<=0.8.0", "hasDeprecations": true, "latestHasDeprecations": false }, "tag": { "version": "<=0.2.3", "hasDeprecations": true, "latestHasDeprecations": false }, "tasks": { "version": "<=1.0.1", "hasDeprecations": true, "latestHasDeprecations": false }, "term": { "version": "<=0.2.2", "hasDeprecations": true, "latestHasDeprecations": true }, "terminal-panel": { "version": "<=1.11.0", "hasDeprecations": true, "latestHasDeprecations": false }, "terminal-status": { "version": "<=1.6.4", "hasDeprecations": true, "latestHasDeprecations": false }, "ternjs": { "hasAlternative": true, "alternative": "atom-ternjs" }, "test-status": { "version": "<=0.27.1", "hasDeprecations": true, "latestHasDeprecations": false }, "the-closer": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": false }, "ti-alloy-related": { "version": "<=0.8.0", "hasDeprecations": true, "latestHasDeprecations": true }, "tidal": { "version": "<=0.6.6", "hasDeprecations": true, "latestHasDeprecations": false }, "tidy-markdown": { "version": "<=0.2.2", "hasDeprecations": true, "latestHasDeprecations": false }, "timecop": { "version": "<=0.23.0", "hasDeprecations": true, "latestHasDeprecations": false }, "timekeeper": { "version": "<=0.4.0", "hasDeprecations": true, "latestHasDeprecations": true }, "toggle-tabs": { "version": "<=0.1.8", "hasDeprecations": true, "latestHasDeprecations": true }, "travis-ci-status": { "version": "<=0.13.0", "hasDeprecations": true, "latestHasDeprecations": false }, "true-color": { "version": "<=0.4.1", "hasDeprecations": true, "latestHasDeprecations": true }, "turbo-javascript": { "version": "<=0.0.10", "hasDeprecations": true, "latestHasDeprecations": false }, "turnip-step": { "version": "<=1.0.0", "hasDeprecations": true, "latestHasDeprecations": true }, "unity-ui": { "version": "<=1.0.5", "hasDeprecations": true, "latestHasDeprecations": false }, "update-package-dependencies": { "version": "<=0.6.0", "hasDeprecations": true, "latestHasDeprecations": false }, "update-packages": { "hasAlternative": true, "alternative": "core" }, "vertical-align": { "version": "<=0.6.1", "hasDeprecations": true, "latestHasDeprecations": false }, "view-tail-large-files": { "version": "<=0.1.17", "hasDeprecations": true, "latestHasDeprecations": true }, "vim-mode": { "version": "<=0.46.0", "hasDeprecations": true, "latestHasDeprecations": false }, "virtualenv": { "version": "<=0.6.2", "hasDeprecations": true, "latestHasDeprecations": true }, "visual-bell": { "version": "<=0.11.0", "hasDeprecations": true, "latestHasDeprecations": false }, "vnc": { "version": "<=0.1.3", "hasDeprecations": true, "latestHasDeprecations": true }, "voicecode": { "version": "<=0.9.0", "hasDeprecations": true, "latestHasDeprecations": true }, "w3c-validation": { "version": "<=0.1.3", "hasDeprecations": true, "latestHasDeprecations": false }, "weather-package": { "version": "<=1.5.4", "hasDeprecations": true, "latestHasDeprecations": true }, "web-view": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": true }, "webbox-color": { "version": "<=0.5.4", "hasDeprecations": true, "latestHasDeprecations": false }, "webview-pane": { "version": "<=0.0.1", "hasDeprecations": true, "latestHasDeprecations": true }, "wercker-status": { "version": "<=0.3.0", "hasDeprecations": true, "latestHasDeprecations": false }, "white-cursor": { "version": "<=0.5.1", "hasDeprecations": true, "latestHasDeprecations": false }, "whitespace": { "version": "<=0.24.0", "hasDeprecations": true, "latestHasDeprecations": false }, "word-count": { "version": "<=0.1.0", "hasDeprecations": true, "latestHasDeprecations": true }, "word-jumper": { "version": "<=0.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "wordcount": { "version": "<=2.2.0", "hasDeprecations": true, "latestHasDeprecations": false }, "wrap-lines": { "hasAlternative": true, "message": "`wrap-lines` has been replaced by a feature in core. Open the command palette and search for `autoflow`.", "alternative": "core" }, "yosemite-unity-ui": { "version": "<=0.3.13", "hasDeprecations": true, "latestHasDeprecations": true }, "yuno-commit": { "version": "<=0.0.2", "hasDeprecations": true, "latestHasDeprecations": true }, "zentabs": { "version": "<=0.6.1", "hasDeprecations": true, "latestHasDeprecations": false } } ================================================ FILE: script/lib/backup-node-modules.js ================================================ const fs = require('fs-extra'); const path = require('path'); module.exports = function(packagePath) { const nodeModulesPath = path.join(packagePath, 'node_modules'); const nodeModulesBackupPath = path.join(packagePath, 'node_modules.bak'); if (fs.existsSync(nodeModulesBackupPath)) { throw new Error( 'Cannot back up ' + nodeModulesPath + '; ' + nodeModulesBackupPath + ' already exists' ); } // some packages may have no node_modules after deduping, but we still want // to "back-up" and later restore that fact if (!fs.existsSync(nodeModulesPath)) { const msg = 'Skipping backing up ' + nodeModulesPath + ' as it does not exist'; console.log(msg.gray); const restore = function stubRestoreNodeModules() { if (fs.existsSync(nodeModulesPath)) { fs.removeSync(nodeModulesPath); } }; return { restore, nodeModulesPath, nodeModulesBackupPath }; } fs.copySync(nodeModulesPath, nodeModulesBackupPath); const restore = function restoreNodeModules() { if (!fs.existsSync(nodeModulesBackupPath)) { throw new Error( 'Cannot restore ' + nodeModulesPath + '; ' + nodeModulesBackupPath + ' does not exist' ); } if (fs.existsSync(nodeModulesPath)) { fs.removeSync(nodeModulesPath); } fs.renameSync(nodeModulesBackupPath, nodeModulesPath); }; return { restore, nodeModulesPath, nodeModulesBackupPath }; }; ================================================ FILE: script/lib/check-chromedriver-version.js ================================================ 'use strict'; const buildMetadata = require('../package.json'); const semver = require('semver'); const chromedriverMetadataPath = require('electron-chromedriver/package.json'); const mksnapshotMetadataPath = require('electron-mksnapshot/package.json'); // The enviroment variable is usually set in install-script-dependencies.js const majorElectronVersion = semver.major( process.env.ELECTRON_CUSTOM_VERSION || require('../config').appMetadata.electronVersion ); module.exports = function() { // Chromedriver should be at least v9.0.0 // Mksnapshot should be at least v9.0.2 const chromedriverVer = buildMetadata.dependencies['electron-chromedriver']; const mksnapshotVer = buildMetadata.dependencies['electron-mksnapshot']; const chromedriverActualVer = chromedriverMetadataPath.version; const mksnapshotActualVer = mksnapshotMetadataPath.version; // Always use caret on electron-chromedriver so that it can pick up the best minor/patch versions if (!chromedriverVer.startsWith('^')) { throw new Error( `electron-chromedriver version in script/package.json should start with a caret to match latest patch version.` ); } if (!mksnapshotVer.startsWith('^')) { throw new Error( `electron-mksnapshot version in script/package.json should start with a caret to match latest patch version.` ); } if (!semver.satisfies(chromedriverActualVer, `>=${majorElectronVersion}`)) { throw new Error( `electron-chromedriver should be at least v${majorElectronVersion} to support the ELECTRON_CUSTOM_VERSION environment variable.` ); } if (!semver.satisfies(mksnapshotActualVer, `>=${majorElectronVersion}`)) { throw new Error( `electron-mksnapshot should be at least v${majorElectronVersion} to support the ELECTRON_CUSTOM_VERSION environment variable.` ); } }; ================================================ FILE: script/lib/clean-caches.js ================================================ 'use strict'; const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const CONFIG = require('../config'); module.exports = function() { const cachePaths = [ path.join(CONFIG.repositoryRootPath, 'electron'), path.join(CONFIG.atomHomeDirPath, '.node-gyp'), path.join(CONFIG.atomHomeDirPath, 'storage'), path.join(CONFIG.atomHomeDirPath, '.apm'), path.join(CONFIG.atomHomeDirPath, '.npm'), path.join(CONFIG.atomHomeDirPath, 'compile-cache'), path.join(CONFIG.atomHomeDirPath, 'snapshot-cache'), path.join(CONFIG.atomHomeDirPath, 'atom-shell'), path.join(CONFIG.atomHomeDirPath, 'electron'), path.join(os.tmpdir(), 'atom-build'), path.join(os.tmpdir(), 'atom-cached-atom-shells') ]; for (let path of cachePaths) { console.log(`Cleaning ${path}`); fs.removeSync(path); } }; ================================================ FILE: script/lib/clean-dependencies.js ================================================ const path = require('path'); const CONFIG = require('../config'); module.exports = function() { // We can't require fs-extra or glob if `script/bootstrap` has never been run, // because they are third party modules. This is okay because cleaning // dependencies only makes sense if dependencies have been installed at least // once. const fs = require('fs-extra'); const glob = require('glob'); const apmDependenciesPath = path.join(CONFIG.apmRootPath, 'node_modules'); console.log(`Cleaning ${apmDependenciesPath}`); fs.removeSync(apmDependenciesPath); const atomDependenciesPath = path.join( CONFIG.repositoryRootPath, 'node_modules' ); console.log(`Cleaning ${atomDependenciesPath}`); fs.removeSync(atomDependenciesPath); const scriptDependenciesPath = path.join( CONFIG.scriptRootPath, 'node_modules' ); console.log(`Cleaning ${scriptDependenciesPath}`); fs.removeSync(scriptDependenciesPath); const bundledPackageDependenciesPaths = path.join( CONFIG.repositoryRootPath, 'packages', '**', 'node_modules' ); for (const bundledPackageDependencyPath of glob.sync( bundledPackageDependenciesPaths )) { fs.removeSync(bundledPackageDependencyPath); } }; ================================================ FILE: script/lib/clean-output-directory.js ================================================ const fs = require('fs-extra'); const CONFIG = require('../config'); module.exports = function() { if (fs.existsSync(CONFIG.buildOutputPath)) { console.log(`Cleaning ${CONFIG.buildOutputPath}`); fs.removeSync(CONFIG.buildOutputPath); } }; ================================================ FILE: script/lib/code-sign-on-mac.js ================================================ const downloadFileFromGithub = require('./download-file-from-github'); const CONFIG = require('../config'); const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const spawnSync = require('./spawn-sync'); const osxSign = require('electron-osx-sign'); const macEntitlementsPath = path.join( CONFIG.repositoryRootPath, 'resources', 'mac', 'entitlements.plist' ); module.exports = async function(packagedAppPath) { if ( !process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH ) { console.log( 'Skipping code signing because the ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined' .gray ); return; } let certPath = process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH; if (!certPath) { certPath = path.join(os.tmpdir(), 'mac.p12'); downloadFileFromGithub( process.env.ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath ); } try { console.log( `Ensuring keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN} exists` ); try { spawnSync( 'security', ['show-keychain-info', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN], { stdio: 'inherit' } ); } catch (err) { console.log( `Creating keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}` ); // The keychain doesn't exist, try to create it spawnSync( 'security', [ 'create-keychain', '-p', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD, process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN ], { stdio: 'inherit' } ); // List the keychain to "activate" it. Somehow this seems // to be needed otherwise the signing operation fails spawnSync( 'security', ['list-keychains', '-s', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN], { stdio: 'inherit' } ); // Make sure it doesn't time out before we use it spawnSync( 'security', [ 'set-keychain-settings', '-t', '3600', '-u', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN ], { stdio: 'inherit' } ); } console.log( `Unlocking keychain ${process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN}` ); const unlockArgs = ['unlock-keychain']; // For signing on local workstations, password could be entered interactively if (process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD) { unlockArgs.push( '-p', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD ); } unlockArgs.push(process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN); spawnSync('security', unlockArgs, { stdio: 'inherit' }); console.log( `Importing certificate at ${certPath} into ${ process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN } keychain` ); spawnSync('security', [ 'import', certPath, '-P', process.env.ATOM_MAC_CODE_SIGNING_CERT_PASSWORD, '-k', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN, '-T', '/usr/bin/codesign' ]); console.log( 'Running incantation to suppress dialog when signing on macOS Sierra' ); try { spawnSync('security', [ 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', '-k', process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD, process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN ]); } catch (e) { console.log("Incantation failed... maybe this isn't Sierra?"); } console.log(`Code-signing application at ${packagedAppPath}`); try { await osxSign.signAsync({ app: packagedAppPath, entitlements: macEntitlementsPath, 'entitlements-inherit': macEntitlementsPath, identity: 'Developer ID Application: GitHub', keychain: process.env.ATOM_MAC_CODE_SIGNING_KEYCHAIN, platform: 'darwin', hardenedRuntime: true }); console.info('Application signing complete'); } catch (err) { console.error('Applicaiton singing failed'); console.error(err); } } finally { if (!process.env.ATOM_MAC_CODE_SIGNING_CERT_PATH) { console.log(`Deleting certificate at ${certPath}`); fs.removeSync(certPath); } } }; ================================================ FILE: script/lib/code-sign-on-windows.js ================================================ const downloadFileFromGithub = require('./download-file-from-github'); const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); module.exports = function(filesToSign) { if ( !process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH ) { console.log( 'Skipping code signing because the ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined' .gray ); return; } let certPath = process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH; if (!certPath) { certPath = path.join(os.tmpdir(), 'win.p12'); downloadFileFromGithub( process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath ); } try { for (const fileToSign of filesToSign) { console.log(`Code-signing executable at ${fileToSign}`); signFile(fileToSign); } } finally { if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) { fs.removeSync(certPath); } } function signFile(fileToSign) { const signCommand = path.resolve( __dirname, '..', 'node_modules', '@atom', 'electron-winstaller', 'vendor', 'signtool.exe' ); const args = [ 'sign', `/f ${certPath}`, // Signing cert file `/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`, // Signing cert password '/fd sha256', // File digest algorithm '/tr http://timestamp.digicert.com', // Time stamp server '/td sha256', // Times stamp algorithm `"${fileToSign}"` ]; const result = spawnSync(signCommand, args, { stdio: 'inherit', shell: true }); if (result.status !== 0) { // Ensure we do not dump the signing password into the logs if something goes wrong throw new Error( `Command ${signCommand} ${args .map(a => a.replace(process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD, '******') ) .join(' ')} exited with code ${result.status}` ); } } }; ================================================ FILE: script/lib/compress-artifacts.js ================================================ 'use strict'; const fs = require('fs-extra'); const path = require('path'); const spawnSync = require('./spawn-sync'); const { path7za } = require('7zip-bin'); const CONFIG = require('../config'); module.exports = function(packagedAppPath) { const appArchivePath = path.join(CONFIG.buildOutputPath, getArchiveName()); compress(packagedAppPath, appArchivePath); if (process.platform === 'darwin') { const symbolsArchivePath = path.join( CONFIG.buildOutputPath, 'atom-mac-symbols.zip' ); compress(CONFIG.symbolsPath, symbolsArchivePath); } }; function getArchiveName() { switch (process.platform) { case 'darwin': return 'atom-mac.zip'; case 'win32': return `atom-${process.arch === 'x64' ? 'x64-' : ''}windows.zip`; default: return `atom-${getLinuxArchiveArch()}.tar.gz`; } } function getLinuxArchiveArch() { switch (process.arch) { case 'ia32': return 'i386'; case 'x64': return 'amd64'; default: return process.arch; } } function compress(inputDirPath, outputArchivePath) { if (fs.existsSync(outputArchivePath)) { console.log(`Deleting "${outputArchivePath}"`); fs.removeSync(outputArchivePath); } console.log(`Compressing "${inputDirPath}" to "${outputArchivePath}"`); let compressCommand, compressArguments; if (process.platform === 'darwin') { compressCommand = 'zip'; compressArguments = ['-r', '--symlinks']; } else if (process.platform === 'win32') { compressCommand = path7za; compressArguments = ['a', '-r']; } else { compressCommand = 'tar'; compressArguments = ['caf']; } compressArguments.push(outputArchivePath, path.basename(inputDirPath)); spawnSync(compressCommand, compressArguments, { cwd: path.dirname(inputDirPath), maxBuffer: 2024 * 2024 }); } ================================================ FILE: script/lib/copy-assets.js ================================================ // This module exports a function that copies all the static assets into the // appropriate location in the build output directory. 'use strict'; const path = require('path'); const fs = require('fs-extra'); const CONFIG = require('../config'); const glob = require('glob'); const includePathInPackagedApp = require('./include-path-in-packaged-app'); module.exports = function() { console.log(`Copying assets to ${CONFIG.intermediateAppPath}`); let srcPaths = [ path.join(CONFIG.repositoryRootPath, 'benchmarks', 'benchmark-runner.js'), path.join(CONFIG.repositoryRootPath, 'dot-atom'), path.join(CONFIG.repositoryRootPath, 'exports'), path.join(CONFIG.repositoryRootPath, 'package.json'), path.join(CONFIG.repositoryRootPath, 'static'), path.join(CONFIG.repositoryRootPath, 'src'), path.join(CONFIG.repositoryRootPath, 'vendor') ]; srcPaths = srcPaths.concat( glob.sync(path.join(CONFIG.repositoryRootPath, 'spec', '*.*'), { ignore: path.join('**', '*-spec.*') }) ); for (let srcPath of srcPaths) { fs.copySync(srcPath, computeDestinationPath(srcPath), { filter: includePathInPackagedApp }); } // Run a copy pass to dereference symlinked directories under node_modules. // We do this to ensure that symlinked repo-local bundled packages get // copied to the output folder correctly. We dereference only the top-level // symlinks and not nested symlinks to avoid issues where symlinked binaries // are duplicated in Atom's installation packages (see atom/atom#18490). const nodeModulesPath = path.join(CONFIG.repositoryRootPath, 'node_modules'); glob .sync(path.join(nodeModulesPath, '*')) .map(p => fs.lstatSync(p).isSymbolicLink() ? path.resolve(nodeModulesPath, fs.readlinkSync(p)) : p ) .forEach(modulePath => { const destPath = path.join( CONFIG.intermediateAppPath, 'node_modules', path.basename(modulePath) ); fs.copySync(modulePath, destPath, { filter: includePathInPackagedApp }); }); fs.copySync( path.join( CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png', '1024.png' ), path.join(CONFIG.intermediateAppPath, 'resources', 'atom.png') ); }; function computeDestinationPath(srcPath) { const relativePath = path.relative(CONFIG.repositoryRootPath, srcPath); return path.join(CONFIG.intermediateAppPath, relativePath); } ================================================ FILE: script/lib/create-debian-package.js ================================================ 'use strict'; const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const spawnSync = require('./spawn-sync'); const template = require('lodash.template'); const CONFIG = require('../config'); module.exports = function(packagedAppPath) { console.log(`Creating Debian package for "${packagedAppPath}"`); const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}`; const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : `apm-${CONFIG.channel}`; const appDescription = CONFIG.appMetadata.description; const appVersion = CONFIG.appMetadata.version; let arch; if (process.arch === 'ia32') { arch = 'i386'; } else if (process.arch === 'x64') { arch = 'amd64'; } else if (process.arch === 'ppc') { arch = 'powerpc'; } else { arch = process.arch; } const outputDebianPackageFilePath = path.join( CONFIG.buildOutputPath, `atom-${arch}.deb` ); const debianPackageDirPath = path.join( os.tmpdir(), path.basename(packagedAppPath) ); const debianPackageConfigPath = path.join(debianPackageDirPath, 'DEBIAN'); const debianPackageInstallDirPath = path.join(debianPackageDirPath, 'usr'); const debianPackageBinDirPath = path.join(debianPackageInstallDirPath, 'bin'); const debianPackageShareDirPath = path.join( debianPackageInstallDirPath, 'share' ); const debianPackageAtomDirPath = path.join( debianPackageShareDirPath, atomExecutableName ); const debianPackageApplicationsDirPath = path.join( debianPackageShareDirPath, 'applications' ); const debianPackageIconsDirPath = path.join( debianPackageShareDirPath, 'pixmaps' ); const debianPackageDocsDirPath = path.join( debianPackageShareDirPath, 'doc', atomExecutableName ); if (fs.existsSync(debianPackageDirPath)) { console.log( `Deleting existing build dir for Debian package at "${debianPackageDirPath}"` ); fs.removeSync(debianPackageDirPath); } if (fs.existsSync(`${debianPackageDirPath}.deb`)) { console.log( `Deleting existing Debian package at "${debianPackageDirPath}.deb"` ); fs.removeSync(`${debianPackageDirPath}.deb`); } if (fs.existsSync(debianPackageDirPath)) { console.log( `Deleting existing Debian package at "${outputDebianPackageFilePath}"` ); fs.removeSync(debianPackageDirPath); } console.log( `Creating Debian package directory structure at "${debianPackageDirPath}"` ); fs.mkdirpSync(debianPackageDirPath); fs.mkdirpSync(debianPackageConfigPath); fs.mkdirpSync(debianPackageInstallDirPath); fs.mkdirpSync(debianPackageShareDirPath); fs.mkdirpSync(debianPackageApplicationsDirPath); fs.mkdirpSync(debianPackageIconsDirPath); fs.mkdirpSync(debianPackageDocsDirPath); fs.mkdirpSync(debianPackageBinDirPath); console.log(`Copying "${packagedAppPath}" to "${debianPackageAtomDirPath}"`); fs.copySync(packagedAppPath, debianPackageAtomDirPath); fs.chmodSync(debianPackageAtomDirPath, '755'); console.log(`Copying binaries into "${debianPackageBinDirPath}"`); fs.copySync( path.join(CONFIG.repositoryRootPath, 'atom.sh'), path.join(debianPackageBinDirPath, atomExecutableName) ); fs.symlinkSync( path.join( '..', 'share', atomExecutableName, 'resources', 'app', 'apm', 'node_modules', '.bin', 'apm' ), path.join(debianPackageBinDirPath, apmExecutableName) ); fs.chmodSync(path.join(debianPackageAtomDirPath, 'chrome-sandbox'), '4755'); console.log(`Writing control file into "${debianPackageConfigPath}"`); const packageSizeInKilobytes = spawnSync('du', ['-sk', packagedAppPath]) .stdout.toString() .split(/\s+/)[0]; const controlFileTemplate = fs.readFileSync( path.join( CONFIG.repositoryRootPath, 'resources', 'linux', 'debian', 'control.in' ) ); const controlFileContents = template(controlFileTemplate)({ appFileName: atomExecutableName, version: appVersion, arch: arch, installedSize: packageSizeInKilobytes, description: appDescription }); fs.writeFileSync( path.join(debianPackageConfigPath, 'control'), controlFileContents ); console.log( `Writing desktop entry file into "${debianPackageApplicationsDirPath}"` ); const desktopEntryTemplate = fs.readFileSync( path.join( CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in' ) ); const desktopEntryContents = template(desktopEntryTemplate)({ appName: CONFIG.appName, appFileName: atomExecutableName, description: appDescription, installDir: '/usr', iconPath: atomExecutableName }); fs.writeFileSync( path.join( debianPackageApplicationsDirPath, `${atomExecutableName}.desktop` ), desktopEntryContents ); console.log(`Copying icon into "${debianPackageIconsDirPath}"`); fs.copySync( path.join( packagedAppPath, 'resources', 'app.asar.unpacked', 'resources', 'atom.png' ), path.join(debianPackageIconsDirPath, `${atomExecutableName}.png`) ); console.log(`Copying license into "${debianPackageDocsDirPath}"`); fs.copySync( path.join(packagedAppPath, 'resources', 'LICENSE.md'), path.join(debianPackageDocsDirPath, 'copyright') ); console.log( `Copying polkit configuration into "${debianPackageShareDirPath}"` ); fs.copySync( path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.policy'), path.join( debianPackageShareDirPath, 'polkit-1', 'actions', `atom-${CONFIG.channel}.policy` ) ); console.log(`Generating .deb file from ${debianPackageDirPath}`); spawnSync('fakeroot', ['dpkg-deb', '-b', debianPackageDirPath], { stdio: 'inherit' }); console.log( `Copying generated package into "${outputDebianPackageFilePath}"` ); fs.copySync(`${debianPackageDirPath}.deb`, outputDebianPackageFilePath); }; ================================================ FILE: script/lib/create-rpm-package.js ================================================ 'use strict'; const assert = require('assert'); const fs = require('fs-extra'); const path = require('path'); const spawnSync = require('./spawn-sync'); const template = require('lodash.template'); const CONFIG = require('../config'); module.exports = function(packagedAppPath) { console.log(`Creating rpm package for "${packagedAppPath}"`); const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}`; const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : `apm-${CONFIG.channel}`; const appName = CONFIG.appName; const appDescription = CONFIG.appMetadata.description; // RPM versions can't have dashes or tildes in them. // (Ref.: https://twiki.cern.ch/twiki/bin/view/Main/RPMAndDebVersioning) const appVersion = CONFIG.appMetadata.version.replace(/-/g, '.'); const policyFileName = `atom-${CONFIG.channel}.policy`; const rpmPackageDirPath = path.join(CONFIG.homeDirPath, 'rpmbuild'); const rpmPackageBuildDirPath = path.join(rpmPackageDirPath, 'BUILD'); const rpmPackageSourcesDirPath = path.join(rpmPackageDirPath, 'SOURCES'); const rpmPackageSpecsDirPath = path.join(rpmPackageDirPath, 'SPECS'); const rpmPackageRpmsDirPath = path.join(rpmPackageDirPath, 'RPMS'); const rpmPackageApplicationDirPath = path.join( rpmPackageBuildDirPath, appName ); const rpmPackageIconsDirPath = path.join(rpmPackageBuildDirPath, 'icons'); if (fs.existsSync(rpmPackageDirPath)) { console.log( `Deleting existing rpm build directory at "${rpmPackageDirPath}"` ); fs.removeSync(rpmPackageDirPath); } console.log( `Creating rpm package directory structure at "${rpmPackageDirPath}"` ); fs.mkdirpSync(rpmPackageDirPath); fs.mkdirpSync(rpmPackageBuildDirPath); fs.mkdirpSync(rpmPackageSourcesDirPath); fs.mkdirpSync(rpmPackageSpecsDirPath); console.log( `Copying "${packagedAppPath}" to "${rpmPackageApplicationDirPath}"` ); fs.copySync(packagedAppPath, rpmPackageApplicationDirPath); console.log(`Copying icons into "${rpmPackageIconsDirPath}"`); fs.copySync( path.join( CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png' ), rpmPackageIconsDirPath ); console.log(`Writing rpm package spec file into "${rpmPackageSpecsDirPath}"`); const rpmPackageSpecFilePath = path.join(rpmPackageSpecsDirPath, 'atom.spec'); const rpmPackageSpecsTemplate = fs.readFileSync( path.join( CONFIG.repositoryRootPath, 'resources', 'linux', 'redhat', 'atom.spec.in' ) ); const rpmPackageSpecsContents = template(rpmPackageSpecsTemplate)({ appName: appName, appFileName: atomExecutableName, apmFileName: apmExecutableName, description: appDescription, installDir: '/usr', version: appVersion, policyFileName }); fs.writeFileSync(rpmPackageSpecFilePath, rpmPackageSpecsContents); console.log(`Writing desktop entry file into "${rpmPackageBuildDirPath}"`); const desktopEntryTemplate = fs.readFileSync( path.join( CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in' ) ); const desktopEntryContents = template(desktopEntryTemplate)({ appName: appName, appFileName: atomExecutableName, description: appDescription, installDir: '/usr', iconPath: atomExecutableName }); fs.writeFileSync( path.join(rpmPackageBuildDirPath, `${atomExecutableName}.desktop`), desktopEntryContents ); console.log(`Copying atom.sh into "${rpmPackageBuildDirPath}"`); fs.copySync( path.join(CONFIG.repositoryRootPath, 'atom.sh'), path.join(rpmPackageBuildDirPath, 'atom.sh') ); console.log(`Copying atom.policy into "${rpmPackageBuildDirPath}"`); fs.copySync( path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.policy'), path.join(rpmPackageBuildDirPath, policyFileName) ); console.log(`Generating .rpm package from "${rpmPackageDirPath}"`); spawnSync('rpmbuild', ['-ba', '--clean', rpmPackageSpecFilePath]); for (let generatedArch of fs.readdirSync(rpmPackageRpmsDirPath)) { const generatedArchDirPath = path.join( rpmPackageRpmsDirPath, generatedArch ); const generatedPackageFileNames = fs.readdirSync(generatedArchDirPath); assert( generatedPackageFileNames.length === 1, 'Generated more than one rpm package' ); const generatedPackageFilePath = path.join( generatedArchDirPath, generatedPackageFileNames[0] ); const outputRpmPackageFilePath = path.join( CONFIG.buildOutputPath, `atom.${generatedArch}.rpm` ); console.log( `Copying "${generatedPackageFilePath}" into "${outputRpmPackageFilePath}"` ); fs.copySync(generatedPackageFilePath, outputRpmPackageFilePath); } }; ================================================ FILE: script/lib/create-windows-installer.js ================================================ 'use strict'; const electronInstaller = require('@atom/electron-winstaller'); const fs = require('fs'); const glob = require('glob'); const path = require('path'); const CONFIG = require('../config'); module.exports = packagedAppPath => { const archSuffix = process.arch === 'ia32' ? '' : '-' + process.arch; const updateUrlPrefix = process.env.ATOM_UPDATE_URL_PREFIX || 'https://atom.io'; const options = { name: CONFIG.channelName, title: CONFIG.appName, exe: CONFIG.executableName, appDirectory: packagedAppPath, authors: 'GitHub Inc.', iconUrl: `https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${ CONFIG.channel }/atom.ico`, loadingGif: path.join( CONFIG.repositoryRootPath, 'resources', 'win', 'loading.gif' ), outputDirectory: CONFIG.buildOutputPath, noMsi: true, remoteReleases: `${updateUrlPrefix}/api/updates${archSuffix}?version=${ CONFIG.computedAppVersion }`, setupExe: `AtomSetup${process.arch === 'x64' ? '-x64' : ''}.exe`, setupIcon: path.join( CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico' ) }; const cleanUp = () => { const releasesPath = `${CONFIG.buildOutputPath}/RELEASES`; if (process.arch === 'x64' && fs.existsSync(releasesPath)) { fs.renameSync(releasesPath, `${releasesPath}-x64`); } let appName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}`; for (let nupkgPath of glob.sync( `${CONFIG.buildOutputPath}/${appName}-*.nupkg` )) { if (!nupkgPath.includes(CONFIG.computedAppVersion)) { console.log( `Deleting downloaded nupkg for previous version at ${nupkgPath} to prevent it from being stored as an artifact` ); fs.unlinkSync(nupkgPath); } else { if (process.arch === 'x64') { // Use the original .nupkg filename to generate the `atom-x64` name by inserting `-x64` after `atom` const newNupkgPath = nupkgPath.replace( `${appName}-`, `${appName}-x64-` ); fs.renameSync(nupkgPath, newNupkgPath); } } } return `${CONFIG.buildOutputPath}/${options.setupExe}`; }; console.log(`Creating Windows Installer for ${packagedAppPath}`); return electronInstaller .createWindowsInstaller(options) .then(cleanUp, error => { cleanUp(); return Promise.reject(error); }); }; ================================================ FILE: script/lib/delete-msbuild-from-path.js ================================================ 'use strict'; const fs = require('fs'); const path = require('path'); module.exports = function() { process.env['PATH'] = process.env['PATH'] .split(';') .filter(function(p) { if (fs.existsSync(path.join(p, 'msbuild.exe'))) { console.log( 'Excluding "' + p + '" from PATH to avoid msbuild.exe mismatch that causes errors during module installation' ); return false; } else { return true; } }) .join(';'); }; ================================================ FILE: script/lib/dependencies-fingerprint.js ================================================ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const CONFIG = require('../config'); const FINGERPRINT_PATH = path.join( CONFIG.repositoryRootPath, 'node_modules', '.dependencies-fingerprint' ); module.exports = { write: function() { const fingerprint = this.compute(); fs.writeFileSync(FINGERPRINT_PATH, fingerprint); console.log( 'Wrote Dependencies Fingerprint:', FINGERPRINT_PATH, fingerprint ); }, read: function() { return fs.existsSync(FINGERPRINT_PATH) ? fs.readFileSync(FINGERPRINT_PATH, 'utf8') : null; }, isOutdated: function() { const fingerprint = this.read(); return fingerprint ? fingerprint !== this.compute() : false; }, compute: function() { // Include the electron minor version in the fingerprint since that changing requires a re-install const electronVersion = CONFIG.appMetadata.electronVersion.replace( /\.\d+$/, '' ); const apmVersion = CONFIG.apmMetadata.dependencies['atom-package-manager']; const body = electronVersion + apmVersion + process.platform + process.version + process.arch; return crypto .createHash('sha1') .update(body) .digest('hex'); } }; ================================================ FILE: script/lib/download-file-from-github.js ================================================ 'use strict'; const fs = require('fs-extra'); const path = require('path'); const syncRequest = require('sync-request'); module.exports = function(downloadURL, destinationPath) { console.log(`Downloading file from GitHub Repository to ${destinationPath}`); const response = syncRequest('GET', downloadURL, { headers: { Accept: 'application/vnd.github.v3.raw', 'User-Agent': 'Atom Build', Authorization: `token ${process.env.GITHUB_TOKEN}` } }); if (response.statusCode === 200) { fs.mkdirpSync(path.dirname(destinationPath)); fs.writeFileSync(destinationPath, response.body); } else { throw new Error( 'Error downloading file. HTTP Status ' + response.statusCode + '.' ); } }; ================================================ FILE: script/lib/dump-symbols.js ================================================ 'use strict'; const fs = require('fs-extra'); const glob = require('glob'); const path = require('path'); const CONFIG = require('../config'); module.exports = function() { if (process.platform === 'win32') { console.log( 'Skipping symbol dumping because minidump is not supported on Windows' .gray ); return Promise.resolve(); } else { console.log(`Dumping symbols in ${CONFIG.symbolsPath}`); const binaryPaths = glob.sync( path.join(CONFIG.intermediateAppPath, 'node_modules', '**', '*.node') ); return Promise.all(binaryPaths.map(dumpSymbol)); } }; function dumpSymbol(binaryPath) { const minidump = require('minidump'); return new Promise(function(resolve, reject) { minidump.dumpSymbol(binaryPath, function(error, content) { if (error) { // fswin.node is only used on windows, ignore the error on other platforms if (process.platform !== 'win32' && binaryPath.match(/fswin.node/)) return resolve(); throw new Error(error); } else { const moduleLine = /MODULE [^ ]+ [^ ]+ ([0-9A-F]+) (.*)\n/.exec( content ); if (moduleLine.length !== 3) { const errorMessage = `Invalid output when dumping symbol for ${binaryPath}`; console.error(errorMessage); throw new Error(errorMessage); } else { const filename = moduleLine[2]; const symbolDirPath = path.join( CONFIG.symbolsPath, filename, moduleLine[1] ); const symbolFilePath = path.join(symbolDirPath, `${filename}.sym`); fs.mkdirpSync(symbolDirPath); fs.writeFileSync(symbolFilePath, content); resolve(); } } }); }); } ================================================ FILE: script/lib/expand-glob-paths.js ================================================ 'use strict'; const glob = require('glob'); module.exports = function(globPaths) { return Promise.all(globPaths.map(g => expandGlobPath(g))).then(paths => paths.reduce((a, b) => a.concat(b), []) ); }; function expandGlobPath(globPath) { return new Promise((resolve, reject) => { glob(globPath, (error, paths) => { if (error) { reject(error); } else { resolve(paths); } }); }); } ================================================ FILE: script/lib/generate-api-docs.js ================================================ 'use strict'; const donna = require('donna'); const tello = require('tello'); const joanna = require('joanna'); const glob = require('glob'); const fs = require('fs-extra'); const path = require('path'); const CONFIG = require('../config'); module.exports = function() { const generatedJSONPath = path.join(CONFIG.docsOutputPath, 'atom-api.json'); console.log(`Generating API docs at ${generatedJSONPath}`); // Unfortunately, correct relative paths depend on a specific working // directory, but this script should be able to run from anywhere, so we // muck with the cwd temporarily. const oldWorkingDirectoryPath = process.cwd(); process.chdir(CONFIG.repositoryRootPath); const coffeeMetadata = donna.generateMetadata(['.'])[0]; const jsMetadata = joanna(glob.sync(`src/**/*.js`)); process.chdir(oldWorkingDirectoryPath); const metadata = { repository: coffeeMetadata.repository, version: coffeeMetadata.version, files: Object.assign(coffeeMetadata.files, jsMetadata.files) }; const api = tello.digest([metadata]); Object.assign(api.classes, getAPIDocsForDependencies()); api.classes = sortObjectByKey(api.classes); fs.mkdirpSync(CONFIG.docsOutputPath); fs.writeFileSync(generatedJSONPath, JSON.stringify(api, null, 2)); }; function getAPIDocsForDependencies() { const classes = {}; for (let apiJSONPath of glob.sync( `${CONFIG.repositoryRootPath}/node_modules/*/api.json` )) { Object.assign(classes, require(apiJSONPath).classes); } return classes; } function sortObjectByKey(object) { const sortedObject = {}; for (let keyName of Object.keys(object).sort()) { sortedObject[keyName] = object[keyName]; } return sortedObject; } ================================================ FILE: script/lib/generate-metadata.js ================================================ 'use strict'; const CSON = require('season'); const deprecatedPackagesMetadata = require('../deprecated-packages'); const fs = require('fs-plus'); const normalizePackageData = require('normalize-package-data'); const path = require('path'); const semver = require('semver'); const CONFIG = require('../config'); let appName = CONFIG.appMetadata.name; if (process.platform === 'win32') { // Use the channel name in the app name on Windows so that the installer will // place it in a different folder in AppData\Local appName = CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}`; } module.exports = function() { console.log( `Generating metadata for ${path.join( CONFIG.intermediateAppPath, 'package.json' )}` ); CONFIG.appMetadata._atomPackages = buildBundledPackagesMetadata(); CONFIG.appMetadata._atomMenu = buildPlatformMenuMetadata(); CONFIG.appMetadata._atomKeymaps = buildPlatformKeymapsMetadata(); CONFIG.appMetadata._deprecatedPackages = deprecatedPackagesMetadata; CONFIG.appMetadata.version = CONFIG.computedAppVersion; CONFIG.appMetadata.name = appName; CONFIG.appMetadata.productName = CONFIG.appName; checkDeprecatedPackagesMetadata(); fs.writeFileSync( path.join(CONFIG.intermediateAppPath, 'package.json'), JSON.stringify(CONFIG.appMetadata) ); }; module.exports = function() { console.log( `Generating metadata for ${path.join( CONFIG.intermediateAppPath, 'package.json' )}` ); CONFIG.appMetadata._atomPackages = buildBundledPackagesMetadata(); CONFIG.appMetadata._atomMenu = buildPlatformMenuMetadata(); CONFIG.appMetadata._atomKeymaps = buildPlatformKeymapsMetadata(); CONFIG.appMetadata._deprecatedPackages = deprecatedPackagesMetadata; CONFIG.appMetadata.version = CONFIG.computedAppVersion; checkDeprecatedPackagesMetadata(); fs.writeFileSync( path.join(CONFIG.intermediateAppPath, 'package.json'), JSON.stringify(CONFIG.appMetadata) ); }; function buildBundledPackagesMetadata() { const packages = {}; for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { const packagePath = path.join( CONFIG.intermediateAppPath, 'node_modules', packageName ); const packageMetadataPath = path.join(packagePath, 'package.json'); const packageMetadata = JSON.parse( fs.readFileSync(packageMetadataPath, 'utf8') ); normalizePackageData( packageMetadata, msg => { if (!msg.match(/No README data$/)) { console.warn( `Invalid package metadata. ${packageMetadata.name}: ${msg}` ); } }, true ); if ( packageMetadata.repository && packageMetadata.repository.url && packageMetadata.repository.type === 'git' ) { packageMetadata.repository.url = packageMetadata.repository.url.replace( /^git\+/, '' ); } delete packageMetadata['_from']; delete packageMetadata['_id']; delete packageMetadata['dist']; delete packageMetadata['readme']; delete packageMetadata['readmeFilename']; const packageModuleCache = packageMetadata._atomModuleCache || {}; if ( packageModuleCache.extensions && packageModuleCache.extensions['.json'] ) { const index = packageModuleCache.extensions['.json'].indexOf( 'package.json' ); if (index !== -1) { packageModuleCache.extensions['.json'].splice(index, 1); } } const packageNewMetadata = { metadata: packageMetadata, keymaps: {}, menus: {}, grammarPaths: [], settings: {} }; packageNewMetadata.rootDirPath = path.relative( CONFIG.intermediateAppPath, packagePath ); if (packageMetadata.main) { const mainPath = require.resolve( path.resolve(packagePath, packageMetadata.main) ); packageNewMetadata.main = path.relative( path.join(CONFIG.intermediateAppPath, 'static'), mainPath ); // Convert backward slashes to forward slashes in order to allow package // main modules to be required from the snapshot. This is because we use // forward slashes to cache the sources in the snapshot, so we need to use // them here as well. packageNewMetadata.main = packageNewMetadata.main.replace(/\\/g, '/'); } const packageKeymapsPath = path.join(packagePath, 'keymaps'); if (fs.existsSync(packageKeymapsPath)) { for (let packageKeymapName of fs.readdirSync(packageKeymapsPath)) { const packageKeymapPath = path.join( packageKeymapsPath, packageKeymapName ); if ( packageKeymapPath.endsWith('.cson') || packageKeymapPath.endsWith('.json') ) { const relativePath = path.relative( CONFIG.intermediateAppPath, packageKeymapPath ); packageNewMetadata.keymaps[relativePath] = CSON.readFileSync( packageKeymapPath ); } } } const packageMenusPath = path.join(packagePath, 'menus'); if (fs.existsSync(packageMenusPath)) { for (let packageMenuName of fs.readdirSync(packageMenusPath)) { const packageMenuPath = path.join(packageMenusPath, packageMenuName); if ( packageMenuPath.endsWith('.cson') || packageMenuPath.endsWith('.json') ) { const relativePath = path.relative( CONFIG.intermediateAppPath, packageMenuPath ); packageNewMetadata.menus[relativePath] = CSON.readFileSync( packageMenuPath ); } } } const packageGrammarsPath = path.join(packagePath, 'grammars'); for (let packageGrammarPath of fs.listSync(packageGrammarsPath, [ 'json', 'cson' ])) { const relativePath = path.relative( CONFIG.intermediateAppPath, packageGrammarPath ); packageNewMetadata.grammarPaths.push(relativePath); } const packageSettingsPath = path.join(packagePath, 'settings'); for (let packageSettingPath of fs.listSync(packageSettingsPath, [ 'json', 'cson' ])) { const relativePath = path.relative( CONFIG.intermediateAppPath, packageSettingPath ); packageNewMetadata.settings[relativePath] = CSON.readFileSync( packageSettingPath ); } const packageStyleSheetsPath = path.join(packagePath, 'styles'); let styleSheets = null; if (packageMetadata.mainStyleSheet) { styleSheets = [fs.resolve(packagePath, packageMetadata.mainStyleSheet)]; } else if (packageMetadata.styleSheets) { styleSheets = packageMetadata.styleSheets.map(name => fs.resolve(packageStyleSheetsPath, name, ['css', 'less', '']) ); } else { const indexStylesheet = fs.resolve(packagePath, 'index', ['css', 'less']); if (indexStylesheet) { styleSheets = [indexStylesheet]; } else { styleSheets = fs.listSync(packageStyleSheetsPath, ['css', 'less']); } } packageNewMetadata.styleSheetPaths = styleSheets.map(styleSheetPath => path.relative(packagePath, styleSheetPath) ); packages[packageMetadata.name] = packageNewMetadata; if (packageModuleCache.extensions) { for (let extension of Object.keys(packageModuleCache.extensions)) { const paths = packageModuleCache.extensions[extension]; if (paths.length === 0) { delete packageModuleCache.extensions[extension]; } } } } return packages; } function buildPlatformMenuMetadata() { const menuPath = path.join( CONFIG.repositoryRootPath, 'menus', `${process.platform}.cson` ); if (fs.existsSync(menuPath)) { return CSON.readFileSync(menuPath); } else { return null; } } function buildPlatformKeymapsMetadata() { const invalidPlatforms = [ 'darwin', 'freebsd', 'linux', 'sunos', 'win32' ].filter(p => p !== process.platform); const keymapsPath = path.join(CONFIG.repositoryRootPath, 'keymaps'); const keymaps = {}; for (let keymapName of fs.readdirSync(keymapsPath)) { const keymapPath = path.join(keymapsPath, keymapName); if (keymapPath.endsWith('.cson') || keymapPath.endsWith('.json')) { const keymapPlatform = path.basename( keymapPath, path.extname(keymapPath) ); if (invalidPlatforms.indexOf(keymapPlatform) === -1) { keymaps[path.basename(keymapPath)] = CSON.readFileSync(keymapPath); } } } return keymaps; } function checkDeprecatedPackagesMetadata() { for (let packageName of Object.keys(deprecatedPackagesMetadata)) { const packageMetadata = deprecatedPackagesMetadata[packageName]; if ( packageMetadata.version && !semver.validRange(packageMetadata.version) ) { throw new Error( `Invalid range: ${packageMetadata.version} (${packageName}).` ); } } } ================================================ FILE: script/lib/generate-module-cache.js ================================================ 'use strict'; const fs = require('fs'); const path = require('path'); const ModuleCache = require('../../src/module-cache'); const CONFIG = require('../config'); module.exports = function() { console.log(`Generating module cache for ${CONFIG.intermediateAppPath}`); for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { ModuleCache.create( path.join(CONFIG.intermediateAppPath, 'node_modules', packageName) ); } ModuleCache.create(CONFIG.intermediateAppPath); const newMetadata = JSON.parse( fs.readFileSync(path.join(CONFIG.intermediateAppPath, 'package.json')) ); for (let folder of newMetadata._atomModuleCache.folders) { if (folder.paths.indexOf('') !== -1) { folder.paths = [ '', 'exports', 'spec', 'src', 'src/main-process', 'static', 'vendor' ]; } } CONFIG.appMetadata = newMetadata; fs.writeFileSync( path.join(CONFIG.intermediateAppPath, 'package.json'), JSON.stringify(CONFIG.appMetadata) ); }; ================================================ FILE: script/lib/generate-startup-snapshot.js ================================================ const childProcess = require('child_process'); const fs = require('fs'); const path = require('path'); const electronLink = require('electron-link'); const terser = require('terser'); const CONFIG = require('../config'); module.exports = function(packagedAppPath) { const snapshotScriptPath = path.join(CONFIG.buildOutputPath, 'startup.js'); const coreModules = new Set([ 'electron', 'atom', 'shell', 'WNdb', 'lapack', 'remote' ]); const baseDirPath = path.join(CONFIG.intermediateAppPath, 'static'); let processedFiles = 0; return electronLink({ baseDirPath, mainPath: path.resolve( baseDirPath, '..', 'src', 'initialize-application-window.js' ), cachePath: path.join(CONFIG.atomHomeDirPath, 'snapshot-cache'), auxiliaryData: CONFIG.snapshotAuxiliaryData, shouldExcludeModule: ({ requiringModulePath, requiredModulePath }) => { if (processedFiles > 0) { process.stdout.write('\r'); } process.stdout.write( `Generating snapshot script at "${snapshotScriptPath}" (${++processedFiles})` ); const requiringModuleRelativePath = path.relative( baseDirPath, requiringModulePath ); const requiredModuleRelativePath = path.relative( baseDirPath, requiredModulePath ); return ( requiredModulePath.endsWith('.node') || coreModules.has(requiredModulePath) || requiringModuleRelativePath.endsWith( path.join('node_modules/xregexp/xregexp-all.js') ) || (requiredModuleRelativePath.startsWith(path.join('..', 'src')) && requiredModuleRelativePath.endsWith('-element.js')) || requiredModuleRelativePath.startsWith( path.join('..', 'node_modules', 'dugite') ) || requiredModuleRelativePath.startsWith( path.join( '..', 'node_modules', 'markdown-preview', 'node_modules', 'yaml-front-matter' ) ) || requiredModuleRelativePath.startsWith( path.join( '..', 'node_modules', 'markdown-preview', 'node_modules', 'cheerio' ) ) || requiredModuleRelativePath.startsWith( path.join( '..', 'node_modules', 'markdown-preview', 'node_modules', 'marked' ) ) || requiredModuleRelativePath.startsWith( path.join('..', 'node_modules', 'typescript-simple') ) || requiredModuleRelativePath.endsWith( path.join( 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js' ) ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'fs-extra', 'lib', 'index.js') ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'graceful-fs', 'graceful-fs.js') ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'htmlparser2', 'lib', 'index.js') ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'minimatch', 'minimatch.js') ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'request', 'index.js') ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'request', 'request.js') ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'superstring', 'index.js') ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'temp', 'lib', 'temp.js') ) || requiredModuleRelativePath.endsWith( path.join('node_modules', 'parse5', 'lib', 'index.js') ) || requiredModuleRelativePath === path.join('..', 'exports', 'atom.js') || requiredModuleRelativePath === path.join('..', 'src', 'electron-shims.js') || requiredModuleRelativePath === path.join( '..', 'node_modules', 'atom-keymap', 'lib', 'command-event.js' ) || requiredModuleRelativePath === path.join('..', 'node_modules', 'babel-core', 'index.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'debug', 'node.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'git-utils', 'src', 'git.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'glob', 'glob.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'iconv-lite', 'lib', 'index.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'less', 'index.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') || requiredModuleRelativePath === path.join( '..', 'node_modules', 'less', 'lib', 'less-node', 'index.js' ) || requiredModuleRelativePath === path.join('..', 'node_modules', 'lodash.isequal', 'index.js') || requiredModuleRelativePath === path.join( '..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js' ) || requiredModuleRelativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'resolve', 'index.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') || requiredModuleRelativePath === path.join( '..', 'node_modules', 'settings-view', 'node_modules', 'glob', 'glob.js' ) || requiredModuleRelativePath === path.join( '..', 'node_modules', 'spell-check', 'lib', 'locale-checker.js' ) || requiredModuleRelativePath === path.join( '..', 'node_modules', 'spell-check', 'lib', 'system-checker.js' ) || requiredModuleRelativePath === path.join( '..', 'node_modules', 'spellchecker', 'lib', 'spellchecker.js' ) || requiredModuleRelativePath === path.join( '..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js' ) || requiredModuleRelativePath === path.join('..', 'node_modules', 'tar', 'tar.js') || requiredModuleRelativePath === path.join( '..', 'node_modules', 'ls-archive', 'node_modules', 'tar', 'tar.js' ) || requiredModuleRelativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'tree-sitter', 'index.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'yauzl', 'index.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'util-deprecate', 'node.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'winreg', 'lib', 'registry.js') || requiredModuleRelativePath === path.join('..', 'node_modules', 'scandal', 'lib', 'scandal.js') || requiredModuleRelativePath === path.join( '..', 'node_modules', '@atom', 'fuzzy-native', 'lib', 'main.js' ) || requiredModuleRelativePath === path.join( '..', 'node_modules', 'vscode-ripgrep', 'lib', 'index.js' ) || // The startup-time script is used by both the renderer and the main process and having it in the // snapshot causes issues. requiredModuleRelativePath === path.join('..', 'src', 'startup-time.js') ); } }).then(({ snapshotScript }) => { process.stdout.write('\n'); process.stdout.write('Minifying startup script'); const minification = terser.minify(snapshotScript, { keep_fnames: true, keep_classnames: true, compress: { keep_fargs: true, keep_infinity: true } }); if (minification.error) throw minification.error; process.stdout.write('\n'); fs.writeFileSync(snapshotScriptPath, minification.code); console.log('Verifying if snapshot can be executed via `mksnapshot`'); const verifySnapshotScriptPath = path.join( CONFIG.repositoryRootPath, 'script', 'verify-snapshot-script' ); let nodeBundledInElectronPath; if (process.platform === 'darwin') { nodeBundledInElectronPath = path.join( packagedAppPath, 'Contents', 'MacOS', CONFIG.executableName ); } else { nodeBundledInElectronPath = path.join( packagedAppPath, CONFIG.executableName ); } childProcess.execFileSync( nodeBundledInElectronPath, [verifySnapshotScriptPath, snapshotScriptPath], { env: Object.assign({}, process.env, { ELECTRON_RUN_AS_NODE: 1 }) } ); console.log('Generating startup blob with mksnapshot'); childProcess.spawnSync(process.execPath, [ path.join( CONFIG.repositoryRootPath, 'script', 'node_modules', 'electron-mksnapshot', 'mksnapshot.js' ), snapshotScriptPath, '--output_dir', CONFIG.buildOutputPath ]); let startupBlobDestinationPath; if (process.platform === 'darwin') { startupBlobDestinationPath = `${packagedAppPath}/Contents/Frameworks/Electron Framework.framework/Resources`; } else { startupBlobDestinationPath = packagedAppPath; } const snapshotBinaries = ['v8_context_snapshot.bin', 'snapshot_blob.bin']; for (let snapshotBinary of snapshotBinaries) { const destinationPath = path.join( startupBlobDestinationPath, snapshotBinary ); console.log(`Moving generated startup blob into "${destinationPath}"`); try { fs.unlinkSync(destinationPath); } catch (err) { // Doesn't matter if the file doesn't exist already if (!err.code || err.code !== 'ENOENT') { throw err; } } fs.renameSync( path.join(CONFIG.buildOutputPath, snapshotBinary), destinationPath ); } }); }; ================================================ FILE: script/lib/get-license-text.js ================================================ 'use strict'; const fs = require('fs'); const path = require('path'); const legalEagle = require('legal-eagle'); const licenseOverrides = require('../license-overrides'); const CONFIG = require('../config'); module.exports = function() { return new Promise((resolve, reject) => { legalEagle( { path: CONFIG.repositoryRootPath, overrides: licenseOverrides }, (err, packagesLicenses) => { if (err) { reject(err); throw new Error(err); } else { let text = fs.readFileSync( path.join(CONFIG.repositoryRootPath, 'LICENSE.md'), 'utf8' ) + '\n\n' + 'This application bundles the following third-party packages in accordance\n' + 'with the following licenses:\n\n'; for (let packageName of Object.keys(packagesLicenses).sort()) { const packageLicense = packagesLicenses[packageName]; text += '-------------------------------------------------------------------------\n\n'; text += `Package: ${packageName}\n`; text += `License: ${packageLicense.license}\n`; if (packageLicense.source) { text += `License Source: ${packageLicense.source}\n`; } if (packageLicense.sourceText) { text += `Source Text:\n\n${packageLicense.sourceText}`; } text += '\n'; } resolve(text); } } ); }); }; ================================================ FILE: script/lib/handle-tilde.js ================================================ 'use strict'; const os = require('os'); const passwdUser = require('passwd-user'); const path = require('path'); module.exports = function(aPath) { if (!aPath.startsWith('~')) { return aPath; } const sepIndex = aPath.indexOf(path.sep); const user = sepIndex < 0 ? aPath.substring(1) : aPath.substring(1, sepIndex); const rest = sepIndex < 0 ? '' : aPath.substring(sepIndex); const home = user === '' ? os.homedir() : (() => { const passwd = passwdUser.sync(user); if (passwd === undefined) { throw new Error( `Failed to expand the tilde in ${aPath} - user "${user}" does not exist` ); } return passwd.homedir; })(); return `${home}${rest}`; }; ================================================ FILE: script/lib/include-path-in-packaged-app.js ================================================ 'use strict'; const path = require('path'); const CONFIG = require('../config'); module.exports = function(filePath) { return ( !EXCLUDED_PATHS_REGEXP.test(filePath) || INCLUDED_PATHS_REGEXP.test(filePath) ); }; const EXCLUDE_REGEXPS_SOURCES = [ escapeRegExp('.DS_Store'), escapeRegExp('.jshintrc'), escapeRegExp('.npmignore'), escapeRegExp('.pairs'), escapeRegExp('.idea'), escapeRegExp('.editorconfig'), escapeRegExp('.lint'), escapeRegExp('.lintignore'), escapeRegExp('.eslintrc'), escapeRegExp('.jshintignore'), escapeRegExp('coffeelint.json'), escapeRegExp('.coffeelintignore'), escapeRegExp('.gitattributes'), escapeRegExp('.gitkeep'), escapeRegExp(path.join('git-utils', 'deps')), escapeRegExp(path.join('oniguruma', 'deps')), escapeRegExp(path.join('less', 'dist')), escapeRegExp(path.join('npm', 'doc')), escapeRegExp(path.join('npm', 'html')), escapeRegExp(path.join('npm', 'man')), escapeRegExp(path.join('npm', 'node_modules', '.bin', 'beep')), escapeRegExp(path.join('npm', 'node_modules', '.bin', 'clear')), escapeRegExp(path.join('npm', 'node_modules', '.bin', 'starwars')), escapeRegExp(path.join('pegjs', 'examples')), escapeRegExp(path.join('get-parameter-names', 'node_modules', 'testla')), escapeRegExp( path.join('get-parameter-names', 'node_modules', '.bin', 'testla') ), escapeRegExp(path.join('jasmine-reporters', 'ext')), escapeRegExp(path.join('node_modules', 'nan')) + '\\b', escapeRegExp(path.join('node_modules', 'native-mate')), escapeRegExp(path.join('build', 'binding.Makefile')), escapeRegExp(path.join('build', 'config.gypi')), escapeRegExp(path.join('build', 'gyp-mac-tool')), escapeRegExp(path.join('build', 'Makefile')), escapeRegExp(path.join('build', 'Release', 'obj.target')), escapeRegExp(path.join('build', 'Release', 'obj')), escapeRegExp(path.join('build', 'Release', '.deps')), escapeRegExp(path.join('deps', 'libgit2')), escapeRegExp(path.join('vendor', 'apm')), // These are only required in dev-mode, when pegjs grammars aren't precompiled escapeRegExp(path.join('node_modules', 'loophole')), escapeRegExp(path.join('node_modules', 'pegjs')), escapeRegExp(path.join('node_modules', '.bin', 'pegjs')), escapeRegExp( path.join('node_modules', 'spellchecker', 'vendor', 'hunspell') + path.sep ) + '.*', // node_modules of the fuzzy-native package are only required for building it. escapeRegExp(path.join('node_modules', 'fuzzy-native', 'node_modules')), // Ignore *.cc and *.h files from native modules escapeRegExp(path.sep) + '.+\\.(cc|h)$', // Ignore build files escapeRegExp(path.sep) + 'binding\\.gyp$', escapeRegExp(path.sep) + '.+\\.target.mk$', escapeRegExp(path.sep) + 'linker\\.lock$', escapeRegExp(path.join('build', 'Release') + path.sep) + '.+\\.node\\.dSYM', escapeRegExp(path.join('build', 'Release') + path.sep) + '.*\\.(pdb|lib|exp|map|ipdb|iobj)', // Ignore node_module files we won't need at runtime 'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + '_*te?sts?_*' + escapeRegExp(path.sep), 'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + 'tests?' + escapeRegExp(path.sep), 'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + 'examples?' + escapeRegExp(path.sep), 'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.d\\.ts$', 'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.js\\.map$', '.*' + escapeRegExp(path.sep) + 'test.*\\.html$', // specific spec folders hand-picked 'node_modules' + escapeRegExp(path.sep) + '(oniguruma|dev-live-reload|deprecation-cop|one-dark-ui|incompatible-packages|git-diff|line-ending-selector|link|grammar-selector|json-schema-traverse|exception-reporting|one-light-ui|autoflow|about|go-to-line|sylvester|apparatus)' + escapeRegExp(path.sep) + 'spec' + escapeRegExp(path.sep), // babel-core spec 'node_modules' + escapeRegExp(path.sep) + 'babel-core' + escapeRegExp(path.sep) + 'lib' + escapeRegExp(path.sep) + 'transformation' + escapeRegExp(path.sep) + 'transforers' + escapeRegExp(path.sep) + 'spec' + escapeRegExp(path.sep) ]; // Ignore spec directories in all bundled packages for (let packageName in CONFIG.appMetadata.packageDependencies) { EXCLUDE_REGEXPS_SOURCES.push( '^' + escapeRegExp( path.join( CONFIG.repositoryRootPath, 'node_modules', packageName, 'spec' ) ) ); } // Ignore Hunspell dictionaries only on macOS. if (process.platform === 'darwin') { EXCLUDE_REGEXPS_SOURCES.push( escapeRegExp(path.join('spellchecker', 'vendor', 'hunspell_dictionaries')) ); } const EXCLUDED_PATHS_REGEXP = new RegExp( EXCLUDE_REGEXPS_SOURCES.map(path => `(${path})`).join('|') ); const INCLUDED_PATHS_REGEXP = new RegExp( escapeRegExp( path.join('node_modules', 'node-gyp', 'src', 'win_delay_load_hook.cc') ) ); function escapeRegExp(string) { return string.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&'); } ================================================ FILE: script/lib/install-apm.js ================================================ 'use strict'; const childProcess = require('child_process'); const CONFIG = require('../config'); module.exports = function(ci) { if (ci) { // Tell apm not to dedupe its own dependencies during its // postinstall script. (Deduping during `npm ci` runs is broken.) process.env.NO_APM_DEDUPE = 'true'; } console.log('Installing apm'); childProcess.execFileSync( CONFIG.getNpmBinPath(), ['--global-style', '--loglevel=error', ci ? 'ci' : 'install'], { env: process.env, cwd: CONFIG.apmRootPath } ); }; ================================================ FILE: script/lib/install-application.js ================================================ 'use strict'; const fs = require('fs-extra'); const handleTilde = require('./handle-tilde'); const path = require('path'); const template = require('lodash.template'); const startCase = require('lodash.startcase'); const execSync = require('child_process').execSync; const CONFIG = require('../config'); function install(installationDirPath, packagedAppFileName, packagedAppPath) { if (fs.existsSync(installationDirPath)) { console.log( `Removing previously installed "${packagedAppFileName}" at "${installationDirPath}"` ); fs.removeSync(installationDirPath); } console.log( `Installing "${packagedAppFileName}" at "${installationDirPath}"` ); fs.copySync(packagedAppPath, installationDirPath); } /** * Finds the path to the base directory of the icon default icon theme * This follows the freedesktop Icon Theme Specification: * https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#install_icons * and the XDG Base Directory Specification: * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables */ function findBaseIconThemeDirPath() { const defaultBaseIconThemeDir = '/usr/share/icons/hicolor'; const dataDirsString = process.env.XDG_DATA_DIRS; if (dataDirsString) { const dataDirs = dataDirsString.split(path.delimiter); if (dataDirs.includes('/usr/share/') || dataDirs.includes('/usr/share')) { return defaultBaseIconThemeDir; } else { return path.join(dataDirs[0], 'icons', 'hicolor'); } } else { return defaultBaseIconThemeDir; } } module.exports = function(packagedAppPath, installDir) { const packagedAppFileName = path.basename(packagedAppPath); if (process.platform === 'darwin') { const installPrefix = installDir !== '' ? handleTilde(installDir) : path.join(path.sep, 'Applications'); const installationDirPath = path.join(installPrefix, packagedAppFileName); install(installationDirPath, packagedAppFileName, packagedAppPath); } else if (process.platform === 'win32') { const installPrefix = installDir !== '' ? installDir : process.env.LOCALAPPDATA; const installationDirPath = path.join( installPrefix, packagedAppFileName, 'app-dev' ); try { install(installationDirPath, packagedAppFileName, packagedAppPath); } catch (e) { console.log( `Administrator elevation required to install into "${installationDirPath}"` ); const fsAdmin = require('fs-admin'); return new Promise((resolve, reject) => { fsAdmin.recursiveCopy(packagedAppPath, installationDirPath, error => { error ? reject(error) : resolve(); }); }); } } else { const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : 'atom-' + CONFIG.channel; const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : 'apm-' + CONFIG.channel; const appName = CONFIG.channel === 'stable' ? 'Atom' : startCase('Atom ' + CONFIG.channel); const appDescription = CONFIG.appMetadata.description; const prefixDirPath = installDir !== '' ? handleTilde(installDir) : path.join('/usr', 'local'); const shareDirPath = path.join(prefixDirPath, 'share'); const installationDirPath = path.join(shareDirPath, atomExecutableName); const applicationsDirPath = path.join(shareDirPath, 'applications'); const binDirPath = path.join(prefixDirPath, 'bin'); fs.mkdirpSync(applicationsDirPath); fs.mkdirpSync(binDirPath); install(installationDirPath, packagedAppFileName, packagedAppPath); { // Install icons const baseIconThemeDirPath = findBaseIconThemeDirPath(); const fullIconName = atomExecutableName + '.png'; let existingIconsFound = false; fs.readdirSync(baseIconThemeDirPath).forEach(size => { const iconPath = path.join( baseIconThemeDirPath, size, 'apps', fullIconName ); if (fs.existsSync(iconPath)) { if (!existingIconsFound) { console.log( `Removing existing icons from "${baseIconThemeDirPath}"` ); } existingIconsFound = true; fs.removeSync(iconPath); } }); console.log(`Installing icons at "${baseIconThemeDirPath}"`); const appIconsPath = path.join( CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png' ); fs.readdirSync(appIconsPath).forEach(imageName => { if (/\.png$/.test(imageName)) { const size = path.basename(imageName, '.png'); const iconPath = path.join(appIconsPath, imageName); fs.copySync( iconPath, path.join( baseIconThemeDirPath, `${size}x${size}`, 'apps', fullIconName ) ); } }); console.log(`Updating icon cache for "${baseIconThemeDirPath}"`); try { execSync(`gtk-update-icon-cache ${baseIconThemeDirPath} --force`); } catch (e) {} } { // Install xdg desktop file const desktopEntryPath = path.join( applicationsDirPath, `${atomExecutableName}.desktop` ); if (fs.existsSync(desktopEntryPath)) { console.log( `Removing existing desktop entry file at "${desktopEntryPath}"` ); fs.removeSync(desktopEntryPath); } console.log(`Writing desktop entry file at "${desktopEntryPath}"`); const desktopEntryTemplate = fs.readFileSync( path.join( CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in' ) ); const desktopEntryContents = template(desktopEntryTemplate)({ appName, appFileName: atomExecutableName, description: appDescription, installDir: prefixDirPath, iconPath: atomExecutableName }); fs.writeFileSync(desktopEntryPath, desktopEntryContents); } { // Add atom executable to the PATH const atomBinDestinationPath = path.join(binDirPath, atomExecutableName); if (fs.existsSync(atomBinDestinationPath)) { console.log( `Removing existing executable at "${atomBinDestinationPath}"` ); fs.removeSync(atomBinDestinationPath); } console.log(`Copying atom.sh to "${atomBinDestinationPath}"`); fs.copySync( path.join(CONFIG.repositoryRootPath, 'atom.sh'), atomBinDestinationPath ); } { // Link apm executable to the PATH const apmBinDestinationPath = path.join(binDirPath, apmExecutableName); try { fs.lstatSync(apmBinDestinationPath); console.log( `Removing existing executable at "${apmBinDestinationPath}"` ); fs.removeSync(apmBinDestinationPath); } catch (e) {} console.log(`Symlinking apm to "${apmBinDestinationPath}"`); fs.symlinkSync( path.join( '..', 'share', atomExecutableName, 'resources', 'app', 'apm', 'node_modules', '.bin', 'apm' ), apmBinDestinationPath ); } console.log(`Changing permissions to 755 for "${installationDirPath}"`); fs.chmodSync(installationDirPath, '755'); } return Promise.resolve(); }; ================================================ FILE: script/lib/install-script-dependencies.js ================================================ 'use strict'; const childProcess = require('child_process'); const CONFIG = require('../config'); // Recognised by '@electron/get', used by the 'electron-mksnapshot' and 'electron-chromedriver' dependencies process.env.ELECTRON_CUSTOM_VERSION = CONFIG.appMetadata.electronVersion; module.exports = function(ci) { console.log('Installing script dependencies'); const npmBinName = process.platform === 'win32' ? 'npm.cmd' : 'npm'; childProcess.execFileSync( npmBinName, ['--loglevel=error', ci ? 'ci' : 'install'], { env: process.env, cwd: CONFIG.scriptRootPath } ); }; ================================================ FILE: script/lib/kill-running-atom-instances.js ================================================ const childProcess = require('child_process'); const CONFIG = require('../config.js'); module.exports = function() { if (process.platform === 'win32') { // Use START as a way to ignore error if Atom.exe isnt running childProcess.execSync(`START taskkill /F /IM ${CONFIG.executableName}`); } else { childProcess.execSync(`pkill -9 ${CONFIG.appMetadata.productName} || true`); } }; ================================================ FILE: script/lib/lint-coffee-script-paths.js ================================================ 'use strict'; const coffeelint = require('coffeelint'); const expandGlobPaths = require('./expand-glob-paths'); const path = require('path'); const readFiles = require('./read-files'); const CONFIG = require('../config'); module.exports = function() { const globPathsToLint = [ path.join(CONFIG.repositoryRootPath, 'dot-atom/**/*.coffee'), path.join(CONFIG.repositoryRootPath, 'src/**/*.coffee'), path.join(CONFIG.repositoryRootPath, 'spec/*.coffee') ]; return expandGlobPaths(globPathsToLint) .then(readFiles) .then(files => { const errors = []; const lintConfiguration = require(path.join( CONFIG.repositoryRootPath, 'coffeelint.json' )); for (let file of files) { const lintErrors = coffeelint.lint( file.content, lintConfiguration, false ); for (let error of lintErrors) { errors.push({ path: file.path, lineNumber: error.lineNumber, message: error.message, rule: error.rule }); } } return errors; }); }; ================================================ FILE: script/lib/lint-java-script-paths.js ================================================ 'use strict'; const path = require('path'); const { spawn } = require('child_process'); const process = require('process'); const CONFIG = require('../config'); module.exports = async function() { return new Promise((resolve, reject) => { const eslintArgs = ['--cache', '--format', 'json']; if (process.argv.includes('--fix')) { eslintArgs.push('--fix'); } const eslintBinary = process.platform === 'win32' ? 'eslint.cmd' : 'eslint'; const eslint = spawn( path.join('script', 'node_modules', '.bin', eslintBinary), [...eslintArgs, '.'], { cwd: CONFIG.repositoryRootPath } ); let output = ''; let errorOutput = ''; eslint.stdout.on('data', data => { output += data.toString(); }); eslint.stderr.on('data', data => { errorOutput += data.toString(); }); eslint.on('error', error => reject(error)); eslint.on('close', exitCode => { const errors = []; let files; try { files = JSON.parse(output); } catch (_) { reject(errorOutput); return; } for (const file of files) { for (const error of file.messages) { errors.push({ path: file.filePath, message: error.message, lineNumber: error.line, rule: error.ruleId }); } } resolve(errors); }); }); }; ================================================ FILE: script/lib/lint-less-paths.js ================================================ 'use strict'; const stylelint = require('stylelint'); const path = require('path'); const CONFIG = require('../config'); module.exports = function() { return stylelint .lint({ files: path.join(CONFIG.repositoryRootPath, 'static/**/*.less'), configBasedir: __dirname, configFile: path.resolve(__dirname, '..', '..', 'stylelint.config.js') }) .then(({ results }) => { const errors = []; for (const result of results) { for (const deprecation of result.deprecations) { console.log('stylelint encountered deprecation:', deprecation.text); if (deprecation.reference != null) { console.log('more information at', deprecation.reference); } } for (const invalidOptionWarning of result.invalidOptionWarnings) { console.warn( 'stylelint encountered invalid option:', invalidOptionWarning.text ); } if (result.errored) { for (const warning of result.warnings) { if (warning.severity === 'error') { errors.push({ path: result.source, lineNumber: warning.line, message: warning.text, rule: warning.rule }); } else { console.warn( 'stylelint encountered non-critical warning in file', result.source, 'at line', warning.line, 'for rule', warning.rule + ':', warning.text ); } } } } return errors; }) .catch(err => { console.error('There was a problem linting LESS:'); throw err; }); }; ================================================ FILE: script/lib/notarize-on-mac.js ================================================ const notarize = require('electron-notarize').notarize; module.exports = async function(packagedAppPath) { const appBundleId = 'com.github.atom'; const appleId = process.env.AC_USER; const appleIdPassword = process.env.AC_PASSWORD; console.log(`Notarizing application at ${packagedAppPath}`); try { await notarize({ appBundleId: appBundleId, appPath: packagedAppPath, appleId: appleId, appleIdPassword: appleIdPassword }); } catch (e) { throw new Error(e); } }; ================================================ FILE: script/lib/package-application.js ================================================ 'use strict'; const assert = require('assert'); const childProcess = require('child_process'); const electronPackager = require('electron-packager'); const fs = require('fs-extra'); const hostArch = require('@electron/get').getHostArch; const includePathInPackagedApp = require('./include-path-in-packaged-app'); const getLicenseText = require('./get-license-text'); const path = require('path'); const spawnSync = require('./spawn-sync'); const template = require('lodash.template'); const CONFIG = require('../config'); const HOST_ARCH = hostArch(); module.exports = function() { const appName = getAppName(); console.log( `Running electron-packager on ${ CONFIG.intermediateAppPath } with app name "${appName}"` ); return runPackager({ appBundleId: 'com.github.atom', appCopyright: `Copyright © 2014-${new Date().getFullYear()} GitHub, Inc. All rights reserved.`, appVersion: CONFIG.appMetadata.version, arch: process.platform === 'darwin' ? 'x64' : HOST_ARCH, // OS X is 64-bit only asar: { unpack: buildAsarUnpackGlobExpression() }, buildVersion: CONFIG.appMetadata.version, derefSymlinks: false, download: { cache: CONFIG.electronDownloadPath }, dir: CONFIG.intermediateAppPath, electronVersion: CONFIG.appMetadata.electronVersion, extendInfo: path.join( CONFIG.repositoryRootPath, 'resources', 'mac', 'atom-Info.plist' ), helperBundleId: 'com.github.atom.helper', icon: path.join( CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom' ), name: appName, out: CONFIG.buildOutputPath, overwrite: true, platform: process.platform, // Atom doesn't have devDependencies, but if prune is true, it will delete the non-standard packageDependencies. prune: false, win32metadata: { CompanyName: 'GitHub, Inc.', FileDescription: 'Atom', ProductName: CONFIG.appName } }).then(packagedAppPath => { let bundledResourcesPath; if (process.platform === 'darwin') { bundledResourcesPath = path.join( packagedAppPath, 'Contents', 'Resources' ); setAtomHelperVersion(packagedAppPath); } else if (process.platform === 'linux') { bundledResourcesPath = path.join(packagedAppPath, 'resources'); chmodNodeFiles(packagedAppPath); } else { bundledResourcesPath = path.join(packagedAppPath, 'resources'); } return copyNonASARResources(packagedAppPath, bundledResourcesPath).then( () => { console.log(`Application bundle created at ${packagedAppPath}`); return packagedAppPath; } ); }); }; function copyNonASARResources(packagedAppPath, bundledResourcesPath) { console.log(`Copying non-ASAR resources to ${bundledResourcesPath}`); fs.copySync( path.join( CONFIG.repositoryRootPath, 'apm', 'node_modules', 'atom-package-manager' ), path.join(bundledResourcesPath, 'app', 'apm'), { filter: includePathInPackagedApp } ); if (process.platform !== 'win32') { // Existing symlinks on user systems point to an outdated path, so just symlink it to the real location of the apm binary. // TODO: Change command installer to point to appropriate path and remove this fallback after a few releases. fs.symlinkSync( path.join('..', '..', 'bin', 'apm'), path.join( bundledResourcesPath, 'app', 'apm', 'node_modules', '.bin', 'apm' ) ); fs.copySync( path.join(CONFIG.repositoryRootPath, 'atom.sh'), path.join(bundledResourcesPath, 'app', 'atom.sh') ); } if (process.platform === 'darwin') { fs.copySync( path.join(CONFIG.repositoryRootPath, 'resources', 'mac', 'file.icns'), path.join(bundledResourcesPath, 'file.icns') ); } else if (process.platform === 'linux') { fs.copySync( path.join( CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png', '1024.png' ), path.join(packagedAppPath, 'atom.png') ); } else if (process.platform === 'win32') { [ 'atom.sh', 'atom.js', 'apm.cmd', 'apm.sh', 'file.ico', 'folder.ico' ].forEach(file => fs.copySync( path.join(CONFIG.repositoryRootPath, 'resources', 'win', file), path.join(bundledResourcesPath, 'cli', file) ) ); // Customize atom.cmd for the channel-specific atom.exe name (e.g. atom-beta.exe) generateAtomCmdForChannel(bundledResourcesPath); } console.log(`Writing LICENSE.md to ${bundledResourcesPath}`); return getLicenseText().then(licenseText => { fs.writeFileSync( path.join(bundledResourcesPath, 'LICENSE.md'), licenseText ); }); } function setAtomHelperVersion(packagedAppPath) { const frameworksPath = path.join(packagedAppPath, 'Contents', 'Frameworks'); const helperPListPath = path.join( frameworksPath, 'Atom Helper.app', 'Contents', 'Info.plist' ); console.log(`Setting Atom Helper Version for ${helperPListPath}`); spawnSync('/usr/libexec/PlistBuddy', [ '-c', `Add CFBundleVersion string ${CONFIG.appMetadata.version}`, helperPListPath ]); spawnSync('/usr/libexec/PlistBuddy', [ '-c', `Add CFBundleShortVersionString string ${CONFIG.appMetadata.version}`, helperPListPath ]); } function chmodNodeFiles(packagedAppPath) { console.log(`Changing permissions for node files in ${packagedAppPath}`); childProcess.execSync( `find "${packagedAppPath}" -type f -name *.node -exec chmod a-x {} \\;` ); } function buildAsarUnpackGlobExpression() { const unpack = [ '*.node', 'ctags-config', 'ctags-darwin', 'ctags-linux', 'ctags-win32.exe', path.join('**', 'node_modules', 'spellchecker', '**'), path.join('**', 'node_modules', 'dugite', 'git', '**'), path.join('**', 'node_modules', 'github', 'bin', '**'), path.join('**', 'node_modules', 'vscode-ripgrep', 'bin', '**'), path.join('**', 'resources', 'atom.png') ]; return `{${unpack.join(',')}}`; } function getAppName() { if (process.platform === 'darwin') { return CONFIG.appName; } else if (process.platform === 'win32') { return CONFIG.channel === 'stable' ? 'atom' : `atom-${CONFIG.channel}`; } else { return 'atom'; } } async function runPackager(options) { const packageOutputDirPaths = await electronPackager(options); assert( packageOutputDirPaths.length === 1, 'Generated more than one electron application!' ); return renamePackagedAppDir(packageOutputDirPaths[0]); } function renamePackagedAppDir(packageOutputDirPath) { let packagedAppPath; if (process.platform === 'darwin') { const appBundleName = getAppName() + '.app'; packagedAppPath = path.join(CONFIG.buildOutputPath, appBundleName); if (fs.existsSync(packagedAppPath)) fs.removeSync(packagedAppPath); fs.renameSync( path.join(packageOutputDirPath, appBundleName), packagedAppPath ); } else if (process.platform === 'linux') { const appName = CONFIG.channel !== 'stable' ? `atom-${CONFIG.channel}` : 'atom'; let architecture; if (HOST_ARCH === 'ia32') { architecture = 'i386'; } else if (HOST_ARCH === 'x64') { architecture = 'amd64'; } else { architecture = HOST_ARCH; } packagedAppPath = path.join( CONFIG.buildOutputPath, `${appName}-${CONFIG.appMetadata.version}-${architecture}` ); if (fs.existsSync(packagedAppPath)) fs.removeSync(packagedAppPath); fs.renameSync(packageOutputDirPath, packagedAppPath); } else { packagedAppPath = path.join(CONFIG.buildOutputPath, CONFIG.appName); if (process.platform === 'win32' && HOST_ARCH !== 'ia32') { packagedAppPath += ` ${process.arch}`; } if (fs.existsSync(packagedAppPath)) fs.removeSync(packagedAppPath); fs.renameSync(packageOutputDirPath, packagedAppPath); } return packagedAppPath; } function generateAtomCmdForChannel(bundledResourcesPath) { const atomCmdTemplate = fs.readFileSync( path.join(CONFIG.repositoryRootPath, 'resources', 'win', 'atom.cmd') ); const atomCmdContents = template(atomCmdTemplate)({ atomExeName: CONFIG.executableName }); fs.writeFileSync( path.join(bundledResourcesPath, 'cli', 'atom.cmd'), atomCmdContents ); } ================================================ FILE: script/lib/prebuild-less-cache.js ================================================ 'use strict'; const fs = require('fs'); const klawSync = require('klaw-sync'); const glob = require('glob'); const path = require('path'); const LessCache = require('less-cache'); const CONFIG = require('../config'); const LESS_CACHE_VERSION = require('less-cache/package.json').version; const FALLBACK_VARIABLE_IMPORTS = '@import "variables/ui-variables";\n@import "variables/syntax-variables";\n'; module.exports = function() { const cacheDirPath = path.join( CONFIG.intermediateAppPath, 'less-compile-cache' ); console.log(`Generating pre-built less cache in ${cacheDirPath}`); // Group bundled packages into UI themes, syntax themes, and non-theme packages const uiThemes = []; const syntaxThemes = []; const nonThemePackages = []; for (let packageName in CONFIG.appMetadata.packageDependencies) { const packageMetadata = require(path.join( CONFIG.intermediateAppPath, 'node_modules', packageName, 'package.json' )); if (packageMetadata.theme === 'ui') { uiThemes.push(packageName); } else if (packageMetadata.theme === 'syntax') { syntaxThemes.push(packageName); } else { nonThemePackages.push(packageName); } } CONFIG.snapshotAuxiliaryData.lessSourcesByRelativeFilePath = {}; function saveIntoSnapshotAuxiliaryData(absoluteFilePath, content) { const relativeFilePath = path.relative( CONFIG.intermediateAppPath, absoluteFilePath ); if ( !CONFIG.snapshotAuxiliaryData.lessSourcesByRelativeFilePath.hasOwnProperty( relativeFilePath ) ) { CONFIG.snapshotAuxiliaryData.lessSourcesByRelativeFilePath[ relativeFilePath ] = { content: content, digest: LessCache.digestForContent(content) }; } } CONFIG.snapshotAuxiliaryData.importedFilePathsByRelativeImportPath = {}; // Warm cache for every combination of the default UI and syntax themes, // because themes assign variables which may be used in any style sheet. for (let uiTheme of uiThemes) { for (let syntaxTheme of syntaxThemes) { // Build a LessCache instance with import paths based on the current theme combination const lessCache = new LessCache({ cacheDir: cacheDirPath, fallbackDir: path.join( CONFIG.atomHomeDirPath, 'compile-cache', 'prebuild-less', LESS_CACHE_VERSION ), syncCaches: true, resourcePath: CONFIG.intermediateAppPath, importPaths: [ path.join( CONFIG.intermediateAppPath, 'node_modules', syntaxTheme, 'styles' ), path.join( CONFIG.intermediateAppPath, 'node_modules', uiTheme, 'styles' ), path.join(CONFIG.intermediateAppPath, 'static', 'variables'), path.join(CONFIG.intermediateAppPath, 'static') ] }); // Store file paths located at the import paths so that we can avoid scanning them at runtime. for (const absoluteImportPath of lessCache.getImportPaths()) { const relativeImportPath = path.relative( CONFIG.intermediateAppPath, absoluteImportPath ); if ( !CONFIG.snapshotAuxiliaryData.importedFilePathsByRelativeImportPath.hasOwnProperty( relativeImportPath ) ) { CONFIG.snapshotAuxiliaryData.importedFilePathsByRelativeImportPath[ relativeImportPath ] = []; for (const importedFile of klawSync(absoluteImportPath, { nodir: true })) { CONFIG.snapshotAuxiliaryData.importedFilePathsByRelativeImportPath[ relativeImportPath ].push( path.relative(CONFIG.intermediateAppPath, importedFile.path) ); } } } // Cache all styles in static; don't append variable imports for (let lessFilePath of glob.sync( path.join(CONFIG.intermediateAppPath, 'static', '**', '*.less') )) { cacheCompiledCSS(lessCache, lessFilePath, false); } // Cache styles for all bundled non-theme packages for (let nonThemePackage of nonThemePackages) { for (let lessFilePath of glob.sync( path.join( CONFIG.intermediateAppPath, 'node_modules', nonThemePackage, '**', '*.less' ) )) { cacheCompiledCSS(lessCache, lessFilePath, true); } } // Cache styles for this UI theme const uiThemeMainPath = path.join( CONFIG.intermediateAppPath, 'node_modules', uiTheme, 'index.less' ); cacheCompiledCSS(lessCache, uiThemeMainPath, true); for (let lessFilePath of glob.sync( path.join( CONFIG.intermediateAppPath, 'node_modules', uiTheme, '**', '*.less' ) )) { if (lessFilePath !== uiThemeMainPath) { saveIntoSnapshotAuxiliaryData( lessFilePath, fs.readFileSync(lessFilePath, 'utf8') ); } } // Cache styles for this syntax theme const syntaxThemeMainPath = path.join( CONFIG.intermediateAppPath, 'node_modules', syntaxTheme, 'index.less' ); cacheCompiledCSS(lessCache, syntaxThemeMainPath, true); for (let lessFilePath of glob.sync( path.join( CONFIG.intermediateAppPath, 'node_modules', syntaxTheme, '**', '*.less' ) )) { if (lessFilePath !== syntaxThemeMainPath) { saveIntoSnapshotAuxiliaryData( lessFilePath, fs.readFileSync(lessFilePath, 'utf8') ); } } } } for (let lessFilePath of glob.sync( path.join( CONFIG.intermediateAppPath, 'node_modules', 'atom-ui', '**', '*.less' ) )) { saveIntoSnapshotAuxiliaryData( lessFilePath, fs.readFileSync(lessFilePath, 'utf8') ); } function cacheCompiledCSS(lessCache, lessFilePath, importFallbackVariables) { let lessSource = fs.readFileSync(lessFilePath, 'utf8'); if (importFallbackVariables) { lessSource = FALLBACK_VARIABLE_IMPORTS + lessSource; } lessCache.cssForFile(lessFilePath, lessSource); saveIntoSnapshotAuxiliaryData(lessFilePath, lessSource); } }; ================================================ FILE: script/lib/read-files.js ================================================ 'use strict'; const fs = require('fs'); module.exports = function(paths) { return Promise.all(paths.map(readFile)); }; function readFile(path) { return new Promise((resolve, reject) => { fs.readFile(path, 'utf8', (error, content) => { if (error) { reject(error); } else { resolve({ path, content }); } }); }); } ================================================ FILE: script/lib/run-apm-install.js ================================================ 'use strict'; const childProcess = require('child_process'); const CONFIG = require('../config'); module.exports = function(packagePath, ci, stdioOptions) { const installEnv = Object.assign({}, process.env); // Set resource path so that apm can load metadata related to Atom. installEnv.ATOM_RESOURCE_PATH = CONFIG.repositoryRootPath; childProcess.execFileSync(CONFIG.getApmBinPath(), [ci ? 'ci' : 'install'], { env: installEnv, cwd: packagePath, stdio: stdioOptions || 'inherit' }); }; ================================================ FILE: script/lib/spawn-sync.js ================================================ // This file exports a function that has the same interface as // `spawnSync`, but it throws if there's an error while executing // the supplied command or if the exit code is not 0. This is similar to what // `execSync` does, but we want to use `spawnSync` because it provides automatic // escaping for the supplied arguments. const childProcess = require('child_process'); module.exports = function() { const result = childProcess.spawnSync.apply(childProcess, arguments); if (result.error) { throw result.error; } else if (result.status !== 0) { if (result.stdout) console.error(result.stdout.toString()); if (result.stderr) console.error(result.stderr.toString()); throw new Error( `Command ${result.args.join(' ')} exited with code "${result.status}"` ); } else { return result; } }; ================================================ FILE: script/lib/test-sign-on-mac.js ================================================ const spawnSync = require('./spawn-sync'); module.exports = function(packagedAppPath) { const result = spawnSync('security', [ 'find-certificate', '-c', 'Mac Developer' ]); const certMatch = (result.stdout || '') .toString() .match(/"(Mac Developer.*\))"/); if (!certMatch || !certMatch[1]) { console.error( 'A "Mac Developer" certificate must be configured to perform test signing' ); } else { // This code-signs the application with a local certificate which won't be // useful anywhere else but the current machine // See this issue for more details: https://github.com/electron/electron/issues/7476#issuecomment-356084754 console.log(`Found development certificate '${certMatch[1]}'`); console.log(`Test-signing application at ${packagedAppPath}`); spawnSync( 'codesign', [ '--deep', '--force', '--verbose', '--sign', certMatch[1], packagedAppPath ], { stdio: 'inherit' } ); } }; ================================================ FILE: script/lib/transpile-babel-paths.js ================================================ 'use strict'; const CompileCache = require('../../src/compile-cache'); const fs = require('fs'); const glob = require('glob'); const path = require('path'); const CONFIG = require('../config'); module.exports = function() { console.log(`Transpiling Babel paths in ${CONFIG.intermediateAppPath}`); for (let path of getPathsToTranspile()) { transpileBabelPath(path); } }; function getPathsToTranspile() { let paths = []; for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { paths = paths.concat( glob.sync( path.join( CONFIG.intermediateAppPath, 'node_modules', packageName, '**', '*.js' ), { ignore: path.join( CONFIG.intermediateAppPath, 'node_modules', packageName, 'spec', '**', '*.js' ), nodir: true } ) ); } return paths; } function transpileBabelPath(path) { fs.writeFileSync( path, CompileCache.addPathToCache(path, CONFIG.atomHomeDirPath) ); } ================================================ FILE: script/lib/transpile-coffee-script-paths.js ================================================ 'use strict'; const CompileCache = require('../../src/compile-cache'); const fs = require('fs'); const glob = require('glob'); const path = require('path'); const CONFIG = require('../config'); module.exports = function() { console.log( `Transpiling CoffeeScript paths in ${CONFIG.intermediateAppPath}` ); for (let path of getPathsToTranspile()) { transpileCoffeeScriptPath(path); } }; function getPathsToTranspile() { let paths = []; paths = paths.concat( glob.sync(path.join(CONFIG.intermediateAppPath, 'src', '**', '*.coffee'), { nodir: true }) ); paths = paths.concat( glob.sync(path.join(CONFIG.intermediateAppPath, 'spec', '*.coffee'), { nodir: true }) ); for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { paths = paths.concat( glob.sync( path.join( CONFIG.intermediateAppPath, 'node_modules', packageName, '**', '*.coffee' ), { ignore: path.join( CONFIG.intermediateAppPath, 'node_modules', packageName, 'spec', '**', '*.coffee' ), nodir: true } ) ); } return paths; } function transpileCoffeeScriptPath(coffeePath) { const jsPath = coffeePath.replace(/coffee$/g, 'js'); fs.writeFileSync( jsPath, CompileCache.addPathToCache(coffeePath, CONFIG.atomHomeDirPath) ); fs.unlinkSync(coffeePath); } ================================================ FILE: script/lib/transpile-cson-paths.js ================================================ 'use strict'; const CompileCache = require('../../src/compile-cache'); const fs = require('fs'); const glob = require('glob'); const path = require('path'); const CONFIG = require('../config'); module.exports = function() { console.log(`Transpiling CSON paths in ${CONFIG.intermediateAppPath}`); for (let path of getPathsToTranspile()) { transpileCsonPath(path); } }; function getPathsToTranspile() { let paths = []; for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { paths = paths.concat( glob.sync( path.join( CONFIG.intermediateAppPath, 'node_modules', packageName, '**', '*.cson' ), { ignore: path.join( CONFIG.intermediateAppPath, 'node_modules', packageName, 'spec', '**', '*.cson' ), nodir: true } ) ); } return paths; } function transpileCsonPath(csonPath) { const jsonPath = csonPath.replace(/cson$/g, 'json'); fs.writeFileSync( jsonPath, JSON.stringify( CompileCache.addPathToCache(csonPath, CONFIG.atomHomeDirPath) ) ); fs.unlinkSync(csonPath); } ================================================ FILE: script/lib/transpile-packages-with-custom-transpiler-paths.js ================================================ 'use strict'; const CompileCache = require('../../src/compile-cache'); const fs = require('fs-extra'); const glob = require('glob'); const path = require('path'); const CONFIG = require('../config'); const backupNodeModules = require('./backup-node-modules'); const runApmInstall = require('./run-apm-install'); require('colors'); module.exports = function() { console.log( `Transpiling packages with custom transpiler configurations in ${ CONFIG.intermediateAppPath }` ); for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { const rootPackagePath = path.join( CONFIG.repositoryRootPath, 'node_modules', packageName ); const intermediatePackagePath = path.join( CONFIG.intermediateAppPath, 'node_modules', packageName ); const metadataPath = path.join(intermediatePackagePath, 'package.json'); const metadata = require(metadataPath); if (metadata.atomTranspilers) { console.log(' transpiling for package '.cyan + packageName.cyan); const rootPackageBackup = backupNodeModules(rootPackagePath); const intermediatePackageBackup = backupNodeModules( intermediatePackagePath ); // 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)) { fs.removeSync(intermediatePackageBackup.nodeModulesPath); } fs.copySync( rootPackageBackup.nodeModulesPath, intermediatePackageBackup.nodeModulesPath ); CompileCache.addTranspilerConfigForPath( intermediatePackagePath, metadata.name, metadata, metadata.atomTranspilers ); for (let config of metadata.atomTranspilers) { const pathsToCompile = glob.sync( path.join(intermediatePackagePath, config.glob), { nodir: true } ); pathsToCompile.forEach(transpilePath); } // Now that we've transpiled everything in-place, we no longer want Atom to try to transpile // the same files when they're being required. delete metadata.atomTranspilers; fs.writeFileSync( metadataPath, JSON.stringify(metadata, null, ' '), 'utf8' ); CompileCache.removeTranspilerConfigForPath(intermediatePackagePath); rootPackageBackup.restore(); intermediatePackageBackup.restore(); } } }; function transpilePath(path) { fs.writeFileSync( path, CompileCache.addPathToCache(path, CONFIG.atomHomeDirPath) ); } ================================================ FILE: script/lib/transpile-peg-js-paths.js ================================================ 'use strict'; const peg = require('pegjs'); const fs = require('fs'); const glob = require('glob'); const path = require('path'); const CONFIG = require('../config'); module.exports = function() { console.log(`Transpiling PEG.js paths in ${CONFIG.intermediateAppPath}`); for (let path of getPathsToTranspile()) { transpilePegJsPath(path); } }; function getPathsToTranspile() { let paths = []; for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) { paths = paths.concat( glob.sync( path.join( CONFIG.intermediateAppPath, 'node_modules', packageName, '**', '*.pegjs' ), { nodir: true } ) ); } return paths; } function transpilePegJsPath(pegJsPath) { const inputCode = fs.readFileSync(pegJsPath, 'utf8'); const jsPath = pegJsPath.replace(/pegjs$/g, 'js'); const outputCode = 'module.exports = ' + peg.buildParser(inputCode, { output: 'source' }); fs.writeFileSync(jsPath, outputCode); fs.unlinkSync(pegJsPath); } ================================================ FILE: script/lib/update-dependency/fetch-outdated-dependencies.js ================================================ const fetch = require('node-fetch'); const npmCheck = require('npm-check'); // this may be updated to use github releases instead const apm = async function({ dependencies, packageDependencies }) { try { console.log('Checking apm registry...'); const coreDependencies = Object.keys(dependencies).filter(dependency => { // all core packages point to a remote url return dependencies[dependency].match(new RegExp('^https?://')); }); const promises = coreDependencies.map(async dependency => { return fetch(`https://atom.io/api/packages/${dependency}`) .then(res => res.json()) .then(res => res) .catch(ex => console.log(ex.message)); }); const packages = await Promise.all(promises); const outdatedPackages = []; packages.map(dependency => { if (dependency.hasOwnProperty('name')) { const latestVersion = dependency.releases.latest; const installed = packageDependencies[dependency.name]; if (latestVersion > installed) { outdatedPackages.push({ moduleName: dependency.name, latest: dependency.releases.latest, isCorePackage: true, installed }); } } }); console.log(`${outdatedPackages.length} outdated package(s) found`); return outdatedPackages; } catch (ex) { console.error(`An error occured: ${ex.message}`); } }; const npm = async function(cwd) { try { console.log('Checking npm registry...', cwd); const currentState = await npmCheck({ cwd, ignoreDev: true, skipUnused: true }); const outdatedPackages = currentState .get('packages') .filter(p => { if (p.packageJson && p.latest && p.installed) { return p.latest > p.installed; } }) .map(({ packageJson, installed, moduleName, latest }) => ({ packageJson, installed, moduleName, latest, isCorePackage: false })); console.log(`${outdatedPackages.length} outdated package(s) found`); return outdatedPackages; } catch (ex) { console.error(`An error occured: ${ex.message}`); } }; module.exports = { apm, npm }; ================================================ FILE: script/lib/update-dependency/git.js ================================================ const git = (git, repositoryRootPath) => { const path = require('path'); const packageJsonFilePath = path.join(repositoryRootPath, 'package.json'); const packageLockFilePath = path.join( repositoryRootPath, 'package-lock.json' ); try { git.getRemotes((err, remotes) => { if (!err && !remotes.map(({ name }) => name).includes('ATOM')) { git.addRemote( 'ATOM', `https://atom:${process.env.AUTH_TOKEN}@github.com/atom/atom.git/` ); } }); } catch (ex) { console.log(ex.message); } async function createOrCheckoutBranch(newBranch) { await git.fetch(); const { branches } = await git.branch(); const found = Object.keys(branches).find( branch => branch.indexOf(newBranch) > -1 ); found ? await git.checkout(found) : await git.checkoutLocalBranch(newBranch); return { found, newBranch }; } return { switchToCleanBranch: async function() { const cleanBranch = 'clean-branch'; const { current } = await git.branch(); if (current !== cleanBranch) createOrCheckoutBranch(cleanBranch); }, makeBranch: async function(dependency) { const newBranch = `${dependency.moduleName}-${dependency.latest}`; const { files } = await git.status(); if (files.length > 0) { await git.reset('hard'); } return createOrCheckoutBranch(newBranch); }, createCommit: async function({ moduleName, latest }) { try { const commitMessage = `:arrow_up: ${moduleName}@${latest}`; await git.add([packageJsonFilePath, packageLockFilePath]); await git.commit(commitMessage); } catch (ex) { throw Error(ex.message); } }, publishBranch: async function(branch) { try { await git.push('ATOM', branch); } catch (ex) { throw Error(ex.message); } }, deleteBranch: async function(branch) { try { await git.deleteLocalBranch(branch, true); } catch (ex) { throw Error(ex.message); } } }; }; module.exports = git; ================================================ FILE: script/lib/update-dependency/index.js ================================================ const run = require('./main'); run(); ================================================ FILE: script/lib/update-dependency/main.js ================================================ /* eslint-disable camelcase */ const simpleGit = require('simple-git'); const path = require('path'); const { repositoryRootPath } = require('../../config'); const packageJSON = require(path.join(repositoryRootPath, 'package.json')); const git = simpleGit(repositoryRootPath); const { createPR, findPR, addLabel, findOpenPRs, checkCIstatus, mergePR } = require('./pull-request'); const runApmInstall = require('../run-apm-install'); const { makeBranch, createCommit, switchToCleanBranch, publishBranch, deleteBranch } = require('./git')(git, repositoryRootPath); const { updatePackageJson, sleep } = require('./util')(repositoryRootPath); const fetchOutdatedDependencies = require('./fetch-outdated-dependencies'); module.exports = async function() { try { // ensure we are on master await switchToCleanBranch(); const failedBumps = []; const successfullBumps = []; const outdateDependencies = [ ...(await fetchOutdatedDependencies.npm(repositoryRootPath)), ...(await fetchOutdatedDependencies.apm(packageJSON)) ]; const totalDependencies = outdateDependencies.length; const pendingPRs = []; for (const dependency of outdateDependencies) { const { found, newBranch } = await makeBranch(dependency); if (found) { console.log(`Branch was found ${found}`); console.log('checking if a PR already exists'); const { data: { total_count } } = await findPR(dependency, newBranch); if (total_count > 0) { console.log(`pull request found!`); } else { console.log(`pull request not found!`); const pr = { dependency, branch: newBranch, branchIsRemote: false }; // confirm if branch found is a local branch if (found.indexOf('remotes') === -1) { await publishBranch(found); } else { pr.branchIsRemote = true; } pendingPRs.push(pr); } } else { await updatePackageJson(dependency); runApmInstall(repositoryRootPath, false); await createCommit(dependency); await publishBranch(newBranch); pendingPRs.push({ dependency, branch: newBranch, branchIsRemote: false }); } await switchToCleanBranch(); } // create PRs here for (const { dependency, branch, branchIsRemote } of pendingPRs) { const { status, data = {} } = await createPR(dependency, branch); if (status === 201) { successfullBumps.push(dependency); await addLabel(data.number); } else { failedBumps.push(dependency); } if (!branchIsRemote) { await deleteBranch(branch); } // https://developer.github.com/v3/guides/best-practices-for-integrators/#dealing-with-abuse-rate-limits await sleep(2000); } console.table([ { totalDependencies, totalSuccessfullBumps: successfullBumps.length, totalFailedBumps: failedBumps.length } ]); console.log('Successfull bumps'); console.table(successfullBumps); console.log('Failed bumps'); console.table(failedBumps); } catch (ex) { console.log(ex.message); } // merge previous bumps that passed CI requirements try { const { data: { items } } = await findOpenPRs(); for (const { title } of items) { const ref = title.replace('⬆️ ', '').replace('@', '-'); const { data: { state } } = await checkCIstatus({ ref }); if (state === 'success') { await mergePR({ ref }); } } } catch (ex) { console.log(ex); } }; ================================================ FILE: script/lib/update-dependency/pull-request.js ================================================ const { request } = require('@octokit/request'); const requestWithAuth = request.defaults({ baseUrl: 'https://api.github.com', headers: { 'user-agent': 'atom', authorization: `token ${process.env.AUTH_TOKEN}` }, owner: 'atom', repo: 'atom' }); module.exports = { createPR: async ( { moduleName, isCorePackage, latest, installed }, branch ) => { let description = `Bumps ${moduleName} from ${installed} to ${latest}`; if (isCorePackage) { description = `*List of changes between ${moduleName}@${installed} and ${moduleName}@${latest}: https://github.com/atom/${moduleName}/compare/v${installed}...v${latest}*`; } return requestWithAuth('POST /repos/:owner/:repo/pulls', { title: `⬆️ ${moduleName}@${latest}`, body: description, base: 'master', head: branch }); }, findPR: async ({ moduleName, latest }, branch) => { return requestWithAuth('GET /search/issues', { q: `${moduleName} type:pr ${moduleName}@${latest} in:title repo:atom/atom head:${branch}` }); }, findOpenPRs: async () => { return requestWithAuth('GET /search/issues', { q: 'type:pr repo:atom/atom state:open label:"depency ⬆️"' }); }, checkCIstatus: async ({ ref }) => { return requestWithAuth('GET /repos/:owner/:repo/commits/:ref/status', { ref }); }, mergePR: async ({ ref }) => { return requestWithAuth('POST /repos/{owner}/{repo}/merges', { base: 'master', head: ref }); }, addLabel: async pullRequestNumber => { return requestWithAuth('PATCH /repos/:owner/:repo/issues/:issue_number', { labels: ['depency ⬆️'], issue_number: pullRequestNumber }); } }; ================================================ FILE: script/lib/update-dependency/spec/fetch-outdated-dependencies-spec.js ================================================ const path = require('path'); const fetchOutdatedDependencies = require('../fetch-outdated-dependencies'); const { nativeDependencies } = require('./helpers'); const repositoryRootPath = path.resolve('.', 'fixtures', 'dummy'); const packageJSON = require(path.join(repositoryRootPath, 'package.json')); describe('Fetch outdated dependencies', function() { it('should fetch outdated native dependencies', async () => { spyOn(fetchOutdatedDependencies, 'npm').andReturn( Promise.resolve(nativeDependencies) ); expect(await fetchOutdatedDependencies.npm(repositoryRootPath)).toEqual( nativeDependencies ); }); it('should fetch outdated core dependencies', async () => { spyOn(fetchOutdatedDependencies, 'apm').andReturn( Promise.resolve(nativeDependencies) ); expect(await fetchOutdatedDependencies.apm(packageJSON)).toEqual( nativeDependencies ); }); }); ================================================ FILE: script/lib/update-dependency/spec/fixtures/create-pr-response.json ================================================ { "url": "https://api.github.com/repos/atom/octocat/pulls/1347", "id": 1, "node_id": "MDExOlB1bGxSZXF1ZXN0MQ==", "html_url": "https://github.com/atom/octocat/pull/1347", "diff_url": "https://github.com/atom/octocat/pull/1347.diff", "patch_url": "https://github.com/atom/octocat/pull/1347.patch", "issue_url": "https://api.github.com/repos/atom/octocat/issues/1347", "commits_url": "https://api.github.com/repos/atom/octocat/pulls/1347/commits", "review_comments_url": "https://api.github.com/repos/atom/octocat/pulls/1347/comments", "review_comment_url": "https://api.github.com/repos/atom/octocat/pulls/comments{/number}", "comments_url": "https://api.github.com/repos/atom/octocat/issues/1347/comments", "statuses_url": "https://api.github.com/repos/atom/octocat/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e", "number": 1347, "state": "open", "locked": true, "title": "⬆️ octocat@2.0.0", "user": { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, "body": "Bumps octocat from 1.0.0 to 2.0.0", "labels": [ { "id": 208045946, "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", "url": "https://api.github.com/repos/atom/octocat/labels/bug", "name": "bug", "description": "Something isn't working", "color": "f29513", "default": true } ], "milestone": { "url": "https://api.github.com/repos/atom/octocat/milestones/1", "html_url": "https://github.com/atom/octocat/milestones/v1.0", "labels_url": "https://api.github.com/repos/atom/octocat/milestones/1/labels", "id": 1002604, "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", "number": 1, "state": "open", "title": "v1.0", "description": "Tracking milestone for version 1.0", "creator": { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, "open_issues": 4, "closed_issues": 8, "created_at": "2011-04-10T20:09:31Z", "updated_at": "2014-03-03T18:58:10Z", "closed_at": "2013-02-12T13:22:01Z", "due_on": "2012-10-09T23:39:01Z" }, "active_lock_reason": "too heated", "created_at": "2011-01-26T19:01:12Z", "updated_at": "2011-01-26T19:01:12Z", "closed_at": "2011-01-26T19:01:12Z", "merged_at": "2011-01-26T19:01:12Z", "merge_commit_sha": "e5bd3914e2e596debea16f433f57875b5b90bcd6", "assignee": { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, "assignees": [ { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, { "login": "hubot", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/hubot_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/hubot", "html_url": "https://github.com/hubot", "followers_url": "https://api.github.com/users/hubot/followers", "following_url": "https://api.github.com/users/hubot/following{/other_user}", "gists_url": "https://api.github.com/users/hubot/gists{/gist_id}", "starred_url": "https://api.github.com/users/hubot/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/hubot/subscriptions", "organizations_url": "https://api.github.com/users/hubot/orgs", "repos_url": "https://api.github.com/users/hubot/repos", "events_url": "https://api.github.com/users/hubot/events{/privacy}", "received_events_url": "https://api.github.com/users/hubot/received_events", "type": "User", "site_admin": true } ], "requested_reviewers": [ { "login": "other_user", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/other_user_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/other_user", "html_url": "https://github.com/other_user", "followers_url": "https://api.github.com/users/other_user/followers", "following_url": "https://api.github.com/users/other_user/following{/other_user}", "gists_url": "https://api.github.com/users/other_user/gists{/gist_id}", "starred_url": "https://api.github.com/users/other_user/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/other_user/subscriptions", "organizations_url": "https://api.github.com/users/other_user/orgs", "repos_url": "https://api.github.com/users/other_user/repos", "events_url": "https://api.github.com/users/other_user/events{/privacy}", "received_events_url": "https://api.github.com/users/other_user/received_events", "type": "User", "site_admin": false } ], "requested_teams": [ { "id": 1, "node_id": "MDQ6VGVhbTE=", "url": "https://api.github.com/teams/1", "html_url": "https://api.github.com/teams/justice-league", "name": "Justice League", "slug": "justice-league", "description": "A great team.", "privacy": "closed", "permission": "admin", "members_url": "https://api.github.com/teams/1/members{/member}", "repositories_url": "https://api.github.com/teams/1/repos", "parent": null } ], "head": { "label": "atom:octocat-2.0.0", "ref": "octocat-2.0.0", "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", "user": { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, "repo": { "id": 1296269, "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", "name": "Hello-World", "full_name": "atom/octocat", "owner": { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, "private": false, "html_url": "https://github.com/atom/octocat", "description": "This your first repo!", "fork": false, "url": "https://api.github.com/repos/atom/octocat", "archive_url": "http://api.github.com/repos/atom/octocat/{archive_format}{/ref}", "assignees_url": "http://api.github.com/repos/atom/octocat/assignees{/user}", "blobs_url": "http://api.github.com/repos/atom/octocat/git/blobs{/sha}", "branches_url": "http://api.github.com/repos/atom/octocat/branches{/branch}", "collaborators_url": "http://api.github.com/repos/atom/octocat/collaborators{/collaborator}", "comments_url": "http://api.github.com/repos/atom/octocat/comments{/number}", "commits_url": "http://api.github.com/repos/atom/octocat/commits{/sha}", "compare_url": "http://api.github.com/repos/atom/octocat/compare/{base}...{head}", "contents_url": "http://api.github.com/repos/atom/octocat/contents/{+path}", "contributors_url": "http://api.github.com/repos/atom/octocat/contributors", "deployments_url": "http://api.github.com/repos/atom/octocat/deployments", "downloads_url": "http://api.github.com/repos/atom/octocat/downloads", "events_url": "http://api.github.com/repos/atom/octocat/events", "forks_url": "http://api.github.com/repos/atom/octocat/forks", "git_commits_url": "http://api.github.com/repos/atom/octocat/git/commits{/sha}", "git_refs_url": "http://api.github.com/repos/atom/octocat/git/refs{/sha}", "git_tags_url": "http://api.github.com/repos/atom/octocat/git/tags{/sha}", "git_url": "git:github.com/atom/octocat.git", "issue_comment_url": "http://api.github.com/repos/atom/octocat/issues/comments{/number}", "issue_events_url": "http://api.github.com/repos/atom/octocat/issues/events{/number}", "issues_url": "http://api.github.com/repos/atom/octocat/issues{/number}", "keys_url": "http://api.github.com/repos/atom/octocat/keys{/key_id}", "labels_url": "http://api.github.com/repos/atom/octocat/labels{/name}", "languages_url": "http://api.github.com/repos/atom/octocat/languages", "merges_url": "http://api.github.com/repos/atom/octocat/merges", "milestones_url": "http://api.github.com/repos/atom/octocat/milestones{/number}", "notifications_url": "http://api.github.com/repos/atom/octocat/notifications{?since,all,participating}", "pulls_url": "http://api.github.com/repos/atom/octocat/pulls{/number}", "releases_url": "http://api.github.com/repos/atom/octocat/releases{/id}", "ssh_url": "git@github.com:atom/octocat.git", "stargazers_url": "http://api.github.com/repos/atom/octocat/stargazers", "statuses_url": "http://api.github.com/repos/atom/octocat/statuses/{sha}", "subscribers_url": "http://api.github.com/repos/atom/octocat/subscribers", "subscription_url": "http://api.github.com/repos/atom/octocat/subscription", "tags_url": "http://api.github.com/repos/atom/octocat/tags", "teams_url": "http://api.github.com/repos/atom/octocat/teams", "trees_url": "http://api.github.com/repos/atom/octocat/git/trees{/sha}", "clone_url": "https://github.com/atom/octocat.git", "mirror_url": "git:git.example.com/atom/octocat", "hooks_url": "http://api.github.com/repos/atom/octocat/hooks", "svn_url": "https://svn.github.com/atom/octocat", "homepage": "https://github.com", "language": null, "forks_count": 9, "stargazers_count": 80, "watchers_count": 80, "size": 108, "default_branch": "master", "open_issues_count": 0, "is_template": true, "topics": [ "octocat", "atom", "electron", "api" ], "has_issues": true, "has_projects": true, "has_wiki": true, "has_pages": false, "has_downloads": true, "archived": false, "disabled": false, "visibility": "public", "pushed_at": "2011-01-26T19:06:43Z", "created_at": "2011-01-26T19:01:12Z", "updated_at": "2011-01-26T19:14:43Z", "permissions": { "admin": false, "push": false, "pull": true }, "allow_rebase_merge": true, "template_repository": null, "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", "allow_squash_merge": true, "delete_branch_on_merge": true, "allow_merge_commit": true, "subscribers_count": 42, "network_count": 0 } }, "base": { "label": "octocat:master", "ref": "master", "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", "user": { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, "repo": { "id": 1296269, "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", "name": "Hello-World", "full_name": "atom/octocat", "owner": { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, "private": false, "html_url": "https://github.com/atom/octocat", "description": "This your first repo!", "fork": false, "url": "https://api.github.com/repos/atom/octocat", "archive_url": "http://api.github.com/repos/atom/octocat/{archive_format}{/ref}", "assignees_url": "http://api.github.com/repos/atom/octocat/assignees{/user}", "blobs_url": "http://api.github.com/repos/atom/octocat/git/blobs{/sha}", "branches_url": "http://api.github.com/repos/atom/octocat/branches{/branch}", "collaborators_url": "http://api.github.com/repos/atom/octocat/collaborators{/collaborator}", "comments_url": "http://api.github.com/repos/atom/octocat/comments{/number}", "commits_url": "http://api.github.com/repos/atom/octocat/commits{/sha}", "compare_url": "http://api.github.com/repos/atom/octocat/compare/{base}...{head}", "contents_url": "http://api.github.com/repos/atom/octocat/contents/{+path}", "contributors_url": "http://api.github.com/repos/atom/octocat/contributors", "deployments_url": "http://api.github.com/repos/atom/octocat/deployments", "downloads_url": "http://api.github.com/repos/atom/octocat/downloads", "events_url": "http://api.github.com/repos/atom/octocat/events", "forks_url": "http://api.github.com/repos/atom/octocat/forks", "git_commits_url": "http://api.github.com/repos/atom/octocat/git/commits{/sha}", "git_refs_url": "http://api.github.com/repos/atom/octocat/git/refs{/sha}", "git_tags_url": "http://api.github.com/repos/atom/octocat/git/tags{/sha}", "git_url": "git:github.com/atom/octocat.git", "issue_comment_url": "http://api.github.com/repos/atom/octocat/issues/comments{/number}", "issue_events_url": "http://api.github.com/repos/atom/octocat/issues/events{/number}", "issues_url": "http://api.github.com/repos/atom/octocat/issues{/number}", "keys_url": "http://api.github.com/repos/atom/octocat/keys{/key_id}", "labels_url": "http://api.github.com/repos/atom/octocat/labels{/name}", "languages_url": "http://api.github.com/repos/atom/octocat/languages", "merges_url": "http://api.github.com/repos/atom/octocat/merges", "milestones_url": "http://api.github.com/repos/atom/octocat/milestones{/number}", "notifications_url": "http://api.github.com/repos/atom/octocat/notifications{?since,all,participating}", "pulls_url": "http://api.github.com/repos/atom/octocat/pulls{/number}", "releases_url": "http://api.github.com/repos/atom/octocat/releases{/id}", "ssh_url": "git@github.com:atom/octocat.git", "stargazers_url": "http://api.github.com/repos/atom/octocat/stargazers", "statuses_url": "http://api.github.com/repos/atom/octocat/statuses/{sha}", "subscribers_url": "http://api.github.com/repos/atom/octocat/subscribers", "subscription_url": "http://api.github.com/repos/atom/octocat/subscription", "tags_url": "http://api.github.com/repos/atom/octocat/tags", "teams_url": "http://api.github.com/repos/atom/octocat/teams", "trees_url": "http://api.github.com/repos/atom/octocat/git/trees{/sha}", "clone_url": "https://github.com/atom/octocat.git", "mirror_url": "git:git.example.com/atom/octocat", "hooks_url": "http://api.github.com/repos/atom/octocat/hooks", "svn_url": "https://svn.github.com/atom/octocat", "homepage": "https://github.com", "language": null, "forks_count": 9, "stargazers_count": 80, "watchers_count": 80, "size": 108, "default_branch": "master", "open_issues_count": 0, "is_template": true, "topics": [ "octocat", "atom", "electron", "api" ], "has_issues": true, "has_projects": true, "has_wiki": true, "has_pages": false, "has_downloads": true, "archived": false, "disabled": false, "visibility": "public", "pushed_at": "2011-01-26T19:06:43Z", "created_at": "2011-01-26T19:01:12Z", "updated_at": "2011-01-26T19:14:43Z", "permissions": { "admin": false, "push": false, "pull": true }, "allow_rebase_merge": true, "template_repository": null, "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", "allow_squash_merge": true, "delete_branch_on_merge": true, "allow_merge_commit": true, "subscribers_count": 42, "network_count": 0 } }, "_links": { "self": { "href": "https://api.github.com/repos/atom/octocat/pulls/1347" }, "html": { "href": "https://github.com/atom/octocat/pull/1347" }, "issue": { "href": "https://api.github.com/repos/atom/octocat/issues/1347" }, "comments": { "href": "https://api.github.com/repos/atom/octocat/issues/1347/comments" }, "review_comments": { "href": "https://api.github.com/repos/atom/octocat/pulls/1347/comments" }, "review_comment": { "href": "https://api.github.com/repos/atom/octocat/pulls/comments{/number}" }, "commits": { "href": "https://api.github.com/repos/atom/octocat/pulls/1347/commits" }, "statuses": { "href": "https://api.github.com/repos/atom/octocat/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e" } }, "author_association": "OWNER", "draft": false, "merged": false, "mergeable": true, "rebaseable": true, "mergeable_state": "clean", "merged_by": { "login": "octocat", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", "gravatar_id": "", "url": "https://api.github.com/users/octocat", "html_url": "https://github.com/octocat", "followers_url": "https://api.github.com/users/octocat/followers", "following_url": "https://api.github.com/users/octocat/following{/other_user}", "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", "organizations_url": "https://api.github.com/users/octocat/orgs", "repos_url": "https://api.github.com/users/octocat/repos", "events_url": "https://api.github.com/users/octocat/events{/privacy}", "received_events_url": "https://api.github.com/users/octocat/received_events", "type": "User", "site_admin": false }, "comments": 10, "review_comments": 0, "maintainer_can_modify": true, "commits": 3, "additions": 100, "deletions": 3, "changed_files": 5 } ================================================ FILE: script/lib/update-dependency/spec/fixtures/latest-package.json ================================================ { "name": "test", "version": "1.0.0", "description": "just test", "main": "index.js", "dependencies": { "spell-check": "https://www.atom.io/api/packages/spell-check/versions/0.79.1/tarball", "status-bar": "https://www.atom.io/api/packages/status-bar/versions/2.8.17/tarball", "styleguide": "https://www.atom.io/api/packages/styleguide/versions/1.49.12/tarball", "symbols-view": "https://www.atom.io/api/packages/symbols-view/versions/0.118.5/tarball", "@atom/watcher": "1.3.1", "clear-cut": "^2.0.3", "dedent": "^1.0.0", "devtron": "1.2.6" }, "packageDependencies": { "spell-check": "0.79.1", "status-bar": "2.8.17", "styleguide": "1.49.12", "symbols-view": "0.118.5" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "darangi", "license": "ISC" } ================================================ FILE: script/lib/update-dependency/spec/fixtures/search-response.json ================================================ { "total_count": 40, "incomplete_results": false, "items": [ { "id": 3081286, "node_id": "MDEwOlJlcG9zaXRvcnkzMDgxMjg2", "name": "Tetris", "full_name": "dtrupenn/Tetris", "owner": { "login": "dtrupenn", "id": 872147, "node_id": "MDQ6VXNlcjg3MjE0Nw==", "avatar_url": "https://secure.gravatar.com/avatar/e7956084e75f239de85d3a31bc172ace?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png", "gravatar_id": "", "url": "https://api.github.com/users/dtrupenn", "received_events_url": "https://api.github.com/users/dtrupenn/received_events", "type": "User" }, "private": false, "html_url": "https://github.com/dtrupenn/Tetris", "description": "A C implementation of Tetris using Pennsim through LC4", "fork": false, "url": "https://api.github.com/repos/dtrupenn/Tetris", "created_at": "2012-01-01T00:31:50Z", "updated_at": "2013-01-05T17:58:47Z", "pushed_at": "2012-01-01T00:37:02Z", "homepage": "", "size": 524, "stargazers_count": 1, "watchers_count": 1, "language": "Assembly", "forks_count": 0, "open_issues_count": 0, "master_branch": "master", "default_branch": "master", "score": 1.0 } ] } ================================================ FILE: script/lib/update-dependency/spec/git-spec.js ================================================ const path = require('path'); const simpleGit = require('simple-git'); const repositoryRootPath = path.resolve('.', 'fixtures', 'dummy'); const git = simpleGit(repositoryRootPath); const { switchToCleanBranch, makeBranch, publishBranch, createCommit, deleteBranch } = require('../git')(git, repositoryRootPath); describe('GIT', () => { async function findBranch(branch) { const { branches } = await git.branch(); return Object.keys(branches).find(_branch => _branch.indexOf(branch) > -1); } const dependency = { moduleName: 'atom', latest: '2.0.0' }; const branch = `${dependency.moduleName}-${dependency.latest}`; beforeEach(async () => { await git.checkout('clean-branch'); }); it('remotes should include ATOM', async () => { const remotes = await git.getRemotes(); expect(remotes.map(({ name }) => name).includes('ATOM')).toBeTruthy(); }); it('current branch should be clean-branch', async () => { const testBranchExists = await findBranch('test'); testBranchExists ? await git.checkout('test') : await git.checkoutLocalBranch('test'); expect((await git.branch()).current).toBe('test'); await switchToCleanBranch(); expect((await git.branch()).current).toBe('clean-branch'); await git.deleteLocalBranch('test', true); }); it('should make new branch and checkout to the new branch', async () => { const { found, newBranch } = await makeBranch(dependency); expect(found).toBe(undefined); expect(newBranch).toBe(branch); expect((await git.branch()).current).toBe(branch); await git.checkout('clean-branch'); await git.deleteLocalBranch(branch, true); }); it('should find an existing branch and checkout to the branch', async () => { await git.checkoutLocalBranch(branch); const { found } = await makeBranch(dependency); expect(found).not.toBe(undefined); expect((await git.branch()).current).toBe(found); await git.checkout('clean-branch'); await git.deleteLocalBranch(branch, true); }); it('should create a commit', async () => { const packageJsonFilePath = path.join(repositoryRootPath, 'package.json'); const packageLockFilePath = path.join( repositoryRootPath, 'package-lock.json' ); spyOn(git, 'commit'); spyOn(git, 'add'); await createCommit(dependency); expect(git.add).toHaveBeenCalledWith([ packageJsonFilePath, packageLockFilePath ]); expect(git.commit).toHaveBeenCalledWith( `${`:arrow_up: ${dependency.moduleName}@${dependency.latest}`}` ); }); it('should publish branch', async () => { spyOn(git, 'push'); await publishBranch(branch); expect(git.push).toHaveBeenCalledWith('ATOM', branch); }); it('should delete an existing branch', async () => { await git.checkoutLocalBranch(branch); await git.checkout('clean-branch'); expect(await findBranch(branch)).not.toBe(undefined); await deleteBranch(branch); expect(await findBranch(branch)).toBe(undefined); }); }); ================================================ FILE: script/lib/update-dependency/spec/helpers.js ================================================ const latestPackageJSON = require('./fixtures/latest-package.json'); const packageJSON = require('./fixtures/dummy/package.json'); module.exports = { coreDependencies: Object.keys(packageJSON.packageDependencies).map( dependency => { return { latest: latestPackageJSON.packageDependencies[dependency], installed: packageJSON.packageDependencies[dependency], moduleName: dependency, isCorePackage: true }; } ), nativeDependencies: Object.keys(packageJSON.dependencies) .filter( dependency => !packageJSON.dependencies[dependency].match(new RegExp('^https?://')) ) .map(dependency => { return { latest: latestPackageJSON.dependencies[dependency], packageJson: packageJSON.dependencies[dependency], installed: packageJSON.dependencies[dependency], moduleName: dependency, isCorePackage: false }; }) }; ================================================ FILE: script/lib/update-dependency/spec/pull-request-spec.js ================================================ const nock = require('nock'); const { createPR, findPR } = require('../pull-request'); const createPrResponse = require('./fixtures/create-pr-response.json'); const searchResponse = require('./fixtures/search-response.json'); describe('Pull Request', () => { it('Should create a pull request', async () => { const scope = nock('https://api.github.com') .post('/repos/atom/atom/pulls', { title: '⬆️ octocat@2.0.0', body: 'Bumps octocat from 1.0.0 to 2.0.0', head: 'octocat-2.0.0', base: 'master' }) .reply(200, createPrResponse); const response = await createPR( { moduleName: 'octocat', installed: '1.0.0', latest: '2.0.0', isCorePackage: false }, 'octocat-2.0.0' ); scope.done(); expect(response.data).toEqual(createPrResponse); }); it('Should search for a pull request', async () => { const scope = nock('https://api.github.com') .get('/search/issues') .query({ q: 'octocat type:pr octocat@2.0.0 in:title repo:atom/atom head:octocat-2.0.0 state:open', owner: 'atom', repo: 'atom' }) .reply(200, searchResponse); const response = await findPR( { moduleName: 'octocat', installed: '1.0.0', latest: '2.0.0' }, 'octocat-2.0.0' ); scope.done(); expect(response.data).toEqual(searchResponse); }); }); ================================================ FILE: script/lib/update-dependency/spec/util-spec.js ================================================ const path = require('path'); const fs = require('fs'); const repositoryRootPath = path.resolve('.', 'fixtures', 'dummy'); const packageJsonFilePath = path.join(repositoryRootPath, 'package.json'); const { updatePackageJson } = require('../util')(repositoryRootPath); const { coreDependencies, nativeDependencies } = require('./helpers'); describe('Update-dependency', function() { const oldPackageJson = JSON.parse( JSON.stringify(require(packageJsonFilePath)) ); var packageJson; it('bumps package.json properly', async function() { const dependencies = [...coreDependencies, ...nativeDependencies]; for (const dependency of dependencies) { await updatePackageJson(dependency); packageJson = JSON.parse(fs.readFileSync(packageJsonFilePath, 'utf-8')); if (dependency.isCorePackage) { expect(packageJson.packageDependencies[dependency.moduleName]).toBe( dependency.latest ); expect(packageJson.dependencies[dependency.moduleName]).toContain( dependency.latest ); } else { expect(packageJson.dependencies[dependency.moduleName]).toBe( dependency.latest ); } } fs.writeFileSync( packageJsonFilePath, JSON.stringify(oldPackageJson, null, 2) ); }); }); ================================================ FILE: script/lib/update-dependency/util.js ================================================ const fs = require('fs'); const path = require('path'); const util = repositoryRootPath => { const packageJsonFilePath = path.join(repositoryRootPath, 'package.json'); const packageJSON = require(packageJsonFilePath); return { updatePackageJson: async function({ moduleName, installed, latest, isCorePackage = false, packageJson = '' }) { console.log(`Bumping ${moduleName} from ${installed} to ${latest}`); const updatePackageJson = JSON.parse(JSON.stringify(packageJSON)); if (updatePackageJson.dependencies[moduleName]) { let searchString = installed; // gets the exact version installed in package json for native packages if (!isCorePackage) { if (/\^|~/.test(packageJson)) { searchString = new RegExp(`\\${packageJson}`); } else { searchString = packageJson; } } updatePackageJson.dependencies[ moduleName ] = updatePackageJson.dependencies[moduleName].replace( searchString, latest ); } if (updatePackageJson.packageDependencies[moduleName]) { updatePackageJson.packageDependencies[ moduleName ] = updatePackageJson.packageDependencies[moduleName].replace( new RegExp(installed), latest ); } return new Promise((resolve, reject) => { fs.writeFile( packageJsonFilePath, JSON.stringify(updatePackageJson, null, 2), function(err) { if (err) { return reject(err); } console.log(`Bumped ${moduleName} from ${installed} to ${latest}`); return resolve(); } ); }); }, sleep: ms => new Promise(resolve => setTimeout(resolve, ms)) }; }; module.exports = util; ================================================ FILE: script/lib/verify-machine-requirements.js ================================================ 'use strict'; const childProcess = require('child_process'); const path = require('path'); module.exports = function(ci) { verifyNode(); verifyPython(); }; function verifyNode() { const fullVersion = process.versions.node; const majorVersion = fullVersion.split('.')[0]; const minorVersion = fullVersion.split('.')[1]; if (majorVersion >= 11 || (majorVersion === '10' && minorVersion >= 12)) { console.log(`Node:\tv${fullVersion}`); } else { throw new Error( `node v10.12+ is required to build Atom. node v${fullVersion} is installed.` ); } } function verifyPython() { // This function essentially re-implements node-gyp's "find-python.js" library, // but in a synchronous, bootstrap-script-friendly way. // It is based off of the logic of the file from node-gyp v5.x: // https://github.com/nodejs/node-gyp/blob/v5.1.1/lib/find-python.js // This node-gyp is the version in use by current npm (in mid 2020). // // TODO: If this repo ships a newer version of node-gyp (v6.x or later), please update this script. // (Currently, the build scripts and apm each depend on npm v6.14, which depends on node-gyp v5.) // Differences between major versions of node-gyp: // node-gyp 5.x looks for python, then python2, then python3. // node-gyp 6.x looks for python3, then python, then python2.) // node-gyp 5.x accepts Python ^2.6 || >= 3.5, node-gyp 6+ only accepts Python == 2.7 || >= 3.5. // node-gyp 7.x stopped using the "-2" flag for "py.exe", // so as to allow finding Python 3 as well, not just Python 2. // https://github.com/nodejs/node-gyp/blob/master/CHANGELOG.md#v700-2020-06-03 let stdout; let fullVersion; let usablePythonWasFound; let triedLog = ''; let binaryPlusFlag; function verifyBinary(binary, prependFlag) { if (binary && !usablePythonWasFound) { // clear re-used "result" variables now that we're checking another python binary. stdout = ''; fullVersion = ''; let allFlags = [ '-c', 'import platform\nprint(platform.python_version())' ]; if (prependFlag) { // prependFlag is an optional argument, // used to prepend "-2" for the "py.exe" launcher. // // TODO: Refactor this script by eliminating "prependFlag" // once we update to node-gyp v7.x or newer; // the "-2" flag is not used in node-gyp v7.x. allFlags.unshift(prependFlag); } try { stdout = childProcess.execFileSync(binary, allFlags, { env: process.env, stdio: ['ignore', 'pipe', 'ignore'] }); } catch (e) {} if (stdout) { if (stdout.indexOf('+') !== -1) stdout = stdout.toString().replace(/\+/g, ''); if (stdout.indexOf('rc') !== -1) stdout = stdout.toString().replace(/rc(.*)$/gi, ''); fullVersion = stdout.toString().trim(); } if (fullVersion) { let versionComponents = fullVersion.split('.'); let majorVersion = Number(versionComponents[0]); let minorVersion = Number(versionComponents[1]); if ( (majorVersion === 2 && minorVersion >= 6) || (majorVersion === 3 && minorVersion >= 5) ) { usablePythonWasFound = true; } } // Prepare to log which commands were tried, and the results, in case no usable Python can be found. if (prependFlag) { binaryPlusFlag = binary + ' ' + prependFlag; } else { binaryPlusFlag = binary; } triedLog = triedLog.concat( `log message: tried to check version of "${binaryPlusFlag}", got: "${fullVersion}"\n` ); } } function verifyForcedBinary(binary) { if (typeof binary !== 'undefined' && binary.length > 0) { verifyBinary(binary); if (!usablePythonWasFound) { throw new Error( `NODE_GYP_FORCE_PYTHON is set to: "${binary}", but this is not a valid Python.\n` + 'Please set NODE_GYP_FORCE_PYTHON to something valid, or unset it entirely.\n' + '(Python 2.6, 2.7 or 3.5+ is required to build Atom.)\n' ); } } } // These first two checks do nothing if the relevant // environment variables aren't set. verifyForcedBinary(process.env.NODE_GYP_FORCE_PYTHON); // All the following checks will no-op if a previous check has succeeded. verifyBinary(process.env.PYTHON); verifyBinary('python'); verifyBinary('python2'); verifyBinary('python3'); if (process.platform === 'win32') { verifyBinary('py.exe', '-2'); verifyBinary( path.join(process.env.SystemDrive || 'C:', 'Python27', 'python.exe') ); verifyBinary( path.join(process.env.SystemDrive || 'C:', 'Python37', 'python.exe') ); } if (usablePythonWasFound) { console.log(`Python:\tv${fullVersion}`); } else { throw new Error( `\n${triedLog}\n` + 'Python 2.6, 2.7 or 3.5+ is required to build Atom.\n' + 'verify-machine-requirements.js was unable to find such a version of Python.\n' + "Set the PYTHON env var to e.g. 'C:/path/to/Python27/python.exe'\n" + 'if your Python is installed in a non-default location.\n' ); } } ================================================ FILE: script/license-overrides.js ================================================ module.exports = { 'aws-sign@0.3.0': { repository: 'https://github.com/mikeal/aws-sign', license: 'MIT', source: 'index.js', sourceText: '/*!\n * knox - auth\n * Copyright(c) 2010 LearnBoost \n * MIT Licensed\n */\n' }, 'bufferjs@2.0.0': { repository: 'https://github.com/coolaj86/node-bufferjs', license: 'MIT', source: 'LICENSE.MIT', sourceText: "Copyright (c) 2010 AJ ONeal (and Contributors)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." }, 'buffers@0.1.1': { repository: 'http://github.com/substack/node-buffers', license: 'MIT', source: 'README.markdown', sourceText: '\nlicense\n=======\n\nMIT/X11' }, 'cheerio@0.15.0': { repository: 'https://github.com/cheeriojs/cheerio', license: 'MIT', source: 'https://github.com/cheeriojs/cheerio/blob/master/package.json' }, 'specificity@0.1.3': { repository: 'https://github.com/keeganstreet/specificity', license: 'MIT', source: 'package.json in repository' }, 'promzard@0.2.0': { license: 'ISC', source: 'LICENSE in the repository', sourceText: "The ISC License\n\nCopyright (c) Isaac Z. Schlueter\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED 'AS IS' AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR\nIN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE." }, 'jschardet@1.1.1': { license: 'LGPL', source: 'README.md in the repository', sourceText: "JsChardet\n=========\n\nPort of python's chardet (http://chardet.feedparser.org/).\n\nLicense\n-------\n\nLGPL" }, 'core-js@0.4.10': { license: 'MIT', source: 'http://rock.mit-license.org linked in source files and bower.json says MIT' }, 'log-driver@1.2.4': { license: 'ISC', source: 'LICENSE file in the repository', sourceText: "Copyright (c) 2014, Gregg Caines, gregg@caines.ca\n\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED 'AS IS' AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE." }, 'shelljs@0.3.0': { license: 'BSD', source: 'LICENSE file in repository - 3-clause BSD (aka BSD-new)', sourceText: "Copyright (c) 2012, Artur Adib \nAll rights reserved.\n\nYou may use this project under the terms of the New BSD license as follows:\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n * Neither the name of Artur Adib nor the\n names of the contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL ARTUR ADIB BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\nTHIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." }, 'json-schema@0.2.2': { repository: 'https://github.com/kriszyp/json-schema', license: 'BSD', source: 'README links to https://github.com/dojo/dojo/blob/8b6a5e4c42f9cf777dd39eaae8b188e0ebb59a4c/LICENSE', sourceText: "Dojo is available under *either* the terms of the modified BSD license *or* the\nAcademic Free License version 2.1. As a recipient of Dojo, you may choose which\nlicense to receive this code under (except as noted in per-module LICENSE\nfiles). Some modules may not be the copyright of the Dojo Foundation. These\nmodules contain explicit declarations of copyright in both the LICENSE files in\nthe directories in which they reside and in the code itself. No external\ncontributions are allowed under licenses which are fundamentally incompatible\nwith the AFL or BSD licenses that Dojo is distributed under.\n\nThe text of the AFL and BSD licenses is reproduced below.\n\n-------------------------------------------------------------------------------\nThe 'New' BSD License:\n**********************\n\nCopyright (c) 2005-2015, The Dojo Foundation\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n * Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n * Neither the name of the Dojo Foundation nor the names of its contributors\n may be used to endorse or promote products derived from this software\n without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n-------------------------------------------------------------------------------\nThe Academic Free License, v. 2.1:\n**********************************\n\nThis Academic Free License (the 'License') applies to any original work of\nauthorship (the 'Original Work') whose owner (the 'Licensor') has placed the\nfollowing notice immediately following the copyright notice for the Original\nWork:\n\nLicensed under the Academic Free License version 2.1\n\n1) Grant of Copyright License. Licensor hereby grants You a world-wide,\nroyalty-free, non-exclusive, perpetual, sublicenseable license to do the\nfollowing:\n\na) to reproduce the Original Work in copies;\n\nb) to prepare derivative works ('Derivative Works') based upon the Original\nWork;\n\nc) to distribute copies of the Original Work and Derivative Works to the\npublic;\n\nd) to perform the Original Work publicly; and\n\ne) to display the Original Work publicly.\n\n2) Grant of Patent License. Licensor hereby grants You a world-wide,\nroyalty-free, non-exclusive, perpetual, sublicenseable license, under patent\nclaims owned or controlled by the Licensor that are embodied in the Original\nWork as furnished by the Licensor, to make, use, sell and offer for sale the\nOriginal Work and Derivative Works.\n\n3) Grant of Source Code License. The term 'Source Code' means the preferred\nform of the Original Work for making modifications to it and all available\ndocumentation describing how to modify the Original Work. Licensor hereby\nagrees to provide a machine-readable copy of the Source Code of the Original\nWork along with each copy of the Original Work that Licensor distributes.\nLicensor reserves the right to satisfy this obligation by placing a\nmachine-readable copy of the Source Code in an information repository\nreasonably calculated to permit inexpensive and convenient access by You for as\nlong as Licensor continues to distribute the Original Work, and by publishing\nthe address of that information repository in a notice immediately following\nthe copyright notice that applies to the Original Work.\n\n4) Exclusions From License Grant. Neither the names of Licensor, nor the names\nof any contributors to the Original Work, nor any of their trademarks or\nservice marks, may be used to endorse or promote products derived from this\nOriginal Work without express prior written permission of the Licensor. Nothing\nin this License shall be deemed to grant any rights to trademarks, copyrights,\npatents, trade secrets or any other intellectual property of Licensor except as\nexpressly stated herein. No patent license is granted to make, use, sell or\noffer to sell embodiments of any patent claims other than the licensed claims\ndefined in Section 2. No right is granted to the trademarks of Licensor even if\nsuch marks are included in the Original Work. Nothing in this License shall be\ninterpreted to prohibit Licensor from licensing under different terms from this\nLicense any Original Work that Licensor otherwise would have a right to\nlicense.\n\n5) This section intentionally omitted.\n\n6) Attribution Rights. You must retain, in the Source Code of any Derivative\nWorks that You create, all copyright, patent or trademark notices from the\nSource Code of the Original Work, as well as any notices of licensing and any\ndescriptive text identified therein as an 'Attribution Notice.' You must cause\nthe Source Code for any Derivative Works that You create to carry a prominent\nAttribution Notice reasonably calculated to inform recipients that You have\nmodified the Original Work.\n\n7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that\nthe copyright in and to the Original Work and the patent rights granted herein\nby Licensor are owned by the Licensor or are sublicensed to You under the terms\nof this License with the permission of the contributor(s) of those copyrights\nand patent rights. Except as expressly stated in the immediately proceeding\nsentence, the Original Work is provided under this License on an 'AS IS' BASIS\nand WITHOUT WARRANTY, either express or implied, including, without limitation,\nthe warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR\nPURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU.\nThis DISCLAIMER OF WARRANTY constitutes an essential part of this License. No\nlicense to Original Work is granted hereunder except under this disclaimer.\n\n8) Limitation of Liability. Under no circumstances and under no legal theory,\nwhether in tort (including negligence), contract, or otherwise, shall the\nLicensor be liable to any person for any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License\nor the use of the Original Work including, without limitation, damages for loss\nof goodwill, work stoppage, computer failure or malfunction, or any and all\nother commercial damages or losses. This limitation of liability shall not\napply to liability for death or personal injury resulting from Licensor's\nnegligence to the extent applicable law prohibits such limitation. Some\njurisdictions do not allow the exclusion or limitation of incidental or\nconsequential damages, so this exclusion and limitation may not apply to You.\n\n9) Acceptance and Termination. If You distribute copies of the Original Work or\na Derivative Work, You must make a reasonable effort under the circumstances to\nobtain the express assent of recipients to the terms of this License. Nothing\nelse but this License (or another written agreement between Licensor and You)\ngrants You permission to create Derivative Works based upon the Original Work\nor to exercise any of the rights granted in Section 1 herein, and any attempt\nto do so except under the terms of this License (or another written agreement\nbetween Licensor and You) is expressly prohibited by U.S. copyright law, the\nequivalent laws of other countries, and by international treaty. Therefore, by\nexercising any of the rights granted to You in Section 1 herein, You indicate\nYour acceptance of this License and all of its terms and conditions.\n\n10) Termination for Patent Action. This License shall terminate automatically\nand You may no longer exercise any of the rights granted to You by this License\nas of the date You commence an action, including a cross-claim or counterclaim,\nagainst Licensor or any licensee alleging that the Original Work infringes a\npatent. This termination provision shall not apply for an action alleging\npatent infringement by combinations of the Original Work with other software or\nhardware.\n\n11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this\nLicense may be brought only in the courts of a jurisdiction wherein the\nLicensor resides or in which Licensor conducts its primary business, and under\nthe laws of that jurisdiction excluding its conflict-of-law provisions. The\napplication of the United Nations Convention on Contracts for the International\nSale of Goods is expressly excluded. Any use of the Original Work outside the\nscope of this License or after its termination shall be subject to the\nrequirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et\nseq., the equivalent laws of other countries, and international treaty. This\nsection shall survive the termination of this License.\n\n12) Attorneys Fees. In any action to enforce the terms of this License or\nseeking damages relating thereto, the prevailing party shall be entitled to\nrecover its costs and expenses, including, without limitation, reasonable\nattorneys' fees and costs incurred in connection with such action, including\nany appeal of such action. This section shall survive the termination of this\nLicense.\n\n13) Miscellaneous. This License represents the complete agreement concerning\nthe subject matter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent necessary to\nmake it enforceable.\n\n14) Definition of 'You' in This License. 'You' throughout this License, whether\nin upper or lower case, means an individual or a legal entity exercising rights\nunder, and complying with all of the terms of, this License. For legal\nentities, 'You' includes any entity that controls, is controlled by, or is\nunder common control with you. For purposes of this definition, 'control' means\n(i) the power, direct or indirect, to cause the direction or management of such\nentity, whether by contract or otherwise, or (ii) ownership of fifty percent\n(50%) or more of the outstanding shares, or (iii) beneficial ownership of such\nentity.\n\n15) Right to Use. You may use the Original Work in all ways not otherwise\nrestricted or conditioned by this License or by law, and Licensor promises not\nto interfere with or be responsible for such uses by You.\n\nThis license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved.\nPermission is hereby granted to copy and distribute this license without\nmodification. This license may not be modified without the express written\npermission of its copyright owner." }, 'inherit@2.2.2': { license: 'MIT', repository: 'https://github.com/dfilatov/inherit', source: 'LICENSE.md', sourceText: "Copyright (c) 2012 Dmitry Filatov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." }, 'tweetnacl@0.14.3': { license: 'Public Domain', repository: 'https://github.com/dchest/tweetnacl', source: 'COPYING.txt', sourceText: 'Public Domain\n\nThe person who associated a work with this deed has dedicated the work to the\npublic domain by waiving all of his or her rights to the work worldwide under\ncopyright law, including all related and neighboring rights, to the extent\nallowed by law.\n\nYou can copy, modify, distribute and perform the work, even for commercial\npurposes, all without asking permission.' } }; ================================================ FILE: script/lint ================================================ #!/usr/bin/env node 'use strict' require('colors') const lintCoffeeScriptPaths = require('./lib/lint-coffee-script-paths') const lintJavaScriptPaths = require('./lib/lint-java-script-paths') const lintLessPaths = require('./lib/lint-less-paths') const path = require('path') const CONFIG = require('./config') process.on('unhandledRejection', function (e) { console.error(e.stack || e) process.exit(1) }) Promise.all([lintCoffeeScriptPaths(), lintJavaScriptPaths(), lintLessPaths()]) .then((lintResults) => { let hasLintErrors = false for (let errors of lintResults) { for (let error of errors) { hasLintErrors = true const relativePath = path.relative(CONFIG.repositoryRootPath, error.path) console.log(`${relativePath}:${error.lineNumber}`.yellow + ` ${error.message} (${error.rule})`.red) } } if (hasLintErrors) { process.exit(1) } else { console.log('No lint errors!'.green) process.exit(0) } }) ================================================ FILE: script/lint.cmd ================================================ @IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\lint" %* ) ELSE ( node "%~dp0\lint" %* ) ================================================ FILE: script/package.json ================================================ { "name": "atom-build-scripts", "description": "Atom build scripts", "dependencies": { "7zip-bin": "^4.0.2", "@atom/electron-winstaller": "0.0.1", "@octokit/request": "^5.4.5", "async": "^3.2.0", "babel-core": "5.8.38", "babel-eslint": "^10.0.1", "cheerio": "1.0.0-rc.2", "coffeelint": "1.15.7", "colors": "1.1.2", "donna": "1.0.16", "electron-chromedriver": "^11.0.0", "electron-link": "^0.6.0", "electron-mksnapshot": "^11.0.1", "electron-packager": "^15.0.0", "eslint": "^5.16.0", "eslint-config-prettier": "^4.2.0", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.17.2", "eslint-plugin-node": "^9.0.1", "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-promise": "^4.1.1", "eslint-plugin-standard": "^4.0.0", "fs-admin": "^0.12.0", "fs-extra": "9.0.1", "glob": "7.0.3", "joanna": "0.0.10", "klaw-sync": "^1.1.2", "legal-eagle": "0.14.0", "lodash.startcase": "4.4.0", "lodash.template": "4.5.0", "minidump": "^0.22.0", "mkdirp": "0.5.1", "nock": "^13.0.2", "node-fetch": "^3.1.1", "normalize-package-data": "2.3.5", "npm": "^6.14.16", "npm-check": "^5.9.2", "passwd-user": "2.1.0", "pegjs": "0.9.0", "prettier": "^1.17.0", "random-seed": "^0.3.0", "season": "5.3.0", "semver": "5.3.0", "simple-git": "^2.7.0", "stylelint": "^9.0.0", "stylelint-config-standard": "^18.1.0", "sync-request": "3.0.1", "tello": "1.2.0", "terser": "^3.8.1", "webdriverio": "^5.9.2", "yargs": "4.8.1" }, "scripts": { "postinstall": "node ./redownload-electron-bins.js" } } ================================================ FILE: script/postprocess-junit-results ================================================ #!/usr/bin/env node const yargs = require('yargs') const argv = yargs .usage('Usage: $0 [options]') .help('help') .option('search-folder', { string: true, demandOption: true, requiresArg: true, describe: 'Directory to search for JUnit XML results' }) .option('test-results-files', { string: true, demandOption: true, requiresArg: true, describe: 'Glob that matches JUnit XML files within searchFolder' }) .wrap(yargs.terminalWidth()) .argv const fs = require('fs') const path = require('path') const glob = require('glob') const cheerio = require('cheerio') function discoverTestFiles() { return new Promise((resolve, reject) => { glob(argv.testResultsFiles, {cwd: argv.searchFolder}, (err, paths) => { if (err) { reject(err) } else { resolve(paths) } }) }) } async function postProcessJUnitXML(junitXmlPath) { const fullPath = path.resolve(argv.searchFolder, junitXmlPath) const friendlyName = path.basename(junitXmlPath, '.xml').replace(/-+/g, ' ') console.log(`${fullPath}: loading`) const original = await new Promise((resolve, reject) => { fs.readFile(fullPath, {encoding: 'utf8'}, (err, content) => { if (err) { reject(err) } else { resolve(content) } }) }) const $ = cheerio.load(original, { xmlMode: true }) $('testcase').attr('name', (i, oldName) => `[${friendlyName}] ${oldName}`) const modified = $.xml() await new Promise((resolve, reject) => { fs.writeFile(fullPath, modified, {encoding: 'utf8'}, err => { if (err) { reject(err) } else { resolve() } }) }) console.log(`${fullPath}: complete`) } ;(async function() { const testResultFiles = await discoverTestFiles() console.log(`Post-processing ${testResultFiles.length} JUnit XML files`) await Promise.all( testResultFiles.map(postProcessJUnitXML) ) console.log(`${testResultFiles.length} JUnit XML files complete`) })().then( () => process.exit(0), err => { console.error(err.stack || err) process.exit(1) } ) ================================================ FILE: script/postprocess-junit-results.cmd ================================================ @IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\postprocess-junit-results" %* ) ELSE ( node "%~dp0\postprocess-junit-results" %* ) ================================================ FILE: script/redownload-electron-bins.js ================================================ const { spawn } = require('child_process'); const electronVersion = require('./config').appMetadata.electronVersion; if (process.env.ELECTRON_CUSTOM_VERSION !== electronVersion) { const electronEnv = process.env.ELECTRON_CUSTOM_VERSION; console.info( `env var ELECTRON_CUSTOM_VERSION is not set,\n` + `or doesn't match electronVersion in ../package.json.\n` + `(is: "${electronEnv}", wanted: "${electronVersion}").\n` + `Setting, and re-downloading chromedriver and mksnapshot.\n` ); process.env.ELECTRON_CUSTOM_VERSION = electronVersion; const downloadChromedriverPath = require.resolve( 'electron-chromedriver/download-chromedriver.js' ); const downloadMksnapshotPath = require.resolve( 'electron-mksnapshot/download-mksnapshot.js' ); const downloadChromedriver = spawn('node', [downloadChromedriverPath]); const downloadMksnapshot = spawn('node', [downloadMksnapshotPath]); var exitStatus; downloadChromedriver.on('close', code => { if (code === 0) { exitStatus = 'success'; } else { exitStatus = 'error'; } console.info( `info: Done re-downloading chromedriver. Status: ${exitStatus}` ); }); downloadMksnapshot.on('close', code => { if (code === 0) { exitStatus = 'success'; } else { exitStatus = 'error'; } console.info(`info: Done re-downloading mksnapshot. Status: ${exitStatus}`); }); } else { console.info( 'info: env var "ELECTRON_CUSTOM_VERSION" is already set correctly.\n(No need to re-download chromedriver or mksnapshot). Skipping.\n' ); } ================================================ FILE: script/test ================================================ #!/usr/bin/env node 'use strict'; require('colors'); const argv = require('yargs') .option('core-main', { describe: 'Run core main process tests', boolean: true, default: false }) .option('skip-main', { describe: 'Skip main process tests if they would otherwise run on your platform', boolean: true, default: false, conflicts: 'core-main' }) .option('core-renderer', { describe: 'Run core renderer process tests', boolean: true, default: false }) .option('core-benchmark', { describe: 'Run core benchmarks', boolean: true, default: false }) .option('package', { describe: 'Run bundled package specs', boolean: true, default: false }) .help().argv; const assert = require('assert'); const asyncSeries = require('async/series'); const childProcess = require('child_process'); const fs = require('fs-extra'); const glob = require('glob'); const path = require('path'); const temp = require('temp').track(); const CONFIG = require('./config'); const backupNodeModules = require('./lib/backup-node-modules'); const runApmInstall = require('./lib/run-apm-install'); function assertExecutablePaths(executablePaths) { assert( executablePaths.length !== 0, `No atom build found. Please run "script/build" and try again.` ); assert( executablePaths.length === 1, `More than one application to run tests against was found. ${executablePaths.join( ',' )}` ); } const resourcePath = CONFIG.repositoryRootPath; let executablePath; if (process.platform === 'darwin') { const executablePaths = glob.sync(path.join(CONFIG.buildOutputPath, '*.app')); assertExecutablePaths(executablePaths); executablePath = path.join( executablePaths[0], 'Contents', 'MacOS', path.basename(executablePaths[0], '.app') ); } else if (process.platform === 'linux') { const executablePaths = glob.sync( path.join(CONFIG.buildOutputPath, 'atom-*', 'atom') ); assertExecutablePaths(executablePaths); executablePath = executablePaths[0]; } else if (process.platform === 'win32') { const executablePaths = glob.sync( path.join(CONFIG.buildOutputPath, '**', 'atom*.exe') ); assertExecutablePaths(executablePaths); executablePath = executablePaths[0]; } else { throw new Error('##[error] Running tests on this platform is not supported.'); } function prepareEnv(suiteName) { const atomHomeDirPath = temp.mkdirSync(suiteName); const env = Object.assign({}, process.env, { ATOM_HOME: atomHomeDirPath }); if (process.env.TEST_JUNIT_XML_ROOT) { // Tell Jasmine to output this suite's results as a JUnit XML file to a subdirectory of the root, so that a // CI system can interpret it. const fileName = suiteName + '.xml'; const outputPath = path.join(process.env.TEST_JUNIT_XML_ROOT, fileName); env.TEST_JUNIT_XML_PATH = outputPath; } return env; } function spawnTest( executablePath, testArguments, options, callback, testName, finalize = null ) { const cp = childProcess.spawn(executablePath, testArguments, options); // collect outputs and errors let stderrOutput = ''; if (cp.stdout) { cp.stderr.on('data', data => { stderrOutput += data; }); cp.stdout.on('data', data => { stderrOutput += data; }); } // on error cp.on('error', error => { console.log(error, 'error'); if (finalize) { finalize(); } // if finalizer provided callback(error); }); // on close cp.on('close', exitCode => { if (exitCode !== 0) { retryOrFailTest( stderrOutput, exitCode, executablePath, testArguments, options, callback, testName, finalize ); } else { // successful test if (finalize) { finalize(); } // if finalizer provided callback(null, { exitCode, step: testName, testCommand: `You can run the test again using: \n\t ${executablePath} ${testArguments.join( ' ' )}` }); } }); } const retryNumber = 6; // the number of times a tests repeats const retriedTests = new Map(); // a cache of retried tests // Retries the tests if it is timed out for a number of times. Fails the rest of the tests or those that are tried enough times. function retryOrFailTest( stderrOutput, exitCode, executablePath, testArguments, options, callback, testName, finalize ) { const testKey = createTestKey(executablePath, testArguments, testName); if (isTimedOut(stderrOutput) && shouldTryAgain(testKey)) { // retry the timed out test let triedNumber = retriedTests.get(testKey) || 0; retriedTests.set(testKey, triedNumber + 1); console.warn(`\n##[warning] Retrying the timed out step: ${testName} \n`); spawnTest( executablePath, testArguments, options, callback, testName, finalize ); } else { // fail the test if (finalize) { finalize(); } // if finalizer provided console.log(`##[error] Tests for ${testName} failed.`.red); console.log(stderrOutput); callback(null, { exitCode, step: testName, testCommand: `You can run the test again using: \n\t ${executablePath} ${testArguments.join( ' ' )}` }); } } // creates a key that is specific to a certain test function createTestKey(executablePath, testArguments, testName) { return `${executablePath} ${testArguments.join(' ')} ${testName}`; } // check if a test is timed out function isTimedOut(stderrOutput) { if (stderrOutput) { return ( stderrOutput.includes('timeout: timed out after') || // happens in core renderer tests stderrOutput.includes('Error: Timed out waiting on') || // happens in core renderer tests stderrOutput.includes('Error: timeout of') || // happens in core main tests stderrOutput.includes( 'Error Downloading Update: Could not get code signature for running application' ) // happens in github tests ); } else { return false; } } // check if a tests should be tried again function shouldTryAgain(testKey) { if (retriedTests.has(testKey)) { return retriedTests.get(testKey) < retryNumber; } else { return true; } } function runCoreMainProcessTests(callback) { const testPath = path.join(CONFIG.repositoryRootPath, 'spec', 'main-process'); const testArguments = [ '--resource-path', resourcePath, '--test', '--main-process', testPath ]; if (process.env.CI && process.platform === 'linux') { testArguments.push('--no-sandbox'); } const testEnv = Object.assign({}, prepareEnv('core-main-process'), { ATOM_GITHUB_INLINE_GIT_EXEC: 'true' }); console.log('##[command] Executing core main process tests'.bold.green); spawnTest( executablePath, testArguments, { stdio: 'inherit', env: testEnv }, callback, 'core-main-process' ); } function getCoreRenderProcessTestSuites() { // Build an array of functions, each running tests for a different rendering test const coreRenderProcessTestSuites = []; const testPath = path.join(CONFIG.repositoryRootPath, 'spec'); let testFiles = glob.sync( path.join(testPath, '*-spec.+(js|coffee|ts|jsx|tsx|mjs)') ); for (let testFile of testFiles) { const testArguments = ['--resource-path', resourcePath, '--test', testFile]; // the function which runs by async: coreRenderProcessTestSuites.push(function(callback) { const testEnv = prepareEnv('core-render-process'); console.log( `##[command] Executing core render process tests for ${testFile}`.bold .green ); spawnTest( executablePath, testArguments, { env: testEnv }, callback, `core-render-process in ${testFile}.` ); }); } return coreRenderProcessTestSuites; } function getPackageTestSuites() { // Build an array of functions, each running tests for a different bundled package const packageTestSuites = []; for (let packageName in CONFIG.appMetadata.packageDependencies) { if (process.env.ATOM_PACKAGES_TO_TEST) { const packagesToTest = process.env.ATOM_PACKAGES_TO_TEST.split(',').map( pkg => pkg.trim() ); if (!packagesToTest.includes(packageName)) continue; } const repositoryPackagePath = path.join( CONFIG.repositoryRootPath, 'node_modules', packageName ); const testSubdir = ['spec', 'test'].find(subdir => fs.existsSync(path.join(repositoryPackagePath, subdir)) ); if (!testSubdir) { console.log(`No test folder found for package: ${packageName}`.yellow); continue; } const testFolder = path.join(repositoryPackagePath, testSubdir); const testArguments = [ '--resource-path', resourcePath, '--test', testFolder ]; const pkgJsonPath = path.join(repositoryPackagePath, 'package.json'); const nodeModulesPath = path.join(repositoryPackagePath, 'node_modules'); // the function which runs by async: packageTestSuites.push(function(callback) { const testEnv = prepareEnv(`bundled-package-${packageName}`); let finalize = () => null; if (require(pkgJsonPath).atomTestRunner) { console.log( `##[command] Installing test runner dependencies for ${packageName}` .bold.green ); if (fs.existsSync(nodeModulesPath)) { const backup = backupNodeModules(repositoryPackagePath); finalize = backup.restore; } else { finalize = () => fs.removeSync(nodeModulesPath); } runApmInstall(repositoryPackagePath); console.log(`##[command] Executing ${packageName} tests`.green); } else { console.log(`##[command] Executing ${packageName} tests`.bold.green); } spawnTest( executablePath, testArguments, { env: testEnv }, callback, `${packageName} package`, finalize ); }); } return packageTestSuites; } function runBenchmarkTests(callback) { const benchmarksPath = path.join(CONFIG.repositoryRootPath, 'benchmarks'); const testArguments = ['--benchmark-test', benchmarksPath]; const testEnv = prepareEnv('benchmark'); console.log('##[command] Executing benchmark tests'.bold.green); spawnTest( executablePath, testArguments, { stdio: 'inherit', env: testEnv }, callback, `core-benchmarks` ); } let testSuitesToRun = requestedTestSuites(process.platform); function requestedTestSuites(platform) { // env variable or argv options let coreAll = process.env.ATOM_RUN_CORE_TESTS === 'true'; let coreMain = process.env.ATOM_RUN_CORE_MAIN_TESTS === 'true' || argv.coreMain; let coreRenderer = argv.coreRenderer || process.env.ATOM_RUN_CORE_RENDER_TESTS === 'true'; let coreRenderer1 = process.env.ATOM_RUN_CORE_RENDER_TESTS === '1'; let coreRenderer2 = process.env.ATOM_RUN_CORE_RENDER_TESTS === '2'; let packageAll = argv.package || process.env.ATOM_RUN_PACKAGE_TESTS === 'true'; let packages1 = process.env.ATOM_RUN_PACKAGE_TESTS === '1'; let packages2 = process.env.ATOM_RUN_PACKAGE_TESTS === '2'; let benchmark = argv.coreBenchmark; // Operating system overrides: coreMain = coreMain || platform === 'linux' || (platform === 'win32' && process.arch === 'x86'); // split package tests (used for macos in CI) const PACKAGES_TO_TEST_IN_PARALLEL = 23; // split core render test (used for windows x64 in CI) const CORE_RENDER_TO_TEST_IN_PARALLEL = 45; let suites = []; // Core tess if (coreAll) { suites.push( ...[runCoreMainProcessTests, ...getCoreRenderProcessTestSuites()] ); } else { // Core main tests if (coreMain) { suites.push(runCoreMainProcessTests); } // Core renderer tests if (coreRenderer) { suites.push(...getCoreRenderProcessTestSuites()); } else { // split if (coreRenderer1) { suites.push( ...getCoreRenderProcessTestSuites().slice( 0, CORE_RENDER_TO_TEST_IN_PARALLEL ) ); } if (coreRenderer2) { suites.push( ...getCoreRenderProcessTestSuites().slice( CORE_RENDER_TO_TEST_IN_PARALLEL ) ); } } } // Package tests if (packageAll) { suites.push(...getPackageTestSuites()); } else { // split if (packages1) { suites.push( ...getPackageTestSuites().slice(0, PACKAGES_TO_TEST_IN_PARALLEL) ); } if (packages2) { suites.push( ...getPackageTestSuites().slice(PACKAGES_TO_TEST_IN_PARALLEL) ); } } // Benchmark tests if (benchmark) { suites.push(runBenchmarkTests); } if (argv.skipMainProcessTests) { suites = suites.filter(suite => suite !== runCoreMainProcessTests); } // Remove duplicates suites = Array.from(new Set(suites)); if (suites.length === 0) { throw new Error('No tests was requested'); } return suites; } asyncSeries(testSuitesToRun, function(err, results) { if (err) { console.error(err); process.exit(1); } else { const failedSteps = results.filter(({ exitCode }) => exitCode !== 0); if (failedSteps.length > 0) { console.warn( '\n \n ##[error] *** Reporting the errors that happened in all of the tests: *** \n \n' ); for (const { step, testCommand } of failedSteps) { console.error( `##[error] The '${step}' test step finished with a non-zero exit code \n ${testCommand}` ); } process.exit(1); } process.exit(0); } }); ================================================ FILE: script/test.cmd ================================================ @IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\test" %* ) ELSE ( node "%~dp0\test" %* ) ================================================ FILE: script/update-server/README.md ================================================ # Atom Update Test Server This folder contains a simple implementation of Atom's update server to be used for testing the update process with local builds. ## Prerequisites On macOS, you will need to configure a "Mac Development" certificate for your local machine so that the `script/build --test-sign` parameter will work. Here are the steps to set one up: 1. Install Xcode if it isn't already 1. Launch Xcode and open the Preferences dialog (Cmd + ,) 1. Switch to the Accounts tab 1. If you don't already see your Apple account in the leftmost column, click the `+` button at the bottom left of the window, select "Apple ID" and then click Continue. Sign in with your Apple account and then you'll be sent back to the Accounts tab. 1. Click the "Manage Certificates..." button in the lower right of the Accounts page 1. Click the `+` button in the lower left of the Signing Certificates popup and then select "Mac Development" 1. A new certificate should now be in the list of the Signing Certificates window with the name of your macOS machine. Click "Done" 1. In a Terminal, verify that your Mac Development certificate is set up by running ``` security find-certificate -c 'Mac Developer' ``` If it returns a lot of information with "Mac Developer: your@apple-id-email.com" inside of it, your certificate is configured correctly and you're now ready to run an Atom build with the `--test-sign` parameter. ## How to use it 1. Since you probably want to try upgrading an installed Atom release to a newer version, start your shell and set the `ATOM_RELEASE_VERSION` environment var to the version that you want the server to advertise as the latest version: **Windows** ``` set ATOM_RELEASE_VERSION="1.32.0-beta1" ``` **macOS** ``` export ATOM_RELEASE_VERSION="1.32.0-beta1" ``` 2. Run a full build of Atom such that the necessary release artifacts are in the `out` folder: **Windows** ``` script/build --create-windows-installer ``` **macOS** ``` script/build --compress-artifacts --test-sign ``` 3. Start up the server in this folder: ``` npm install npm start ``` **NOTE:** You can customize the port by setting the `PORT` environment variable. 4. Start Atom from the command line with the `ATOM_UPDATE_URL_PREFIX` environment variable set to `http://localhost:3456` (change this to reflect any `PORT` override you might have used) 5. Open the About page and try to update Atom. The update server will write output to the console when requests are received. ================================================ FILE: script/update-server/package.json ================================================ { "name": "atom-test-update-server", "version": "0.1.0", "private": true, "description": "A test update server that replicates the one on atom.io", "main": "run-server.js", "scripts": { "start": "node run-server.js" }, "author": "David Wilson", "dependencies": { "colors": "^1.3.2", "express": "^4.16.3" } } ================================================ FILE: script/update-server/run-server.js ================================================ require('colors'); const fs = require('fs'); const path = require('path'); const express = require('express'); const app = express(); const port = process.env.PORT || 3456; // Load the metadata for the local build of Atom const buildPath = path.resolve(__dirname, '..', '..', 'out'); const packageJsonPath = path.join(buildPath, 'app', 'package.json'); if (!fs.existsSync(buildPath) || !fs.existsSync(packageJsonPath)) { console.log( `This script requires a full Atom build with release packages for the current platform in the following path:\n ${buildPath}\n` ); if (process.platform === 'darwin') { console.log( `Run this command before trying again:\n script/build --compress-artifacts --test-sign\n\n` ); } else if (process.platform === 'win32') { console.log( `Run this command before trying again:\n script/build --create-windows-installer\n\n` ); } process.exit(1); } const appMetadata = require(packageJsonPath); const versionMatch = appMetadata.version.match(/-(beta|nightly)\d+$/); const releaseChannel = versionMatch ? versionMatch[1] : 'stable'; console.log( `Serving ${ appMetadata.productName } release assets (channel = ${releaseChannel})\n`.green ); function getMacZip(req, res) { console.log(`Received request for atom-mac.zip, sending it`); res.sendFile(path.join(buildPath, 'atom-mac.zip')); } function getMacUpdates(req, res) { if (req.query.version !== appMetadata.version) { const updateInfo = { name: appMetadata.version, pub_date: new Date().toISOString(), url: `http://localhost:${port}/mac/atom-mac.zip`, notes: '

    No Details

    ' }; console.log( `Received request for macOS updates (version = ${ req.query.version }), sending\n`, updateInfo ); res.json(updateInfo); } else { console.log( `Received request for macOS updates, sending 204 as Atom is up to date (version = ${ req.query.version })` ); res.sendStatus(204); } } function getReleasesFile(fileName) { return function(req, res) { console.log( `Received request for ${fileName}, version: ${req.query.version}` ); if (req.query.version) { const versionMatch = (req.query.version || '').match( /-(beta|nightly)\d+$/ ); const versionChannel = (versionMatch && versionMatch[1]) || 'stable'; if (releaseChannel !== versionChannel) { console.log( `Atom requested an update for version ${ req.query.version } but the current release channel is ${releaseChannel}` ); res.sendStatus(404); return; } } res.sendFile(path.join(buildPath, fileName)); }; } function getNupkgFile(is64bit) { return function(req, res) { let nupkgFile = req.params.nupkg; if (is64bit) { const nupkgMatch = nupkgFile.match(/atom-(.+)-(delta|full)\.nupkg/); if (nupkgMatch) { nupkgFile = `atom-x64-${nupkgMatch[1]}-${nupkgMatch[2]}.nupkg`; } } console.log( `Received request for ${req.params.nupkg}, sending ${nupkgFile}` ); res.sendFile(path.join(buildPath, nupkgFile)); }; } if (process.platform === 'darwin') { app.get('/mac/atom-mac.zip', getMacZip); app.get('/api/updates', getMacUpdates); } else if (process.platform === 'win32') { app.get('/api/updates/RELEASES', getReleasesFile('RELEASES')); app.get('/api/updates/:nupkg', getNupkgFile()); app.get('/api/updates-x64/RELEASES', getReleasesFile('RELEASES-x64')); app.get('/api/updates-x64/:nupkg', getNupkgFile(true)); } else { console.log( `The current platform '${ process.platform }' doesn't support Squirrel updates, exiting.`.red ); process.exit(1); } app.listen(port, () => { console.log( `Run Atom with ATOM_UPDATE_URL_PREFIX="http://localhost:${port}" set to test updates!\n` .yellow ); }); ================================================ FILE: script/verify-snapshot-script ================================================ #!/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}) ================================================ FILE: script/vsts/README.md ================================================ # Atom Release Build Documentation ## Overview This folder contains build configuration and scripts for automating Atom's release pipeline using [Visual Studio Team Services](https://azure.microsoft.com/en-us/services/visual-studio-team-services/). VSTS allows us to leverage [multi-phase jobs](https://github.com/Microsoft/vsts-agent/blob/master/docs/preview/yamlgettingstarted-jobs.md) to generate Atom installation packages on Windows, macOS, and Linux and then publish a new release automatically once the build completes successfully. ## Nightly Release Build Our scheduled nightly release uses a mutli-phase job to automatically generate Atom Nightly installation packages and then publish them to GitHub and atom.io. The [Atom Nightly build definition](https://github.visualstudio.com/Atom/_build/index?context=mine&path=%5C&definitionId=1&_a=completed) is configured with the [`nightly-release.yml`](nightly-release.yml) file. More information on VSTS' YAML configuration format can be found in their [Getting Started](https://github.com/Microsoft/vsts-agent/blob/master/docs/preview/yamlgettingstarted.md) documentation. ### Versioning Phase In this phase, we run [`script/vsts/generate-version.js`](generate-version.js) to determine the version of the next Atom Nightly release. This script consults the GitHub v3 API to get the list of releases on the [`atom/atom-nightly-releases`](https://github.com/atom/atom-nightly-releases) repo. We look for the most recent, non-draft release and then parse its version number (e.g. `1.30.0-nightly4`) to extract the base version and the monotonically-increasing nightly release number. Once we have the version and release number, we compare the base version number (`1.30.0`) against the one in `package.json` of the latest commit in the local repo. If those versions are the same, we increment the release number (`1.30.0-nightly5`). If those versions are different, we use `0` for the release number to start a new series of Nightly releases for the new version (`1.31.0-nightly0`). Once the release version has been determined, it is set as our custom `ReleaseVersion` [output variable](https://github.com/Microsoft/vsts-agent/blob/master/docs/preview/yamlgettingstarted-outputvariables.md) by writing out a special string to `stdout` which is recognized by VSTS. This variable will be used in later build steps. If any part of the build process fails from this point forward, the same version number *should* be chosen in the next build unless the base version number has been changed in `master`. ### OS-specific Build Phases In this part of the build, we use [phase templates](https://github.com/Microsoft/vsts-agent/blob/master/docs/preview/yamlgettingstarted-templates.md) for [Windows](windows.yml), [macOS](macos.yml), and [Linux](linux.yml) to build Atom simultaneously across those platforms and then run the Atom test suite to verify the builds. If build, test, and linting come back clean, we take the build assets generated in the `out` folder on each OS and then stage them as build artifacts. For each OS build, we refer to the `ReleaseVersion` variable, set in the previous phase, to configure the `ATOM_RELEASE_VERSION` environment variable to override the version contained in Atom's `package.json`. ### Publish Phase If all three OS builds have completed successfully, the publish phase will launch the [`script/publish-release`](../publish-release) script to collect the release artifacts created from those builds and then upload them to the S3 bucket from which Atom release assets are served. If the upload process is successful, a new release will be created on the `atom/atom-nightly-releases` repo using the `ReleaseVersion` with a `v` prefix as the tag name. The release assets will also be uploaded to the GitHub release at this time. ================================================ FILE: script/vsts/get-release-version.js ================================================ const path = require('path'); const request = require('request-promise-native'); const repositoryRootPath = path.resolve(__dirname, '..', '..'); const appMetadata = require(path.join(repositoryRootPath, 'package.json')); const yargs = require('yargs'); const argv = yargs .usage('Usage: $0 [options]') .help('help') .describe('nightly', 'Indicates that a nightly version should be produced') .wrap(yargs.terminalWidth()).argv; function getAppName(version) { const match = version.match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/); if (!match) { throw new Error(`Found incorrectly formatted Atom version ${version}`); } else if (match[2]) { return `atom-${match[2]}`; } return 'atom'; } async function getReleaseVersion() { let releaseVersion = process.env.ATOM_RELEASE_VERSION || appMetadata.version; if (argv.nightly) { const releases = await request({ url: 'https://api.github.com/repos/atom/atom-nightly-releases/releases', headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'Atom Release Build' }, json: true }); let releaseNumber = 0; const baseVersion = appMetadata.version.split('-')[0]; if (releases && releases.length > 0) { const latestRelease = releases.find(r => !r.draft); const versionMatch = latestRelease.tag_name.match( /^v?(\d+\.\d+\.\d+)-nightly(\d+)$/ ); if (versionMatch && versionMatch[1] === baseVersion) { releaseNumber = parseInt(versionMatch[2]) + 1; } } releaseVersion = `${baseVersion}-nightly${releaseNumber}`; } // Set our ReleaseVersion build variable and update VSTS' build number to // include the version. Writing these strings to stdout causes VSTS to set // the associated variables. console.log( `##vso[task.setvariable variable=ReleaseVersion;isOutput=true]${releaseVersion}` ); if (!process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER) { // Only set the build number on non-PR builds as it causes build errors when // non-admins send PRs to the repo console.log( `##vso[build.updatebuildnumber]${releaseVersion}+${ process.env.BUILD_BUILDID }` ); } // Write out some variables that indicate whether artifacts should be uploaded const buildBranch = process.env.BUILD_SOURCEBRANCHNAME; const isReleaseBranch = process.env.IS_RELEASE_BRANCH || argv.nightly || buildBranch.match(/\d\.\d+-releases/) !== null; const isSignedZipBranch = !isReleaseBranch && (process.env.IS_SIGNED_ZIP_BRANCH || buildBranch.startsWith('electron-') || (buildBranch === 'master' && !process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER)); const SHOULD_SIGN = process.env.SHOULD_SIGN; console.log( `##vso[task.setvariable variable=AppName;isOutput=true]${getAppName( releaseVersion )}` ); console.log( `##vso[task.setvariable variable=IsReleaseBranch;isOutput=true]${isReleaseBranch}` ); console.log( `##vso[task.setvariable variable=IsSignedZipBranch;isOutput=true]${isSignedZipBranch}` ); console.log( `##vso[task.setvariable variable=SHOULD_SIGN;isOutput=true]${SHOULD_SIGN}` ); } getReleaseVersion(); ================================================ FILE: script/vsts/lib/release-notes.js ================================================ const semver = require('semver'); const octokit = require('@octokit/rest')(); const changelog = require('pr-changelog'); const childProcess = require('child_process'); module.exports.getRelease = async function(releaseVersion, githubToken) { if (githubToken) { octokit.authenticate({ type: 'token', token: githubToken }); } const releases = await octokit.repos.getReleases({ owner: 'atom', repo: 'atom' }); const release = releases.data.find(r => semver.eq(r.name, releaseVersion)); return { exists: release !== undefined, isDraft: release && release.draft, releaseNotes: release ? release.body : undefined }; }; module.exports.generateForVersion = async function( releaseVersion, githubToken, oldReleaseNotes ) { let oldVersion = null; let oldVersionName = null; const parsedVersion = semver.parse(releaseVersion); const newVersionBranch = getBranchForVersion(parsedVersion); if (githubToken) { changelog.setGithubAccessToken(githubToken); octokit.authenticate({ type: 'token', token: githubToken }); } if (parsedVersion.prerelease && parsedVersion.prerelease[0] === 'beta0') { // For beta0 releases, stable hasn't been released yet so compare against // the stable version's release branch oldVersion = `${parsedVersion.major}.${parsedVersion.minor - 1}-releases`; oldVersionName = `v${parsedVersion.major}.${parsedVersion.minor - 1}.0`; } else { let releases = await octokit.repos.getReleases({ owner: 'atom', repo: 'atom' }); oldVersion = 'v' + getPreviousRelease(releaseVersion, releases.data).name; oldVersionName = oldVersion; } const allChangesText = await changelog.getChangelog({ owner: 'atom', repo: 'atom', fromTag: oldVersion, toTag: newVersionBranch, dependencyKey: 'packageDependencies', changelogFormatter: function({ pullRequests, owner, repo, fromTag, toTag }) { let prString = changelog.pullRequestsToString(pullRequests); let title = repo; if (repo === 'atom') { title = 'Atom Core'; fromTag = oldVersionName; toTag = releaseVersion; } return `### [${title}](https://github.com/${owner}/${repo})\n\n${fromTag}...${toTag}\n\n${prString}`; } }); const writtenReleaseNotes = extractWrittenReleaseNotes(oldReleaseNotes) || '**TODO**: Pull relevant changes here!'; return `## Notable Changes\n ${writtenReleaseNotes}\n
    All Changes\n ${allChangesText}
    `; }; module.exports.generateForNightly = async function( releaseVersion, githubToken ) { const latestCommitResult = childProcess.spawnSync('git', [ 'rev-parse', '--short', 'HEAD' ]); if (!latestCommitResult) { console.log("Couldn't get the current commmit from git."); return undefined; } const latestCommit = latestCommitResult.stdout.toString().trim(); const output = [ `### This nightly release is based on https://github.com/atom/atom/commit/${latestCommit} :atom: :night_with_stars:` ]; try { const releases = await octokit.repos.getReleases({ owner: 'atom', repo: 'atom-nightly-releases' }); const previousRelease = getPreviousRelease(releaseVersion, releases.data); const oldReleaseNotes = previousRelease ? previousRelease.body : undefined; if (oldReleaseNotes) { const extractMatch = oldReleaseNotes.match( /atom\/atom\/commit\/([0-9a-f]{5,40})/ ); if (extractMatch.length > 1 && extractMatch[1]) { output.push('', '---', ''); const previousCommit = extractMatch[1]; if ( previousCommit === latestCommit || previousCommit.startsWith(latestCommit) || latestCommit.startsWith(previousCommit) ) { // TODO: Maybe we can bail out and not publish a release if it contains no commits? output.push('No changes have been included in this release'); } else { output.push( `Click [here](https://github.com/atom/atom/compare/${previousCommit}...${latestCommit}) to see the changes included with this release!` ); } } } } catch (e) { console.log( 'Error when trying to find the previous nightly release: ' + e.message ); } return output.join('\n'); }; function extractWrittenReleaseNotes(oldReleaseNotes) { if (oldReleaseNotes) { const extractMatch = oldReleaseNotes.match( /^## Notable Changes\r\n([\s\S]*)
    / ); if (extractMatch && extractMatch[1]) { return extractMatch[1].trim(); } } return undefined; } function getPreviousRelease(version, allReleases) { const versionIsStable = semver.prerelease(version) === null; // Make sure versions are sorted before using them allReleases.sort((v1, v2) => semver.rcompare(v1.name, v2.name)); for (let release of allReleases) { if (versionIsStable && semver.prerelease(release.name)) { continue; } if (semver.lt(release.name, version)) { return release; } } return null; } function getBranchForVersion(version) { let parsedVersion = version; if (!(version instanceof semver.SemVer)) { parsedVersion = semver.parse(version); } return `${parsedVersion.major}.${parsedVersion.minor}-releases`; } ================================================ FILE: script/vsts/lib/upload-linux-packages.js ================================================ const fs = require('fs'); const path = require('path'); const request = require('request-promise-native'); module.exports = async function(packageRepoName, apiToken, version, artifacts) { for (let artifact of artifacts) { let fileExt = path.extname(artifact); switch (fileExt) { case '.deb': await uploadDebPackage(version, artifact); break; case '.rpm': await uploadRpmPackage(version, artifact); break; default: continue; } } async function uploadDebPackage(version, filePath) { // NOTE: Not sure if distro IDs update over time, might need // to query the following endpoint dynamically to find the right IDs: // // https://{apiToken}:@packagecloud.io/api/v1/distributions.json await uploadPackage({ version, filePath, type: 'deb', arch: 'amd64', fileName: 'atom-amd64.deb', distroId: 35 /* Any .deb distribution */, distroName: 'any', distroVersion: 'any' }); } async function uploadRpmPackage(version, filePath) { await uploadPackage({ version, filePath, type: 'rpm', arch: 'x86_64', fileName: 'atom.x86_64.rpm', distroId: 140 /* Enterprise Linux 7 */, distroName: 'el', distroVersion: '7' }); } async function uploadPackage(packageDetails) { // Infer the package suffix from the version if (/-beta\d+/.test(packageDetails.version)) { packageDetails.releaseSuffix = '-beta'; } else if (/-nightly\d+/.test(packageDetails.version)) { packageDetails.releaseSuffix = '-nightly'; } await removePackageIfExists(packageDetails); await uploadToPackageCloud(packageDetails); } function uploadToPackageCloud(packageDetails) { return new Promise(async (resolve, reject) => { console.log( `Uploading ${ packageDetails.fileName } to https://packagecloud.io/AtomEditor/${packageRepoName}` ); var uploadOptions = { url: `https://${apiToken}:@packagecloud.io/api/v1/repos/AtomEditor/${packageRepoName}/packages.json`, formData: { 'package[distro_version_id]': packageDetails.distroId, 'package[package_file]': fs.createReadStream(packageDetails.filePath) } }; request.post(uploadOptions, (error, uploadResponse, body) => { if (error || uploadResponse.statusCode !== 201) { console.log( `Error while uploading '${packageDetails.fileName}' v${ packageDetails.version }: ${uploadResponse}` ); reject(uploadResponse); } else { console.log(`Successfully uploaded ${packageDetails.fileName}!`); resolve(uploadResponse); } }); }); } async function removePackageIfExists({ version, type, arch, fileName, distroName, distroVersion, releaseSuffix }) { // RPM URI paths have an extra '/0.1' thrown in let versionJsonPath = type === 'rpm' ? `${version.replace('-', '.')}/0.1` : version; try { const existingPackageDetails = await request({ uri: `https://${apiToken}:@packagecloud.io/api/v1/repos/AtomEditor/${packageRepoName}/package/${type}/${distroName}/${distroVersion}/atom${releaseSuffix || ''}/${arch}/${versionJsonPath}.json`, method: 'get', json: true }); if (existingPackageDetails && existingPackageDetails.destroy_url) { console.log( `Deleting pre-existing package ${fileName} in ${packageRepoName}` ); await request({ uri: `https://${apiToken}:@packagecloud.io/${ existingPackageDetails.destroy_url }`, method: 'delete' }); } } catch (err) { if (err.statusCode !== 404) { console.log( `Error while checking for existing '${fileName}' v${version}:\n\n`, err ); } } } }; ================================================ FILE: script/vsts/lib/upload-to-azure-blob.js ================================================ 'use strict'; const path = require('path'); const { BlobServiceClient } = require('@azure/storage-blob'); module.exports = function upload(connStr, directory, assets) { const blobServiceClient = BlobServiceClient.fromConnectionString(connStr); const containerName = 'atom-build'; const containerClient = blobServiceClient.getContainerClient(containerName); async function listExistingAssetsForDirectory() { return containerClient.listBlobsFlat({ prefix: directory }); } async function deleteExistingAssets(existingAssets = []) { try { for await (const asset of existingAssets) { console.log(`Deleting blob ${asset.name}`); containerClient.deleteBlob(asset.name); } return Promise.resolve(true); } catch (ex) { return Promise.reject(ex.message); } } function uploadAssets(assets) { return assets.reduce(function(promise, asset) { return promise.then(() => uploadAsset(asset)); }, Promise.resolve()); } function uploadAsset(assetPath) { return new Promise(async (resolve, reject) => { try { console.info(`Uploading ${assetPath}`); const blockBlobClient = containerClient.getBlockBlobClient( path.join(directory, path.basename(assetPath)) ); const result = await blockBlobClient.uploadFile(assetPath); resolve(result); } catch (ex) { reject(ex.message); } }); } return listExistingAssetsForDirectory() .then(deleteExistingAssets) .then(() => uploadAssets(assets)); }; ================================================ FILE: script/vsts/lint.yml ================================================ jobs: - job: Lint timeoutInMinutes: 10 pool: vmImage: ubuntu-latest steps: - script: | cd script npm ci displayName: Install script dependencies - script: script/lint displayName: Run linter ================================================ FILE: script/vsts/nightly-release.yml ================================================ jobs: # Import "GetReleaseVersion" job definition, with the "NightlyFlag" parameter set - template: platforms/templates/get-release-version.yml parameters: NightlyFlag: --nightly # Import lint definition - template: lint.yml # Import OS-specific build definitions - template: platforms/windows.yml - template: platforms/macos.yml - template: platforms/linux.yml - job: Release pool: vmImage: 'ubuntu-latest' dependsOn: - GetReleaseVersion - Lint - Windows_tests - Linux - macOS_tests variables: ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] steps: - template: platforms/templates/preparation.yml - script: | cd script/vsts npm install displayName: npm install - task: DownloadBuildArtifacts@0 inputs: itemPattern: '**' downloadType: 'specific' displayName: Download Release Artifacts - script: | node $(Build.SourcesDirectory)/script/vsts/upload-artifacts.js --create-github-release --assets-path "$(System.ArtifactsDirectory)" --linux-repo-name "atom" env: GITHUB_TOKEN: $(GITHUB_TOKEN) ATOM_RELEASE_VERSION: $(ReleaseVersion) ATOM_RELEASES_S3_KEY: $(ATOM_RELEASES_S3_KEY) ATOM_RELEASES_AZURE_CONN_STRING: $(ATOM_RELEASES_AZURE_CONN_STRING) ATOM_RELEASES_S3_SECRET: $(ATOM_RELEASES_S3_SECRET) ATOM_RELEASES_S3_BUCKET: $(ATOM_RELEASES_S3_BUCKET) PACKAGE_CLOUD_API_KEY: $(PACKAGE_CLOUD_API_KEY) displayName: Create Nightly Release - job: bump_dependencies displayName: Bump Dependencies timeoutInMinutes: 180 pool: vmImage: macos-10.15 steps: - template: platforms/templates/preparation.yml - template: platforms/templates/bootstrap.yml - script: | cd script/lib npm install displayName: npm install - script: | cd script/lib/update-dependency node index.js displayName: Bump depedencies env: AUTH_TOKEN: $(GITHUB_TOKEN) ================================================ FILE: script/vsts/package.json ================================================ { "name": "atom-release-scripts", "description": "Atom release scripts", "dependencies": { "@azure/storage-blob": "^12.5.0", "@octokit/rest": "^15.9.5", "download": "^7.1.0", "glob": "7.0.3", "pr-changelog": "^0.3.2", "publish-release": "^1.6.0", "request": "^2.87.0", "request-promise-native": "^1.0.5", "semver": "5.3.0", "yargs": "4.8.1" } } ================================================ FILE: script/vsts/platforms/linux.yml ================================================ jobs: - job: Linux dependsOn: GetReleaseVersion timeoutInMinutes: 180 variables: ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] pool: vmImage: ubuntu-18.04 steps: - template: templates/preparation.yml - template: templates/cache.yml parameters: OS: linux - template: templates/bootstrap.yml - template: templates/build.yml - template: templates/test.yml - template: templates/publish.yml parameters: artifacts: - fileName: atom.x86_64.rpm fileDir: $(Build.SourcesDirectory)/out condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - fileName: atom-amd64.deb fileDir: $(Build.SourcesDirectory)/out condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - fileName: atom-amd64.tar.gz fileDir: $(Build.SourcesDirectory)/out condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) ================================================ FILE: script/vsts/platforms/macos.yml ================================================ jobs: - job: macOS_build displayName: macOS Build dependsOn: GetReleaseVersion timeoutInMinutes: 180 variables: ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] IsReleaseBranch: $[ dependencies.GetReleaseVersion.outputs['Version.IsReleaseBranch'] ] IsSignedZipBranch: $[ dependencies.GetReleaseVersion.outputs['Version.IsSignedZipBranch'] ] RunCoreMainTests: true pool: vmImage: macos-10.15 steps: - template: templates/preparation.yml - template: templates/cache.yml parameters: OS: macos - template: templates/bootstrap.yml - template: templates/build.yml # core main tests - template: templates/test.yml - script: | cp $(Build.SourcesDirectory)/out/*.zip $(Build.ArtifactStagingDirectory) displayName: Stage Artifacts - template: templates/publish.yml parameters: artifacts: - fileName: atom-mac.zip fileDir: $(Build.ArtifactStagingDirectory) condition: succeeded() - fileName: atom-mac-symbols.zip fileDir: $(Build.ArtifactStagingDirectory) condition: succeeded() - fileName: atom-api.json fileDir: $(Build.SourcesDirectory)/docs/output condition: succeeded() - job: macOS_tests displayName: macOS Tests dependsOn: macOS_build timeoutInMinutes: 180 pool: vmImage: macos-10.15 strategy: maxParallel: 3 matrix: renderer: RunCoreRendererTests: true RunPackageTests: false packages-1: RunCoreTests: false RunPackageTests: 1 packages-2: RunCoreTests: false RunPackageTests: 2 steps: - template: templates/preparation.yml - template: templates/cache.yml parameters: OS: macos # The artifact caching task does not work on forks, so we need to # bootstrap again for pull requests coming from forked repositories. - template: templates/bootstrap.yml - template: templates/download-unzip.yml parameters: artifacts: - atom-mac.zip - atom-mac-symbols.zip - template: templates/test.yml ================================================ FILE: script/vsts/platforms/templates/bootstrap.yml ================================================ steps: - pwsh: | # OS specific env variables if ($env:AGENT_OS -eq "Windows_NT") { $env:NPM_BIN_PATH="C:/npm/prefix/npm.cmd" $env:npm_config_build_from_source=true } if ($env:AGENT_OS -eq "Darwin") { $env:NPM_BIN_PATH="/usr/local/bin/npm" $env:npm_config_build_from_source=true } if ($env:AGENT_OS -eq "Linux") { $env:NPM_BIN_PATH="/usr/local/bin/npm" $env:CC=clang $env:CXX=clang++ $env:npm_config_clang=1 } # Bootstrap script/bootstrap displayName: Bootstrap build environment env: GITHUB_TOKEN: $(GITHUB_TOKEN) CI: true CI_PROVIDER: VSTS condition: or(ne(variables['MainNodeModulesRestored'], 'true'), ne(variables['ScriptNodeModulesRestored'], 'true'), ne(variables['ApmNodeModulesRestored'], 'true'), ne(variables['LocalPackagesRestored'], 'true')) ================================================ FILE: script/vsts/platforms/templates/build.yml ================================================ steps: - pwsh: | # OS specific env variables if ($env:AGENT_OS -eq "Windows_NT") { $env:SQUIRREL_TEMP="C:/tmp" $env:npm_config_build_from_source=true } elseif ($env:AGENT_OS -eq "Linux") { $env:CC=clang $env:CXX=clang++ $env:npm_config_clang=1 $env:LinuxArgs="--create-debian-package --create-rpm-package" $env:SHOULD_SIGN="false" } # Build Arguments ## Creation of Windows Installaer if ($env:AGENT_OS -eq "Windows_NT") { mkdir -f -p $env:SQUIRREL_TEMP if ($env:IS_RELEASE_BRANCH -eq "true") { $CreateWindowsInstallaer="--create-windows-installer" } } ## Code Sign if ( ($env:SHOULD_SIGN -eq "true") -and (($env:IS_RELEASE_BRANCH -eq "true") -or ($env:IS_SIGNED_ZIP_BRANCH -eq "true")) ) { $CodeSign="--code-sign" } # Build $esc = '--%' script/build --no-bootstrap --compress-artifacts $esc $env:LinuxArgs $CodeSign $CreateWindowsInstallaer displayName: Build Atom env: GITHUB_TOKEN: $(GITHUB_TOKEN) IS_RELEASE_BRANCH: $(IsReleaseBranch) IS_SIGNED_ZIP_BRANCH: $(IsSignedZipBranch) ATOM_RELEASE_VERSION: $(ReleaseVersion) ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL: $(ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL) ATOM_MAC_CODE_SIGNING_CERT_PASSWORD: $(ATOM_MAC_CODE_SIGNING_CERT_PASSWORD) ATOM_MAC_CODE_SIGNING_KEYCHAIN: $(ATOM_MAC_CODE_SIGNING_KEYCHAIN) ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD: $(ATOM_MAC_CODE_SIGNING_KEYCHAIN_PASSWORD) AC_USER: $(AC_USER) AC_PASSWORD: $(AC_PASSWORD) ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL: $(ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL) ATOM_WIN_CODE_SIGNING_CERT_PASSWORD: $(ATOM_WIN_CODE_SIGNING_CERT_PASSWORD) - script: | sudo chown root ./out/atom*-amd64/chrome-sandbox sudo chmod 4755 ./out/atom*-amd64/chrome-sandbox displayName: Tweaking chrome-sandbox binary condition: eq(variables['Agent.OS'], 'Linux') ================================================ FILE: script/vsts/platforms/templates/cache.yml ================================================ parameters: - name: OS displayName: Operating System type: string values: - windows - linux - macos steps: - task: Cache@2 displayName: Cache node_modules inputs: key: 'npm_main | "$(Agent.OS)" | "$(BUILD_ARCH)" | packages/**, !packages/**/node_modules/** | package.json, package-lock.json, apm/package.json, script/package.json, script/package-lock.json, script/vsts/platforms/${{ parameters.OS }}.yml, script/vsts/platforms/templates/preparation.yml' path: 'node_modules' cacheHitVar: MainNodeModulesRestored - task: Cache@2 displayName: Cache script/node_modules inputs: key: 'npm_script | "$(Agent.OS)" | "$(BUILD_ARCH)" | packages/**, !packages/**/node_modules/** | package.json, package-lock.json, apm/package.json, script/package.json, script/package-lock.json, script/vsts/platforms/${{ parameters.OS }}.yml, script/vsts/platforms/templates/preparation.yml' path: 'script/node_modules' cacheHitVar: ScriptNodeModulesRestored - task: Cache@2 displayName: Cache apm/node_modules inputs: key: 'npm_apm | "$(Agent.OS)" | "$(BUILD_ARCH)" | packages/**, !packages/**/node_modules/** | package.json, package-lock.json, apm/package.json, script/package.json, script/package-lock.json, script/vsts/platforms/${{ parameters.OS }}.yml, script/vsts/platforms/templates/preparation.yml' path: 'apm/node_modules' cacheHitVar: ApmNodeModulesRestored - task: Cache@2 displayName: Cache packages/ inputs: key: 'npm_local_packages | "$(Agent.OS)" | "$(BUILD_ARCH)" | packages/**, !packages/**/node_modules/** | package.json, package-lock.json, apm/package.json, script/package.json, script/package-lock.json, script/vsts/platforms/${{ parameters.OS }}.yml, script/vsts/platforms/templates/preparation.yml' path: 'packages' cacheHitVar: LocalPackagesRestored ================================================ FILE: script/vsts/platforms/templates/download-unzip.yml ================================================ parameters: - name: artifacts type: object default: {} - name: downloadPath type: string default: $(Build.SourcesDirectory) steps: - ${{ each artifact in parameters.artifacts }}: - task: DownloadBuildArtifacts@0 displayName: Download ${{artifact}} inputs: artifactName: ${{artifact}} downloadPath: ${{parameters.downloadPath}} - script: unzip ${{artifact}}/${{artifact}} -d out displayName: Unzip ${{artifact}} ================================================ FILE: script/vsts/platforms/templates/get-release-version.yml ================================================ parameters: - name: NightlyFlag type: string values: - ' ' - --nightly default: ' ' jobs: - job: GetReleaseVersion displayName: Get Release Version pool: vmImage: 'ubuntu-latest' steps: - script: | cd script/vsts npm install node get-release-version.js ${{ parameters.NightlyFlag }} name: Version ================================================ FILE: script/vsts/platforms/templates/preparation.yml ================================================ steps: # Linux Specific - script: | sudo apt-get update sudo apt-get install -y build-essential ca-certificates xvfb fakeroot git rpm libsecret-1-dev libx11-dev libxkbfile-dev xz-utils xorriso zsync libxss1 libgtk-3-0 libasound2 libicu-dev software-properties-common wget # clang 9 is included in the image clang -v displayName: Install apt dependencies condition: eq(variables['Agent.OS'], 'Linux') - script: sudo /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 displayName: Start Xvfb condition: eq(variables['Agent.OS'], 'Linux') # Common - pwsh: | if ($env:BUILD_ARCH -eq "x86") { echo "##vso[task.setvariable variable=IsWinX86]true" } displayName: Set "IsWinX86" based on "BUILD_ARCH" # Convert "BUILD_ARCH" to a boolean ("IsWinX86") for the following NodeTool task. # Templates evaluate too early for the matrix variable "BUILD_ARCH" to be available in ${{ template expressions }}. # Scripts are interpreted at runtime, so "BUILD_ARCH" will be available to this script, and we can set "IsWinX86". - task: NodeTool@0 inputs: versionSpec: 12.16.3 force32bit: $(IsWinX86) displayName: Install Node.js 12.16.3 - script: npm install --global npm@6.14.8 displayName: Update npm # Windows Specific - task: UsePythonVersion@0 inputs: versionSpec: '3.8' condition: eq(variables['Agent.OS'], 'Windows_NT') - pwsh: | cd script/vsts npm install displayName: Install script/vsts dependencies on Windows condition: eq(variables['Agent.OS'], 'Windows_NT') ================================================ FILE: script/vsts/platforms/templates/publish.yml ================================================ parameters: - name: artifacts type: object # artifacts is an array with each element having these properties: # - fileName # - fileDir # - condition # - continueOnError steps: - ${{ each artifact in parameters.artifacts }}: - task: PublishBuildArtifacts@1 inputs: PathtoPublish: ${{artifact.fileDir}}/${{artifact.fileName}} ArtifactName: ${{artifact.fileName}} ArtifactType: Container displayName: Upload ${{artifact.fileName}} ${{ if artifact.condition }}: condition: ${{artifact.condition}} ${{ if artifact.continueOnError }}: continueOnError: ${{artifact.continueOnError}} ================================================ FILE: script/vsts/platforms/templates/test.yml ================================================ steps: - pwsh: | # OS specific env variables if ($env:AGENT_OS -eq "Linux") { $env:DISPLAY=":99.0" $env:npm_config_build_from_source=true } # Test if ($env:AGENT_OS -eq "Darwin") { osascript -e 'tell application "System Events" to keystroke "x"' # clear screen saver caffeinate -s script/test # Run with caffeinate to prevent screen saver } else { script/test } env: CI: true CI_PROVIDER: VSTS ATOM_JASMINE_REPORTER: list TEST_JUNIT_XML_ROOT: $(Common.TestResultsDirectory)/junit ATOM_RUN_CORE_TESTS: $(RunCoreTests) ATOM_RUN_CORE_MAIN_TESTS: $(RunCoreMainTests) ATOM_RUN_CORE_RENDER_TESTS: $(RunCoreRendererTests) ATOM_RUN_PACKAGE_TESTS: $(RunPackageTests) displayName: Run tests condition: and(succeeded(), ne(variables['Atom.SkipTests'], 'true')) # Test results - pwsh: script/postprocess-junit-results --search-folder "$env:TEST_JUNIT_XML_ROOT" --test-results-files "**/*.xml" env: TEST_JUNIT_XML_ROOT: $(Common.TestResultsDirectory)/junit displayName: Post-process test results condition: ne(variables['Atom.SkipTests'], 'true') - task: PublishTestResults@2 inputs: testResultsFormat: JUnit searchFolder: $(Common.TestResultsDirectory)/junit testResultsFiles: '**/*.xml' mergeTestResults: true testRunTitle: $(Agent.OS) $(BUILD_ARCH) condition: ne(variables['Atom.SkipTests'], 'true') # Crash Reports - pwsh: | New-Item -Path $env:ARTIFACT_STAGING_DIR/crash-reports -Type Directory -Force if (($env:AGENT_OS -eq "Windows_NT") -and (Test-Path "$env:TEMP/Atom Crashes")) { cp "$env:TEMP/Atom Crashes/*.dmp" $env:ARTIFACT_STAGING_DIR/crash-reports } else { cp $env:HOME/Library/Logs/DiagnosticReports/*.crash $env:ARTIFACT_STAGING_DIR/crash-reports } env: ARTIFACT_STAGING_DIR: $(Build.ArtifactStagingDirectory) displayName: Stage Crash Reports condition: failed() - task: PublishBuildArtifacts@1 inputs: PathtoPublish: $(Build.ArtifactStagingDirectory)/crash-reports ArtifactName: crash-reports.zip ${{ if eq(variables['Agent.OS'], 'Windows_NT') }}: condition: and(failed(), eq(variables['ATOM_RELEASES_S3_KEY'], '')) displayName: Publish crash reports on non-release branch ${{ if ne(variables['Agent.OS'], 'Windows_NT') }}: condition: failed() displayName: Upload Crash Reports - script: > node $(Build.SourcesDirectory)\script\vsts\upload-crash-reports.js --crash-report-path "%ARTIFACT_STAGING_DIR%\crash-reports" --azure-blob-path "vsts-artifacts/%BUILD_ID%/" env: ATOM_RELEASES_S3_KEY: $(ATOM_RELEASES_S3_KEY) ATOM_RELEASES_AZURE_CONN_STRING: $(ATOM_RELEASES_AZURE_CONN_STRING) ATOM_RELEASES_S3_SECRET: $(ATOM_RELEASES_S3_SECRET) ATOM_RELEASES_S3_BUCKET: $(ATOM_RELEASES_S3_BUCKET) ARTIFACT_STAGING_DIR: $(Build.ArtifactStagingDirectory) BUILD_ID: $(Build.BuildId) displayName: Upload crash reports to S3 on release branch condition: and(failed(), ne(variables['ATOM_RELEASES_S3_KEY'], ''), eq(variables['Agent.OS'], 'Windows_NT')) ================================================ FILE: script/vsts/platforms/windows.yml ================================================ jobs: - job: Windows_build displayName: Windows Build dependsOn: GetReleaseVersion timeoutInMinutes: 180 strategy: maxParallel: 2 matrix: x64: BUILD_ARCH: x64 RunCoreMainTests: true x86: BUILD_ARCH: x86 RunCoreMainTests: true pool: vmImage: vs2017-win2016 variables: AppName: $[ dependencies.GetReleaseVersion.outputs['Version.AppName'] ] ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] IsReleaseBranch: $[ dependencies.GetReleaseVersion.outputs['Version.IsReleaseBranch'] ] IsSignedZipBranch: $[ dependencies.GetReleaseVersion.outputs['Version.IsSignedZipBranch'] ] steps: - template: templates/preparation.yml - template: templates/cache.yml parameters: OS: windows - template: templates/bootstrap.yml - template: templates/build.yml - template: templates/test.yml - pwsh: | if ($env:BUILD_ARCH -eq "x64") { $env:FileID="-x64" } else { $env:FileID="" } echo "##vso[task.setvariable variable=FileID]$env:FileID" # Azure syntax displayName: Set FileID based on the arch - template: templates/publish.yml parameters: artifacts: - fileName: atom$(FileID)-windows.zip fileDir: $(Build.SourcesDirectory)/out condition: and( succeeded(), or( eq(variables['BUILD_ARCH'], 'x64'), ne(variables['Build.Reason'], 'PullRequest') ) ) - fileName: AtomSetup$(FileID).exe fileDir: $(Build.SourcesDirectory)/out condition: and(succeeded(), eq(variables['IsReleaseBranch'], 'true')) - fileName: $(AppName)$(FileID)-$(ReleaseVersion)-full.nupkg fileDir: $(Build.SourcesDirectory)/out condition: and(succeeded(), eq(variables['IsReleaseBranch'], 'true')) - fileName: $(AppName)$(FileID)-$(ReleaseVersion)-delta.nupkg fileDir: $(Build.SourcesDirectory)/out condition: and(succeeded(), eq(variables['IsReleaseBranch'], 'true')) continueOnError: true # Nightly builds don't produce delta packages yet, so don't fail the build - fileName: RELEASES$(FileID) fileDir: $(Build.SourcesDirectory)/out condition: and(succeeded(), eq(variables['IsReleaseBranch'], 'true')) - job: Windows_tests displayName: Windows Tests dependsOn: Windows_build timeoutInMinutes: 180 strategy: maxParallel: 2 matrix: x64_Renderer_Test1: RunCoreMainTests: false RunCoreRendererTests: 1 BUILD_ARCH: x64 os: windows-2019 x64_Renderer_Test2: RunCoreMainTests: false RunCoreRendererTests: 2 BUILD_ARCH: x64 os: windows-2019 pool: vmImage: $(os) variables: AppName: $[ dependencies.GetReleaseVersion.outputs['Version.AppName'] ] ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] IsReleaseBranch: $[ dependencies.GetReleaseVersion.outputs['Version.IsReleaseBranch'] ] IsSignedZipBranch: $[ dependencies.GetReleaseVersion.outputs['Version.IsSignedZipBranch'] ] steps: - template: templates/preparation.yml - template: templates/cache.yml parameters: OS: windows - template: templates/bootstrap.yml # Downloading the build artifacts - pwsh: | if ($env:BUILD_ARCH -eq "x64") { $env:FileID="-x64" } else { $env:FileID="" } echo "##vso[task.setvariable variable=FileID]$env:FileID" # Azure syntax displayName: Set FileID based on the arch - template: templates/download-unzip.yml parameters: artifacts: - atom$(FileID)-windows.zip # Core renderer tests - template: templates/test.yml ================================================ FILE: script/vsts/pull-requests.yml ================================================ trigger: none # No CI builds, only PR builds jobs: # Import "GetReleaseVersion" job definition - template: platforms/templates/get-release-version.yml # Import lint definition - template: lint.yml # Import OS-specific build definitions - template: platforms/windows.yml - template: platforms/macos.yml - template: platforms/linux.yml ================================================ FILE: script/vsts/release-branch-build.yml ================================================ trigger: - master - 1.* # VSTS only supports wildcards at the end - electron-* pr: none # no PR triggers jobs: # Import "GetReleaseVersion" job definition - template: platforms/templates/get-release-version.yml # Import lint definition - template: lint.yml # Import OS-specific build definitions. - template: platforms/windows.yml - template: platforms/macos.yml - template: platforms/linux.yml - job: UploadArtifacts pool: vmImage: 'ubuntu-latest' dependsOn: - GetReleaseVersion - Lint - Windows_tests - Linux - macOS_tests variables: ReleaseVersion: $[ dependencies.GetReleaseVersion.outputs['Version.ReleaseVersion'] ] IsReleaseBranch: $[ dependencies.GetReleaseVersion.outputs['Version.IsReleaseBranch'] ] IsSignedZipBranch: $[ dependencies.GetReleaseVersion.outputs['Version.IsSignedZipBranch'] ] steps: - template: platforms/templates/preparation.yml - script: | cd script/vsts npm install env: GITHUB_TOKEN: $(GITHUB_TOKEN) displayName: npm install - task: DownloadBuildArtifacts@0 inputs: itemPattern: '**' downloadType: 'specific' displayName: Download Release Artifacts - script: | node $(Build.SourcesDirectory)/script/vsts/upload-artifacts.js --create-github-release --assets-path "$(System.ArtifactsDirectory)" --linux-repo-name "atom-staging" env: GITHUB_TOKEN: $(GITHUB_TOKEN) ATOM_RELEASE_VERSION: $(ReleaseVersion) ATOM_RELEASES_S3_KEY: $(ATOM_RELEASES_S3_KEY) ATOM_RELEASES_AZURE_CONN_STRING: $(ATOM_RELEASES_AZURE_CONN_STRING) ATOM_RELEASES_S3_SECRET: $(ATOM_RELEASES_S3_SECRET) ATOM_RELEASES_S3_BUCKET: $(ATOM_RELEASES_S3_BUCKET) PACKAGE_CLOUD_API_KEY: $(PACKAGE_CLOUD_API_KEY) displayName: Create Draft Release condition: and(succeeded(), eq(variables['Atom.AutoDraftRelease'], 'true'), eq(variables['IsReleaseBranch'], 'true')) - script: | node $(Build.SourcesDirectory)/script/vsts/upload-artifacts.js --assets-path "$(System.ArtifactsDirectory)" --azure-blob-path "vsts-artifacts/$(Build.BuildId)/" env: ATOM_RELEASE_VERSION: $(ReleaseVersion) ATOM_RELEASES_S3_KEY: $(ATOM_RELEASES_S3_KEY) ATOM_RELEASES_AZURE_CONN_STRING: $(ATOM_RELEASES_AZURE_CONN_STRING) ATOM_RELEASES_S3_SECRET: $(ATOM_RELEASES_S3_SECRET) ATOM_RELEASES_S3_BUCKET: $(ATOM_RELEASES_S3_BUCKET) displayName: Upload CI Artifacts to S3 condition: and(succeeded(), eq(variables['IsSignedZipBranch'], 'true')) ================================================ FILE: script/vsts/upload-artifacts.js ================================================ 'use strict'; const fs = require('fs'); const os = require('os'); const path = require('path'); const glob = require('glob'); const spawnSync = require('../lib/spawn-sync'); const publishRelease = require('publish-release'); const releaseNotes = require('./lib/release-notes'); const uploadToAzure = require('./lib/upload-to-azure-blob'); const uploadLinuxPackages = require('./lib/upload-linux-packages'); const CONFIG = require('../config'); const yargs = require('yargs'); const argv = yargs .usage('Usage: $0 [options]') .help('help') .describe( 'assets-path', 'Path to the folder where all release assets are stored' ) .describe( 'azure-blob-path', 'Indicates the Azure Blob Path path in which the assets should be uploaded' ) .describe( 'create-github-release', 'Creates a GitHub release for this build, draft if release branch or public if Nightly' ) .describe( 'linux-repo-name', 'If specified, uploads Linux packages to the given repo name on packagecloud' ) .wrap(yargs.terminalWidth()).argv; const releaseVersion = CONFIG.computedAppVersion; const isNightlyRelease = CONFIG.channel === 'nightly'; const assetsPath = argv.assetsPath || CONFIG.buildOutputPath; const assetsPattern = '/**/*(*.exe|*.zip|*.nupkg|*.tar.gz|*.rpm|*.deb|RELEASES*|atom-api.json)'; const assets = glob.sync(assetsPattern, { root: assetsPath, nodir: true }); const azureBlobPath = argv.azureBlobPath || `releases/v${releaseVersion}/`; if (!assets || assets.length === 0) { console.error(`No assets found under specified path: ${assetsPath}`); process.exit(1); } async function uploadArtifacts() { let releaseForVersion = await releaseNotes.getRelease( releaseVersion, process.env.GITHUB_TOKEN ); if (releaseForVersion.exists && !releaseForVersion.isDraft) { console.log( `Published release already exists for ${releaseVersion}, skipping upload.` ); return; } console.log( `Uploading ${ assets.length } release assets for ${releaseVersion} to Azure Blob Storage under '${azureBlobPath}'` ); await uploadToAzure( process.env.ATOM_RELEASES_AZURE_CONN_STRING, azureBlobPath, assets ); if (argv.linuxRepoName) { await uploadLinuxPackages( argv.linuxRepoName, process.env.PACKAGE_CLOUD_API_KEY, releaseVersion, assets ); } else { console.log( '\nNo Linux package repo name specified, skipping Linux package upload.' ); } const oldReleaseNotes = releaseForVersion.releaseNotes; if (oldReleaseNotes) { const oldReleaseNotesPath = path.resolve( os.tmpdir(), 'OLD_RELEASE_NOTES.md' ); console.log( `Saving existing ${releaseVersion} release notes to ${oldReleaseNotesPath}` ); fs.writeFileSync(oldReleaseNotesPath, oldReleaseNotes, 'utf8'); // This line instructs VSTS to upload the file as an artifact console.log( `##vso[artifact.upload containerfolder=OldReleaseNotes;artifactname=OldReleaseNotes;]${oldReleaseNotesPath}` ); } if (argv.createGithubRelease) { console.log(`\nGenerating new release notes for ${releaseVersion}`); let newReleaseNotes = ''; if (isNightlyRelease) { newReleaseNotes = await releaseNotes.generateForNightly( releaseVersion, process.env.GITHUB_TOKEN, oldReleaseNotes ); } else { newReleaseNotes = await releaseNotes.generateForVersion( releaseVersion, process.env.GITHUB_TOKEN, oldReleaseNotes ); } console.log(`New release notes:\n\n${newReleaseNotes}`); const releaseSha = !isNightlyRelease ? spawnSync('git', ['rev-parse', 'HEAD']) .stdout.toString() .trimEnd() : 'master'; // Nightly tags are created in atom/atom-nightly-releases so the SHA is irrelevant console.log(`Creating GitHub release v${releaseVersion}`); const release = await publishReleaseAsync({ token: process.env.GITHUB_TOKEN, owner: 'atom', repo: !isNightlyRelease ? 'atom' : 'atom-nightly-releases', name: CONFIG.computedAppVersion, notes: newReleaseNotes, target_commitish: releaseSha, tag: `v${CONFIG.computedAppVersion}`, draft: !isNightlyRelease, prerelease: CONFIG.channel !== 'stable', editRelease: true, reuseRelease: true, skipIfPublished: true, assets }); console.log('Release published successfully: ', release.html_url); } else { console.log('Skipping GitHub release creation'); } } async function publishReleaseAsync(options) { return new Promise((resolve, reject) => { publishRelease(options, (err, release) => { if (err) { reject(err); } else { resolve(release); } }); }); } // Wrap the call the async function and catch errors from its promise because // Node.js doesn't yet allow use of await at the script scope uploadArtifacts().catch(err => { console.error('An error occurred while uploading the release:\n\n', err); process.exit(1); }); ================================================ FILE: script/vsts/upload-crash-reports.js ================================================ 'use strict'; const glob = require('glob'); const uploadToAzure = require('./lib/upload-to-azure-blob'); const yargs = require('yargs'); const argv = yargs .usage('Usage: $0 [options]') .help('help') .describe( 'crash-report-path', 'The local path of a directory containing crash reports to upload' ) .describe( 'azure-blob-path', 'Indicates the azure blob storage path in which the crash reports should be uploaded' ) .wrap(yargs.terminalWidth()).argv; async function uploadCrashReports() { const crashesPath = argv.crashReportPath; const crashes = glob.sync('/*.dmp', { root: crashesPath }); const azureBlobPath = argv.azureBlobPath; if (crashes && crashes.length > 0) { console.log( `Uploading ${ crashes.length } private crash reports to Azure Blob Storage under '${azureBlobPath}'` ); await uploadToAzure( process.env.ATOM_RELEASES_AZURE_CONN_STRING, azureBlobPath, crashes, 'private' ); } } // Wrap the call the async function and catch errors from its promise because // Node.js doesn't yet allow use of await at the script scope uploadCrashReports().catch(err => { console.error('An error occurred while uploading crash reports:\n\n', err); process.exit(1); }); ================================================ FILE: script/vsts/x64-cache-key ================================================ x64 ================================================ FILE: script/vsts/x86-cache-key ================================================ x86 ================================================ FILE: spec/application-delegate-spec.js ================================================ /** @babel */ import ApplicationDelegate from '../src/application-delegate'; describe('ApplicationDelegate', function() { describe('set/getTemporaryWindowState', function() { it('can serialize object trees containing redundant child object references', async function() { const applicationDelegate = new ApplicationDelegate(); const childObject = { c: 1 }; const sentObject = { a: childObject, b: childObject }; await applicationDelegate.setTemporaryWindowState(sentObject); const receivedObject = await applicationDelegate.getTemporaryWindowState(); expect(receivedObject).toEqual(sentObject); }); }); }); ================================================ FILE: spec/async-spec-helpers.js ================================================ async function conditionPromise( condition, description = 'anonymous condition' ) { const startTime = Date.now(); while (true) { await timeoutPromise(100); // if condition is sync if (condition.constructor.name !== 'AsyncFunction' && condition()) { return; } // if condition is async else if (await condition()) { return; } if (Date.now() - startTime > 5000) { throw new Error('Timed out waiting on ' + description); } } } function timeoutPromise(timeout) { return new Promise(resolve => { global.setTimeout(resolve, timeout); }); } function emitterEventPromise(emitter, event, timeout = 15000) { return new Promise((resolve, reject) => { const timeoutHandle = setTimeout(() => { reject(new Error(`Timed out waiting for '${event}' event`)); }, timeout); emitter.once(event, () => { clearTimeout(timeoutHandle); resolve(); }); }); } exports.conditionPromise = conditionPromise; exports.emitterEventPromise = emitterEventPromise; exports.timeoutPromise = timeoutPromise; ================================================ FILE: spec/atom-environment-spec.js ================================================ const { conditionPromise } = require('./async-spec-helpers'); const fs = require('fs'); const path = require('path'); const temp = require('temp').track(); const AtomEnvironment = require('../src/atom-environment'); describe('AtomEnvironment', () => { afterEach(() => { try { temp.cleanupSync(); } catch (error) {} }); describe('window sizing methods', () => { describe('::getPosition and ::setPosition', () => { let originalPosition = null; beforeEach(() => (originalPosition = atom.getPosition())); afterEach(() => atom.setPosition(originalPosition.x, originalPosition.y)); it('sets the position of the window, and can retrieve the position just set', () => { atom.setPosition(22, 45); expect(atom.getPosition()).toEqual({ x: 22, y: 45 }); }); }); describe('::getSize and ::setSize', () => { let originalSize = null; beforeEach(() => (originalSize = atom.getSize())); afterEach(() => atom.setSize(originalSize.width, originalSize.height)); it('sets the size of the window, and can retrieve the size just set', async () => { const newWidth = originalSize.width - 12; const newHeight = originalSize.height - 23; await atom.setSize(newWidth, newHeight); expect(atom.getSize()).toEqual({ width: newWidth, height: newHeight }); }); }); }); describe('.isReleasedVersion()', () => { it('returns false if the version is a SHA and true otherwise', () => { let version = '0.1.0'; spyOn(atom, 'getVersion').andCallFake(() => version); expect(atom.isReleasedVersion()).toBe(true); version = '36b5518'; expect(atom.isReleasedVersion()).toBe(false); }); }); describe('loading default config', () => { it('loads the default core config schema', () => { expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe(true); expect(atom.config.get('core.followSymlinks')).toBe(true); expect(atom.config.get('editor.showInvisibles')).toBe(false); }); }); describe('window onerror handler', () => { let devToolsPromise = null; beforeEach(() => { devToolsPromise = Promise.resolve(); spyOn(atom, 'openDevTools').andReturn(devToolsPromise); spyOn(atom, 'executeJavaScriptInDevTools'); }); it('will open the dev tools when an error is triggered', async () => { try { a + 1; // eslint-disable-line no-undef, no-unused-expressions } catch (e) { window.onerror(e.toString(), 'abc', 2, 3, e); } await devToolsPromise; expect(atom.openDevTools).toHaveBeenCalled(); expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled(); }); describe('::onWillThrowError', () => { let willThrowSpy = null; beforeEach(() => { willThrowSpy = jasmine.createSpy(); }); it('is called when there is an error', () => { let error = null; atom.onWillThrowError(willThrowSpy); try { a + 1; // eslint-disable-line no-undef, no-unused-expressions } catch (e) { error = e; window.onerror(e.toString(), 'abc', 2, 3, e); } delete willThrowSpy.mostRecentCall.args[0].preventDefault; expect(willThrowSpy).toHaveBeenCalledWith({ message: error.toString(), url: 'abc', line: 2, column: 3, originalError: error }); }); it('will not show the devtools when preventDefault() is called', () => { willThrowSpy.andCallFake(errorObject => errorObject.preventDefault()); atom.onWillThrowError(willThrowSpy); try { a + 1; // eslint-disable-line no-undef, no-unused-expressions } catch (e) { window.onerror(e.toString(), 'abc', 2, 3, e); } expect(willThrowSpy).toHaveBeenCalled(); expect(atom.openDevTools).not.toHaveBeenCalled(); expect(atom.executeJavaScriptInDevTools).not.toHaveBeenCalled(); }); }); describe('::onDidThrowError', () => { let didThrowSpy = null; beforeEach(() => (didThrowSpy = jasmine.createSpy())); it('is called when there is an error', () => { let error = null; atom.onDidThrowError(didThrowSpy); try { a + 1; // eslint-disable-line no-undef, no-unused-expressions } catch (e) { error = e; window.onerror(e.toString(), 'abc', 2, 3, e); } expect(didThrowSpy).toHaveBeenCalledWith({ message: error.toString(), url: 'abc', line: 2, column: 3, originalError: error }); }); }); }); describe('.assert(condition, message, callback)', () => { let errors = null; beforeEach(() => { errors = []; spyOn(atom, 'isReleasedVersion').andReturn(true); atom.onDidFailAssertion(error => errors.push(error)); }); describe('if the condition is false', () => { it('notifies onDidFailAssertion handlers with an error object based on the call site of the assertion', () => { const result = atom.assert(false, 'a == b'); expect(result).toBe(false); expect(errors.length).toBe(1); expect(errors[0].message).toBe('Assertion failed: a == b'); expect(errors[0].stack).toContain('atom-environment-spec'); }); describe('if passed a callback function', () => { it("calls the callback with the assertion failure's error object", () => { let error = null; atom.assert(false, 'a == b', e => (error = e)); expect(error).toBe(errors[0]); }); }); describe('if passed metadata', () => { it("assigns the metadata on the assertion failure's error object", () => { atom.assert(false, 'a == b', { foo: 'bar' }); expect(errors[0].metadata).toEqual({ foo: 'bar' }); }); }); describe('when Atom has been built from source', () => { it('throws an error', () => { atom.isReleasedVersion.andReturn(false); expect(() => atom.assert(false, 'testing')).toThrow( 'Assertion failed: testing' ); }); }); }); describe('if the condition is true', () => { it('does nothing', () => { const result = atom.assert(true, 'a == b'); expect(result).toBe(true); expect(errors).toEqual([]); }); }); }); describe('saving and loading', () => { beforeEach(() => (atom.enablePersistence = true)); afterEach(() => (atom.enablePersistence = false)); it('selects the state based on the current project paths', async () => { jasmine.useRealClock(); const [dir1, dir2] = [temp.mkdirSync('dir1-'), temp.mkdirSync('dir2-')]; const loadSettings = Object.assign(atom.getLoadSettings(), { initialProjectRoots: [dir1], windowState: null }); spyOn(atom, 'getLoadSettings').andCallFake(() => loadSettings); spyOn(atom, 'serialize').andReturn({ stuff: 'cool' }); atom.project.setPaths([dir1, dir2]); // State persistence will fail if other Atom instances are running expect(await atom.stateStore.connect()).toBe(true); await atom.saveState(); expect(await atom.loadState()).toBeFalsy(); loadSettings.initialProjectRoots = [dir2, dir1]; expect(await atom.loadState()).toEqual({ stuff: 'cool' }); }); it('saves state when the CPU is idle after a keydown or mousedown event', () => { const atomEnv = new AtomEnvironment({ applicationDelegate: global.atom.applicationDelegate }); const idleCallbacks = []; atomEnv.initialize({ window: { requestIdleCallback(callback) { idleCallbacks.push(callback); }, addEventListener() {}, removeEventListener() {} }, document: document.implementation.createHTMLDocument() }); spyOn(atomEnv, 'saveState'); const keydown = new KeyboardEvent('keydown'); atomEnv.document.dispatchEvent(keydown); advanceClock(atomEnv.saveStateDebounceInterval); idleCallbacks.shift()(); expect(atomEnv.saveState).toHaveBeenCalledWith({ isUnloading: false }); expect(atomEnv.saveState).not.toHaveBeenCalledWith({ isUnloading: true }); atomEnv.saveState.reset(); const mousedown = new MouseEvent('mousedown'); atomEnv.document.dispatchEvent(mousedown); advanceClock(atomEnv.saveStateDebounceInterval); idleCallbacks.shift()(); expect(atomEnv.saveState).toHaveBeenCalledWith({ isUnloading: false }); expect(atomEnv.saveState).not.toHaveBeenCalledWith({ isUnloading: true }); atomEnv.destroy(); }); it('ignores mousedown/keydown events happening after calling prepareToUnloadEditorWindow', async () => { const atomEnv = new AtomEnvironment({ applicationDelegate: global.atom.applicationDelegate }); const idleCallbacks = []; atomEnv.initialize({ window: { requestIdleCallback(callback) { idleCallbacks.push(callback); }, addEventListener() {}, removeEventListener() {} }, document: document.implementation.createHTMLDocument() }); spyOn(atomEnv, 'saveState'); let mousedown = new MouseEvent('mousedown'); atomEnv.document.dispatchEvent(mousedown); expect(atomEnv.saveState).not.toHaveBeenCalled(); await atomEnv.prepareToUnloadEditorWindow(); expect(atomEnv.saveState).toHaveBeenCalledWith({ isUnloading: true }); advanceClock(atomEnv.saveStateDebounceInterval); idleCallbacks.shift()(); expect(atomEnv.saveState.calls.length).toBe(1); mousedown = new MouseEvent('mousedown'); atomEnv.document.dispatchEvent(mousedown); advanceClock(atomEnv.saveStateDebounceInterval); idleCallbacks.shift()(); expect(atomEnv.saveState.calls.length).toBe(1); atomEnv.destroy(); }); it('serializes the project state with all the options supplied in saveState', async () => { spyOn(atom.project, 'serialize').andReturn({ foo: 42 }); await atom.saveState({ anyOption: 'any option' }); expect(atom.project.serialize.calls.length).toBe(1); expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({ anyOption: 'any option' }); }); it('serializes the text editor registry', async () => { await atom.packages.activatePackage('language-text'); const editor = await atom.workspace.open('sample.js'); expect(atom.grammars.assignLanguageMode(editor, 'text.plain')).toBe(true); const atom2 = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate, window: document.createElement('div'), document: Object.assign(document.createElement('div'), { body: document.createElement('div'), head: document.createElement('div') }) }); atom2.initialize({ document, window }); await atom2.deserialize(atom.serialize()); await atom2.packages.activatePackage('language-text'); const editor2 = atom2.workspace.getActiveTextEditor(); expect( editor2 .getBuffer() .getLanguageMode() .getLanguageId() ).toBe('text.plain'); atom2.destroy(); }); describe('deserialization failures', () => { it('propagates unrecognized project state restoration failures', async () => { let err; spyOn(atom.project, 'deserialize').andCallFake(() => { err = new Error('deserialization failure'); return Promise.reject(err); }); spyOn(atom.notifications, 'addError'); await atom.deserialize({ project: 'should work' }); expect(atom.notifications.addError).toHaveBeenCalledWith( 'Unable to deserialize project', { description: 'deserialization failure', stack: err.stack } ); }); it('disregards missing project folder errors', async () => { spyOn(atom.project, 'deserialize').andCallFake(() => { const err = new Error('deserialization failure'); err.missingProjectPaths = ['nah']; return Promise.reject(err); }); spyOn(atom.notifications, 'addError'); await atom.deserialize({ project: 'should work' }); expect(atom.notifications.addError).not.toHaveBeenCalled(); }); }); }); describe('openInitialEmptyEditorIfNecessary', () => { describe('when there are no paths set', () => { beforeEach(() => spyOn(atom, 'getLoadSettings').andReturn({ hasOpenFiles: false }) ); it('opens an empty buffer', () => { spyOn(atom.workspace, 'open'); atom.openInitialEmptyEditorIfNecessary(); expect(atom.workspace.open).toHaveBeenCalledWith(null, { pending: true }); }); it('does not open an empty buffer when a buffer is already open', async () => { await atom.workspace.open(); spyOn(atom.workspace, 'open'); atom.openInitialEmptyEditorIfNecessary(); expect(atom.workspace.open).not.toHaveBeenCalled(); }); it('does not open an empty buffer when core.openEmptyEditorOnStart is false', async () => { atom.config.set('core.openEmptyEditorOnStart', false); spyOn(atom.workspace, 'open'); atom.openInitialEmptyEditorIfNecessary(); expect(atom.workspace.open).not.toHaveBeenCalled(); }); }); describe('when the project has a path', () => { beforeEach(() => { spyOn(atom, 'getLoadSettings').andReturn({ hasOpenFiles: true }); spyOn(atom.workspace, 'open'); }); it('does not open an empty buffer', () => { atom.openInitialEmptyEditorIfNecessary(); expect(atom.workspace.open).not.toHaveBeenCalled(); }); }); }); describe('adding a project folder', () => { it('does nothing if the user dismisses the file picker', () => { const projectRoots = atom.project.getPaths(); spyOn(atom, 'pickFolder').andCallFake(callback => callback(null)); atom.addProjectFolder(); expect(atom.project.getPaths()).toEqual(projectRoots); }); describe('when there is no saved state for the added folders', () => { beforeEach(() => { spyOn(atom, 'loadState').andReturn(Promise.resolve(null)); spyOn(atom, 'attemptRestoreProjectStateForPaths'); }); it('adds the selected folder to the project', async () => { atom.project.setPaths([]); const tempDirectory = temp.mkdirSync('a-new-directory'); spyOn(atom, 'pickFolder').andCallFake(callback => callback([tempDirectory]) ); await atom.addProjectFolder(); expect(atom.project.getPaths()).toEqual([tempDirectory]); expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled(); }); }); describe('when there is saved state for the relevant directories', () => { const state = Symbol('savedState'); beforeEach(() => { spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':')); spyOn(atom, 'loadState').andCallFake(async key => key === __dirname ? state : null ); spyOn(atom, 'attemptRestoreProjectStateForPaths'); spyOn(atom, 'pickFolder').andCallFake(callback => callback([__dirname]) ); atom.project.setPaths([]); }); describe('when there are no project folders', () => { it('attempts to restore the project state', async () => { await atom.addProjectFolder(); expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith( state, [__dirname] ); expect(atom.project.getPaths()).toEqual([]); }); }); describe('when there are already project folders', () => { const openedPath = path.join(__dirname, 'fixtures'); beforeEach(() => atom.project.setPaths([openedPath])); it('does not attempt to restore the project state, instead adding the project paths', async () => { await atom.addProjectFolder(); expect( atom.attemptRestoreProjectStateForPaths ).not.toHaveBeenCalled(); expect(atom.project.getPaths()).toEqual([openedPath, __dirname]); }); }); }); }); describe('attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)', () => { describe('when the window is clean (empty or has only unnamed, unmodified buffers)', () => { beforeEach(async () => { // Unnamed, unmodified buffer doesn't count toward "clean"-ness await atom.workspace.open(); }); it('automatically restores the saved state into the current environment', async () => { const projectPath = temp.mkdirSync(); const filePath1 = path.join(projectPath, 'file-1'); const filePath2 = path.join(projectPath, 'file-2'); const filePath3 = path.join(projectPath, 'file-3'); fs.writeFileSync(filePath1, 'abc'); fs.writeFileSync(filePath2, 'def'); fs.writeFileSync(filePath3, 'ghi'); const env1 = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate }); env1.project.setPaths([projectPath]); await env1.workspace.open(filePath1); await env1.workspace.open(filePath2); await env1.workspace.open(filePath3); const env1State = env1.serialize(); env1.destroy(); const env2 = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate }); await env2.attemptRestoreProjectStateForPaths( env1State, [projectPath], [filePath2] ); const restoredURIs = env2.workspace.getPaneItems().map(p => p.getURI()); expect(restoredURIs).toEqual([filePath1, filePath2, filePath3]); env2.destroy(); }); describe('when a dock has a non-text editor', () => { it("doesn't prompt the user to restore state", () => { const dock = atom.workspace.getLeftDock(); dock.getActivePane().addItem({ getTitle() { return 'title'; }, element: document.createElement('div') }); const state = {}; spyOn(atom, 'confirm'); atom.attemptRestoreProjectStateForPaths( state, [__dirname], [__filename] ); expect(atom.confirm).not.toHaveBeenCalled(); }); }); }); describe('when the window is dirty', () => { let editor; beforeEach(async () => { editor = await atom.workspace.open(); editor.setText('new editor'); }); describe('when a dock has a modified editor', () => { it('prompts the user to restore the state', () => { const dock = atom.workspace.getLeftDock(); dock.getActivePane().addItem(editor); spyOn(atom, 'confirm').andReturn(1); spyOn(atom.project, 'addPath'); spyOn(atom.workspace, 'open'); const state = Symbol('state'); atom.attemptRestoreProjectStateForPaths( state, [__dirname], [__filename] ); expect(atom.confirm).toHaveBeenCalled(); }); }); it('prompts the user to restore the state in a new window, discarding it and adding folder to current window', async () => { jasmine.useRealClock(); spyOn(atom, 'confirm').andCallFake((options, callback) => callback(1)); spyOn(atom.project, 'addPath'); spyOn(atom.workspace, 'open'); const state = Symbol('state'); atom.attemptRestoreProjectStateForPaths( state, [__dirname], [__filename] ); expect(atom.confirm).toHaveBeenCalled(); await conditionPromise(() => atom.project.addPath.callCount === 1); expect(atom.project.addPath).toHaveBeenCalledWith(__dirname); expect(atom.workspace.open.callCount).toBe(1); expect(atom.workspace.open).toHaveBeenCalledWith(__filename); }); it('prompts the user to restore the state in a new window, opening a new window', async () => { jasmine.useRealClock(); spyOn(atom, 'confirm').andCallFake((options, callback) => callback(0)); spyOn(atom, 'open'); const state = Symbol('state'); atom.attemptRestoreProjectStateForPaths( state, [__dirname], [__filename] ); expect(atom.confirm).toHaveBeenCalled(); await conditionPromise(() => atom.open.callCount === 1); expect(atom.open).toHaveBeenCalledWith({ pathsToOpen: [__dirname, __filename], newWindow: true, devMode: atom.inDevMode(), safeMode: atom.inSafeMode() }); }); }); }); describe('::unloadEditorWindow()', () => { it('saves the BlobStore so it can be loaded after reload', () => { const configDirPath = temp.mkdirSync('atom-spec-environment'); const fakeBlobStore = jasmine.createSpyObj('blob store', ['save']); const atomEnvironment = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate, enablePersistence: true }); atomEnvironment.initialize({ configDirPath, blobStore: fakeBlobStore, window, document }); atomEnvironment.unloadEditorWindow(); expect(fakeBlobStore.save).toHaveBeenCalled(); atomEnvironment.destroy(); }); }); describe('::destroy()', () => { it('does not throw exceptions when unsubscribing from ipc events (regression)', async () => { const fakeDocument = { addEventListener() {}, removeEventListener() {}, head: document.createElement('head'), body: document.createElement('body') }; const atomEnvironment = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate }); atomEnvironment.initialize({ window, document: fakeDocument }); spyOn(atomEnvironment.packages, 'loadPackages').andReturn( Promise.resolve() ); spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve()); spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve()); await atomEnvironment.startEditorWindow(); atomEnvironment.unloadEditorWindow(); atomEnvironment.destroy(); }); }); describe('::whenShellEnvironmentLoaded()', () => { let atomEnvironment, envLoaded, spy; beforeEach(() => { let resolvePromise = null; const promise = new Promise(resolve => { resolvePromise = resolve; }); envLoaded = () => { resolvePromise(); return promise; }; atomEnvironment = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate, updateProcessEnv() { return promise; } }); atomEnvironment.initialize({ window, document }); spy = jasmine.createSpy(); }); afterEach(() => atomEnvironment.destroy()); it('is triggered once the shell environment is loaded', async () => { atomEnvironment.whenShellEnvironmentLoaded(spy); atomEnvironment.updateProcessEnvAndTriggerHooks(); await envLoaded(); expect(spy).toHaveBeenCalled(); }); it('triggers the callback immediately if the shell environment is already loaded', async () => { atomEnvironment.updateProcessEnvAndTriggerHooks(); await envLoaded(); atomEnvironment.whenShellEnvironmentLoaded(spy); expect(spy).toHaveBeenCalled(); }); }); describe('::openLocations(locations)', () => { beforeEach(() => { atom.project.setPaths([]); }); describe('when there is no saved state', () => { beforeEach(() => { spyOn(atom, 'loadState').andReturn(Promise.resolve(null)); }); describe('when the opened path exists', () => { it('opens a file', async () => { const pathToOpen = __filename; await atom.openLocations([ { pathToOpen, exists: true, isFile: true } ]); expect(atom.project.getPaths()).toEqual([]); }); it('opens a directory as a project folder', async () => { const pathToOpen = __dirname; await atom.openLocations([ { pathToOpen, exists: true, isDirectory: true } ]); expect(atom.workspace.getTextEditors().map(e => e.getPath())).toEqual( [] ); expect(atom.project.getPaths()).toEqual([pathToOpen]); }); }); describe('when the opened path does not exist', () => { it('opens it as a new file', async () => { const pathToOpen = path.join( __dirname, 'this-path-does-not-exist.txt' ); await atom.openLocations([{ pathToOpen, exists: false }]); expect(atom.workspace.getTextEditors().map(e => e.getPath())).toEqual( [pathToOpen] ); expect(atom.project.getPaths()).toEqual([]); }); it('may be required to be an existing directory', async () => { spyOn(atom.notifications, 'addWarning'); const nonExistent = path.join(__dirname, 'no'); const existingFile = __filename; const existingDir = path.join(__dirname, 'fixtures'); await atom.openLocations([ { pathToOpen: nonExistent, isDirectory: true }, { pathToOpen: existingFile, isDirectory: true }, { pathToOpen: existingDir, isDirectory: true } ]); expect(atom.workspace.getTextEditors()).toEqual([]); expect(atom.project.getPaths()).toEqual([existingDir]); expect(atom.notifications.addWarning).toHaveBeenCalledWith( 'Unable to open project folders', { description: `The directories \`${nonExistent}\` and \`${existingFile}\` do not exist.` } ); }); }); describe('when the opened path is handled by a registered directory provider', () => { let serviceDisposable; beforeEach(() => { serviceDisposable = atom.packages.serviceHub.provide( 'atom.directory-provider', '0.1.0', { directoryForURISync(uri) { if (uri.startsWith('remote://')) { return { getPath() { return uri; } }; } else { return null; } } } ); waitsFor(() => atom.project.directoryProviders.length > 0); }); afterEach(() => { serviceDisposable.dispose(); }); it("adds it to the project's paths as is", async () => { const pathToOpen = 'remote://server:7644/some/dir/path'; spyOn(atom.project, 'addPath'); await atom.openLocations([{ pathToOpen }]); expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen); }); }); }); describe('when there is saved state for the relevant directories', () => { const state = Symbol('savedState'); beforeEach(() => { spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':')); spyOn(atom, 'loadState').andCallFake(function(key) { if (key === __dirname) { return Promise.resolve(state); } else { return Promise.resolve(null); } }); spyOn(atom, 'attemptRestoreProjectStateForPaths'); }); describe('when there are no project folders', () => { it('attempts to restore the project state', async () => { const pathToOpen = __dirname; await atom.openLocations([{ pathToOpen, isDirectory: true }]); expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith( state, [pathToOpen], [] ); expect(atom.project.getPaths()).toEqual([]); }); it('includes missing mandatory project folders in computation of initial state key', async () => { const existingDir = path.join(__dirname, 'fixtures'); const missingDir = path.join(__dirname, 'no'); atom.loadState.andCallFake(function(key) { if (key === `${existingDir}:${missingDir}`) { return Promise.resolve(state); } else { return Promise.resolve(null); } }); await atom.openLocations([ { pathToOpen: existingDir }, { pathToOpen: missingDir, isDirectory: true } ]); expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith( state, [existingDir], [] ); expect(atom.project.getPaths(), [existingDir]); }); it('opens the specified files', async () => { await atom.openLocations([ { pathToOpen: __dirname, isDirectory: true }, { pathToOpen: __filename } ]); expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith( state, [__dirname], [__filename] ); expect(atom.project.getPaths()).toEqual([]); }); }); describe('when there are already project folders', () => { beforeEach(() => atom.project.setPaths([__dirname])); it('does not attempt to restore the project state, instead adding the project paths', async () => { const pathToOpen = path.join(__dirname, 'fixtures'); await atom.openLocations([ { pathToOpen, exists: true, isDirectory: true } ]); expect( atom.attemptRestoreProjectStateForPaths ).not.toHaveBeenCalled(); expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen]); }); it('opens the specified files', async () => { const pathToOpen = path.join(__dirname, 'fixtures'); const fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt'); await atom.openLocations([ { pathToOpen, exists: true, isDirectory: true }, { pathToOpen: fileToOpen, exists: true, isFile: true } ]); expect( atom.attemptRestoreProjectStateForPaths ).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen]); expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen]); }); }); }); }); describe('::getReleaseChannel()', () => { let version; beforeEach(() => { spyOn(atom, 'getVersion').andCallFake(() => version); }); it('returns the correct channel based on the version number', () => { version = '1.5.6'; expect(atom.getReleaseChannel()).toBe('stable'); version = '1.5.0-beta10'; expect(atom.getReleaseChannel()).toBe('beta'); version = '1.7.0-dev-5340c91'; expect(atom.getReleaseChannel()).toBe('dev'); }); }); }); ================================================ FILE: spec/atom-paths-spec.js ================================================ /** @babel */ import { remote } from 'electron'; import atomPaths from '../src/atom-paths'; import fs from 'fs-plus'; import path from 'path'; const app = remote.app; const temp = require('temp').track(); describe('AtomPaths', () => { const portableAtomHomePath = path.join( atomPaths.getAppDirectory(), '..', '.atom' ); afterEach(() => { atomPaths.setAtomHome(app.getPath('home')); }); describe('SetAtomHomePath', () => { describe('when a portable .atom folder exists', () => { beforeEach(() => { delete process.env.ATOM_HOME; if (!fs.existsSync(portableAtomHomePath)) { fs.mkdirSync(portableAtomHomePath); } }); afterEach(() => { delete process.env.ATOM_HOME; fs.removeSync(portableAtomHomePath); }); it('sets ATOM_HOME to the portable .atom folder if it has permission', () => { atomPaths.setAtomHome(app.getPath('home')); expect(process.env.ATOM_HOME).toEqual(portableAtomHomePath); }); it('uses ATOM_HOME if no write access to portable .atom folder', () => { if (process.platform === 'win32') return; const readOnlyPath = temp.mkdirSync('atom-path-spec-no-write-access'); process.env.ATOM_HOME = readOnlyPath; fs.chmodSync(portableAtomHomePath, 444); atomPaths.setAtomHome(app.getPath('home')); expect(process.env.ATOM_HOME).toEqual(readOnlyPath); }); }); describe('when a portable folder does not exist', () => { beforeEach(() => { delete process.env.ATOM_HOME; fs.removeSync(portableAtomHomePath); }); afterEach(() => { delete process.env.ATOM_HOME; }); it('leaves ATOM_HOME unmodified if it was already set', () => { const temporaryHome = temp.mkdirSync('atom-spec-setatomhomepath'); process.env.ATOM_HOME = temporaryHome; atomPaths.setAtomHome(app.getPath('home')); expect(process.env.ATOM_HOME).toEqual(temporaryHome); }); it('sets ATOM_HOME to a default location if not yet set', () => { const expectedPath = path.join(app.getPath('home'), '.atom'); atomPaths.setAtomHome(app.getPath('home')); expect(process.env.ATOM_HOME).toEqual(expectedPath); }); }); }); describe('setUserData', () => { let tempAtomConfigPath = null; let tempAtomHomePath = null; let electronUserDataPath = null; let defaultElectronUserDataPath = null; beforeEach(() => { defaultElectronUserDataPath = app.getPath('userData'); delete process.env.ATOM_HOME; tempAtomHomePath = temp.mkdirSync('atom-paths-specs-userdata-home'); tempAtomConfigPath = path.join(tempAtomHomePath, '.atom'); fs.mkdirSync(tempAtomConfigPath); electronUserDataPath = path.join(tempAtomConfigPath, 'electronUserData'); atomPaths.setAtomHome(tempAtomHomePath); }); afterEach(() => { delete process.env.ATOM_HOME; fs.removeSync(electronUserDataPath); try { temp.cleanupSync(); } catch (e) { // Ignore } app.setPath('userData', defaultElectronUserDataPath); }); describe('when an electronUserData folder exists', () => { it('sets userData path to the folder if it has permission', () => { fs.mkdirSync(electronUserDataPath); atomPaths.setUserData(app); expect(app.getPath('userData')).toEqual(electronUserDataPath); }); it('leaves userData unchanged if no write access to electronUserData folder', () => { if (process.platform === 'win32') return; fs.mkdirSync(electronUserDataPath); fs.chmodSync(electronUserDataPath, 444); atomPaths.setUserData(app); fs.chmodSync(electronUserDataPath, 666); expect(app.getPath('userData')).toEqual(defaultElectronUserDataPath); }); }); describe('when an electronUserDataPath folder does not exist', () => { it('leaves userData app path unchanged', () => { atomPaths.setUserData(app); expect(app.getPath('userData')).toEqual(defaultElectronUserDataPath); }); }); }); }); ================================================ FILE: spec/atom-protocol-handler-spec.js ================================================ describe('"atom" protocol URL', () => it('sends the file relative in the package as response', function() { let called = false; const request = new XMLHttpRequest(); request.addEventListener('load', () => (called = true)); request.open('GET', 'atom://async/package.json', true); request.send(); waitsFor('request to be done', () => called === true); })); ================================================ FILE: spec/atom-reporter.coffee ================================================ path = require 'path' process = require 'process' _ = require 'underscore-plus' grim = require 'grim' listen = require '../src/delegated-listener' ipcHelpers = require '../src/ipc-helpers' formatStackTrace = (spec, message='', stackTrace) -> return stackTrace unless stackTrace # at ... (.../jasmine.js:1:2) jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/ # at jasmine.Something... (.../jasmine.js:1:2) firstJasmineLinePattern = /^\s*at\s+jasmine\.[A-Z][^\s]*\s+\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/ lines = [] for line in stackTrace.split('\n') break if firstJasmineLinePattern.test(line) lines.push(line) unless jasminePattern.test(line) # Remove first line of stack when it is the same as the error message errorMatch = lines[0]?.match(/^Error: (.*)/) lines.shift() if message.trim() is errorMatch?[1]?.trim() lines = lines.map (line) -> # Only format actual stacktrace lines if /^\s*at\s/.test(line) # Needs to occur before path relativization if process.platform is 'win32' and /file:\/\/\//.test(line) # file:///C:/some/file -> C:\some\file line = line.replace('file:///', '').replace(///#{path.posix.sep}///g, path.win32.sep) line = line.trim() # at jasmine.Spec. (path:1:2) -> at path:1:2 .replace(/^at jasmine\.Spec\. \(([^)]+)\)/, 'at $1') # at jasmine.Spec.it (path:1:2) -> at path:1:2 .replace(/^at jasmine\.Spec\.f*it \(([^)]+)\)/, 'at $1') # at it (path:1:2) -> at path:1:2 .replace(/^at f*it \(([^)]+)\)/, 'at $1') # at spec/file-test.js -> at file-test.js .replace(spec.specDirectory + path.sep, '') return line lines.join('\n').trim() module.exports = class AtomReporter constructor: -> @element = document.createElement('div') @element.classList.add('spec-reporter-container') @element.innerHTML = """
          """ for element in @element.querySelectorAll('[outlet]') this[element.getAttribute('outlet')] = element startedAt: null runningSpecCount: 0 completeSpecCount: 0 passedCount: 0 failedCount: 0 skippedCount: 0 totalSpecCount: 0 deprecationCount: 0 @timeoutId: 0 reportRunnerStarting: (runner) -> @handleEvents() @startedAt = Date.now() specs = runner.specs() @totalSpecCount = specs.length @addSpecs(specs) document.body.appendChild(@element) reportRunnerResults: (runner) -> @updateSpecCounts() if @failedCount is 0 @status.classList.add('alert-success') @status.classList.remove('alert-info') if @failedCount is 1 @message.textContent = "#{@failedCount} failure" else @message.textContent = "#{@failedCount} failures" reportSuiteResults: (suite) -> reportSpecResults: (spec) -> @completeSpecCount++ spec.endedAt = Date.now() @specComplete(spec) @updateStatusView(spec) reportSpecStarting: (spec) -> @specStarted(spec) handleEvents: -> listen document, 'click', '.spec-toggle', (event) -> specFailures = event.currentTarget.parentElement.querySelector('.spec-failures') if specFailures.style.display is 'none' specFailures.style.display = '' event.currentTarget.classList.remove('folded') else specFailures.style.display = 'none' event.currentTarget.classList.add('folded') event.preventDefault() listen document, 'click', '.deprecation-list', (event) -> deprecationList = event.currentTarget.parentElement.querySelector('.deprecation-list') if deprecationList.style.display is 'none' deprecationList.style.display = '' event.currentTarget.classList.remove('folded') else deprecationList.style.display = 'none' event.currentTarget.classList.add('folded') event.preventDefault() listen document, 'click', '.stack-trace', (event) -> event.currentTarget.classList.toggle('expanded') @reloadButton.addEventListener('click', -> ipcHelpers.call('window-method', 'reload')) updateSpecCounts: -> if @skippedCount specCount = "#{@completeSpecCount - @skippedCount}/#{@totalSpecCount - @skippedCount} (#{@skippedCount} skipped)" else specCount = "#{@completeSpecCount}/#{@totalSpecCount}" @specCount.textContent = specCount updateStatusView: (spec) -> if @failedCount > 0 @status.classList.add('alert-danger') @status.classList.remove('alert-info') @updateSpecCounts() rootSuite = spec.suite rootSuite = rootSuite.parentSuite while rootSuite.parentSuite @message.textContent = rootSuite.description time = "#{Math.round((spec.endedAt - @startedAt) / 10)}" time = "0#{time}" if time.length < 3 @time.textContent = "#{time[0...-2]}.#{time[-2..]}s" specTitle: (spec) -> parentDescs = [] s = spec.suite while s parentDescs.unshift(s.description) s = s.parentSuite suiteString = "" indent = "" for desc in parentDescs suiteString += indent + desc + "\n" indent += " " "#{suiteString} #{indent} it #{spec.description}" addSpecs: (specs) -> coreSpecs = 0 bundledPackageSpecs = 0 userPackageSpecs = 0 for spec in specs symbol = document.createElement('li') symbol.setAttribute('id', "spec-summary-#{spec.id}") symbol.setAttribute('title', @specTitle(spec)) symbol.className = "spec-summary pending" switch spec.specType when 'core' coreSpecs++ @coreSummary.appendChild symbol when 'bundled' bundledPackageSpecs++ @bundledSummary.appendChild symbol when 'user' userPackageSpecs++ @userSummary.appendChild symbol if coreSpecs > 0 @coreHeader.textContent = "Core Specs (#{coreSpecs})" else @coreArea.style.display = 'none' if bundledPackageSpecs > 0 @bundledHeader.textContent = "Bundled Package Specs (#{bundledPackageSpecs})" else @bundledArea.style.display = 'none' if userPackageSpecs > 0 if coreSpecs is 0 and bundledPackageSpecs is 0 # Package specs being run, show a more descriptive label {specDirectory} = specs[0] packageFolderName = path.basename(path.dirname(specDirectory)) packageName = _.undasherize(_.uncamelcase(packageFolderName)) @userHeader.textContent = "#{packageName} Specs" else @userHeader.textContent = "User Package Specs (#{userPackageSpecs})" else @userArea.style.display = 'none' specStarted: (spec) -> @runningSpecCount++ specComplete: (spec) -> specSummaryElement = document.getElementById("spec-summary-#{spec.id}") specSummaryElement.classList.remove('pending') results = spec.results() if results.skipped specSummaryElement.classList.add("skipped") @skippedCount++ else if results.passed() specSummaryElement.classList.add("passed") @passedCount++ else specSummaryElement.classList.add("failed") specView = new SpecResultView(spec) specView.attach() @failedCount++ class SuiteResultView constructor: (@suite) -> @element = document.createElement('div') @element.className = 'suite' @element.setAttribute('id', "suite-view-#{@suite.id}") @description = document.createElement('div') @description.className = 'description' @description.textContent = @suite.description @element.appendChild(@description) attach: -> (@parentSuiteView() or document.querySelector('.results')).appendChild(@element) parentSuiteView: -> return unless @suite.parentSuite unless suiteViewElement = document.querySelector("#suite-view-#{@suite.parentSuite.id}") suiteView = new SuiteResultView(@suite.parentSuite) suiteView.attach() suiteViewElement = suiteView.element suiteViewElement class SpecResultView constructor: (@spec) -> @element = document.createElement('div') @element.className = 'spec' @element.innerHTML = """
          """ @description = @element.querySelector('[outlet="description"]') @specFailures = @element.querySelector('[outlet="specFailures"]') @element.classList.add("spec-view-#{@spec.id}") description = @spec.description description = "it #{description}" if description.indexOf('it ') isnt 0 @description.textContent = description for result in @spec.results().getItems() when not result.passed() stackTrace = formatStackTrace(@spec, result.message, result.trace.stack) resultElement = document.createElement('div') resultElement.className = 'result-message fail' resultElement.textContent = result.message @specFailures.appendChild(resultElement) if stackTrace traceElement = document.createElement('pre') traceElement.className = 'stack-trace padded' traceElement.textContent = stackTrace @specFailures.appendChild(traceElement) attach: -> @parentSuiteView().appendChild(@element) parentSuiteView: -> unless suiteViewElement = document.querySelector("#suite-view-#{@spec.suite.id}") suiteView = new SuiteResultView(@spec.suite) suiteView.attach() suiteViewElement = suiteView.element suiteViewElement ================================================ FILE: spec/auto-update-manager-spec.js ================================================ const AutoUpdateManager = require('../src/auto-update-manager'); const { remote } = require('electron'); const electronAutoUpdater = remote.require('electron').autoUpdater; describe('AutoUpdateManager (renderer)', () => { if (process.platform !== 'darwin') return; // Tests are tied to electron autoUpdater, we use something else on Linux and Win32 let autoUpdateManager; beforeEach(() => { autoUpdateManager = new AutoUpdateManager({ applicationDelegate: atom.applicationDelegate }); autoUpdateManager.initialize(); }); afterEach(() => { autoUpdateManager.destroy(); }); describe('::onDidBeginCheckingForUpdate', () => { it('subscribes to "did-begin-checking-for-update" event', () => { const spy = jasmine.createSpy('spy'); autoUpdateManager.onDidBeginCheckingForUpdate(spy); electronAutoUpdater.emit('checking-for-update'); waitsFor(() => { return spy.callCount === 1; }); }); }); describe('::onDidBeginDownloadingUpdate', () => { it('subscribes to "did-begin-downloading-update" event', () => { const spy = jasmine.createSpy('spy'); autoUpdateManager.onDidBeginDownloadingUpdate(spy); electronAutoUpdater.emit('update-available'); waitsFor(() => { return spy.callCount === 1; }); }); }); describe('::onDidCompleteDownloadingUpdate', () => { it('subscribes to "did-complete-downloading-update" event', () => { const spy = jasmine.createSpy('spy'); autoUpdateManager.onDidCompleteDownloadingUpdate(spy); electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3'); waitsFor(() => { return spy.callCount === 1; }); runs(() => { expect(spy.mostRecentCall.args[0].releaseVersion).toBe('1.2.3'); }); }); }); describe('::onUpdateNotAvailable', () => { it('subscribes to "update-not-available" event', () => { const spy = jasmine.createSpy('spy'); autoUpdateManager.onUpdateNotAvailable(spy); electronAutoUpdater.emit('update-not-available'); waitsFor(() => { return spy.callCount === 1; }); }); }); describe('::onUpdateError', () => { it('subscribes to "update-error" event', () => { const spy = jasmine.createSpy('spy'); autoUpdateManager.onUpdateError(spy); electronAutoUpdater.emit('error', {}, 'an error message'); waitsFor(() => spy.callCount === 1); runs(() => expect(autoUpdateManager.getErrorMessage()).toBe('an error message') ); }); }); describe('::platformSupportsUpdates', () => { let state, releaseChannel; it('returns true on macOS and Windows when in stable', () => { spyOn(autoUpdateManager, 'getState').andCallFake(() => state); spyOn(atom, 'getReleaseChannel').andCallFake(() => releaseChannel); state = 'idle'; releaseChannel = 'stable'; expect(autoUpdateManager.platformSupportsUpdates()).toBe(true); state = 'idle'; releaseChannel = 'dev'; expect(autoUpdateManager.platformSupportsUpdates()).toBe(false); state = 'unsupported'; releaseChannel = 'stable'; expect(autoUpdateManager.platformSupportsUpdates()).toBe(false); state = 'unsupported'; releaseChannel = 'dev'; expect(autoUpdateManager.platformSupportsUpdates()).toBe(false); }); }); describe('::destroy', () => { it('unsubscribes from all events', () => { const spy = jasmine.createSpy('spy'); const doneIndicator = jasmine.createSpy('spy'); atom.applicationDelegate.onUpdateNotAvailable(doneIndicator); autoUpdateManager.onDidBeginCheckingForUpdate(spy); autoUpdateManager.onDidBeginDownloadingUpdate(spy); autoUpdateManager.onDidCompleteDownloadingUpdate(spy); autoUpdateManager.onUpdateNotAvailable(spy); autoUpdateManager.destroy(); electronAutoUpdater.emit('checking-for-update'); electronAutoUpdater.emit('update-available'); electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3'); electronAutoUpdater.emit('update-not-available'); waitsFor(() => { return doneIndicator.callCount === 1; }); runs(() => { expect(spy.callCount).toBe(0); }); }); }); }); ================================================ FILE: spec/babel-spec.js ================================================ // Users may have this environment variable set. Currently, it causes babel to // log to stderr, which causes errors on Windows. // See https://github.com/atom/electron/issues/2033 process.env.DEBUG = '*'; const path = require('path'); const temp = require('temp').track(); const CompileCache = require('../src/compile-cache'); describe('Babel transpiler support', function() { let originalCacheDir = null; beforeEach(function() { originalCacheDir = CompileCache.getCacheDirectory(); CompileCache.setCacheDirectory(temp.mkdirSync('compile-cache')); // TODO: rework to avoid using IIFE https://developer.mozilla.org/en-US/docs/Glossary/IIFE return (() => { const result = []; for (let cacheKey of Object.keys(require.cache)) { if (cacheKey.startsWith(path.join(__dirname, 'fixtures', 'babel'))) { result.push(delete require.cache[cacheKey]); } else { result.push(undefined); } } return result; })(); }); afterEach(function() { CompileCache.setCacheDirectory(originalCacheDir); try { return temp.cleanupSync(); } catch (error) {} }); describe('when a .js file starts with /** @babel */;', () => it('transpiles it using babel', function() { const transpiled = require('./fixtures/babel/babel-comment.js'); expect(transpiled(3)).toBe(4); })); describe("when a .js file starts with 'use babel';", () => it('transpiles it using babel', function() { const transpiled = require('./fixtures/babel/babel-single-quotes.js'); expect(transpiled(3)).toBe(4); })); describe('when a .js file starts with "use babel";', () => it('transpiles it using babel', function() { const transpiled = require('./fixtures/babel/babel-double-quotes.js'); expect(transpiled(3)).toBe(4); })); describe('when a .js file starts with /* @flow */', () => it('transpiles it using babel', function() { const transpiled = require('./fixtures/babel/flow-comment.js'); expect(transpiled(3)).toBe(4); })); describe('when a .js file starts with // @flow', () => it('transpiles it using babel', function() { const transpiled = require('./fixtures/babel/flow-slash-comment.js'); expect(transpiled(3)).toBe(4); })); describe("when a .js file does not start with 'use babel';", function() { it('does not transpile it using babel', function() { spyOn(console, 'error'); expect(() => require('./fixtures/babel/invalid.js')).toThrow(); }); it('does not try to log to stdout or stderr while parsing the file', function() { spyOn(process.stderr, 'write'); spyOn(process.stdout, 'write'); require('./fixtures/babel/babel-double-quotes.js'); expect(process.stdout.write).not.toHaveBeenCalled(); expect(process.stderr.write).not.toHaveBeenCalled(); }); }); }); ================================================ FILE: spec/buffered-node-process-spec.js ================================================ /* eslint-disable no-new */ const path = require('path'); const BufferedNodeProcess = require('../src/buffered-node-process'); describe('BufferedNodeProcess', function() { it('executes the script in a new process', function() { const exit = jasmine.createSpy('exitCallback'); let output = ''; const stdout = lines => (output += lines); let error = ''; const stderr = lines => (error += lines); const args = ['hi']; const command = path.join(__dirname, 'fixtures', 'script.js'); new BufferedNodeProcess({ command, args, stdout, stderr, exit }); waitsFor(() => exit.callCount === 1); runs(function() { expect(output).toBe('hi'); expect(error).toBe(''); expect(args).toEqual(['hi']); }); }); it('suppresses deprecations in the new process', function() { const exit = jasmine.createSpy('exitCallback'); let output = ''; const stdout = lines => (output += lines); let error = ''; const stderr = lines => (error += lines); const command = path.join( __dirname, 'fixtures', 'script-with-deprecations.js' ); new BufferedNodeProcess({ command, stdout, stderr, exit }); waitsFor(() => exit.callCount === 1); runs(function() { expect(output).toBe('hi'); expect(error).toBe(''); }); }); }); ================================================ FILE: spec/buffered-process-spec.js ================================================ /* eslint-disable no-new */ const ChildProcess = require('child_process'); const path = require('path'); const fs = require('fs-plus'); const BufferedProcess = require('../src/buffered-process'); describe('BufferedProcess', function() { describe('when a bad command is specified', function() { let [oldOnError] = []; beforeEach(function() { oldOnError = window.onerror; window.onerror = jasmine.createSpy(); }); afterEach(() => (window.onerror = oldOnError)); describe('when there is an error handler specified', function() { describe('when an error event is emitted by the process', () => it('calls the error handler and does not throw an exception', function() { const bufferedProcess = new BufferedProcess({ command: 'bad-command-nope1', args: ['nothing'], options: { shell: false } }); const errorSpy = jasmine .createSpy() .andCallFake(error => error.handle()); bufferedProcess.onWillThrowError(errorSpy); waitsFor(() => errorSpy.callCount > 0); runs(function() { expect(window.onerror).not.toHaveBeenCalled(); expect(errorSpy).toHaveBeenCalled(); expect(errorSpy.mostRecentCall.args[0].error.message).toContain( 'spawn bad-command-nope1 ENOENT' ); }); })); describe('when an error is thrown spawning the process', () => it('calls the error handler and does not throw an exception', function() { spyOn(ChildProcess, 'spawn').andCallFake(function() { const error = new Error('Something is really wrong'); error.code = 'EAGAIN'; throw error; }); const bufferedProcess = new BufferedProcess({ command: 'ls', args: [], options: {} }); const errorSpy = jasmine .createSpy() .andCallFake(error => error.handle()); bufferedProcess.onWillThrowError(errorSpy); waitsFor(() => errorSpy.callCount > 0); runs(function() { expect(window.onerror).not.toHaveBeenCalled(); expect(errorSpy).toHaveBeenCalled(); expect(errorSpy.mostRecentCall.args[0].error.message).toContain( 'Something is really wrong' ); }); })); }); describe('when there is not an error handler specified', () => it('does throw an exception', function() { new BufferedProcess({ command: 'bad-command-nope2', args: ['nothing'], options: { shell: false } }); waitsFor(() => window.onerror.callCount > 0); runs(function() { expect(window.onerror).toHaveBeenCalled(); expect(window.onerror.mostRecentCall.args[0]).toContain( 'Failed to spawn command `bad-command-nope2`' ); expect(window.onerror.mostRecentCall.args[4].name).toBe( 'BufferedProcessError' ); }); })); }); describe('when autoStart is false', () => it('doesnt start unless start method is called', function() { let stdout = ''; let stderr = ''; const exitCallback = jasmine.createSpy('exit callback'); const apmProcess = new BufferedProcess({ autoStart: false, command: atom.packages.getApmPath(), args: ['-h'], options: {}, stdout(lines) { stdout += lines; }, stderr(lines) { stderr += lines; }, exit: exitCallback }); expect(apmProcess.started).not.toBe(true); apmProcess.start(); expect(apmProcess.started).toBe(true); waitsFor(() => exitCallback.callCount === 1); runs(function() { expect(stderr).toContain('apm - Atom Package Manager'); expect(stdout).toEqual(''); }); })); it('calls the specified stdout, stderr, and exit callbacks', function() { let stdout = ''; let stderr = ''; const exitCallback = jasmine.createSpy('exit callback'); new BufferedProcess({ command: atom.packages.getApmPath(), args: ['-h'], options: {}, stdout(lines) { stdout += lines; }, stderr(lines) { stderr += lines; }, exit: exitCallback }); waitsFor(() => exitCallback.callCount === 1); runs(function() { expect(stderr).toContain('apm - Atom Package Manager'); expect(stdout).toEqual(''); }); }); it('calls the specified stdout callback with whole lines', function() { const exitCallback = jasmine.createSpy('exit callback'); const loremPath = require.resolve('./fixtures/lorem.txt'); const content = fs.readFileSync(loremPath).toString(); let stdout = ''; let allLinesEndWithNewline = true; new BufferedProcess({ command: process.platform === 'win32' ? 'type' : 'cat', args: [loremPath], options: {}, stdout(lines) { const endsWithNewline = lines.charAt(lines.length - 1) === '\n'; if (!endsWithNewline) { allLinesEndWithNewline = false; } stdout += lines; }, exit: exitCallback }); waitsFor(() => exitCallback.callCount === 1); runs(function() { expect(allLinesEndWithNewline).toBe(true); expect(stdout).toBe(content); }); }); describe('on Windows', function() { let originalPlatform = null; beforeEach(function() { // Prevent any commands from actually running and affecting the host spyOn(ChildProcess, 'spawn'); originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'win32' }); }); afterEach(() => Object.defineProperty(process, 'platform', { value: originalPlatform }) ); describe('when the explorer command is spawned on Windows', () => it("doesn't quote arguments of the form /root,C...", function() { new BufferedProcess({ command: 'explorer.exe', args: ['/root,C:\\foo'] }); expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe( '"explorer.exe /root,C:\\foo"' ); })); it('spawns the command using a cmd.exe wrapper when options.shell is undefined', function() { new BufferedProcess({ command: 'dir' }); expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe( 'cmd.exe' ); expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe('/s'); expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe('/d'); expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe('/c'); expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe('"dir"'); }); }); }); ================================================ FILE: spec/clipboard-spec.js ================================================ describe('Clipboard', () => { describe('write(text, metadata) and read()', () => { it('writes and reads text to/from the native clipboard', () => { expect(atom.clipboard.read()).toBe('initial clipboard content'); atom.clipboard.write('next'); expect(atom.clipboard.read()).toBe('next'); }); it('returns metadata if the item on the native clipboard matches the last written item', () => { atom.clipboard.write('next', { meta: 'data' }); expect(atom.clipboard.read()).toBe('next'); expect(atom.clipboard.readWithMetadata().text).toBe('next'); expect(atom.clipboard.readWithMetadata().metadata).toEqual({ meta: 'data' }); }); }); describe('line endings', () => { let originalPlatform = process.platform; const eols = new Map([ ['win32', '\r\n'], ['darwin', '\n'], ['linux', '\n'] ]); for (let [platform, eol] of eols) { it(`converts line endings to the OS's native line endings on ${platform}`, () => { Object.defineProperty(process, 'platform', { value: platform }); atom.clipboard.write('next\ndone\r\n\n', { meta: 'data' }); expect(atom.clipboard.readWithMetadata()).toEqual({ text: `next${eol}done${eol}${eol}`, metadata: { meta: 'data' } }); Object.defineProperty(process, 'platform', { value: originalPlatform }); }); } }); }); ================================================ FILE: spec/command-installer-spec.js ================================================ const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); const CommandInstaller = require('../src/command-installer'); describe('CommandInstaller on #darwin', () => { let installer, resourcesPath, installationPath, atomBinPath, apmBinPath; beforeEach(() => { installationPath = temp.mkdirSync('atom-bin'); resourcesPath = temp.mkdirSync('atom-app'); atomBinPath = path.join(resourcesPath, 'app', 'atom.sh'); apmBinPath = path.join( resourcesPath, 'app', 'apm', 'node_modules', '.bin', 'apm' ); fs.writeFileSync(atomBinPath, ''); fs.writeFileSync(apmBinPath, ''); fs.chmodSync(atomBinPath, '755'); fs.chmodSync(apmBinPath, '755'); spyOn(CommandInstaller.prototype, 'getResourcesDirectory').andReturn( resourcesPath ); spyOn(CommandInstaller.prototype, 'getInstallDirectory').andReturn( installationPath ); }); afterEach(() => { try { temp.cleanupSync(); } catch (error) {} }); it('shows an error dialog when installing commands interactively fails', () => { const appDelegate = jasmine.createSpyObj('appDelegate', ['confirm']); installer = new CommandInstaller(appDelegate); installer.initialize('2.0.2'); spyOn(installer, 'installAtomCommand').andCallFake((__, callback) => callback(new Error('an error')) ); installer.installShellCommandsInteractively(); expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({ message: 'Failed to install shell commands', detail: 'an error' }); appDelegate.confirm.reset(); installer.installAtomCommand.andCallFake((__, callback) => callback()); spyOn(installer, 'installApmCommand').andCallFake((__, callback) => callback(new Error('another error')) ); installer.installShellCommandsInteractively(); expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({ message: 'Failed to install shell commands', detail: 'another error' }); }); it('shows a success dialog when installing commands interactively succeeds', () => { const appDelegate = jasmine.createSpyObj('appDelegate', ['confirm']); installer = new CommandInstaller(appDelegate); installer.initialize('2.0.2'); spyOn(installer, 'installAtomCommand').andCallFake((__, callback) => callback(undefined, 'atom') ); spyOn(installer, 'installApmCommand').andCallFake((__, callback) => callback(undefined, 'apm') ); installer.installShellCommandsInteractively(); expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({ message: 'Commands installed.', detail: 'The shell commands `atom` and `apm` are installed.' }); }); describe('when using a stable version of atom', () => { beforeEach(() => { installer = new CommandInstaller(); installer.initialize('2.0.2'); }); it("symlinks the atom command as 'atom'", () => { const installedAtomPath = path.join(installationPath, 'atom'); expect(fs.isFileSync(installedAtomPath)).toBeFalsy(); waitsFor(done => { installer.installAtomCommand(false, error => { expect(error).toBeNull(); expect(fs.realpathSync(installedAtomPath)).toBe( fs.realpathSync(atomBinPath) ); expect(fs.isExecutableSync(installedAtomPath)).toBe(true); expect(fs.isFileSync(path.join(installationPath, 'atom-beta'))).toBe( false ); done(); }); }); }); it("symlinks the apm command as 'apm'", () => { const installedApmPath = path.join(installationPath, 'apm'); expect(fs.isFileSync(installedApmPath)).toBeFalsy(); waitsFor(done => { installer.installApmCommand(false, error => { expect(error).toBeNull(); expect(fs.realpathSync(installedApmPath)).toBe( fs.realpathSync(apmBinPath) ); expect(fs.isExecutableSync(installedApmPath)).toBeTruthy(); expect(fs.isFileSync(path.join(installationPath, 'apm-beta'))).toBe( false ); done(); }); }); }); }); describe('when using a beta version of atom', () => { beforeEach(() => { installer = new CommandInstaller(); installer.initialize('2.2.0-beta.0'); }); it("symlinks the atom command as 'atom-beta'", () => { const installedAtomPath = path.join(installationPath, 'atom-beta'); expect(fs.isFileSync(installedAtomPath)).toBeFalsy(); waitsFor(done => { installer.installAtomCommand(false, error => { expect(error).toBeNull(); expect(fs.realpathSync(installedAtomPath)).toBe( fs.realpathSync(atomBinPath) ); expect(fs.isExecutableSync(installedAtomPath)).toBe(true); expect(fs.isFileSync(path.join(installationPath, 'atom'))).toBe( false ); done(); }); }); }); it("symlinks the apm command as 'apm-beta'", () => { const installedApmPath = path.join(installationPath, 'apm-beta'); expect(fs.isFileSync(installedApmPath)).toBeFalsy(); waitsFor(done => { installer.installApmCommand(false, error => { expect(error).toBeNull(); expect(fs.realpathSync(installedApmPath)).toBe( fs.realpathSync(apmBinPath) ); expect(fs.isExecutableSync(installedApmPath)).toBeTruthy(); expect(fs.isFileSync(path.join(installationPath, 'apm'))).toBe(false); done(); }); }); }); }); describe('when using a nightly version of atom', () => { beforeEach(() => { installer = new CommandInstaller(); installer.initialize('2.2.0-nightly0'); }); it("symlinks the atom command as 'atom-nightly'", () => { const installedAtomPath = path.join(installationPath, 'atom-nightly'); expect(fs.isFileSync(installedAtomPath)).toBeFalsy(); waitsFor(done => { installer.installAtomCommand(false, error => { expect(error).toBeNull(); expect(fs.realpathSync(installedAtomPath)).toBe( fs.realpathSync(atomBinPath) ); expect(fs.isExecutableSync(installedAtomPath)).toBe(true); expect(fs.isFileSync(path.join(installationPath, 'atom'))).toBe( false ); done(); }); }); }); it("symlinks the apm command as 'apm-nightly'", () => { const installedApmPath = path.join(installationPath, 'apm-nightly'); expect(fs.isFileSync(installedApmPath)).toBeFalsy(); waitsFor(done => { installer.installApmCommand(false, error => { expect(error).toBeNull(); expect(fs.realpathSync(installedApmPath)).toBe( fs.realpathSync(apmBinPath) ); expect(fs.isExecutableSync(installedApmPath)).toBeTruthy(); expect(fs.isFileSync(path.join(installationPath, 'nightly'))).toBe( false ); done(); }); }); }); }); }); ================================================ FILE: spec/command-registry-spec.js ================================================ const CommandRegistry = require('../src/command-registry'); const _ = require('underscore-plus'); describe('CommandRegistry', () => { let registry, parent, child, grandchild; beforeEach(() => { parent = document.createElement('div'); child = document.createElement('div'); grandchild = document.createElement('div'); parent.classList.add('parent'); child.classList.add('child'); grandchild.classList.add('grandchild'); child.appendChild(grandchild); parent.appendChild(child); document.querySelector('#jasmine-content').appendChild(parent); registry = new CommandRegistry(); registry.attach(parent); }); afterEach(() => registry.destroy()); describe('when a command event is dispatched on an element', () => { it('invokes callbacks with selectors matching the target', () => { let called = false; registry.add('.grandchild', 'command', function(event) { expect(this).toBe(grandchild); expect(event.type).toBe('command'); expect(event.eventPhase).toBe(Event.BUBBLING_PHASE); expect(event.target).toBe(grandchild); expect(event.currentTarget).toBe(grandchild); called = true; }); grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true })); expect(called).toBe(true); }); it('invokes callbacks with selectors matching ancestors of the target', () => { const calls = []; registry.add('.child', 'command', function(event) { expect(this).toBe(child); expect(event.target).toBe(grandchild); expect(event.currentTarget).toBe(child); calls.push('child'); }); registry.add('.parent', 'command', function(event) { expect(this).toBe(parent); expect(event.target).toBe(grandchild); expect(event.currentTarget).toBe(parent); calls.push('parent'); }); grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true })); expect(calls).toEqual(['child', 'parent']); }); it('invokes inline listeners prior to listeners applied via selectors', () => { const calls = []; registry.add('.grandchild', 'command', () => calls.push('grandchild')); registry.add(child, 'command', () => calls.push('child-inline')); registry.add('.child', 'command', () => calls.push('child')); registry.add('.parent', 'command', () => calls.push('parent')); grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true })); expect(calls).toEqual(['grandchild', 'child-inline', 'child', 'parent']); }); it('orders multiple matching listeners for an element by selector specificity', () => { child.classList.add('foo', 'bar'); const calls = []; registry.add('.foo.bar', 'command', () => calls.push('.foo.bar')); registry.add('.foo', 'command', () => calls.push('.foo')); registry.add('.bar', 'command', () => calls.push('.bar')); // specificity ties favor commands added later, like CSS grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true })); expect(calls).toEqual(['.foo.bar', '.bar', '.foo']); }); it('orders inline listeners by reverse registration order', () => { const calls = []; registry.add(child, 'command', () => calls.push('child1')); registry.add(child, 'command', () => calls.push('child2')); child.dispatchEvent(new CustomEvent('command', { bubbles: true })); expect(calls).toEqual(['child2', 'child1']); }); it('stops bubbling through ancestors when .stopPropagation() is called on the event', () => { const calls = []; registry.add('.parent', 'command', () => calls.push('parent')); registry.add('.child', 'command', () => calls.push('child-2')); registry.add('.child', 'command', event => { calls.push('child-1'); event.stopPropagation(); }); const dispatchedEvent = new CustomEvent('command', { bubbles: true }); spyOn(dispatchedEvent, 'stopPropagation'); grandchild.dispatchEvent(dispatchedEvent); expect(calls).toEqual(['child-1', 'child-2']); expect(dispatchedEvent.stopPropagation).toHaveBeenCalled(); }); it('stops invoking callbacks when .stopImmediatePropagation() is called on the event', () => { const calls = []; registry.add('.parent', 'command', () => calls.push('parent')); registry.add('.child', 'command', () => calls.push('child-2')); registry.add('.child', 'command', event => { calls.push('child-1'); event.stopImmediatePropagation(); }); const dispatchedEvent = new CustomEvent('command', { bubbles: true }); spyOn(dispatchedEvent, 'stopImmediatePropagation'); grandchild.dispatchEvent(dispatchedEvent); expect(calls).toEqual(['child-1']); expect(dispatchedEvent.stopImmediatePropagation).toHaveBeenCalled(); }); it('forwards .preventDefault() calls from the synthetic event to the original', () => { registry.add('.child', 'command', event => event.preventDefault()); const dispatchedEvent = new CustomEvent('command', { bubbles: true }); spyOn(dispatchedEvent, 'preventDefault'); grandchild.dispatchEvent(dispatchedEvent); expect(dispatchedEvent.preventDefault).toHaveBeenCalled(); }); it('forwards .abortKeyBinding() calls from the synthetic event to the original', () => { registry.add('.child', 'command', event => event.abortKeyBinding()); const dispatchedEvent = new CustomEvent('command', { bubbles: true }); dispatchedEvent.abortKeyBinding = jasmine.createSpy('abortKeyBinding'); grandchild.dispatchEvent(dispatchedEvent); expect(dispatchedEvent.abortKeyBinding).toHaveBeenCalled(); }); it('copies non-standard properties from the original event to the synthetic event', () => { let syntheticEvent = null; registry.add('.child', 'command', event => (syntheticEvent = event)); const dispatchedEvent = new CustomEvent('command', { bubbles: true }); dispatchedEvent.nonStandardProperty = 'testing'; grandchild.dispatchEvent(dispatchedEvent); expect(syntheticEvent.nonStandardProperty).toBe('testing'); }); it('allows listeners to be removed via a disposable returned by ::add', () => { let calls = []; const disposable1 = registry.add('.parent', 'command', () => calls.push('parent') ); const disposable2 = registry.add('.child', 'command', () => calls.push('child') ); disposable1.dispose(); grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true })); expect(calls).toEqual(['child']); calls = []; disposable2.dispose(); grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true })); expect(calls).toEqual([]); }); it('allows multiple commands to be registered under one selector when called with an object', () => { let calls = []; const disposable = registry.add('.child', { 'command-1'() { calls.push('command-1'); }, 'command-2'() { calls.push('command-2'); } }); grandchild.dispatchEvent(new CustomEvent('command-1', { bubbles: true })); grandchild.dispatchEvent(new CustomEvent('command-2', { bubbles: true })); expect(calls).toEqual(['command-1', 'command-2']); calls = []; disposable.dispose(); grandchild.dispatchEvent(new CustomEvent('command-1', { bubbles: true })); grandchild.dispatchEvent(new CustomEvent('command-2', { bubbles: true })); expect(calls).toEqual([]); }); it('invokes callbacks registered with ::onWillDispatch and ::onDidDispatch', () => { const sequence = []; registry.onDidDispatch(event => sequence.push(['onDidDispatch', event])); registry.add('.grandchild', 'command', event => sequence.push(['listener', event]) ); registry.onWillDispatch(event => sequence.push(['onWillDispatch', event]) ); grandchild.dispatchEvent(new CustomEvent('command', { bubbles: true })); expect(sequence[0][0]).toBe('onWillDispatch'); expect(sequence[1][0]).toBe('listener'); expect(sequence[2][0]).toBe('onDidDispatch'); expect( sequence[0][1] === sequence[1][1] && sequence[1][1] === sequence[2][1] ).toBe(true); expect(sequence[0][1].constructor).toBe(CustomEvent); expect(sequence[0][1].target).toBe(grandchild); }); }); describe('::add(selector, commandName, callback)', () => { it('throws an error when called with an invalid selector', () => { const badSelector = '<>'; let addError = null; try { registry.add(badSelector, 'foo:bar', () => {}); } catch (error) { addError = error; } expect(addError.message).toContain(badSelector); }); it('throws an error when called with a null callback and selector target', () => { const badCallback = null; expect(() => { registry.add('.selector', 'foo:bar', badCallback); }).toThrow(new Error('Cannot register a command with a null listener.')); }); it('throws an error when called with a null callback and object target', () => { const badCallback = null; expect(() => { registry.add(document.body, 'foo:bar', badCallback); }).toThrow(new Error('Cannot register a command with a null listener.')); }); it('throws an error when called with an object listener without a didDispatch method', () => { const badListener = { title: 'a listener without a didDispatch callback', description: 'this should throw an error' }; expect(() => { registry.add(document.body, 'foo:bar', badListener); }).toThrow( new Error( 'Listener must be a callback function or an object with a didDispatch method.' ) ); }); }); describe('::findCommands({target})', () => { it('returns command descriptors that can be invoked on the target or its ancestors', () => { registry.add('.parent', 'namespace:command-1', () => {}); registry.add('.child', 'namespace:command-2', () => {}); registry.add('.grandchild', 'namespace:command-3', () => {}); registry.add('.grandchild.no-match', 'namespace:command-4', () => {}); registry.add(grandchild, 'namespace:inline-command-1', () => {}); registry.add(child, 'namespace:inline-command-2', () => {}); const commands = registry.findCommands({ target: grandchild }); const nonJqueryCommands = _.reject(commands, cmd => cmd.jQuery); expect(nonJqueryCommands).toEqual([ { name: 'namespace:inline-command-1', displayName: 'Namespace: Inline Command 1' }, { name: 'namespace:command-3', displayName: 'Namespace: Command 3' }, { name: 'namespace:inline-command-2', displayName: 'Namespace: Inline Command 2' }, { name: 'namespace:command-2', displayName: 'Namespace: Command 2' }, { name: 'namespace:command-1', displayName: 'Namespace: Command 1' } ]); }); it('returns command descriptors with arbitrary metadata if set in a listener object', () => { registry.add('.grandchild', 'namespace:command-1', () => {}); registry.add('.grandchild', 'namespace:command-2', { displayName: 'Custom Command 2', metadata: { some: 'other', object: 'data' }, didDispatch() {} }); registry.add('.grandchild', 'namespace:command-3', { name: 'some:other:incorrect:commandname', displayName: 'Custom Command 3', metadata: { some: 'other', object: 'data' }, didDispatch() {} }); const commands = registry.findCommands({ target: grandchild }); expect(commands).toEqual([ { displayName: 'Namespace: Command 1', name: 'namespace:command-1' }, { displayName: 'Custom Command 2', metadata: { some: 'other', object: 'data' }, name: 'namespace:command-2' }, { displayName: 'Custom Command 3', metadata: { some: 'other', object: 'data' }, name: 'namespace:command-3' } ]); }); it('returns command descriptors with arbitrary metadata if set on a listener function', () => { function listener() {} listener.displayName = 'Custom Command 2'; listener.metadata = { some: 'other', object: 'data' }; registry.add('.grandchild', 'namespace:command-2', listener); const commands = registry.findCommands({ target: grandchild }); expect(commands).toEqual([ { displayName: 'Custom Command 2', metadata: { some: 'other', object: 'data' }, name: 'namespace:command-2' } ]); }); }); describe('::dispatch(target, commandName)', () => { it('simulates invocation of the given command ', () => { let called = false; registry.add('.grandchild', 'command', function(event) { expect(this).toBe(grandchild); expect(event.type).toBe('command'); expect(event.eventPhase).toBe(Event.BUBBLING_PHASE); expect(event.target).toBe(grandchild); expect(event.currentTarget).toBe(grandchild); called = true; }); registry.dispatch(grandchild, 'command'); expect(called).toBe(true); }); it('returns a promise if any listeners matched the command', () => { registry.add('.grandchild', 'command', () => {}); expect(registry.dispatch(grandchild, 'command').constructor.name).toBe( 'Promise' ); expect(registry.dispatch(grandchild, 'bogus')).toBe(null); expect(registry.dispatch(parent, 'command')).toBe(null); }); it('returns a promise that resolves when the listeners resolve', async () => { jasmine.useRealClock(); registry.add('.grandchild', 'command', () => 1); registry.add('.grandchild', 'command', () => Promise.resolve(2)); registry.add( '.grandchild', 'command', () => new Promise(resolve => { setTimeout(() => { resolve(3); }, 1); }) ); const values = await registry.dispatch(grandchild, 'command'); expect(values).toEqual([3, 2, 1]); }); it('returns a promise that rejects when a listener is rejected', async () => { jasmine.useRealClock(); registry.add('.grandchild', 'command', () => 1); registry.add('.grandchild', 'command', () => Promise.resolve(2)); registry.add( '.grandchild', 'command', () => new Promise((resolve, reject) => { setTimeout(() => { reject(3); // eslint-disable-line prefer-promise-reject-errors }, 1); }) ); let value; try { value = await registry.dispatch(grandchild, 'command'); } catch (err) { value = err; } expect(value).toBe(3); }); }); describe('::getSnapshot and ::restoreSnapshot', () => it('removes all command handlers except for those in the snapshot', () => { registry.add('.parent', 'namespace:command-1', () => {}); registry.add('.child', 'namespace:command-2', () => {}); const snapshot = registry.getSnapshot(); registry.add('.grandchild', 'namespace:command-3', () => {}); expect(registry.findCommands({ target: grandchild }).slice(0, 3)).toEqual( [ { name: 'namespace:command-3', displayName: 'Namespace: Command 3' }, { name: 'namespace:command-2', displayName: 'Namespace: Command 2' }, { name: 'namespace:command-1', displayName: 'Namespace: Command 1' } ] ); registry.restoreSnapshot(snapshot); expect(registry.findCommands({ target: grandchild }).slice(0, 2)).toEqual( [ { name: 'namespace:command-2', displayName: 'Namespace: Command 2' }, { name: 'namespace:command-1', displayName: 'Namespace: Command 1' } ] ); registry.add('.grandchild', 'namespace:command-3', () => {}); registry.restoreSnapshot(snapshot); expect(registry.findCommands({ target: grandchild }).slice(0, 2)).toEqual( [ { name: 'namespace:command-2', displayName: 'Namespace: Command 2' }, { name: 'namespace:command-1', displayName: 'Namespace: Command 1' } ] ); })); describe('::attach(rootNode)', () => it('adds event listeners for any previously-added commands', () => { const registry2 = new CommandRegistry(); const commandSpy = jasmine.createSpy('command-callback'); registry2.add('.grandchild', 'command-1', commandSpy); grandchild.dispatchEvent(new CustomEvent('command-1', { bubbles: true })); expect(commandSpy).not.toHaveBeenCalled(); registry2.attach(parent); grandchild.dispatchEvent(new CustomEvent('command-1', { bubbles: true })); expect(commandSpy).toHaveBeenCalled(); })); }); ================================================ FILE: spec/compile-cache-spec.coffee ================================================ path = require 'path' temp = require('temp').track() Babel = require 'babel-core' CoffeeScript = require 'coffee-script' {TypeScriptSimple} = require 'typescript-simple' CSON = require 'season' CompileCache = require '../src/compile-cache' describe 'CompileCache', -> [atomHome, fixtures] = [] beforeEach -> fixtures = atom.project.getPaths()[0] atomHome = temp.mkdirSync('fake-atom-home') CSON.setCacheDir(null) CompileCache.resetCacheStats() spyOn(Babel, 'transform').andReturn {code: 'the-babel-code'} spyOn(CoffeeScript, 'compile').andReturn 'the-coffee-code' spyOn(TypeScriptSimple::, 'compile').andReturn 'the-typescript-code' afterEach -> CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME) CSON.setCacheDir(CompileCache.getCacheDirectory()) try temp.cleanupSync() describe 'addPathToCache(filePath, atomHome)', -> describe 'when the given file is plain javascript', -> it 'does not compile or cache the file', -> CompileCache.addPathToCache(path.join(fixtures, 'sample.js'), atomHome) expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 0, misses: 0} describe 'when the given file uses babel', -> it 'compiles the file with babel and caches it', -> CompileCache.addPathToCache(path.join(fixtures, 'babel', 'babel-comment.js'), atomHome) expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 0, misses: 1} expect(Babel.transform.callCount).toBe 1 CompileCache.addPathToCache(path.join(fixtures, 'babel', 'babel-comment.js'), atomHome) expect(CompileCache.getCacheStats()['.js']).toEqual {hits: 1, misses: 1} expect(Babel.transform.callCount).toBe 1 describe 'when the given file is coffee-script', -> it 'compiles the file with coffee-script and caches it', -> CompileCache.addPathToCache(path.join(fixtures, 'coffee.coffee'), atomHome) expect(CompileCache.getCacheStats()['.coffee']).toEqual {hits: 0, misses: 1} expect(CoffeeScript.compile.callCount).toBe 1 CompileCache.addPathToCache(path.join(fixtures, 'coffee.coffee'), atomHome) expect(CompileCache.getCacheStats()['.coffee']).toEqual {hits: 1, misses: 1} expect(CoffeeScript.compile.callCount).toBe 1 describe 'when the given file is typescript', -> it 'compiles the file with typescript and caches it', -> CompileCache.addPathToCache(path.join(fixtures, 'typescript', 'valid.ts'), atomHome) expect(CompileCache.getCacheStats()['.ts']).toEqual {hits: 0, misses: 1} expect(TypeScriptSimple::compile.callCount).toBe 1 CompileCache.addPathToCache(path.join(fixtures, 'typescript', 'valid.ts'), atomHome) expect(CompileCache.getCacheStats()['.ts']).toEqual {hits: 1, misses: 1} expect(TypeScriptSimple::compile.callCount).toBe 1 describe 'when the given file is CSON', -> it 'compiles the file to JSON and caches it', -> spyOn(CSON, 'setCacheDir').andCallThrough() spyOn(CSON, 'readFileSync').andCallThrough() CompileCache.addPathToCache(path.join(fixtures, 'cson.cson'), atomHome) expect(CSON.readFileSync).toHaveBeenCalledWith(path.join(fixtures, 'cson.cson')) expect(CSON.setCacheDir).toHaveBeenCalledWith(path.join(atomHome, '/compile-cache')) CSON.readFileSync.reset() CSON.setCacheDir.reset() CompileCache.addPathToCache(path.join(fixtures, 'cson.cson'), atomHome) expect(CSON.readFileSync).toHaveBeenCalledWith(path.join(fixtures, 'cson.cson')) expect(CSON.setCacheDir).not.toHaveBeenCalled() describe 'overriding Error.prepareStackTrace', -> it 'removes the override on the next tick, and always assigns the raw stack', -> return if process.platform is 'win32' # Flakey Error.stack contents on Win32 Error.prepareStackTrace = -> 'a-stack-trace' error = new Error("Oops") expect(error.stack).toBe 'a-stack-trace' expect(Array.isArray(error.getRawStack())).toBe true waits(1) runs -> error = new Error("Oops again") expect(error.stack).toContain('compile-cache-spec.coffee') expect(Array.isArray(error.getRawStack())).toBe true it 'does not infinitely loop when the original prepareStackTrace value is reassigned', -> originalPrepareStackTrace = Error.prepareStackTrace Error.prepareStackTrace = -> 'a-stack-trace' Error.prepareStackTrace = originalPrepareStackTrace error = new Error('Oops') expect(error.stack).toContain('compile-cache-spec.coffee') expect(Array.isArray(error.getRawStack())).toBe true it 'does not infinitely loop when the assigned prepareStackTrace calls the original prepareStackTrace', -> originalPrepareStackTrace = Error.prepareStackTrace Error.prepareStackTrace = (error, stack) -> error.foo = 'bar' originalPrepareStackTrace(error, stack) error = new Error('Oops') expect(error.stack).toContain('compile-cache-spec.coffee') expect(error.foo).toBe('bar') expect(Array.isArray(error.getRawStack())).toBe true ================================================ FILE: spec/config-file-spec.js ================================================ const fs = require('fs-plus'); const path = require('path'); const temp = require('temp').track(); const dedent = require('dedent'); const ConfigFile = require('../src/config-file'); describe('ConfigFile', () => { let filePath, configFile, subscription; beforeEach(async () => { jasmine.useRealClock(); const tempDir = fs.realpathSync(temp.mkdirSync()); filePath = path.join(tempDir, 'the-config.cson'); }); afterEach(() => { subscription.dispose(); }); describe('when the file does not exist', () => { it('returns an empty object from .get()', async () => { configFile = new ConfigFile(filePath); subscription = await configFile.watch(); expect(configFile.get()).toEqual({}); }); }); describe('when the file is empty', () => { it('returns an empty object from .get()', async () => { writeFileSync(filePath, ''); configFile = new ConfigFile(filePath); subscription = await configFile.watch(); expect(configFile.get()).toEqual({}); }); }); describe('when the file is updated with valid CSON', () => { it('notifies onDidChange observers with the data', async () => { configFile = new ConfigFile(filePath); subscription = await configFile.watch(); const event = new Promise(resolve => configFile.onDidChange(resolve)); writeFileSync( filePath, dedent` '*': foo: 'bar' 'javascript': foo: 'baz' ` ); expect(await event).toEqual({ '*': { foo: 'bar' }, javascript: { foo: 'baz' } }); expect(configFile.get()).toEqual({ '*': { foo: 'bar' }, javascript: { foo: 'baz' } }); }); }); describe('when the file is updated with invalid CSON', () => { it('notifies onDidError observers', async () => { configFile = new ConfigFile(filePath); subscription = await configFile.watch(); const message = new Promise(resolve => configFile.onDidError(resolve)); writeFileSync( filePath, dedent` um what? `, 2 ); expect(await message).toContain('Failed to load `the-config.cson`'); const event = new Promise(resolve => configFile.onDidChange(resolve)); writeFileSync( filePath, dedent` '*': foo: 'bar' 'javascript': foo: 'baz' `, 4 ); expect(await event).toEqual({ '*': { foo: 'bar' }, javascript: { foo: 'baz' } }); }); }); describe('ConfigFile.at()', () => { let path0, path1; beforeEach(() => { path0 = filePath; path1 = path.join(fs.realpathSync(temp.mkdirSync()), 'the-config.cson'); configFile = ConfigFile.at(path0); }); it('returns an existing ConfigFile', () => { const cf = ConfigFile.at(path0); expect(cf).toEqual(configFile); }); it('creates a new ConfigFile for unrecognized paths', () => { const cf = ConfigFile.at(path1); expect(cf).not.toEqual(configFile); }); }); }); function writeFileSync(filePath, content, seconds = 2) { const utime = Date.now() / 1000 + seconds; fs.writeFileSync(filePath, content); fs.utimesSync(filePath, utime, utime); } ================================================ FILE: spec/config-spec.js ================================================ describe('Config', () => { let savedSettings; beforeEach(() => { spyOn(console, 'warn'); atom.config.settingsLoaded = true; savedSettings = []; atom.config.saveCallback = function(settings) { savedSettings.push(settings); }; }); describe('.get(keyPath, {scope, sources, excludeSources})', () => { it("allows a key path's value to be read", () => { expect(atom.config.set('foo.bar.baz', 42)).toBe(true); expect(atom.config.get('foo.bar.baz')).toBe(42); expect(atom.config.get('foo.quux')).toBeUndefined(); }); it("returns a deep clone of the key path's value", () => { atom.config.set('value', { array: [1, { b: 2 }, 3] }); const retrievedValue = atom.config.get('value'); retrievedValue.array[0] = 4; retrievedValue.array[1].b = 2.1; expect(atom.config.get('value')).toEqual({ array: [1, { b: 2 }, 3] }); }); it('merges defaults into the returned value if both the assigned value and the default value are objects', () => { atom.config.setDefaults('foo.bar', { baz: 1, ok: 2 }); atom.config.set('foo.bar', { baz: 3 }); expect(atom.config.get('foo.bar')).toEqual({ baz: 3, ok: 2 }); atom.config.setDefaults('other', { baz: 1 }); atom.config.set('other', 7); expect(atom.config.get('other')).toBe(7); atom.config.set('bar.baz', { a: 3 }); atom.config.setDefaults('bar', { baz: 7 }); expect(atom.config.get('bar.baz')).toEqual({ a: 3 }); }); describe("when a 'sources' option is specified", () => it('only retrieves values from the specified sources', () => { atom.config.set('x.y', 1, { scopeSelector: '.foo', source: 'a' }); atom.config.set('x.y', 2, { scopeSelector: '.foo', source: 'b' }); atom.config.set('x.y', 3, { scopeSelector: '.foo', source: 'c' }); atom.config.setSchema('x.y', { type: 'integer', default: 4 }); expect( atom.config.get('x.y', { sources: ['a'], scope: ['.foo'] }) ).toBe(1); expect( atom.config.get('x.y', { sources: ['b'], scope: ['.foo'] }) ).toBe(2); expect( atom.config.get('x.y', { sources: ['c'], scope: ['.foo'] }) ).toBe(3); // Schema defaults never match a specific source. We could potentially add a special "schema" source. expect( atom.config.get('x.y', { sources: ['x'], scope: ['.foo'] }) ).toBeUndefined(); expect( atom.config.get(null, { sources: ['a'], scope: ['.foo'] }).x.y ).toBe(1); })); describe("when an 'excludeSources' option is specified", () => it('only retrieves values from the specified sources', () => { atom.config.set('x.y', 0); atom.config.set('x.y', 1, { scopeSelector: '.foo', source: 'a' }); atom.config.set('x.y', 2, { scopeSelector: '.foo', source: 'b' }); atom.config.set('x.y', 3, { scopeSelector: '.foo', source: 'c' }); atom.config.setSchema('x.y', { type: 'integer', default: 4 }); expect( atom.config.get('x.y', { excludeSources: ['a'], scope: ['.foo'] }) ).toBe(3); expect( atom.config.get('x.y', { excludeSources: ['c'], scope: ['.foo'] }) ).toBe(2); expect( atom.config.get('x.y', { excludeSources: ['b', 'c'], scope: ['.foo'] }) ).toBe(1); expect( atom.config.get('x.y', { excludeSources: ['b', 'c', 'a'], scope: ['.foo'] }) ).toBe(0); expect( atom.config.get('x.y', { excludeSources: ['b', 'c', 'a', atom.config.getUserConfigPath()], scope: ['.foo'] }) ).toBe(4); expect( atom.config.get('x.y', { excludeSources: [atom.config.getUserConfigPath()] }) ).toBe(4); })); describe("when a 'scope' option is given", () => { it('returns the property with the most specific scope selector', () => { atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.double.coffee' }); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source .string.quoted.double' }); atom.config.set('foo.bar.baz', 11, { scopeSelector: '.source' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }) ).toBe(42); expect( atom.config.get('foo.bar.baz', { scope: ['.source.js', '.string.quoted.double.js'] }) ).toBe(22); expect( atom.config.get('foo.bar.baz', { scope: ['.source.js', '.variable.assignment.js'] }) ).toBe(11); expect( atom.config.get('foo.bar.baz', { scope: ['.text'] }) ).toBeUndefined(); }); it('favors the most recently added properties in the event of a specificity tie', () => { atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.single' }); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source.coffee .string.quoted.double' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.single'] }) ).toBe(42); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.single.double'] }) ).toBe(22); }); describe('when there are global defaults', () => it('falls back to the global when there is no scoped property specified', () => { atom.config.setDefaults('foo', { hasDefault: 'ok' }); expect( atom.config.get('foo.hasDefault', { scope: ['.source.coffee', '.string.quoted.single'] }) ).toBe('ok'); })); describe('when package settings are added after user settings', () => it("returns the user's setting because the user's setting has higher priority", () => { atom.config.set('foo.bar.baz', 100, { scopeSelector: '.source.coffee' }); atom.config.set('foo.bar.baz', 1, { scopeSelector: '.source.coffee', source: 'some-package' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(100); })); }); }); describe('.getAll(keyPath, {scope, sources, excludeSources})', () => { it('reads all of the values for a given key-path', () => { expect(atom.config.set('foo', 41)).toBe(true); expect(atom.config.set('foo', 43, { scopeSelector: '.a .b' })).toBe(true); expect(atom.config.set('foo', 42, { scopeSelector: '.a' })).toBe(true); expect(atom.config.set('foo', 44, { scopeSelector: '.a .b.c' })).toBe( true ); expect(atom.config.set('foo', -44, { scopeSelector: '.d' })).toBe(true); expect(atom.config.getAll('foo', { scope: ['.a', '.b.c'] })).toEqual([ { scopeSelector: '.a .b.c', value: 44 }, { scopeSelector: '.a .b', value: 43 }, { scopeSelector: '.a', value: 42 }, { scopeSelector: '*', value: 41 } ]); }); it("includes the schema's default value", () => { atom.config.setSchema('foo', { type: 'number', default: 40 }); expect(atom.config.set('foo', 43, { scopeSelector: '.a .b' })).toBe(true); expect(atom.config.getAll('foo', { scope: ['.a', '.b.c'] })).toEqual([ { scopeSelector: '.a .b', value: 43 }, { scopeSelector: '*', value: 40 } ]); }); }); describe('.set(keyPath, value, {source, scopeSelector})', () => { it("allows a key path's value to be written", () => { expect(atom.config.set('foo.bar.baz', 42)).toBe(true); expect(atom.config.get('foo.bar.baz')).toBe(42); }); it("saves the user's config to disk after it stops changing", () => { atom.config.set('foo.bar.baz', 42); expect(savedSettings.length).toBe(0); atom.config.set('foo.bar.baz', 43); expect(savedSettings.length).toBe(0); atom.config.set('foo.bar.baz', 44); advanceClock(10); expect(savedSettings.length).toBe(1); }); it("does not save when a non-default 'source' is given", () => { atom.config.set('foo.bar.baz', 42, { source: 'some-other-source', scopeSelector: '.a' }); advanceClock(500); expect(savedSettings.length).toBe(0); }); it("does not allow a 'source' option without a 'scopeSelector'", () => { expect(() => atom.config.set('foo', 1, { source: ['.source.ruby'] }) ).toThrow(); }); describe('when the key-path is null', () => it('sets the root object', () => { expect(atom.config.set(null, { editor: { tabLength: 6 } })).toBe(true); expect(atom.config.get('editor.tabLength')).toBe(6); expect( atom.config.set(null, { editor: { tabLength: 8, scopeSelector: ['.source.js'] } }) ).toBe(true); expect( atom.config.get('editor.tabLength', { scope: ['.source.js'] }) ).toBe(8); })); describe('when the value equals the default value', () => it("does not store the value in the user's config", () => { atom.config.setSchema('foo', { type: 'object', properties: { same: { type: 'number', default: 1 }, changes: { type: 'number', default: 1 }, sameArray: { type: 'array', default: [1, 2, 3] }, sameObject: { type: 'object', default: { a: 1, b: 2 } }, null: { type: '*', default: null }, undefined: { type: '*', default: undefined } } }); expect(atom.config.settings.foo).toBeUndefined(); atom.config.set('foo.same', 1); atom.config.set('foo.changes', 2); atom.config.set('foo.sameArray', [1, 2, 3]); atom.config.set('foo.null', undefined); atom.config.set('foo.undefined', null); atom.config.set('foo.sameObject', { b: 2, a: 1 }); const userConfigPath = atom.config.getUserConfigPath(); expect( atom.config.get('foo.same', { sources: [userConfigPath] }) ).toBeUndefined(); expect(atom.config.get('foo.changes')).toBe(2); expect( atom.config.get('foo.changes', { sources: [userConfigPath] }) ).toBe(2); atom.config.set('foo.changes', 1); expect( atom.config.get('foo.changes', { sources: [userConfigPath] }) ).toBeUndefined(); })); describe("when a 'scopeSelector' is given", () => it('sets the value and overrides the others', () => { atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.double.coffee' }); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source .string.quoted.double' }); atom.config.set('foo.bar.baz', 11, { scopeSelector: '.source' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }) ).toBe(42); expect( atom.config.set('foo.bar.baz', 100, { scopeSelector: '.source.coffee .string.quoted.double.coffee' }) ).toBe(true); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }) ).toBe(100); })); }); describe('.unset(keyPath, {source, scopeSelector})', () => { beforeEach(() => atom.config.setSchema('foo', { type: 'object', properties: { bar: { type: 'object', properties: { baz: { type: 'integer', default: 0 }, ok: { type: 'integer', default: 0 } } }, quux: { type: 'integer', default: 0 } } }) ); it('sets the value of the key path to its default', () => { atom.config.setDefaults('a', { b: 3 }); atom.config.set('a.b', 4); expect(atom.config.get('a.b')).toBe(4); atom.config.unset('a.b'); expect(atom.config.get('a.b')).toBe(3); atom.config.set('a.c', 5); expect(atom.config.get('a.c')).toBe(5); atom.config.unset('a.c'); expect(atom.config.get('a.c')).toBeUndefined(); }); it('calls ::save()', () => { atom.config.setDefaults('a', { b: 3 }); atom.config.set('a.b', 4); savedSettings.length = 0; atom.config.unset('a.c'); advanceClock(500); expect(savedSettings.length).toBe(1); }); describe("when no 'scopeSelector' is given", () => { describe("when a 'source' but no key-path is given", () => it('removes all scoped settings with the given source', () => { atom.config.set('foo.bar.baz', 1, { scopeSelector: '.a', source: 'source-a' }); atom.config.set('foo.bar.quux', 2, { scopeSelector: '.b', source: 'source-a' }); expect(atom.config.get('foo.bar', { scope: ['.a.b'] })).toEqual({ baz: 1, quux: 2 }); atom.config.unset(null, { source: 'source-a' }); expect(atom.config.get('foo.bar', { scope: ['.a'] })).toEqual({ baz: 0, ok: 0 }); })); describe("when a 'source' and a key-path is given", () => it('removes all scoped settings with the given source and key-path', () => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2, { scopeSelector: '.a', source: 'source-a' }); atom.config.set('foo.bar.baz', 3, { scopeSelector: '.a.b', source: 'source-b' }); expect(atom.config.get('foo.bar.baz', { scope: ['.a.b'] })).toEqual( 3 ); atom.config.unset('foo.bar.baz', { source: 'source-b' }); expect(atom.config.get('foo.bar.baz', { scope: ['.a.b'] })).toEqual( 2 ); expect(atom.config.get('foo.bar.baz')).toEqual(1); })); describe("when no 'source' is given", () => it('removes all scoped and unscoped properties for that key-path', () => { atom.config.setDefaults('foo.bar', { baz: 100 }); atom.config.set( 'foo.bar', { baz: 1, ok: 2 }, { scopeSelector: '.a' } ); atom.config.set( 'foo.bar', { baz: 11, ok: 12 }, { scopeSelector: '.b' } ); atom.config.set('foo.bar', { baz: 21, ok: 22 }); atom.config.unset('foo.bar.baz'); expect(atom.config.get('foo.bar.baz', { scope: ['.a'] })).toBe(100); expect(atom.config.get('foo.bar.baz', { scope: ['.b'] })).toBe(100); expect(atom.config.get('foo.bar.baz')).toBe(100); expect(atom.config.get('foo.bar.ok', { scope: ['.a'] })).toBe(2); expect(atom.config.get('foo.bar.ok', { scope: ['.b'] })).toBe(12); expect(atom.config.get('foo.bar.ok')).toBe(22); })); }); describe("when a 'scopeSelector' is given", () => { it('restores the global default when no scoped default set', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(55); atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(10); }); it('restores the scoped default when a scoped default is set', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee', source: 'some-source' }); atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee' }); atom.config.set('foo.bar.ok', 100, { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(55); atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(42); expect( atom.config.get('foo.bar.ok', { scope: ['.source.coffee'] }) ).toBe(100); }); it('calls ::save()', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee' }); savedSettings.length = 0; atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); advanceClock(150); expect(savedSettings.length).toBe(1); }); it('allows removing settings for a specific source and scope selector', () => { atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee', source: 'source-a' }); atom.config.set('foo.bar.baz', 65, { scopeSelector: '.source.coffee', source: 'source-b' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(65); atom.config.unset('foo.bar.baz', { source: 'source-b', scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string'] }) ).toBe(55); }); it('allows removing all settings for a specific source', () => { atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee', source: 'source-a' }); atom.config.set('foo.bar.baz', 65, { scopeSelector: '.source.coffee', source: 'source-b' }); atom.config.set('foo.bar.ok', 65, { scopeSelector: '.source.coffee', source: 'source-b' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(65); atom.config.unset(null, { source: 'source-b', scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee', '.string'] }) ).toBe(55); expect( atom.config.get('foo.bar.ok', { scope: ['.source.coffee', '.string'] }) ).toBe(0); }); it('does not call ::save or add a scoped property when no value has been set', () => { // see https://github.com/atom/atom/issues/4175 atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(10); expect(savedSettings.length).toBe(0); const scopedProperties = atom.config.scopedSettingsStore.propertiesForSource( 'user-config' ); expect(scopedProperties['.coffee.source']).toBeUndefined(); }); it('removes the scoped value when it was the only set value on the object', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 55, { scopeSelector: '.source.coffee' }); atom.config.set('foo.bar.ok', 20, { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(55); advanceClock(150); savedSettings.length = 0; atom.config.unset('foo.bar.baz', { scopeSelector: '.source.coffee' }); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(10); expect( atom.config.get('foo.bar.ok', { scope: ['.source.coffee'] }) ).toBe(20); advanceClock(150); expect(savedSettings[0]['.coffee.source']).toEqual({ foo: { bar: { ok: 20 } } }); atom.config.unset('foo.bar.ok', { scopeSelector: '.source.coffee' }); advanceClock(150); expect(savedSettings.length).toBe(2); expect(savedSettings[1]['.coffee.source']).toBeUndefined(); }); it('does not call ::save when the value is already at the default', () => { atom.config.setDefaults('foo', { bar: { baz: 10 } }); atom.config.set('foo.bar.baz', 55); advanceClock(150); savedSettings.length = 0; atom.config.unset('foo.bar.ok', { scopeSelector: '.source.coffee' }); advanceClock(150); expect(savedSettings.length).toBe(0); expect( atom.config.get('foo.bar.baz', { scope: ['.source.coffee'] }) ).toBe(55); }); }); }); describe('.onDidChange(keyPath, {scope})', () => { let observeHandler = []; describe('when a keyPath is specified', () => { beforeEach(() => { observeHandler = jasmine.createSpy('observeHandler'); atom.config.set('foo.bar.baz', 'value 1'); atom.config.onDidChange('foo.bar.baz', observeHandler); }); it('does not fire the given callback with the current value at the keypath', () => expect(observeHandler).not.toHaveBeenCalled()); it('fires the callback every time the observed value changes', () => { atom.config.set('foo.bar.baz', 'value 2'); expect(observeHandler).toHaveBeenCalledWith({ newValue: 'value 2', oldValue: 'value 1' }); observeHandler.reset(); observeHandler.andCallFake(() => { throw new Error('oops'); }); expect(() => atom.config.set('foo.bar.baz', 'value 1')).toThrow('oops'); expect(observeHandler).toHaveBeenCalledWith({ newValue: 'value 1', oldValue: 'value 2' }); observeHandler.reset(); // Regression: exception in earlier handler shouldn't put observer // into a bad state. atom.config.set('something.else', 'new value'); expect(observeHandler).not.toHaveBeenCalled(); }); }); describe('when a keyPath is not specified', () => { beforeEach(() => { observeHandler = jasmine.createSpy('observeHandler'); atom.config.set('foo.bar.baz', 'value 1'); atom.config.onDidChange(observeHandler); }); it('does not fire the given callback initially', () => expect(observeHandler).not.toHaveBeenCalled()); it('fires the callback every time any value changes', () => { observeHandler.reset(); // clear the initial call atom.config.set('foo.bar.baz', 'value 2'); expect(observeHandler).toHaveBeenCalled(); expect(observeHandler.mostRecentCall.args[0].newValue.foo.bar.baz).toBe( 'value 2' ); expect(observeHandler.mostRecentCall.args[0].oldValue.foo.bar.baz).toBe( 'value 1' ); observeHandler.reset(); atom.config.set('foo.bar.baz', 'value 1'); expect(observeHandler).toHaveBeenCalled(); expect(observeHandler.mostRecentCall.args[0].newValue.foo.bar.baz).toBe( 'value 1' ); expect(observeHandler.mostRecentCall.args[0].oldValue.foo.bar.baz).toBe( 'value 2' ); observeHandler.reset(); atom.config.set('foo.bar.int', 1); expect(observeHandler).toHaveBeenCalled(); expect(observeHandler.mostRecentCall.args[0].newValue.foo.bar.int).toBe( 1 ); expect(observeHandler.mostRecentCall.args[0].oldValue.foo.bar.int).toBe( undefined ); }); }); describe("when a 'scope' is given", () => it('calls the supplied callback when the value at the descriptor/keypath changes', () => { const changeSpy = jasmine.createSpy('onDidChange callback'); atom.config.onDidChange( 'foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }, changeSpy ); atom.config.set('foo.bar.baz', 12); expect(changeSpy).toHaveBeenCalledWith({ oldValue: undefined, newValue: 12 }); changeSpy.reset(); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source .string.quoted.double', source: 'a' }); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 12, newValue: 22 }); changeSpy.reset(); atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.double.coffee', source: 'b' }); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 22, newValue: 42 }); changeSpy.reset(); atom.config.unset(null, { scopeSelector: '.source.coffee .string.quoted.double.coffee', source: 'b' }); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 42, newValue: 22 }); changeSpy.reset(); atom.config.unset(null, { scopeSelector: '.source .string.quoted.double', source: 'a' }); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 22, newValue: 12 }); changeSpy.reset(); atom.config.set('foo.bar.baz', undefined); expect(changeSpy).toHaveBeenCalledWith({ oldValue: 12, newValue: undefined }); changeSpy.reset(); })); }); describe('.observe(keyPath, {scope})', () => { let [observeHandler, observeSubscription] = []; beforeEach(() => { observeHandler = jasmine.createSpy('observeHandler'); atom.config.set('foo.bar.baz', 'value 1'); observeSubscription = atom.config.observe('foo.bar.baz', observeHandler); }); it('fires the given callback with the current value at the keypath', () => expect(observeHandler).toHaveBeenCalledWith('value 1')); it('fires the callback every time the observed value changes', () => { observeHandler.reset(); // clear the initial call atom.config.set('foo.bar.baz', 'value 2'); expect(observeHandler).toHaveBeenCalledWith('value 2'); observeHandler.reset(); atom.config.set('foo.bar.baz', 'value 1'); expect(observeHandler).toHaveBeenCalledWith('value 1'); advanceClock(100); // complete pending save that was requested in ::set observeHandler.reset(); atom.config.resetUserSettings({ foo: {} }); expect(observeHandler).toHaveBeenCalledWith(undefined); }); it('fires the callback when the observed value is deleted', () => { observeHandler.reset(); // clear the initial call atom.config.set('foo.bar.baz', undefined); expect(observeHandler).toHaveBeenCalledWith(undefined); }); it('fires the callback when the full key path goes into and out of existence', () => { observeHandler.reset(); // clear the initial call atom.config.set('foo.bar', undefined); expect(observeHandler).toHaveBeenCalledWith(undefined); observeHandler.reset(); atom.config.set('foo.bar.baz', "i'm back"); expect(observeHandler).toHaveBeenCalledWith("i'm back"); }); it('does not fire the callback once the subscription is disposed', () => { observeHandler.reset(); // clear the initial call observeSubscription.dispose(); atom.config.set('foo.bar.baz', 'value 2'); expect(observeHandler).not.toHaveBeenCalled(); }); it('does not fire the callback for a similarly named keyPath', () => { const bazCatHandler = jasmine.createSpy('bazCatHandler'); observeSubscription = atom.config.observe( 'foo.bar.bazCat', bazCatHandler ); bazCatHandler.reset(); atom.config.set('foo.bar.baz', 'value 10'); expect(bazCatHandler).not.toHaveBeenCalled(); }); describe("when a 'scope' is given", () => { let otherHandler = null; beforeEach(() => { observeSubscription.dispose(); otherHandler = jasmine.createSpy('otherHandler'); }); it('allows settings to be observed in a specific scope', () => { atom.config.observe( 'foo.bar.baz', { scope: ['.some.scope'] }, observeHandler ); atom.config.observe( 'foo.bar.baz', { scope: ['.another.scope'] }, otherHandler ); atom.config.set('foo.bar.baz', 'value 2', { scopeSelector: '.some' }); expect(observeHandler).toHaveBeenCalledWith('value 2'); expect(otherHandler).not.toHaveBeenCalledWith('value 2'); }); it('calls the callback when properties with more specific selectors are removed', () => { const changeSpy = jasmine.createSpy(); atom.config.observe( 'foo.bar.baz', { scope: ['.source.coffee', '.string.quoted.double.coffee'] }, changeSpy ); expect(changeSpy).toHaveBeenCalledWith('value 1'); changeSpy.reset(); atom.config.set('foo.bar.baz', 12); expect(changeSpy).toHaveBeenCalledWith(12); changeSpy.reset(); atom.config.set('foo.bar.baz', 22, { scopeSelector: '.source .string.quoted.double', source: 'a' }); expect(changeSpy).toHaveBeenCalledWith(22); changeSpy.reset(); atom.config.set('foo.bar.baz', 42, { scopeSelector: '.source.coffee .string.quoted.double.coffee', source: 'b' }); expect(changeSpy).toHaveBeenCalledWith(42); changeSpy.reset(); atom.config.unset(null, { scopeSelector: '.source.coffee .string.quoted.double.coffee', source: 'b' }); expect(changeSpy).toHaveBeenCalledWith(22); changeSpy.reset(); atom.config.unset(null, { scopeSelector: '.source .string.quoted.double', source: 'a' }); expect(changeSpy).toHaveBeenCalledWith(12); changeSpy.reset(); atom.config.set('foo.bar.baz', undefined); expect(changeSpy).toHaveBeenCalledWith(undefined); changeSpy.reset(); }); }); }); describe('.transact(callback)', () => { let changeSpy = null; beforeEach(() => { changeSpy = jasmine.createSpy('onDidChange callback'); atom.config.onDidChange('foo.bar.baz', changeSpy); }); it('allows only one change event for the duration of the given callback', () => { atom.config.transact(() => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2); atom.config.set('foo.bar.baz', 3); }); expect(changeSpy.callCount).toBe(1); expect(changeSpy.argsForCall[0][0]).toEqual({ newValue: 3, oldValue: undefined }); }); it('does not emit an event if no changes occur while paused', () => { atom.config.transact(() => {}); expect(changeSpy).not.toHaveBeenCalled(); }); }); describe('.transactAsync(callback)', () => { let changeSpy = null; beforeEach(() => { changeSpy = jasmine.createSpy('onDidChange callback'); atom.config.onDidChange('foo.bar.baz', changeSpy); }); it('allows only one change event for the duration of the given promise if it gets resolved', () => { let promiseResult = null; const transactionPromise = atom.config.transactAsync(() => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2); atom.config.set('foo.bar.baz', 3); return Promise.resolve('a result'); }); waitsForPromise(() => transactionPromise.then(result => { promiseResult = result; }) ); runs(() => { expect(promiseResult).toBe('a result'); expect(changeSpy.callCount).toBe(1); expect(changeSpy.argsForCall[0][0]).toEqual({ newValue: 3, oldValue: undefined }); }); }); it('allows only one change event for the duration of the given promise if it gets rejected', () => { let promiseError = null; const transactionPromise = atom.config.transactAsync(() => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2); atom.config.set('foo.bar.baz', 3); return Promise.reject(new Error('an error')); }); waitsForPromise(() => transactionPromise.catch(error => { promiseError = error; }) ); runs(() => { expect(promiseError.message).toBe('an error'); expect(changeSpy.callCount).toBe(1); expect(changeSpy.argsForCall[0][0]).toEqual({ newValue: 3, oldValue: undefined }); }); }); it('allows only one change event even when the given callback throws', () => { const error = new Error('Oops!'); let promiseError = null; const transactionPromise = atom.config.transactAsync(() => { atom.config.set('foo.bar.baz', 1); atom.config.set('foo.bar.baz', 2); atom.config.set('foo.bar.baz', 3); throw error; }); waitsForPromise(() => transactionPromise.catch(e => { promiseError = e; }) ); runs(() => { expect(promiseError).toBe(error); expect(changeSpy.callCount).toBe(1); expect(changeSpy.argsForCall[0][0]).toEqual({ newValue: 3, oldValue: undefined }); }); }); }); describe('.getSources()', () => { it("returns an array of all of the config's source names", () => { expect(atom.config.getSources()).toEqual([]); atom.config.set('a.b', 1, { scopeSelector: '.x1', source: 'source-1' }); atom.config.set('a.c', 1, { scopeSelector: '.x1', source: 'source-1' }); atom.config.set('a.b', 2, { scopeSelector: '.x2', source: 'source-2' }); atom.config.set('a.b', 1, { scopeSelector: '.x3', source: 'source-3' }); expect(atom.config.getSources()).toEqual([ 'source-1', 'source-2', 'source-3' ]); }); }); describe('.save()', () => { it('calls the save callback with any non-default properties', () => { atom.config.set('a.b.c', 1); atom.config.set('a.b.d', 2); atom.config.set('x.y.z', 3); atom.config.setDefaults('a.b', { e: 4, f: 5 }); atom.config.save(); expect(savedSettings).toEqual([{ '*': atom.config.settings }]); }); it('serializes properties in alphabetical order', () => { atom.config.set('foo', 1); atom.config.set('bar', 2); atom.config.set('baz.foo', 3); atom.config.set('baz.bar', 4); savedSettings.length = 0; atom.config.save(); const writtenConfig = savedSettings[0]; expect(writtenConfig).toEqual({ '*': atom.config.settings }); let expectedKeys = ['bar', 'baz', 'foo']; let foundKeys = []; for (const key in writtenConfig['*']) { if (expectedKeys.includes(key)) { foundKeys.push(key); } } expect(foundKeys).toEqual(expectedKeys); expectedKeys = ['bar', 'foo']; foundKeys = []; for (const key in writtenConfig['*']['baz']) { if (expectedKeys.includes(key)) { foundKeys.push(key); } } expect(foundKeys).toEqual(expectedKeys); }); describe('when scoped settings are defined', () => { it('serializes any explicitly set config settings', () => { atom.config.set('foo.bar', 'ruby', { scopeSelector: '.source.ruby' }); atom.config.set('foo.omg', 'wow', { scopeSelector: '.source.ruby' }); atom.config.set('foo.bar', 'coffee', { scopeSelector: '.source.coffee' }); savedSettings.length = 0; atom.config.save(); const writtenConfig = savedSettings[0]; expect(writtenConfig).toEqualJson({ '*': atom.config.settings, '.ruby.source': { foo: { bar: 'ruby', omg: 'wow' } }, '.coffee.source': { foo: { bar: 'coffee' } } }); }); }); }); describe('.resetUserSettings()', () => { beforeEach(() => { atom.config.setSchema('foo', { type: 'object', properties: { bar: { type: 'string', default: 'def' }, int: { type: 'integer', default: 12 } } }); }); describe('when the config file contains scoped settings', () => { it('updates the config data based on the file contents', () => { atom.config.resetUserSettings({ '*': { foo: { bar: 'baz' } }, '.source.ruby': { foo: { bar: 'more-specific' } } }); expect(atom.config.get('foo.bar')).toBe('baz'); expect(atom.config.get('foo.bar', { scope: ['.source.ruby'] })).toBe( 'more-specific' ); }); }); describe('when the config file does not conform to the schema', () => { it('validates and does not load the incorrect values', () => { atom.config.resetUserSettings({ '*': { foo: { bar: 'omg', int: 'baz' } }, '.source.ruby': { foo: { bar: 'scoped', int: 'nope' } } }); expect(atom.config.get('foo.int')).toBe(12); expect(atom.config.get('foo.bar')).toBe('omg'); expect(atom.config.get('foo.int', { scope: ['.source.ruby'] })).toBe( 12 ); expect(atom.config.get('foo.bar', { scope: ['.source.ruby'] })).toBe( 'scoped' ); }); }); it('updates the config data based on the file contents', () => { atom.config.resetUserSettings({ foo: { bar: 'baz' } }); expect(atom.config.get('foo.bar')).toBe('baz'); }); it('notifies observers for updated keypaths on load', () => { const observeHandler = jasmine.createSpy('observeHandler'); atom.config.observe('foo.bar', observeHandler); atom.config.resetUserSettings({ foo: { bar: 'baz' } }); expect(observeHandler).toHaveBeenCalledWith('baz'); }); describe('when the config file contains values that do not adhere to the schema', () => { it('updates the only the settings that have values matching the schema', () => { atom.config.resetUserSettings({ foo: { bar: 'baz', int: 'bad value' } }); expect(atom.config.get('foo.bar')).toBe('baz'); expect(atom.config.get('foo.int')).toBe(12); expect(console.warn).toHaveBeenCalled(); expect(console.warn.mostRecentCall.args[0]).toContain('foo.int'); }); }); it('does not fire a change event for paths that did not change', () => { atom.config.resetUserSettings({ foo: { bar: 'baz', int: 3 } }); const noChangeSpy = jasmine.createSpy('unchanged'); atom.config.onDidChange('foo.bar', noChangeSpy); atom.config.resetUserSettings({ foo: { bar: 'baz', int: 4 } }); expect(noChangeSpy).not.toHaveBeenCalled(); expect(atom.config.get('foo.bar')).toBe('baz'); expect(atom.config.get('foo.int')).toBe(4); }); it('does not fire a change event for paths whose non-primitive values did not change', () => { atom.config.setSchema('foo.bar', { type: 'array', items: { type: 'string' } }); atom.config.resetUserSettings({ foo: { bar: ['baz', 'quux'], int: 2 } }); const noChangeSpy = jasmine.createSpy('unchanged'); atom.config.onDidChange('foo.bar', noChangeSpy); atom.config.resetUserSettings({ foo: { bar: ['baz', 'quux'], int: 2 } }); expect(noChangeSpy).not.toHaveBeenCalled(); expect(atom.config.get('foo.bar')).toEqual(['baz', 'quux']); }); describe('when a setting with a default is removed', () => { it('resets the setting back to the default', () => { atom.config.resetUserSettings({ foo: { bar: ['baz', 'quux'], int: 2 } }); const events = []; atom.config.onDidChange('foo.int', event => events.push(event)); atom.config.resetUserSettings({ foo: { bar: ['baz', 'quux'] } }); expect(events.length).toBe(1); expect(events[0]).toEqual({ oldValue: 2, newValue: 12 }); }); }); it('keeps all the global scope settings after overriding one', () => { atom.config.resetUserSettings({ '*': { foo: { bar: 'baz', int: 99 } } }); atom.config.set('foo.int', 50, { scopeSelector: '*' }); advanceClock(100); expect(savedSettings[0]['*'].foo).toEqual({ bar: 'baz', int: 50 }); expect(atom.config.get('foo.int', { scope: ['*'] })).toEqual(50); expect(atom.config.get('foo.bar', { scope: ['*'] })).toEqual('baz'); expect(atom.config.get('foo.int')).toEqual(50); }); }); describe('.pushAtKeyPath(keyPath, value)', () => { it('pushes the given value to the array at the key path and updates observers', () => { atom.config.set('foo.bar.baz', ['a']); const observeHandler = jasmine.createSpy('observeHandler'); atom.config.observe('foo.bar.baz', observeHandler); observeHandler.reset(); expect(atom.config.pushAtKeyPath('foo.bar.baz', 'b')).toBe(2); expect(atom.config.get('foo.bar.baz')).toEqual(['a', 'b']); expect(observeHandler).toHaveBeenCalledWith( atom.config.get('foo.bar.baz') ); }); }); describe('.unshiftAtKeyPath(keyPath, value)', () => { it('unshifts the given value to the array at the key path and updates observers', () => { atom.config.set('foo.bar.baz', ['b']); const observeHandler = jasmine.createSpy('observeHandler'); atom.config.observe('foo.bar.baz', observeHandler); observeHandler.reset(); expect(atom.config.unshiftAtKeyPath('foo.bar.baz', 'a')).toBe(2); expect(atom.config.get('foo.bar.baz')).toEqual(['a', 'b']); expect(observeHandler).toHaveBeenCalledWith( atom.config.get('foo.bar.baz') ); }); }); describe('.removeAtKeyPath(keyPath, value)', () => { it('removes the given value from the array at the key path and updates observers', () => { atom.config.set('foo.bar.baz', ['a', 'b', 'c']); const observeHandler = jasmine.createSpy('observeHandler'); atom.config.observe('foo.bar.baz', observeHandler); observeHandler.reset(); expect(atom.config.removeAtKeyPath('foo.bar.baz', 'b')).toEqual([ 'a', 'c' ]); expect(atom.config.get('foo.bar.baz')).toEqual(['a', 'c']); expect(observeHandler).toHaveBeenCalledWith( atom.config.get('foo.bar.baz') ); }); }); describe('.setDefaults(keyPath, defaults)', () => { it('assigns any previously-unassigned keys to the object at the key path', () => { atom.config.set('foo.bar.baz', { a: 1 }); atom.config.setDefaults('foo.bar.baz', { a: 2, b: 3, c: 4 }); expect(atom.config.get('foo.bar.baz.a')).toBe(1); expect(atom.config.get('foo.bar.baz.b')).toBe(3); expect(atom.config.get('foo.bar.baz.c')).toBe(4); atom.config.setDefaults('foo.quux', { x: 0, y: 1 }); expect(atom.config.get('foo.quux.x')).toBe(0); expect(atom.config.get('foo.quux.y')).toBe(1); }); it('emits an updated event', () => { const updatedCallback = jasmine.createSpy('updated'); atom.config.onDidChange('foo.bar.baz.a', updatedCallback); expect(updatedCallback.callCount).toBe(0); atom.config.setDefaults('foo.bar.baz', { a: 2 }); expect(updatedCallback.callCount).toBe(1); }); }); describe('.setSchema(keyPath, schema)', () => { it('creates a properly nested schema', () => { const schema = { type: 'object', properties: { anInt: { type: 'integer', default: 12 } } }; atom.config.setSchema('foo.bar', schema); expect(atom.config.getSchema('foo')).toEqual({ type: 'object', properties: { bar: { type: 'object', properties: { anInt: { type: 'integer', default: 12 } } } } }); }); it('sets defaults specified by the schema', () => { const schema = { type: 'object', properties: { anInt: { type: 'integer', default: 12 }, anObject: { type: 'object', properties: { nestedInt: { type: 'integer', default: 24 }, nestedObject: { type: 'object', properties: { superNestedInt: { type: 'integer', default: 36 } } } } } } }; atom.config.setSchema('foo.bar', schema); expect(atom.config.get('foo.bar.anInt')).toBe(12); expect(atom.config.get('foo.bar.anObject')).toEqual({ nestedInt: 24, nestedObject: { superNestedInt: 36 } }); expect(atom.config.get('foo')).toEqual({ bar: { anInt: 12, anObject: { nestedInt: 24, nestedObject: { superNestedInt: 36 } } } }); atom.config.set('foo.bar.anObject.nestedObject.superNestedInt', 37); expect(atom.config.get('foo')).toEqual({ bar: { anInt: 12, anObject: { nestedInt: 24, nestedObject: { superNestedInt: 37 } } } }); }); it('can set a non-object schema', () => { const schema = { type: 'integer', default: 12 }; atom.config.setSchema('foo.bar.anInt', schema); expect(atom.config.get('foo.bar.anInt')).toBe(12); expect(atom.config.getSchema('foo.bar.anInt')).toEqual({ type: 'integer', default: 12 }); }); it('allows the schema to be retrieved via ::getSchema', () => { const schema = { type: 'object', properties: { anInt: { type: 'integer', default: 12 } } }; atom.config.setSchema('foo.bar', schema); expect(atom.config.getSchema('foo.bar')).toEqual({ type: 'object', properties: { anInt: { type: 'integer', default: 12 } } }); expect(atom.config.getSchema('foo.bar.anInt')).toEqual({ type: 'integer', default: 12 }); expect(atom.config.getSchema('foo.baz')).toEqual({ type: 'any' }); expect(atom.config.getSchema('foo.bar.anInt.baz')).toBe(null); }); it('respects the schema for scoped settings', () => { const schema = { type: 'string', default: 'ok', scopes: { '.source.js': { default: 'omg' } } }; atom.config.setSchema('foo.bar.str', schema); expect(atom.config.get('foo.bar.str')).toBe('ok'); expect(atom.config.get('foo.bar.str', { scope: ['.source.js'] })).toBe( 'omg' ); expect( atom.config.get('foo.bar.str', { scope: ['.source.coffee'] }) ).toBe('ok'); }); describe('when a schema is added after config values have been set', () => { let schema = null; beforeEach(() => { schema = { type: 'object', properties: { int: { type: 'integer', default: 2 }, str: { type: 'string', default: 'def' } } }; }); it('respects the new schema when values are set', () => { expect(atom.config.set('foo.bar.str', 'global')).toBe(true); expect( atom.config.set('foo.bar.str', 'scoped', { scopeSelector: '.source.js' }) ).toBe(true); expect(atom.config.get('foo.bar.str')).toBe('global'); expect(atom.config.get('foo.bar.str', { scope: ['.source.js'] })).toBe( 'scoped' ); expect(atom.config.set('foo.bar.noschema', 'nsGlobal')).toBe(true); expect( atom.config.set('foo.bar.noschema', 'nsScoped', { scopeSelector: '.source.js' }) ).toBe(true); expect(atom.config.get('foo.bar.noschema')).toBe('nsGlobal'); expect( atom.config.get('foo.bar.noschema', { scope: ['.source.js'] }) ).toBe('nsScoped'); expect(atom.config.set('foo.bar.int', 'nope')).toBe(true); expect( atom.config.set('foo.bar.int', 'notanint', { scopeSelector: '.source.js' }) ).toBe(true); expect( atom.config.set('foo.bar.int', 23, { scopeSelector: '.source.coffee' }) ).toBe(true); expect(atom.config.get('foo.bar.int')).toBe('nope'); expect(atom.config.get('foo.bar.int', { scope: ['.source.js'] })).toBe( 'notanint' ); expect( atom.config.get('foo.bar.int', { scope: ['.source.coffee'] }) ).toBe(23); atom.config.setSchema('foo.bar', schema); expect(atom.config.get('foo.bar.str')).toBe('global'); expect(atom.config.get('foo.bar.str', { scope: ['.source.js'] })).toBe( 'scoped' ); expect(atom.config.get('foo.bar.noschema')).toBe('nsGlobal'); expect( atom.config.get('foo.bar.noschema', { scope: ['.source.js'] }) ).toBe('nsScoped'); expect(atom.config.get('foo.bar.int')).toBe(2); expect(atom.config.get('foo.bar.int', { scope: ['.source.js'] })).toBe( 2 ); expect( atom.config.get('foo.bar.int', { scope: ['.source.coffee'] }) ).toBe(23); }); it('sets all values that adhere to the schema', () => { expect(atom.config.set('foo.bar.int', 10)).toBe(true); expect( atom.config.set('foo.bar.int', 15, { scopeSelector: '.source.js' }) ).toBe(true); expect( atom.config.set('foo.bar.int', 23, { scopeSelector: '.source.coffee' }) ).toBe(true); expect(atom.config.get('foo.bar.int')).toBe(10); expect(atom.config.get('foo.bar.int', { scope: ['.source.js'] })).toBe( 15 ); expect( atom.config.get('foo.bar.int', { scope: ['.source.coffee'] }) ).toBe(23); atom.config.setSchema('foo.bar', schema); expect(atom.config.get('foo.bar.int')).toBe(10); expect(atom.config.get('foo.bar.int', { scope: ['.source.js'] })).toBe( 15 ); expect( atom.config.get('foo.bar.int', { scope: ['.source.coffee'] }) ).toBe(23); }); }); describe('when the value has an "integer" type', () => { beforeEach(() => { const schema = { type: 'integer', default: 12 }; atom.config.setSchema('foo.bar.anInt', schema); }); it('coerces a string to an int', () => { atom.config.set('foo.bar.anInt', '123'); expect(atom.config.get('foo.bar.anInt')).toBe(123); }); it('does not allow infinity', () => { atom.config.set('foo.bar.anInt', Infinity); expect(atom.config.get('foo.bar.anInt')).toBe(12); }); it('coerces a float to an int', () => { atom.config.set('foo.bar.anInt', 12.3); expect(atom.config.get('foo.bar.anInt')).toBe(12); }); it('will not set non-integers', () => { atom.config.set('foo.bar.anInt', null); expect(atom.config.get('foo.bar.anInt')).toBe(12); atom.config.set('foo.bar.anInt', 'nope'); expect(atom.config.get('foo.bar.anInt')).toBe(12); }); describe('when the minimum and maximum keys are used', () => { beforeEach(() => { const schema = { type: 'integer', minimum: 10, maximum: 20, default: 12 }; atom.config.setSchema('foo.bar.anInt', schema); }); it('keeps the specified value within the specified range', () => { atom.config.set('foo.bar.anInt', '123'); expect(atom.config.get('foo.bar.anInt')).toBe(20); atom.config.set('foo.bar.anInt', '1'); expect(atom.config.get('foo.bar.anInt')).toBe(10); }); }); }); describe('when the value has an "integer" and "string" type', () => { beforeEach(() => { const schema = { type: ['integer', 'string'], default: 12 }; atom.config.setSchema('foo.bar.anInt', schema); }); it('can coerce an int, and fallback to a string', () => { atom.config.set('foo.bar.anInt', '123'); expect(atom.config.get('foo.bar.anInt')).toBe(123); atom.config.set('foo.bar.anInt', 'cats'); expect(atom.config.get('foo.bar.anInt')).toBe('cats'); }); }); describe('when the value has an "string" and "boolean" type', () => { beforeEach(() => { const schema = { type: ['string', 'boolean'], default: 'def' }; atom.config.setSchema('foo.bar', schema); }); it('can set a string, a boolean, and revert back to the default', () => { atom.config.set('foo.bar', 'ok'); expect(atom.config.get('foo.bar')).toBe('ok'); atom.config.set('foo.bar', false); expect(atom.config.get('foo.bar')).toBe(false); atom.config.set('foo.bar', undefined); expect(atom.config.get('foo.bar')).toBe('def'); }); }); describe('when the value has a "number" type', () => { beforeEach(() => { const schema = { type: 'number', default: 12.1 }; atom.config.setSchema('foo.bar.aFloat', schema); }); it('coerces a string to a float', () => { atom.config.set('foo.bar.aFloat', '12.23'); expect(atom.config.get('foo.bar.aFloat')).toBe(12.23); }); it('will not set non-numbers', () => { atom.config.set('foo.bar.aFloat', null); expect(atom.config.get('foo.bar.aFloat')).toBe(12.1); atom.config.set('foo.bar.aFloat', 'nope'); expect(atom.config.get('foo.bar.aFloat')).toBe(12.1); }); describe('when the minimum and maximum keys are used', () => { beforeEach(() => { const schema = { type: 'number', minimum: 11.2, maximum: 25.4, default: 12.1 }; atom.config.setSchema('foo.bar.aFloat', schema); }); it('keeps the specified value within the specified range', () => { atom.config.set('foo.bar.aFloat', '123.2'); expect(atom.config.get('foo.bar.aFloat')).toBe(25.4); atom.config.set('foo.bar.aFloat', '1.0'); expect(atom.config.get('foo.bar.aFloat')).toBe(11.2); }); }); }); describe('when the value has a "boolean" type', () => { beforeEach(() => { const schema = { type: 'boolean', default: true }; atom.config.setSchema('foo.bar.aBool', schema); }); it('coerces various types to a boolean', () => { atom.config.set('foo.bar.aBool', 'true'); expect(atom.config.get('foo.bar.aBool')).toBe(true); atom.config.set('foo.bar.aBool', 'false'); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', 'TRUE'); expect(atom.config.get('foo.bar.aBool')).toBe(true); atom.config.set('foo.bar.aBool', 'FALSE'); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', 1); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', 0); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', {}); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', null); expect(atom.config.get('foo.bar.aBool')).toBe(false); }); it('reverts back to the default value when undefined is passed to set', () => { atom.config.set('foo.bar.aBool', 'false'); expect(atom.config.get('foo.bar.aBool')).toBe(false); atom.config.set('foo.bar.aBool', undefined); expect(atom.config.get('foo.bar.aBool')).toBe(true); }); }); describe('when the value has an "string" type', () => { beforeEach(() => { const schema = { type: 'string', default: 'ok' }; atom.config.setSchema('foo.bar.aString', schema); }); it('allows strings', () => { atom.config.set('foo.bar.aString', 'yep'); expect(atom.config.get('foo.bar.aString')).toBe('yep'); }); it('will only set strings', () => { expect(atom.config.set('foo.bar.aString', 123)).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); expect(atom.config.set('foo.bar.aString', true)).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); expect(atom.config.set('foo.bar.aString', null)).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); expect(atom.config.set('foo.bar.aString', [])).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); expect(atom.config.set('foo.bar.aString', { nope: 'nope' })).toBe( false ); expect(atom.config.get('foo.bar.aString')).toBe('ok'); }); it('does not allow setting children of that key-path', () => { expect(atom.config.set('foo.bar.aString.something', 123)).toBe(false); expect(atom.config.get('foo.bar.aString')).toBe('ok'); }); describe('when the schema has a "maximumLength" key', () => it('trims the string to be no longer than the specified maximum', () => { const schema = { type: 'string', default: 'ok', maximumLength: 3 }; atom.config.setSchema('foo.bar.aString', schema); atom.config.set('foo.bar.aString', 'abcdefg'); expect(atom.config.get('foo.bar.aString')).toBe('abc'); })); }); describe('when the value has an "object" type', () => { beforeEach(() => { const schema = { type: 'object', properties: { anInt: { type: 'integer', default: 12 }, nestedObject: { type: 'object', properties: { nestedBool: { type: 'boolean', default: false } } } } }; atom.config.setSchema('foo.bar', schema); }); it('converts and validates all the children', () => { atom.config.set('foo.bar', { anInt: '23', nestedObject: { nestedBool: 'true' } }); expect(atom.config.get('foo.bar')).toEqual({ anInt: 23, nestedObject: { nestedBool: true } }); }); it('will set only the values that adhere to the schema', () => { expect( atom.config.set('foo.bar', { anInt: 'nope', nestedObject: { nestedBool: true } }) ).toBe(true); expect(atom.config.get('foo.bar.anInt')).toEqual(12); expect(atom.config.get('foo.bar.nestedObject.nestedBool')).toEqual( true ); }); describe('when the value has additionalProperties set to false', () => it('does not allow other properties to be set on the object', () => { atom.config.setSchema('foo.bar', { type: 'object', properties: { anInt: { type: 'integer', default: 12 } }, additionalProperties: false }); expect( atom.config.set('foo.bar', { anInt: 5, somethingElse: 'ok' }) ).toBe(true); expect(atom.config.get('foo.bar.anInt')).toBe(5); expect(atom.config.get('foo.bar.somethingElse')).toBeUndefined(); expect(atom.config.set('foo.bar.somethingElse', { anInt: 5 })).toBe( false ); expect(atom.config.get('foo.bar.somethingElse')).toBeUndefined(); })); describe('when the value has an additionalProperties schema', () => it('validates properties of the object against that schema', () => { atom.config.setSchema('foo.bar', { type: 'object', properties: { anInt: { type: 'integer', default: 12 } }, additionalProperties: { type: 'string' } }); expect( atom.config.set('foo.bar', { anInt: 5, somethingElse: 'ok' }) ).toBe(true); expect(atom.config.get('foo.bar.anInt')).toBe(5); expect(atom.config.get('foo.bar.somethingElse')).toBe('ok'); expect(atom.config.set('foo.bar.somethingElse', 7)).toBe(false); expect(atom.config.get('foo.bar.somethingElse')).toBe('ok'); expect( atom.config.set('foo.bar', { anInt: 6, somethingElse: 7 }) ).toBe(true); expect(atom.config.get('foo.bar.anInt')).toBe(6); expect(atom.config.get('foo.bar.somethingElse')).toBe(undefined); })); }); describe('when the value has an "array" type', () => { beforeEach(() => { const schema = { type: 'array', default: [1, 2, 3], items: { type: 'integer' } }; atom.config.setSchema('foo.bar', schema); }); it('converts an array of strings to an array of ints', () => { atom.config.set('foo.bar', ['2', '3', '4']); expect(atom.config.get('foo.bar')).toEqual([2, 3, 4]); }); it('does not allow setting children of that key-path', () => { expect(atom.config.set('foo.bar.child', 123)).toBe(false); expect(atom.config.set('foo.bar.child.grandchild', 123)).toBe(false); expect(atom.config.get('foo.bar')).toEqual([1, 2, 3]); }); }); describe('when the value has a "color" type', () => { beforeEach(() => { const schema = { type: 'color', default: 'white' }; atom.config.setSchema('foo.bar.aColor', schema); }); it('returns a Color object', () => { let color = atom.config.get('foo.bar.aColor'); expect(color.toHexString()).toBe('#ffffff'); expect(color.toRGBAString()).toBe('rgba(255, 255, 255, 1)'); color.red = 0; color.green = 0; color.blue = 0; color.alpha = 0; atom.config.set('foo.bar.aColor', color); color = atom.config.get('foo.bar.aColor'); expect(color.toHexString()).toBe('#000000'); expect(color.toRGBAString()).toBe('rgba(0, 0, 0, 0)'); color.red = 300; color.green = -200; color.blue = -1; color.alpha = 'not see through'; atom.config.set('foo.bar.aColor', color); color = atom.config.get('foo.bar.aColor'); expect(color.toHexString()).toBe('#ff0000'); expect(color.toRGBAString()).toBe('rgba(255, 0, 0, 1)'); color.red = 11; color.green = 11; color.blue = 124; color.alpha = 1; atom.config.set('foo.bar.aColor', color); color = atom.config.get('foo.bar.aColor'); expect(color.toHexString()).toBe('#0b0b7c'); expect(color.toRGBAString()).toBe('rgba(11, 11, 124, 1)'); }); it('coerces various types to a color object', () => { atom.config.set('foo.bar.aColor', 'red'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 0, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', '#020'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 0, green: 34, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', '#abcdef'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 171, green: 205, blue: 239, alpha: 1 }); atom.config.set('foo.bar.aColor', 'rgb(1,2,3)'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 1, green: 2, blue: 3, alpha: 1 }); atom.config.set('foo.bar.aColor', 'rgba(4,5,6,.7)'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 4, green: 5, blue: 6, alpha: 0.7 }); atom.config.set('foo.bar.aColor', 'hsl(120,100%,50%)'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 0, green: 255, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', 'hsla(120,100%,50%,0.3)'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 0, green: 255, blue: 0, alpha: 0.3 }); atom.config.set('foo.bar.aColor', { red: 100, green: 255, blue: 2, alpha: 0.5 }); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 100, green: 255, blue: 2, alpha: 0.5 }); atom.config.set('foo.bar.aColor', { red: 255 }); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 0, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', { red: 1000 }); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 0, blue: 0, alpha: 1 }); atom.config.set('foo.bar.aColor', { red: 'dark' }); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 0, green: 0, blue: 0, alpha: 1 }); }); it('reverts back to the default value when undefined is passed to set', () => { atom.config.set('foo.bar.aColor', undefined); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); }); it('will not set non-colors', () => { atom.config.set('foo.bar.aColor', null); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); atom.config.set('foo.bar.aColor', 'nope'); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); atom.config.set('foo.bar.aColor', 30); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); atom.config.set('foo.bar.aColor', false); expect(atom.config.get('foo.bar.aColor')).toEqual({ red: 255, green: 255, blue: 255, alpha: 1 }); }); it('returns a clone of the Color when returned in a parent object', () => { const color1 = atom.config.get('foo.bar').aColor; const color2 = atom.config.get('foo.bar').aColor; expect(color1.toRGBAString()).toBe('rgba(255, 255, 255, 1)'); expect(color2.toRGBAString()).toBe('rgba(255, 255, 255, 1)'); expect(color1).not.toBe(color2); expect(color1).toEqual(color2); }); }); describe('when the `enum` key is used', () => { beforeEach(() => { const schema = { type: 'object', properties: { str: { type: 'string', default: 'ok', enum: ['ok', 'one', 'two'] }, int: { type: 'integer', default: 2, enum: [2, 3, 5] }, arr: { type: 'array', default: ['one', 'two'], items: { type: 'string', enum: ['one', 'two', 'three'] } }, str_options: { type: 'string', default: 'one', enum: [ { value: 'one', description: 'One' }, 'two', { value: 'three', description: 'Three' } ] } } }; atom.config.setSchema('foo.bar', schema); }); it('will only set a string when the string is in the enum values', () => { expect(atom.config.set('foo.bar.str', 'nope')).toBe(false); expect(atom.config.get('foo.bar.str')).toBe('ok'); expect(atom.config.set('foo.bar.str', 'one')).toBe(true); expect(atom.config.get('foo.bar.str')).toBe('one'); }); it('will only set an integer when the integer is in the enum values', () => { expect(atom.config.set('foo.bar.int', '400')).toBe(false); expect(atom.config.get('foo.bar.int')).toBe(2); expect(atom.config.set('foo.bar.int', '3')).toBe(true); expect(atom.config.get('foo.bar.int')).toBe(3); }); it('will only set an array when the array values are in the enum values', () => { expect(atom.config.set('foo.bar.arr', ['one', 'five'])).toBe(true); expect(atom.config.get('foo.bar.arr')).toEqual(['one']); expect(atom.config.set('foo.bar.arr', ['two', 'three'])).toBe(true); expect(atom.config.get('foo.bar.arr')).toEqual(['two', 'three']); }); it('will honor the enum when specified as an array', () => { expect(atom.config.set('foo.bar.str_options', 'one')).toBe(true); expect(atom.config.get('foo.bar.str_options')).toEqual('one'); expect(atom.config.set('foo.bar.str_options', 'two')).toBe(true); expect(atom.config.get('foo.bar.str_options')).toEqual('two'); 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 .resetUserSettings', () => { beforeEach(() => { atom.config.settingsLoaded = false; }); 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(savedSettings.length).toBe(0); atom.config.resetUserSettings({ '*': { foo: { bar: 'baz' }, do: { ray: 'me' } } }); advanceClock(100); expect(savedSettings.length).toBe(1); expect(atom.config.get('foo.bar')).toBeUndefined(); expect(atom.config.get('foo.qux')).toBe('boo'); expect(atom.config.get('do.ray')).toBe('me'); }); }); describe('project specific settings', () => { describe('config.resetProjectSettings', () => { it('gracefully handles invalid config objects', () => { atom.config.resetProjectSettings({}); expect(atom.config.get('foo.bar')).toBeUndefined(); }); }); describe('config.get', () => { const dummyPath = '/Users/dummy/path.json'; describe('project settings', () => { it('returns a deep clone of the property value', () => { atom.config.resetProjectSettings( { '*': { value: { array: [1, { b: 2 }, 3] } } }, dummyPath ); const retrievedValue = atom.config.get('value'); retrievedValue.array[0] = 4; retrievedValue.array[1].b = 2.1; expect(atom.config.get('value')).toEqual({ array: [1, { b: 2 }, 3] }); }); it('properly gets project settings', () => { atom.config.resetProjectSettings({ '*': { foo: 'wei' } }, dummyPath); expect(atom.config.get('foo')).toBe('wei'); atom.config.resetProjectSettings( { '*': { foo: { bar: 'baz' } } }, dummyPath ); expect(atom.config.get('foo.bar')).toBe('baz'); }); it('gets project settings with higher priority than regular settings', () => { atom.config.set('foo', 'bar'); atom.config.resetProjectSettings({ '*': { foo: 'baz' } }, dummyPath); expect(atom.config.get('foo')).toBe('baz'); }); it('correctly gets nested and scoped properties for project settings', () => { expect(atom.config.set('foo.bar.str', 'global')).toBe(true); expect( atom.config.set('foo.bar.str', 'scoped', { scopeSelector: '.source.js' }) ).toBe(true); expect(atom.config.get('foo.bar.str')).toBe('global'); expect( atom.config.get('foo.bar.str', { scope: ['.source.js'] }) ).toBe('scoped'); }); it('returns a deep clone of the property value', () => { atom.config.set('value', { array: [1, { b: 2 }, 3] }); const retrievedValue = atom.config.get('value'); retrievedValue.array[0] = 4; retrievedValue.array[1].b = 2.1; expect(atom.config.get('value')).toEqual({ array: [1, { b: 2 }, 3] }); }); it('gets scoped values correctly', () => { atom.config.set('foo', 'bam', { scope: ['second'] }); expect(atom.config.get('foo', { scopeSelector: 'second' })).toBe( 'bam' ); atom.config.resetProjectSettings( { '*': { foo: 'baz' }, second: { foo: 'bar' } }, dummyPath ); expect(atom.config.get('foo', { scopeSelector: 'second' })).toBe( 'baz' ); atom.config.clearProjectSettings(); expect(atom.config.get('foo', { scopeSelector: 'second' })).toBe( 'bam' ); }); it('clears project settings correctly', () => { atom.config.set('foo', 'bar'); expect(atom.config.get('foo')).toBe('bar'); atom.config.resetProjectSettings( { '*': { foo: 'baz' }, second: { foo: 'bar' } }, dummyPath ); expect(atom.config.get('foo')).toBe('baz'); expect(atom.config.getSources().length).toBe(1); atom.config.clearProjectSettings(); expect(atom.config.get('foo')).toBe('bar'); expect(atom.config.getSources().length).toBe(0); }); }); }); describe('config.getAll', () => { const dummyPath = '/Users/dummy/path.json'; it('gets settings in the same way .get would return them', () => { atom.config.resetProjectSettings({ '*': { a: 'b' } }, dummyPath); atom.config.set('a', 'f'); expect(atom.config.getAll('a')).toEqual([ { scopeSelector: '*', value: 'b' } ]); }); }); }); }); ================================================ FILE: spec/context-menu-manager-spec.js ================================================ const ContextMenuManager = require('../src/context-menu-manager'); describe('ContextMenuManager', function() { let [contextMenu, parent, child, grandchild] = []; beforeEach(function() { const { resourcePath } = atom.getLoadSettings(); contextMenu = new ContextMenuManager({ keymapManager: atom.keymaps }); contextMenu.initialize({ resourcePath }); parent = document.createElement('div'); child = document.createElement('div'); grandchild = document.createElement('div'); parent.tabIndex = -1; child.tabIndex = -1; grandchild.tabIndex = -1; parent.classList.add('parent'); child.classList.add('child'); grandchild.classList.add('grandchild'); child.appendChild(grandchild); parent.appendChild(child); document.body.appendChild(parent); }); afterEach(function() { document.body.blur(); document.body.removeChild(parent); }); describe('::add(itemsBySelector)', function() { it('can add top-level menu items that can be removed with the returned disposable', function() { const disposable = contextMenu.add({ '.parent': [{ label: 'A', command: 'a' }], '.child': [{ label: 'B', command: 'b' }], '.grandchild': [{ label: 'C', command: 'c' }] }); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'C', id: 'C', command: 'c' }, { label: 'B', id: 'B', command: 'b' }, { label: 'A', id: 'A', command: 'a' } ]); disposable.dispose(); expect(contextMenu.templateForElement(grandchild)).toEqual([]); }); it('can add submenu items to existing menus that can be removed with the returned disposable', function() { const disposable1 = contextMenu.add({ '.grandchild': [{ label: 'A', submenu: [{ label: 'B', command: 'b' }] }] }); const disposable2 = contextMenu.add({ '.grandchild': [{ label: 'A', submenu: [{ label: 'C', command: 'c' }] }] }); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'A', id: 'A', submenu: [ { label: 'B', id: 'B', command: 'b' }, { label: 'C', id: 'C', command: 'c' } ] } ]); disposable2.dispose(); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'A', id: 'A', submenu: [{ label: 'B', id: 'B', command: 'b' }] } ]); disposable1.dispose(); expect(contextMenu.templateForElement(grandchild)).toEqual([]); }); it('favors the most specific / recently added item in the case of a duplicate label', function() { grandchild.classList.add('foo'); const disposable1 = contextMenu.add({ '.grandchild': [{ label: 'A', command: 'a' }] }); const disposable2 = contextMenu.add({ '.grandchild.foo': [{ label: 'A', command: 'b' }] }); const disposable3 = contextMenu.add({ '.grandchild': [{ label: 'A', command: 'c' }] }); contextMenu.add({ '.child': [{ label: 'A', command: 'd' }] }); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'A', id: 'A', command: 'b' } ]); disposable2.dispose(); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'A', id: 'A', command: 'c' } ]); disposable3.dispose(); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'A', id: 'A', command: 'a' } ]); disposable1.dispose(); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'A', id: 'A', command: 'd' } ]); }); it('allows multiple separators, but not adjacent to each other', function() { contextMenu.add({ '.grandchild': [ { label: 'A', command: 'a' }, { type: 'separator' }, { type: 'separator' }, { label: 'B', command: 'b' }, { type: 'separator' }, { type: 'separator' }, { label: 'C', command: 'c' } ] }); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'A', id: 'A', command: 'a' }, { type: 'separator' }, { label: 'B', id: 'B', command: 'b' }, { type: 'separator' }, { label: 'C', id: 'C', command: 'c' } ]); }); it('excludes items marked for display in devMode unless in dev mode', function() { contextMenu.add({ '.grandchild': [ { label: 'A', command: 'a', devMode: true }, { label: 'B', command: 'b', devMode: false } ] }); expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'B', id: 'B', command: 'b' } ]); contextMenu.devMode = true; expect(contextMenu.templateForElement(grandchild)).toEqual([ { label: 'A', id: 'A', command: 'a' }, { label: 'B', id: 'B', command: 'b' } ]); }); it('allows items to be associated with `created` hooks which are invoked on template construction with the item and event', function() { let createdEvent = null; const item = { label: 'A', command: 'a', created(event) { this.command = 'b'; createdEvent = event; } }; contextMenu.add({ '.grandchild': [item] }); const dispatchedEvent = { target: grandchild }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'A', id: 'A', command: 'b' } ]); expect(item.command).toBe('a'); // doesn't modify original item template expect(createdEvent).toBe(dispatchedEvent); }); it('allows items to be associated with `shouldDisplay` hooks which are invoked on construction to determine whether the item should be included', function() { let shouldDisplayEvent = null; let shouldDisplay = true; const item = { label: 'A', command: 'a', shouldDisplay(event) { this.foo = 'bar'; shouldDisplayEvent = event; return shouldDisplay; } }; contextMenu.add({ '.grandchild': [item] }); const dispatchedEvent = { target: grandchild }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'A', id: 'A', command: 'a' } ]); expect(item.foo).toBeUndefined(); // doesn't modify original item template expect(shouldDisplayEvent).toBe(dispatchedEvent); shouldDisplay = false; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([]); }); it('prunes a trailing separator', function() { contextMenu.add({ '.grandchild': [ { label: 'A', command: 'a' }, { type: 'separator' }, { label: 'B', command: 'b' }, { type: 'separator' } ] }); expect(contextMenu.templateForEvent({ target: grandchild }).length).toBe( 3 ); }); it('prunes a leading separator', function() { contextMenu.add({ '.grandchild': [ { type: 'separator' }, { label: 'A', command: 'a' }, { type: 'separator' }, { label: 'B', command: 'b' } ] }); expect(contextMenu.templateForEvent({ target: grandchild }).length).toBe( 3 ); }); it('prunes duplicate separators', function() { contextMenu.add({ '.grandchild': [ { label: 'A', command: 'a' }, { type: 'separator' }, { type: 'separator' }, { label: 'B', command: 'b' } ] }); expect(contextMenu.templateForEvent({ target: grandchild }).length).toBe( 3 ); }); it('prunes all redundant separators', function() { contextMenu.add({ '.grandchild': [ { type: 'separator' }, { type: 'separator' }, { label: 'A', command: 'a' }, { type: 'separator' }, { type: 'separator' }, { label: 'B', command: 'b' }, { label: 'C', command: 'c' }, { type: 'separator' }, { type: 'separator' } ] }); expect(contextMenu.templateForEvent({ target: grandchild }).length).toBe( 4 ); }); it('throws an error when the selector is invalid', function() { let addError = null; try { contextMenu.add({ '<>': [{ label: 'A', command: 'a' }] }); } catch (error) { addError = error; } expect(addError.message).toContain('<>'); }); it('calls `created` hooks for submenu items', function() { const item = { label: 'A', command: 'B', submenu: [ { label: 'C', created(event) { this.label = 'D'; } } ] }; contextMenu.add({ '.grandchild': [item] }); const dispatchedEvent = { target: grandchild }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'A', id: 'A', command: 'B', submenu: [ { label: 'D', id: 'D' } ] } ]); }); }); describe('::templateForEvent(target)', function() { let [keymaps, item] = []; beforeEach(function() { keymaps = atom.keymaps.add('source', { '.child': { 'ctrl-a': 'test:my-command', 'shift-b': 'test:my-other-command' } }); item = { label: 'My Command', command: 'test:my-command', submenu: [ { label: 'My Other Command', command: 'test:my-other-command' } ] }; contextMenu.add({ '.parent': [item] }); }); afterEach(() => keymaps.dispose()); it('adds Electron-style accelerators to items that have keybindings', function() { child.focus(); const dispatchedEvent = { target: child }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'My Command', id: 'My Command', command: 'test:my-command', accelerator: 'Ctrl+A', submenu: [ { label: 'My Other Command', id: 'My Other Command', command: 'test:my-other-command', accelerator: 'Shift+B' } ] } ]); }); it('adds accelerators when a parent node has key bindings for a given command', function() { grandchild.focus(); const dispatchedEvent = { target: grandchild }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'My Command', id: 'My Command', command: 'test:my-command', accelerator: 'Ctrl+A', submenu: [ { label: 'My Other Command', id: 'My Other Command', command: 'test:my-other-command', accelerator: 'Shift+B' } ] } ]); }); it('does not add accelerators when a child node has key bindings for a given command', function() { parent.focus(); const dispatchedEvent = { target: parent }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'My Command', id: 'My Command', command: 'test:my-command', submenu: [ { label: 'My Other Command', id: 'My Other Command', command: 'test:my-other-command' } ] } ]); }); it('adds accelerators based on focus, not context menu target', function() { grandchild.focus(); const dispatchedEvent = { target: parent }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'My Command', id: 'My Command', command: 'test:my-command', accelerator: 'Ctrl+A', submenu: [ { label: 'My Other Command', id: 'My Other Command', command: 'test:my-other-command', accelerator: 'Shift+B' } ] } ]); }); it('does not add accelerators for multi-keystroke key bindings', function() { atom.keymaps.add('source', { '.child': { 'ctrl-a ctrl-b': 'test:multi-keystroke-command' } }); contextMenu.clear(); contextMenu.add({ '.parent': [ { label: 'Multi-keystroke command', command: 'test:multi-keystroke-command' } ] }); child.focus(); const label = process.platform === 'darwin' ? '⌃A ⌃B' : 'Ctrl+A Ctrl+B'; expect(contextMenu.templateForEvent({ target: child })).toEqual([ { label: `Multi-keystroke command [${label}]`, id: `Multi-keystroke command`, command: 'test:multi-keystroke-command' } ]); }); }); describe('::templateForEvent(target) (sorting)', function() { it('applies simple sorting rules', function() { contextMenu.add({ '.parent': [ { label: 'My Command', command: 'test:my-command', after: ['test:my-other-command'] }, { label: 'My Other Command', command: 'test:my-other-command' } ] }); const dispatchedEvent = { target: parent }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'My Other Command', id: 'My Other Command', command: 'test:my-other-command' }, { label: 'My Command', id: 'My Command', command: 'test:my-command', after: ['test:my-other-command'] } ]); }); it('applies sorting rules recursively to submenus', function() { contextMenu.add({ '.parent': [ { label: 'Parent', submenu: [ { label: 'My Command', command: 'test:my-command', after: ['test:my-other-command'] }, { label: 'My Other Command', command: 'test:my-other-command' } ] } ] }); const dispatchedEvent = { target: parent }; expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual([ { label: 'Parent', id: `Parent`, submenu: [ { label: 'My Other Command', id: 'My Other Command', command: 'test:my-other-command' }, { label: 'My Command', id: 'My Command', command: 'test:my-command', after: ['test:my-other-command'] } ] } ]); }); }); }); ================================================ FILE: spec/decoration-manager-spec.js ================================================ const DecorationManager = require('../src/decoration-manager'); const TextEditor = require('../src/text-editor'); describe('DecorationManager', function() { let [decorationManager, buffer, editor, markerLayer1, markerLayer2] = []; beforeEach(function() { buffer = atom.project.bufferForPathSync('sample.js'); editor = new TextEditor({ buffer }); markerLayer1 = editor.addMarkerLayer(); markerLayer2 = editor.addMarkerLayer(); decorationManager = new DecorationManager(editor); waitsForPromise(() => atom.packages.activatePackage('language-javascript')); }); afterEach(() => buffer.destroy()); describe('decorations', function() { let [ layer1Marker, layer2Marker, layer1MarkerDecoration, layer2MarkerDecoration, decorationProperties ] = []; beforeEach(function() { layer1Marker = markerLayer1.markBufferRange([[2, 13], [3, 15]]); decorationProperties = { type: 'line-number', class: 'one' }; layer1MarkerDecoration = decorationManager.decorateMarker( layer1Marker, decorationProperties ); layer2Marker = markerLayer2.markBufferRange([[2, 13], [3, 15]]); layer2MarkerDecoration = decorationManager.decorateMarker( layer2Marker, decorationProperties ); }); it('can add decorations associated with markers and remove them', function() { expect(layer1MarkerDecoration).toBeDefined(); expect(layer1MarkerDecoration.getProperties()).toBe(decorationProperties); expect(decorationManager.decorationsForScreenRowRange(2, 3)).toEqual({ [layer1Marker.id]: [layer1MarkerDecoration], [layer2Marker.id]: [layer2MarkerDecoration] }); layer1MarkerDecoration.destroy(); expect( decorationManager.decorationsForScreenRowRange(2, 3)[layer1Marker.id] ).not.toBeDefined(); layer2MarkerDecoration.destroy(); expect( decorationManager.decorationsForScreenRowRange(2, 3)[layer2Marker.id] ).not.toBeDefined(); }); it('will not fail if the decoration is removed twice', function() { layer1MarkerDecoration.destroy(); layer1MarkerDecoration.destroy(); }); it('does not allow destroyed markers to be decorated', function() { layer1Marker.destroy(); expect(() => decorationManager.decorateMarker(layer1Marker, { type: 'overlay', item: document.createElement('div') }) ).toThrow('Cannot decorate a destroyed marker'); expect(decorationManager.getOverlayDecorations()).toEqual([]); }); it('does not allow destroyed marker layers to be decorated', function() { const layer = editor.addMarkerLayer(); layer.destroy(); expect(() => decorationManager.decorateMarkerLayer(layer, { type: 'highlight' }) ).toThrow('Cannot decorate a destroyed marker layer'); }); describe('when a decoration is updated via Decoration::update()', () => it("emits an 'updated' event containing the new and old params", function() { let updatedSpy; layer1MarkerDecoration.onDidChangeProperties( (updatedSpy = jasmine.createSpy()) ); layer1MarkerDecoration.setProperties({ type: 'line-number', class: 'two' }); const { oldProperties, newProperties } = updatedSpy.mostRecentCall.args[0]; expect(oldProperties).toEqual(decorationProperties); expect(newProperties.type).toBe('line-number'); expect(newProperties.gutterName).toBe('line-number'); expect(newProperties.class).toBe('two'); })); describe('::getDecorations(properties)', () => it('returns decorations matching the given optional properties', function() { expect(decorationManager.getDecorations()).toEqual([ layer1MarkerDecoration, layer2MarkerDecoration ]); expect( decorationManager.getDecorations({ class: 'two' }).length ).toEqual(0); expect( decorationManager.getDecorations({ class: 'one' }).length ).toEqual(2); })); }); describe('::decorateMarker', () => describe('when decorating gutters', function() { let [layer1Marker] = []; beforeEach( () => (layer1Marker = markerLayer1.markBufferRange([[1, 0], [1, 0]])) ); it("creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", function() { const decorationProperties = { type: 'line-number', class: 'one' }; const layer1MarkerDecoration = decorationManager.decorateMarker( layer1Marker, decorationProperties ); expect(layer1MarkerDecoration.isType('line-number')).toBe(true); expect(layer1MarkerDecoration.isType('gutter')).toBe(true); expect(layer1MarkerDecoration.getProperties().gutterName).toBe( 'line-number' ); expect(layer1MarkerDecoration.getProperties().class).toBe('one'); }); it("creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", function() { const decorationProperties = { type: 'gutter', gutterName: 'test-gutter', class: 'one' }; const layer1MarkerDecoration = decorationManager.decorateMarker( layer1Marker, decorationProperties ); expect(layer1MarkerDecoration.isType('gutter')).toBe(true); expect(layer1MarkerDecoration.isType('line-number')).toBe(false); expect(layer1MarkerDecoration.getProperties().gutterName).toBe( 'test-gutter' ); expect(layer1MarkerDecoration.getProperties().class).toBe('one'); }); })); }); ================================================ FILE: spec/default-directory-provider-spec.js ================================================ const DefaultDirectoryProvider = require('../src/default-directory-provider'); const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); describe('DefaultDirectoryProvider', function() { let tmp = null; beforeEach(() => (tmp = temp.mkdirSync('atom-spec-default-dir-provider'))); afterEach(function() { try { temp.cleanupSync(); } catch (error) {} }); describe('.directoryForURISync(uri)', function() { it('returns a Directory with a path that matches the uri', function() { const provider = new DefaultDirectoryProvider(); const directory = provider.directoryForURISync(tmp); expect(directory.getPath()).toEqual(tmp); }); it('normalizes its input before creating a Directory for it', function() { const provider = new DefaultDirectoryProvider(); const nonNormalizedPath = tmp + path.sep + '..' + path.sep + path.basename(tmp); expect(tmp.includes('..')).toBe(false); expect(nonNormalizedPath.includes('..')).toBe(true); const directory = provider.directoryForURISync(nonNormalizedPath); expect(directory.getPath()).toEqual(tmp); }); it('normalizes disk drive letter in path on #win32', function() { const provider = new DefaultDirectoryProvider(); const nonNormalizedPath = tmp[0].toLowerCase() + tmp.slice(1); expect(tmp).not.toMatch(/^[a-z]:/); expect(nonNormalizedPath).toMatch(/^[a-z]:/); const directory = provider.directoryForURISync(nonNormalizedPath); expect(directory.getPath()).toEqual(tmp); }); it('creates a Directory for its parent dir when passed a file', function() { const provider = new DefaultDirectoryProvider(); const file = path.join(tmp, 'example.txt'); fs.writeFileSync(file, 'data'); const directory = provider.directoryForURISync(file); expect(directory.getPath()).toEqual(tmp); }); it('creates a Directory with a path as a uri when passed a uri', function() { const provider = new DefaultDirectoryProvider(); const uri = 'remote://server:6792/path/to/a/dir'; const directory = provider.directoryForURISync(uri); expect(directory.getPath()).toEqual(uri); }); }); describe('.directoryForURI(uri)', () => it('returns a Promise that resolves to a Directory with a path that matches the uri', function() { const provider = new DefaultDirectoryProvider(); waitsForPromise(() => provider .directoryForURI(tmp) .then(directory => expect(directory.getPath()).toEqual(tmp)) ); })); }); ================================================ FILE: spec/default-directory-searcher-spec.js ================================================ const DefaultDirectorySearcher = require('../src/default-directory-searcher'); const Task = require('../src/task'); const path = require('path'); describe('DefaultDirectorySearcher', function() { let searcher; let dirPath; beforeEach(function() { dirPath = path.resolve(__dirname, 'fixtures', 'dir'); searcher = new DefaultDirectorySearcher(); }); it('terminates the task after running a search', async function() { const options = { ignoreCase: false, includeHidden: false, excludeVcsIgnores: true, inclusions: [], globalExclusions: ['a-dir'], didMatch() {}, didError() {}, didSearchPaths() {} }; spyOn(Task.prototype, 'terminate').andCallThrough(); await searcher.search( [ { getPath() { return dirPath; } } ], /abcdefg/, options ); expect(Task.prototype.terminate).toHaveBeenCalled(); }); }); ================================================ FILE: spec/deserializer-manager-spec.js ================================================ const DeserializerManager = require('../src/deserializer-manager'); describe('DeserializerManager', function() { let manager = null; class Foo { static deserialize({ name }) { return new Foo(name); } constructor(name) { this.name = name; } } beforeEach(() => (manager = new DeserializerManager())); describe('::add(deserializer)', () => it('returns a disposable that can be used to remove the manager', function() { const disposable = manager.add(Foo); expect( manager.deserialize({ deserializer: 'Foo', name: 'Bar' }) ).toBeDefined(); disposable.dispose(); spyOn(console, 'warn'); expect( manager.deserialize({ deserializer: 'Foo', name: 'Bar' }) ).toBeUndefined(); })); describe('::deserialize(state)', function() { beforeEach(() => manager.add(Foo)); it("calls deserialize on the manager for the given state object, or returns undefined if one can't be found", function() { spyOn(console, 'warn'); const object = manager.deserialize({ deserializer: 'Foo', name: 'Bar' }); expect(object.name).toBe('Bar'); expect(manager.deserialize({ deserializer: 'Bogus' })).toBeUndefined(); }); describe('when the manager has a version', function() { beforeEach(() => (Foo.version = 2)); describe('when the deserialized state has a matching version', () => it('attempts to deserialize the state', function() { const object = manager.deserialize({ deserializer: 'Foo', version: 2, name: 'Bar' }); expect(object.name).toBe('Bar'); })); describe('when the deserialized state has a non-matching version', () => it('returns undefined', function() { expect( manager.deserialize({ deserializer: 'Foo', version: 3, name: 'Bar' }) ).toBeUndefined(); expect( manager.deserialize({ deserializer: 'Foo', version: 1, name: 'Bar' }) ).toBeUndefined(); expect( manager.deserialize({ deserializer: 'Foo', name: 'Bar' }) ).toBeUndefined(); })); }); }); }); ================================================ FILE: spec/dock-spec.js ================================================ /** @babel */ import etch from 'etch'; const Grim = require('grim'); const getNextUpdatePromise = () => etch.getScheduler().nextUpdatePromise; describe('Dock', () => { describe('when a dock is activated', () => { it('opens the dock and activates its active pane', () => { jasmine.attachToDOM(atom.workspace.getElement()); const dock = atom.workspace.getLeftDock(); const didChangeVisibleSpy = jasmine.createSpy(); dock.onDidChangeVisible(didChangeVisibleSpy); expect(dock.isVisible()).toBe(false); expect(document.activeElement).toBe( atom.workspace .getCenter() .getActivePane() .getElement() ); dock.activate(); expect(dock.isVisible()).toBe(true); expect(document.activeElement).toBe(dock.getActivePane().getElement()); expect(didChangeVisibleSpy).toHaveBeenCalledWith(true); }); }); describe('when a dock is hidden', () => { it('transfers focus back to the active center pane if the dock had focus', () => { jasmine.attachToDOM(atom.workspace.getElement()); const dock = atom.workspace.getLeftDock(); const didChangeVisibleSpy = jasmine.createSpy(); dock.onDidChangeVisible(didChangeVisibleSpy); dock.activate(); expect(document.activeElement).toBe(dock.getActivePane().getElement()); expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true); dock.hide(); expect(document.activeElement).toBe( atom.workspace .getCenter() .getActivePane() .getElement() ); expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false); dock.activate(); expect(document.activeElement).toBe(dock.getActivePane().getElement()); expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true); dock.toggle(); expect(document.activeElement).toBe( atom.workspace .getCenter() .getActivePane() .getElement() ); expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false); // Don't change focus if the dock was not focused in the first place const modalElement = document.createElement('div'); modalElement.setAttribute('tabindex', -1); atom.workspace.addModalPanel({ item: modalElement }); modalElement.focus(); expect(document.activeElement).toBe(modalElement); dock.show(); expect(document.activeElement).toBe(modalElement); expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true); dock.hide(); expect(document.activeElement).toBe(modalElement); expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false); }); }); describe('when a pane in a dock is activated', () => { it('opens the dock', async () => { const item = { element: document.createElement('div'), getDefaultLocation() { return 'left'; } }; await atom.workspace.open(item, { activatePane: false }); expect(atom.workspace.getLeftDock().isVisible()).toBe(false); atom.workspace .getLeftDock() .getPanes()[0] .activate(); expect(atom.workspace.getLeftDock().isVisible()).toBe(true); }); }); describe('activating the next pane', () => { describe('when the dock has more than one pane', () => { it('activates the next pane', () => { const dock = atom.workspace.getLeftDock(); const pane1 = dock.getPanes()[0]; const pane2 = pane1.splitRight(); const pane3 = pane2.splitRight(); pane2.activate(); expect(pane1.isActive()).toBe(false); expect(pane2.isActive()).toBe(true); expect(pane3.isActive()).toBe(false); dock.activateNextPane(); expect(pane1.isActive()).toBe(false); expect(pane2.isActive()).toBe(false); expect(pane3.isActive()).toBe(true); }); }); describe('when the dock has only one pane', () => { it('leaves the current pane active', () => { const dock = atom.workspace.getLeftDock(); expect(dock.getPanes().length).toBe(1); const pane = dock.getPanes()[0]; expect(pane.isActive()).toBe(true); dock.activateNextPane(); expect(pane.isActive()).toBe(true); }); }); }); describe('activating the previous pane', () => { describe('when the dock has more than one pane', () => { it('activates the previous pane', () => { const dock = atom.workspace.getLeftDock(); const pane1 = dock.getPanes()[0]; const pane2 = pane1.splitRight(); const pane3 = pane2.splitRight(); pane2.activate(); expect(pane1.isActive()).toBe(false); expect(pane2.isActive()).toBe(true); expect(pane3.isActive()).toBe(false); dock.activatePreviousPane(); expect(pane1.isActive()).toBe(true); expect(pane2.isActive()).toBe(false); expect(pane3.isActive()).toBe(false); }); }); describe('when the dock has only one pane', () => { it('leaves the current pane active', () => { const dock = atom.workspace.getLeftDock(); expect(dock.getPanes().length).toBe(1); const pane = dock.getPanes()[0]; expect(pane.isActive()).toBe(true); dock.activatePreviousPane(); expect(pane.isActive()).toBe(true); }); }); }); describe('when the dock resize handle is double-clicked', () => { describe('when the dock is open', () => { it("resizes a vertically-oriented dock to the current item's preferred width", async () => { jasmine.attachToDOM(atom.workspace.getElement()); const item = { element: document.createElement('div'), getDefaultLocation() { return 'left'; }, getPreferredWidth() { return 142; }, getPreferredHeight() { return 122; } }; await atom.workspace.open(item); const dock = atom.workspace.getLeftDock(); const dockElement = dock.getElement(); dock.setState({ size: 300 }); await getNextUpdatePromise(); expect(dockElement.offsetWidth).toBe(300); dockElement .querySelector('.atom-dock-resize-handle') .dispatchEvent(new MouseEvent('mousedown', { detail: 2 })); await getNextUpdatePromise(); expect(dockElement.offsetWidth).toBe(item.getPreferredWidth()); }); it("resizes a horizontally-oriented dock to the current item's preferred width", async () => { jasmine.attachToDOM(atom.workspace.getElement()); const item = { element: document.createElement('div'), getDefaultLocation() { return 'bottom'; }, getPreferredWidth() { return 122; }, getPreferredHeight() { return 142; } }; await atom.workspace.open(item); const dock = atom.workspace.getBottomDock(); const dockElement = dock.getElement(); dock.setState({ size: 300 }); await getNextUpdatePromise(); expect(dockElement.offsetHeight).toBe(300); dockElement .querySelector('.atom-dock-resize-handle') .dispatchEvent(new MouseEvent('mousedown', { detail: 2 })); await getNextUpdatePromise(); expect(dockElement.offsetHeight).toBe(item.getPreferredHeight()); }); }); describe('when the dock is closed', () => { it('does nothing', async () => { jasmine.attachToDOM(atom.workspace.getElement()); const item = { element: document.createElement('div'), getDefaultLocation() { return 'bottom'; }, getPreferredWidth() { return 122; }, getPreferredHeight() { return 142; } }; await atom.workspace.open(item, { activatePane: false }); const dockElement = atom.workspace.getBottomDock().getElement(); dockElement .querySelector('.atom-dock-resize-handle') .dispatchEvent(new MouseEvent('mousedown', { detail: 2 })); expect(dockElement.offsetHeight).toBe(0); expect(dockElement.querySelector('.atom-dock-inner').offsetHeight).toBe( 0 ); // The content should be masked away. expect(dockElement.querySelector('.atom-dock-mask').offsetHeight).toBe( 0 ); }); }); }); describe('when you add an item to an empty dock', () => { describe('when the item has a preferred size', () => { it('is takes the preferred size of the item', async () => { jasmine.attachToDOM(atom.workspace.getElement()); const createItem = preferredWidth => ({ element: document.createElement('div'), getDefaultLocation() { return 'left'; }, getPreferredWidth() { return preferredWidth; } }); const dock = atom.workspace.getLeftDock(); const dockElement = dock.getElement(); expect(dock.getPaneItems()).toHaveLength(0); const item1 = createItem(111); await atom.workspace.open(item1); // It should update the width every time we go from 0 -> 1 items, not just the first. expect(dock.isVisible()).toBe(true); expect(dockElement.offsetWidth).toBe(111); dock.destroyActivePane(); expect(dock.getPaneItems()).toHaveLength(0); expect(dock.isVisible()).toBe(false); const item2 = createItem(222); await atom.workspace.open(item2); expect(dock.isVisible()).toBe(true); expect(dockElement.offsetWidth).toBe(222); // Adding a second shouldn't change the size. const item3 = createItem(333); await atom.workspace.open(item3); expect(dockElement.offsetWidth).toBe(222); }); }); describe('when the item has no preferred size', () => { it('is still has an explicit size', async () => { jasmine.attachToDOM(atom.workspace.getElement()); const item = { element: document.createElement('div'), getDefaultLocation() { return 'left'; } }; const dock = atom.workspace.getLeftDock(); expect(dock.getPaneItems()).toHaveLength(0); expect(dock.state.size).toBe(null); await atom.workspace.open(item); expect(dock.state.size).not.toBe(null); }); }); }); describe('a deserialized dock', () => { it('restores the serialized size', async () => { jasmine.attachToDOM(atom.workspace.getElement()); const item = { element: document.createElement('div'), getDefaultLocation() { return 'left'; }, getPreferredWidth() { return 122; }, serialize: () => ({ deserializer: 'DockTestItem' }) }; atom.deserializers.add({ name: 'DockTestItem', deserialize: () => item }); const dock = atom.workspace.getLeftDock(); const dockElement = dock.getElement(); await atom.workspace.open(item); dock.setState({ size: 150 }); expect(dockElement.offsetWidth).toBe(150); const serialized = dock.serialize(); dock.setState({ size: 122 }); expect(dockElement.offsetWidth).toBe(122); dock.destroyActivePane(); dock.deserialize(serialized, atom.deserializers); expect(dockElement.offsetWidth).toBe(150); }); it("isn't visible if it has no items", async () => { jasmine.attachToDOM(atom.workspace.getElement()); const item = { element: document.createElement('div'), getDefaultLocation() { return 'left'; }, getPreferredWidth() { return 122; } }; const dock = atom.workspace.getLeftDock(); await atom.workspace.open(item); expect(dock.isVisible()).toBe(true); const serialized = dock.serialize(); dock.deserialize(serialized, atom.deserializers); expect(dock.getPaneItems()).toHaveLength(0); expect(dock.isVisible()).toBe(false); }); }); describe('drag handling', () => { it('expands docks to match the preferred size of the dragged item', async () => { jasmine.attachToDOM(atom.workspace.getElement()); const element = document.createElement('div'); element.setAttribute('is', 'tabs-tab'); element.item = { element, getDefaultLocation() { return 'left'; }, getPreferredWidth() { return 144; } }; const dragEvent = new DragEvent('dragstart'); Object.defineProperty(dragEvent, 'target', { value: element }); atom.workspace.getElement().handleDragStart(dragEvent); await getNextUpdatePromise(); expect(atom.workspace.getLeftDock().refs.wrapperElement.offsetWidth).toBe( 144 ); }); it('does nothing when text nodes are dragged', () => { jasmine.attachToDOM(atom.workspace.getElement()); const textNode = document.createTextNode('hello'); const dragEvent = new DragEvent('dragstart'); Object.defineProperty(dragEvent, 'target', { value: textNode }); expect(() => atom.workspace.getElement().handleDragStart(dragEvent) ).not.toThrow(); }); }); describe('::getActiveTextEditor()', () => { it('is deprecated', () => { spyOn(Grim, 'deprecate'); atom.workspace.getLeftDock().getActiveTextEditor(); expect(Grim.deprecate.callCount).toBe(1); }); }); }); ================================================ FILE: spec/file-system-blob-store-spec.js ================================================ const temp = require('temp').track(); const path = require('path'); const fs = require('fs-plus'); const FileSystemBlobStore = require('../src/file-system-blob-store'); describe('FileSystemBlobStore', function() { let [storageDirectory, blobStore] = []; beforeEach(function() { storageDirectory = temp.path('atom-spec-filesystemblobstore'); blobStore = FileSystemBlobStore.load(storageDirectory); }); afterEach(() => fs.removeSync(storageDirectory)); it("is empty when the file doesn't exist", function() { expect(blobStore.get('foo')).toBeUndefined(); expect(blobStore.get('bar')).toBeUndefined(); }); it('allows to read and write buffers from/to memory without persisting them', function() { blobStore.set('foo', Buffer.from('foo')); blobStore.set('bar', Buffer.from('bar')); expect(blobStore.get('foo')).toEqual(Buffer.from('foo')); expect(blobStore.get('bar')).toEqual(Buffer.from('bar')); expect(blobStore.get('baz')).toBeUndefined(); expect(blobStore.get('qux')).toBeUndefined(); }); it('persists buffers when saved and retrieves them on load, giving priority to in-memory ones', function() { blobStore.set('foo', Buffer.from('foo')); blobStore.set('bar', Buffer.from('bar')); blobStore.save(); blobStore = FileSystemBlobStore.load(storageDirectory); expect(blobStore.get('foo')).toEqual(Buffer.from('foo')); expect(blobStore.get('bar')).toEqual(Buffer.from('bar')); expect(blobStore.get('baz')).toBeUndefined(); expect(blobStore.get('qux')).toBeUndefined(); blobStore.set('foo', Buffer.from('changed')); expect(blobStore.get('foo')).toEqual(Buffer.from('changed')); }); it('persists in-memory and previously stored buffers, and deletes unused keys when saved', function() { blobStore.set('foo', Buffer.from('foo')); blobStore.set('bar', Buffer.from('bar')); blobStore.save(); blobStore = FileSystemBlobStore.load(storageDirectory); blobStore.set('bar', Buffer.from('changed')); blobStore.set('qux', Buffer.from('qux')); blobStore.save(); blobStore = FileSystemBlobStore.load(storageDirectory); expect(blobStore.get('foo')).toBeUndefined(); expect(blobStore.get('bar')).toEqual(Buffer.from('changed')); expect(blobStore.get('qux')).toEqual(Buffer.from('qux')); }); it('allows to delete keys from both memory and stored buffers', function() { blobStore.set('a', Buffer.from('a')); blobStore.set('b', Buffer.from('b')); blobStore.save(); blobStore = FileSystemBlobStore.load(storageDirectory); blobStore.get('a'); // prevent the key from being deleted on save blobStore.set('b', Buffer.from('b')); blobStore.set('c', Buffer.from('c')); blobStore.delete('b'); blobStore.delete('c'); blobStore.save(); blobStore = FileSystemBlobStore.load(storageDirectory); expect(blobStore.get('a')).toEqual(Buffer.from('a')); expect(blobStore.get('b')).toBeUndefined(); expect(blobStore.get('b')).toBeUndefined(); expect(blobStore.get('c')).toBeUndefined(); }); it('ignores errors when loading an invalid blob store', function() { blobStore.set('a', Buffer.from('a')); blobStore.set('b', Buffer.from('b')); blobStore.save(); // Simulate corruption fs.writeFileSync(path.join(storageDirectory, 'MAP'), Buffer.from([0])); fs.writeFileSync(path.join(storageDirectory, 'INVKEYS'), Buffer.from([0])); fs.writeFileSync(path.join(storageDirectory, 'BLOB'), Buffer.from([0])); blobStore = FileSystemBlobStore.load(storageDirectory); expect(blobStore.get('a')).toBeUndefined(); expect(blobStore.get('b')).toBeUndefined(); blobStore.set('a', Buffer.from('x')); blobStore.set('b', Buffer.from('y')); blobStore.save(); blobStore = FileSystemBlobStore.load(storageDirectory); expect(blobStore.get('a')).toEqual(Buffer.from('x')); expect(blobStore.get('b')).toEqual(Buffer.from('y')); }); }); ================================================ FILE: spec/fixtures/babel/babel-comment.js ================================================ /** @babel */ module.exports = v => v + 1 ================================================ FILE: spec/fixtures/babel/babel-double-quotes.js ================================================ "use babel"; module.exports = v => v + 1 ================================================ FILE: spec/fixtures/babel/babel-single-quotes.js ================================================ 'use babel'; module.exports = v => v + 1 ================================================ FILE: spec/fixtures/babel/flow-comment.js ================================================ /* @flow */ const f: Function = v => v + 1 module.exports = f ================================================ FILE: spec/fixtures/babel/flow-slash-comment.js ================================================ // @flow const f: Function = v => v + 1 module.exports = f ================================================ FILE: spec/fixtures/babel/invalid.js ================================================ 'use 6to6'; export default 42; ================================================ FILE: spec/fixtures/coffee.coffee ================================================ class quicksort sort: (items) -> return items if items.length <= 1 pivot = items.shift() left = [] right = [] # Comment in the middle while items.length > 0 current = items.shift() if current < pivot left.push(current) else right.push(current); sort(left).concat(pivot).concat(sort(right)) noop: -> # just a noop exports.modules = quicksort ================================================ FILE: spec/fixtures/cson.cson ================================================ a: 4 ================================================ FILE: spec/fixtures/css.css ================================================ body { font-size: 1234px; width: 110%; font-weight: bold !important; } ================================================ FILE: spec/fixtures/dir/a ================================================ aaa bbb cc aa cc dollar$bill ================================================ FILE: spec/fixtures/dir/a-dir/oh-git ================================================ bbb aaaa ================================================ FILE: spec/fixtures/dir/b ================================================ aaa ccc ================================================ FILE: spec/fixtures/dir/c ================================================ line 1 line 2 line 3 line 4 line 5 result 1 line 6 line 7 line 8 line 9 line 10 result 2 result 3 line 11 line 12 result 4 line 13 line 14 line 15 ================================================ FILE: spec/fixtures/dir/file-detected-as-binary ================================================ asciiProperty=Foo utf8Property=Fòò latin1Property=F ================================================ FILE: spec/fixtures/dir/file-with-newline-literal ================================================ newline1 newline2 newline3 first second\nthird newline4 newline5 ================================================ FILE: spec/fixtures/dir/file-with-unicode ================================================ ДДДДДДДДДДДДДДДДДД line with unicode ================================================ FILE: spec/fixtures/dir/file1 ================================================ ================================================ FILE: spec/fixtures/git/ignore.git/HEAD ================================================ ref: refs/heads/master ================================================ FILE: spec/fixtures/git/ignore.git/config ================================================ [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true ================================================ FILE: spec/fixtures/git/ignore.git/info/exclude ================================================ a.txt ================================================ FILE: spec/fixtures/git/ignore.git/refs/heads/master ================================================ ef046e9eecaa5255ea5e9817132d4001724d6ae1 ================================================ FILE: spec/fixtures/git/master.git/HEAD ================================================ ref: refs/heads/master ================================================ FILE: spec/fixtures/git/master.git/config ================================================ [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true [remote "origin"] url = https://github.com/example-user/example-repo.git fetch = +refs/heads/*:refs/remotes/origin/* [gc] worktreePruneExpire = never ================================================ FILE: spec/fixtures/git/master.git/refs/heads/master ================================================ ef046e9eecaa5255ea5e9817132d4001724d6ae1 ================================================ FILE: spec/fixtures/git/master.git/worktrees/worktree-dir/HEAD ================================================ ef046e9eecaa5255ea5e9817132d4001724d6ae1 ================================================ FILE: spec/fixtures/git/master.git/worktrees/worktree-dir/commondir ================================================ ../.. ================================================ FILE: spec/fixtures/git/repo-with-submodules/.gitmodules ================================================ [submodule "jstips"] path = jstips url = https://github.com/loverajoel/jstips [submodule "You-Dont-Need-jQuery"] path = You-Dont-Need-jQuery url = https://github.com/oneuijs/You-Dont-Need-jQuery ================================================ FILE: spec/fixtures/git/repo-with-submodules/README ================================================ ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.babelrc ================================================ { presets: ["es2015", "stage-0"] } ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.eslintrc ================================================ { "extends": "eslint-config-airbnb", "env": { "browser": true, "mocha": true, "node": true }, "rules": { "valid-jsdoc": 2, "no-param-reassign": 0, "comma-dangle": 0, "one-var": 0, "no-else-return": 1, "no-unused-expressions": 0, "indent": 1, "eol-last": 0 } } ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.gitignore ================================================ .DS_Store *.log node_modules coverage logs ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/.travis.yml ================================================ language: node_js node_js: - "5" - "4" before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start script: - npm run lint - npm test ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 oneuijs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-es.md ================================================ > #### You Don't Need jQuery Tú no necesitas jQuery --- El desarrollo Frontend evoluciona día a día, y los navegadores modernos ya han implementado nativamente APIs para trabajar con DOM/BOM, las cuales son muy buenas, por lo que definitivamente no es necesario aprender jQuery desde cero para manipular el DOM. En la actualidad, gracias al surgimiento de librerías frontend como React, Angular y Vue, manipular el DOM es contrario a los patrones establecidos, y jQuery se ha vuelto menos importante. Este proyecto resume la mayoría de métodos alternativos a jQuery, pero de forma nativa con soporte IE 10+. ## Tabla de Contenidos 1. [Query Selector](#query-selector) 1. [CSS & Estilo](#css--estilo) 1. [Manipulación DOM](#manipulación-dom) 1. [Ajax](#ajax) 1. [Eventos](#eventos) 1. [Utilidades](#utilidades) 1. [Traducción](#traducción) 1. [Soporte de Navegadores](#soporte-de-navegadores) ## Query Selector En lugar de los selectores comunes como clase, id o atributos podemos usar `document.querySelector` o `document.querySelectorAll` como alternativas. Las diferencias radican en: * `document.querySelector` devuelve el primer elemento que cumpla con la condición * `document.querySelectorAll` devuelve todos los elementos que cumplen con la condición en forma de NodeList. Puede ser convertido a Array usando `[].slice.call(document.querySelectorAll(selector) || []);` * Si ningún elemento cumple con la condición, jQuery retornaría `[]` mientras la API DOM retornaría `null`. Nótese el NullPointerException. Se puede usar `||` para establecer el valor por defecto al no encontrar elementos, como en `document.querySelectorAll(selector) || []` > Notice: `document.querySelector` and `document.querySelectorAll` are quite **SLOW**, try to use `getElementById`, `document.getElementsByClassName` o `document.getElementsByTagName` if you want to Obtener a performance bonus. - [1.0](#1.0)
          Buscar por selector ```js // jQuery $('selector'); // Nativo document.querySelectorAll('selector'); ``` - [1.1](#1.1) Buscar por Clase ```js // jQuery $('.class'); // Nativo document.querySelectorAll('.class'); // Forma alternativa document.getElementsByClassName('class'); ``` - [1.2](#1.2) Buscar por id ```js // jQuery $('#id'); // Nativo document.querySelector('#id'); // Forma alternativa document.getElementById('id'); ``` - [1.3](#1.3) Buscar por atributo ```js // jQuery $('a[target=_blank]'); // Nativo document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Buscar + Buscar nodos ```js // jQuery $el.find('li'); // Nativo el.querySelectorAll('li'); ``` + Buscar "body" ```js // jQuery $('body'); // Nativo document.body; ``` + Buscar Atributo ```js // jQuery $el.attr('foo'); // Nativo e.getAttribute('foo'); ``` + Buscar atributo "data" ```js // jQuery $el.data('foo'); // Nativo // Usando getAttribute el.getAttribute('data-foo'); // También puedes utilizar `dataset` desde IE 11+ el.dataset['foo']; ``` - [1.5](#1.5) Elementos Hermanos/Previos/Siguientes + Elementos hermanos ```js // jQuery $el.siblings(); // Nativo [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Elementos previos ```js // jQuery $el.prev(); // Nativo el.previousElementSibling; ``` + Elementos siguientes ```js // jQuery $el.next(); // Nativo el.nextElementSibling; ``` - [1.6](#1.6) Closest Retorna el elemento más cercano que coincida con la condición, partiendo desde el nodo actual hasta document. ```js // jQuery $el.closest(queryString); // Nativo function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Parents Until Obtiene los ancestros de cada elemento en el set actual de elementos que cumplan con la condición, sin incluir el actual ```js // jQuery $el.parentsUntil(selector, filter); // Nativo function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // Partir desde el elemento padre el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Formularios + Input/Textarea ```js // jQuery $('#my-input').val(); // Nativo document.querySelector('#my-input').value; ``` + Obtener el índice de e.currentTarget en `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Nativo [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Contenidos de Iframe `$('iframe').contents()` devuelve `contentDocument` para este iframe específico + Contenidos de Iframe ```js // jQuery $iframe.contents(); // Nativo iframe.contentDocument; ``` + Buscar dentro de un Iframe ```js // jQuery $iframe.contents().find('.css'); // Nativo iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ volver al inicio](#tabla-de-contenidos)** ## CSS & Estilo - [2.1](#2.1) CSS + Obtener Estilo ```js // jQuery $el.css("color"); // Nativo // NOTA: Bug conocido, retornará 'auto' si el valor de estilo es 'auto' const win = el.ownerDocument.defaultView; // null significa que no tiene pseudo estilos win.getComputedStyle(el, null).color; ``` + Establecer style ```js // jQuery $el.css({ color: "#ff0011" }); // Nativo el.style.color = '#ff0011'; ``` + Obtener/Establecer Estilos Nótese que si se desea establecer múltiples estilos a la vez, se puede utilizar el método [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) en el paquete oui-dom-utils. + Agregar clase ```js // jQuery $el.addClass(className); // Nativo el.classList.add(className); ``` + Quitar Clase ```js // jQuery $el.removeClass(className); // Nativo el.classList.remove(className); ``` + Consultar si tiene clase ```js // jQuery $el.hasClass(className); // Nativo el.classList.contains(className); ``` + Toggle class ```js // jQuery $el.toggleClass(className); // Nativo el.classList.toggle(className); ``` - [2.2](#2.2) Width & Height Ancho y Alto son teóricamente idénticos. Usaremos el Alto como ejemplo: + Alto de Ventana ```js // alto de ventana $(window).height(); // Sin scrollbar, se comporta como jQuery window.document.documentElement.clientHeight; // Con scrollbar window.innerHeight; ``` + Alto de Documento ```js // jQuery $(document).height(); // Nativo document.documentElement.scrollHeight; ``` + Alto de Elemento ```js // jQuery $el.height(); // Nativo function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // Precisión de integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) el.clientHeight; // Precisión de decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Posición & Offset + Posición ```js // jQuery $el.position(); // Nativo { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Nativo function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Posición del Scroll Vertical ```js // jQuery $(window).scrollTop(); // Nativo (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ volver al inicio](#tabla-de-contenidos)** ## Manipulación DOM - [3.1](#3.1) Remove ```js // jQuery $el.remove(); // Nativo el.parentNode.removeChild(el); ``` - [3.2](#3.2) Text + Obtener Texto ```js // jQuery $el.text(); // Nativo el.textContent; ``` + Establecer Texto ```js // jQuery $el.text(string); // Nativo el.textContent = string; ``` - [3.3](#3.3) HTML + Obtener HTML ```js // jQuery $el.html(); // Nativo el.innerHTML; ``` + Establecer HTML ```js // jQuery $el.html(htmlString); // Nativo el.innerHTML = htmlString; ``` - [3.4](#3.4) Append Añadir elemento hijo después del último hijo del elemento padre ```js // jQuery $el.append("
          hello
          "); // Nativo el.insertAdjacentHTML("beforeend","
          hello
          "); ``` - [3.5](#3.5) Prepend Añadir elemento hijo después del último hijo del elemento padre ```js // jQuery $el.prepend("
          hello
          "); // Nativo el.insertAdjacentHTML("afterbegin","
          hello
          "); ``` - [3.6](#3.6) insertBefore Insertar un nuevo nodo antes del primero de los elementos seleccionados ```js // jQuery $newEl.insertBefore(queryString); // Nativo const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter Insertar un nuevo nodo después de los elementos seleccionados ```js // jQuery $newEl.insertAfter(queryString); // Nativo const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` **[⬆ volver al inicio](#tabla-de-contenidos)** ## Ajax Reemplazar con [fetch](https://github.com/camsong/fetch-ie8) y [fetch-jsonp](https://github.com/camsong/fetch-jsonp) +[Fetch API](https://fetch.spec.whatwg.org/) es el nuevo estándar quue reemplaza a XMLHttpRequest para efectuar peticiones AJAX. Funciona en Chrome y Firefox, como también es posible usar un polyfill en otros navegadores. + +Es una buena alternativa utilizar [github/fetch](http://github.com/github/fetch) en IE9+ o [fetch-ie8](https://github.com/camsong/fetch-ie8/) en IE8+, [fetch-jsonp](https://github.com/camsong/fetch-jsonp) para efectuar peticiones JSONP. **[⬆ volver al inicio](#tabla-de-contenidos)** ## Eventos Para un reemplazo completo con namespace y delegación, utilizar https://github.com/oneuijs/oui-dom-events - [5.1](#5.1) Asignar un evento con "on" ```js // jQuery $el.on(eventName, eventHandler); // Nativo el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) Desasignar un evento con "off" ```js // jQuery $el.off(eventName, eventHandler); // Nativo el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Nativo if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ volver al inicio](#tabla-de-contenidos)** ## Utilidades - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Nativo Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Nativo string.trim(); ``` - [6.3](#6.3) Object Assign Utilizar polyfill para object.assign https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Nativo Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Nativo el !== child && el.contains(child); ``` **[⬆ volver al inicio](#tabla-de-contenidos)** ## Traducción * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Bahasa Indonesia](./README-id.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Español](./README-es.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) ## Soporte de Navegadores ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Última ✔ | Última ✔ | 10+ ✔ | Última ✔ | 6.1+ ✔ | # Licencia MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-id.md ================================================ ## Anda tidak memerlukan jQuery Dewasa ini perkembangan environment frontend sangatlah pesat, dimana banyak browser sudah mengimplementasikan DOM/BOM APIs dengan baik. Kita tidak perlu lagi belajar jQuery dari nol untuk keperluan manipulasi DOM atau events. Disaat yang sama; dengan berterimakasih kepada library frontend terkini seperti React, Angular dan Vue; Memanipulasi DOM secara langsung telah menjadi anti-pattern alias sesuatu yang tidak perlu dilakukan. Dengan kata lain, jQuery sekarang menjadi semakin tidak diperlukan. Projek ini memberikan informasi mengenai metode alternatif dari jQuery untuk implementasi Native dengan support untuk browser IE 10+. ## Daftar Isi 1. [Query Selector](#query-selector) 1. [CSS & Style](#css-style) 1. [DOM Manipulation](#dom-manipulation) 1. [Ajax](#ajax) 1. [Events](#events) 1. [Utilities](#utilities) 1. [Translation](#translation) 1. [Browser Support](#browser-yang-di-support) ## Query Selector Untuk selector-selector umum seperti class, id atau attribute, kita dapat menggunakan `document.querySelector` atau `document.querySelectorAll` sebagai pengganti. Perbedaan diantaranya adalah: * `document.querySelector` mengembalikan elemen pertama yang cocok * `document.querySelectorAll` mengembalikan semua elemen yang cocok sebagai NodeList. Hasilnya bisa dikonversikan menjadi Array `[].slice.call(document.querySelectorAll(selector) || []);` * Bila tidak ada hasil pengembalian elemen yang cocok, jQuery akan mengembalikan `[]` sedangkan DOM API akan mengembalikan `null`. Mohon diperhatikan mengenai Null Pointer Exception. Anda juga bisa menggunakan operator `||` untuk set nilai awal jika hasil pencarian tidak ditemukan : `document.querySelectorAll(selector) || []` > Perhatian: `document.querySelector` dan `document.querySelectorAll` sedikit **LAMBAT**. Silahkan menggunakan `getElementById`, `document.getElementsByClassName` atau `document.getElementsByTagName` jika anda menginginkan tambahan performa. - [1.0](#1.0) Query by selector ```js // jQuery $('selector'); // Native document.querySelectorAll('selector'); ``` - [1.1](#1.1) Query by class ```js // jQuery $('.class'); // Native document.querySelectorAll('.class'); // or document.getElementsByClassName('class'); ``` - [1.2](#1.2) Query by id ```js // jQuery $('#id'); // Native document.querySelector('#id'); // or document.getElementById('id'); ``` - [1.3](#1.3) Query menggunakan attribute ```js // jQuery $('a[target=_blank]'); // Native document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Pencarian. + Mencari nodes ```js // jQuery $el.find('li'); // Native el.querySelectorAll('li'); ``` + Mencari body ```js // jQuery $('body'); // Native document.body; ``` + Mencari Attribute ```js // jQuery $el.attr('foo'); // Native e.getAttribute('foo'); ``` + Mencari data attribute ```js // jQuery $el.data('foo'); // Native // gunakan getAttribute el.getAttribute('data-foo'); // anda juga bisa menggunakan `dataset` bila anda perlu support IE 11+ el.dataset['foo']; ``` - [1.5](#1.5) Elemen-elemen Sibling/Previous/Next + Elemen Sibling ```js // jQuery $el.siblings(); // Native [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Elemen Previous ```js // jQuery $el.prev(); // Native el.previousElementSibling; ``` + Elemen Next ```js // next $el.next(); el.nextElementSibling; ``` - [1.6](#1.6) Closest Mengembalikan elemen pertama yang cocok dari selector yang digunakan, dengan cara mencari mulai dari elemen-sekarang sampai ke document. ```js // jQuery $el.closest(queryString); // Native function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Parents Until Digunakan untuk mendapatkan "ancestor" dari setiap elemen yang ditemukan. Namun tidak termasuk elemen-sekarang yang didapat dari pencarian oleh selector, DOM node, atau object jQuery. ```js // jQuery $el.parentsUntil(selector, filter); // Native function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // match start from parent el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Native document.querySelector('#my-input').value; ``` + Get index of e.currentTarget between `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Native [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Iframe Contents `$('iframe').contents()` mengembalikan `contentDocument` + Iframe contents ```js // jQuery $iframe.contents(); // Native iframe.contentDocument; ``` + Iframe Query ```js // jQuery $iframe.contents().find('.css'); // Native iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ back to top](#daftar-isi)** ## CSS Style - [2.1](#2.1) CSS + Get style ```js // jQuery $el.css("color"); // Native // PERHATIAN: ada bug disini, dimana fungsi ini akan mengembalikan nilai 'auto' bila nilai dari atribut style adalah 'auto' const win = el.ownerDocument.defaultView; // null artinya tidak mengembalikan pseudo styles win.getComputedStyle(el, null).color; ``` + Set style ```js // jQuery $el.css({ color: "#ff0011" }); // Native el.style.color = '#ff0011'; ``` + Get/Set Styles Mohon dicatat jika anda ingin men-set banyak style bersamaan, anda dapat menemukan referensi di metode [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) pada package oui-dom-utils + Add class ```js // jQuery $el.addClass(className); // Native el.classList.add(className); ``` + Remove class ```js // jQuery $el.removeClass(className); // Native el.classList.remove(className); ``` + has class ```js // jQuery $el.hasClass(className); // Native el.classList.contains(className); ``` + Toggle class ```js // jQuery $el.toggleClass(className); // Native el.classList.toggle(className); ``` - [2.2](#2.2) Width & Height Secara teori, width dan height identik, contohnya Height: + Window height ```js // window height $(window).height(); // without scrollbar, behaves like jQuery window.document.documentElement.clientHeight; // with scrollbar window.innerHeight; ``` + Document height ```js // jQuery $(document).height(); // Native document.documentElement.scrollHeight; ``` + Element height ```js // jQuery $el.height(); // Native function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) el.clientHeight; // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Position & Offset + Position ```js // jQuery $el.position(); // Native { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Native function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Scroll Top ```js // jQuery $(window).scrollTop(); // Native (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ back to top](#daftar-isi)** ## DOM Manipulation - [3.1](#3.1) Remove ```js // jQuery $el.remove(); // Native el.parentNode.removeChild(el); ``` - [3.2](#3.2) Text + Get text ```js // jQuery $el.text(); // Native el.textContent; ``` + Set text ```js // jQuery $el.text(string); // Native el.textContent = string; ``` - [3.3](#3.3) HTML + Get HTML ```js // jQuery $el.html(); // Native el.innerHTML; ``` + Set HTML ```js // jQuery $el.html(htmlString); // Native el.innerHTML = htmlString; ``` - [3.4](#3.4) Append Menambahkan elemen-anak setelah anak terakhir dari elemen-parent ```js // jQuery $el.append("
          hello
          "); // Native let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.appendChild(newEl); ``` - [3.5](#3.5) Prepend ```js // jQuery $el.prepend("
          hello
          "); // Native let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.insertBefore(newEl, el.firstChild); ``` - [3.6](#3.6) insertBefore Memasukkan node baru sebelum elemen yang dipilih. ```js // jQuery $newEl.insertBefore(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter Memasukkan node baru sesudah elemen yang dipilih. ```js // jQuery $newEl.insertAfter(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` **[⬆ back to top](#daftar-isi)** ## Ajax Gantikan dengan [fetch](https://github.com/camsong/fetch-ie8) dan [fetch-jsonp](https://github.com/camsong/fetch-jsonp) **[⬆ back to top](#daftar-isi)** ## Events Untuk penggantian secara menyeluruh dengan namespace dan delegation, rujuk ke https://github.com/oneuijs/oui-dom-events - [5.1](#5.1) Bind event dengan menggunakan on ```js // jQuery $el.on(eventName, eventHandler); // Native el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) Unbind event dengan menggunakan off ```js // jQuery $el.off(eventName, eventHandler); // Native el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Native if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ back to top](#daftar-isi)** ## Utilities - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Native Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Native string.trim(); ``` - [6.3](#6.3) Object Assign Extend, use object.assign polyfill https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Native Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Native el !== child && el.contains(child); ``` **[⬆ back to top](#daftar-isi)** ## Terjemahan * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Bahasa Indonesia](./README-id.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) ## Browser yang di Support ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # License MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-it.md ================================================ ## Non hai bisogno di jQuery Il mondo del Frontend si evolve rapidamente oggigiorno, i browsers moderni hanno gia' implementato un'ampia gamma di DOM/BOM API soddisfacenti. Non dobbiamo imparare jQuery dalle fondamenta per la manipolazione del DOM o di eventi. Nel frattempo, grazie al prevalicare di librerie per il frontend come React, Angular a Vue, manipolare il DOM direttamente diventa un anti-pattern, di consequenza jQuery non e' mai stato meno importante. Questo progetto sommarizza la maggior parte dei metodi e implementazioni alternative a jQuery, con il supporto di IE 10+. ## Tabella contenuti 1. [Query Selector](#query-selector) 1. [CSS & Style](#css--style) 1. [Manipolazione DOM](#manipolazione-dom) 1. [Ajax](#ajax) 1. [Eventi](#eventi) 1. [Utilities](#utilities) 1. [Alternative](#alternative) 1. [Traduzioni](#traduzioni) 1. [Supporto Browsers](#supporto-browsers) ## Query Selector Al posto di comuni selettori come class, id o attributi possiamo usare `document.querySelector` o `document.querySelectorAll` per sostituzioni. La differenza risiede in: * `document.querySelector` restituisce il primo elemento combiaciante * `document.querySelectorAll` restituisce tutti gli elementi combiacianti della NodeList. Puo' essere convertito in Array usando `[].slice.call(document.querySelectorAll(selector) || []);` * Se nessun elemento combiacia, jQuery restituitirebbe `[]` li' dove il DOM API ritornera' `null`. Prestate attenzione al Null Pointer Exception. Potete anche usare `||` per settare valori di default se non trovato, come `document.querySelectorAll(selector) || []` > Notare: `document.querySelector` e `document.querySelectorAll` sono abbastanza **SLOW**, provate ad usare `getElementById`, `document.getElementsByClassName` o `document.getElementsByTagName` se volete avere un bonus in termini di performance. - [1.0](#1.0) Query da selettore ```js // jQuery $('selector'); // Nativo document.querySelectorAll('selector'); ``` - [1.1](#1.1) Query da classe ```js // jQuery $('.class'); // Nativo document.querySelectorAll('.class'); // or document.getElementsByClassName('class'); ``` - [1.2](#1.2) Query da id ```js // jQuery $('#id'); // Nativo document.querySelector('#id'); // o document.getElementById('id'); ``` - [1.3](#1.3) Query da attributo ```js // jQuery $('a[target=_blank]'); // Nativo document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Trovare qualcosa. + Trovare nodes ```js // jQuery $el.find('li'); // Nativo el.querySelectorAll('li'); ``` + Trovare body ```js // jQuery $('body'); // Nativo document.body; ``` + Trovare Attributi ```js // jQuery $el.attr('foo'); // Nativo e.getAttribute('foo'); ``` + Trovare attributo data ```js // jQuery $el.data('foo'); // Nativo // using getAttribute el.getAttribute('data-foo'); // potete usare `dataset` solo se supportate IE 11+ el.dataset['foo']; ``` - [1.5](#1.5) Fratelli/Precedento/Successivo Elemento + Elementi fratelli ```js // jQuery $el.siblings(); // Nativo [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Elementi precedenti ```js // jQuery $el.prev(); // Nativo el.previousElementSibling; ``` + Elementi successivi ```js // jQuery $el.next(); // Nativo el.nextElementSibling; ``` - [1.6](#1.6) Il piu' vicino Restituisce il primo elementi combiaciante il selettore fornito, attraversando dall'elemento corrente fino al document . ```js // jQuery $el.closest(queryString); // Nativo - Solo ultimo, NO IE el.closest(selector); // Nativo - IE10+ function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Fino a parenti Ottiene il parente di ogni elemento nel set corrente di elementi combiacianti, fino a ma non incluso, l'elemento combiaciante il selettorer, DOM node, o jQuery object. ```js // jQuery $el.parentsUntil(selector, filter); // Nativo function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // il match parte dal parente el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Native document.querySelector('#my-input').value; ``` + Get index of e.currentTarget between `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Nativo [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Iframe Contents `$('iframe').contents()` restituisce `contentDocument` per questo specifico iframe + Iframe contenuti ```js // jQuery $iframe.contents(); // Nativo iframe.contentDocument; ``` + Iframe Query ```js // jQuery $iframe.contents().find('.css'); // Nativo iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ back to top](#table-of-contents)** ## CSS & Style - [2.1](#2.1) CSS + Ottenere style ```js // jQuery $el.css("color"); // Nativo // NOTA: Bug conosciuto, restituira' 'auto' se il valore di style e' 'auto' const win = el.ownerDocument.defaultView; // null significa che non restituira' lo psuedo style win.getComputedStyle(el, null).color; ``` + Settare style ```js // jQuery $el.css({ color: "#ff0011" }); // Nativo el.style.color = '#ff0011'; ``` + Ottenere/Settare Styles Nota che se volete settare styles multipli in una sola volta, potete riferire [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) metodo in oui-dom-utils package. + Aggiungere classe ```js // jQuery $el.addClass(className); // Nativo el.classList.add(className); ``` + Rimouvere class ```js // jQuery $el.removeClass(className); // Nativo el.classList.remove(className); ``` + has class ```js // jQuery $el.hasClass(className); // Nativo el.classList.contains(className); ``` + Toggle class ```js // jQuery $el.toggleClass(className); // Nativo el.classList.toggle(className); ``` - [2.2](#2.2) Width & Height Width e Height sono teoricamente identici, prendendo Height come esempio: + Window height ```js // window height $(window).height(); // senza scrollbar, si comporta comporta jQuery window.document.documentElement.clientHeight; // con scrollbar window.innerHeight; ``` + Document height ```js // jQuery $(document).height(); // Nativo document.documentElement.scrollHeight; ``` + Element height ```js // jQuery $el.height(); // Nativo function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // preciso a intero(quando `border-box`, e' `height`; quando `content-box`, e' `height + padding + border`) el.clientHeight; // preciso a decimale(quando `border-box`, e' `height`; quando `content-box`, e' `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Position & Offset + Position ```js // jQuery $el.position(); // Nativo { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Nativo function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Scroll Top ```js // jQuery $(window).scrollTop(); // Nativo (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ back to top](#table-of-contents)** ## Manipolazione DOM - [3.1](#3.1) Remove ```js // jQuery $el.remove(); // Nativo el.parentNode.removeChild(el); ``` - [3.2](#3.2) Text + Get text ```js // jQuery $el.text(); // Nativo el.textContent; ``` + Set text ```js // jQuery $el.text(string); // Nativo el.textContent = string; ``` - [3.3](#3.3) HTML + Ottenere HTML ```js // jQuery $el.html(); // Nativo el.innerHTML; ``` + Settare HTML ```js // jQuery $el.html(htmlString); // Nativo el.innerHTML = htmlString; ``` - [3.4](#3.4) Append appendere elemento figlio dopo l'ultimo elemento figlio del genitore ```js // jQuery $el.append("
          hello
          "); // Nativo el.insertAdjacentHTML("beforeend","
          hello
          "); ``` - [3.5](#3.5) Prepend ```js // jQuery $el.prepend("
          hello
          "); // Nativo el.insertAdjacentHTML("afterbegin","
          hello
          "); ``` - [3.6](#3.6) insertBefore Inserire un nuovo node dopo l'elmento selezionato ```js // jQuery $newEl.insertBefore(queryString); // Nativo const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter Insert a new node after the selected elements ```js // jQuery $newEl.insertAfter(queryString); // Nativo const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` - [3.8](#3.8) is Restituisce `true` se combacia con l'elemento selezionato ```js // jQuery - Notare `is` funziona anche con `function` o `elements` non di importanza qui $el.is(selector); // Nativo el.matches(selector); ``` **[⬆ back to top](#table-of-contents)** ## Ajax Sostituire con [fetch](https://github.com/camsong/fetch-ie8) and [fetch-jsonp](https://github.com/camsong/fetch-jsonp) **[⬆ back to top](#table-of-contents)** ## Eventi Per una completa sostituzione con namespace e delegation, riferire a https://github.com/oneuijs/oui-dom-events - [5.1](#5.1) Bind un evento con on ```js // jQuery $el.on(eventName, eventHandler); // Nativo el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) Unbind an event with off ```js // jQuery $el.off(eventName, eventHandler); // Nativo el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Nativo if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ back to top](#table-of-contents)** ## Utilities - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Nativo Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Nativo string.trim(); ``` - [6.3](#6.3) Object Assign Extend, usa object.assign polyfill https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Nativo Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Nativo el !== child && el.contains(child); ``` **[⬆ back to top](#table-of-contents)** ## Alternative * [Forse non hai bisogno di jQuery](http://youmightnotneedjquery.com/) - Esempi di come creare eventi comuni, elementi, ajax etc usando puramente javascript. * [npm-dom](http://github.com/npm-dom) e [webmodules](http://github.com/webmodules) - Organizzazione dove puoi trovare moduli per il DOM individuale su NPM ## Traduzioni * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Bahasa Indonesia](./README-id.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Español](./README-es.md) * [Italiano](./README-it.md) * [Türkçe](./README-tr.md) ## Supporto Browsers ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Ultimo ✔ | Ultimo ✔ | 10+ ✔ | Ultimo ✔ | 6.1+ ✔ | # Licenza MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-my.md ================================================ ## Anda tidak memerlukan jQuery Mutakhir ini perkembangan dalam persekitaran frontend berlaku begitu pesat sekali. Justeru itu kebanyakan pelayar moden telahpun menyediakan API yang memadai untuk pengaksesan DOM/BOM. Kita tak payah lagi belajar jQuery dari asas untuk memanipulasi DOM dan acara-acara. Projek ini menawarkan perlaksanaan alternatif kepada kebanyakan kaedah-kaedah jQuery yang menyokong IE 10+. ## Isi Kandungan 1. [Pemilihan elemen](#pemilihan-elemen) 1. [CSS & Penggayaan](#css-penggayaan) 1. [Manipulasi DOM](#manipulasi-dom) 1. [Ajax](#ajax) 1. [Events](#events) 1. [Utiliti](#utiliti) 1. [Terjemahan](#terjemahan) 1. [Browser Support](#browser-support) ## Pemilihan Elemen Pemilihan elemen yang umum seperti class, id atau atribut, biasanya kita boleh pakai `document.querySelector` atau `document.querySelectorAll` sebagai ganti. Bezanya terletak pada * `document.querySelector` akan mengembalikan elemen pertama sekali yang sepadan dijumpai * `document.querySelectorAll` akan mengembalikan kesemua elemen yang sepadan dijumpai kedalam sebuah NodeList. Ia boleh ditukar kedalam bentuk array menggunakan `[].slice.call` * Sekiranya tiada elemen yang sepadan dijumpai, jQuery akan mengembalikan `[]` dimana API DOM pula akan mengembalikan `null`. Sila ambil perhatian pada Null Pointer Exception > AWAS: `document.querySelector` dan `document.querySelectorAll` agak **LEMBAB** berbanding `getElementById`, `document.getElementsByClassName` atau `document.getElementsByTagName` jika anda menginginkan bonus dari segi prestasi. - [1.1](#1.1) Pemilihan menggunakan class ```js // jQuery $('.css'); // Native document.querySelectorAll('.css'); ``` - [1.2](#1.2) Pemilihan menggunakan id ```js // jQuery $('#id'); // Native document.querySelector('#id'); ``` - [1.3](#1.3) Pemilihan menggunakan atribut ```js // jQuery $('a[target=_blank]'); // Native document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Cari sth. + Find nodes ```js // jQuery $el.find('li'); // Native el.querySelectorAll('li'); ``` + Cari body ```js // jQuery $('body'); // Native document.body; ``` + Cari Attribute ```js // jQuery $el.attr('foo'); // Native e.getAttribute('foo'); ``` + Cari atribut data ```js // jQuery $el.data('foo'); // Native // menggunakan getAttribute el.getAttribute('data-foo'); // anda boleh juga gunakan `dataset` jika ingin pakai IE 11+ el.dataset['foo']; ``` - [1.5](#1.5) Sibling/Previous/Next Elements + Sibling elements ```js // jQuery $el.siblings(); // Native [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Previous elements ```js // jQuery $el.prev(); // Native el.previousElementSibling; ``` + Next elements ```js // next $el.next(); el.nextElementSibling; ``` - [1.6](#1.6) Closest Return the first matched element by provided selector, traversing from current element to document. ```js // jQuery $el.closest(queryString); // Native function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Parents Until Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. ```js // jQuery $el.parentsUntil(selector, filter); // Native function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // match start from parent el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Native document.querySelector('#my-input').value; ``` + Get index of e.currentTarget between `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Native [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Iframe Contents `$('iframe').contents()` returns `contentDocument` for this specific iframe + Iframe contents ```js // jQuery $iframe.contents(); // Native iframe.contentDocument; ``` + Iframe Query ```js // jQuery $iframe.contents().find('.css'); // Native iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ back to top](#table-of-contents)** ## CSS & Style - [2.1](#2.1) CSS + Get style ```js // jQuery $el.css("color"); // Native // NOTE: Known bug, will return 'auto' if style value is 'auto' const win = el.ownerDocument.defaultView; // null means not return presudo styles win.getComputedStyle(el, null).color; ``` + Set style ```js // jQuery $el.css({ color: "#ff0011" }); // Native el.style.color = '#ff0011'; ``` + Get/Set Styles Note that if you want to set multiple styles once, you could refer to [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) method in oui-dom-utils package. + Add class ```js // jQuery $el.addClass(className); // Native el.classList.add(className); ``` + Remove class ```js // jQuery $el.removeClass(className); // Native el.classList.remove(className); ``` + has class ```js // jQuery $el.hasClass(className); // Native el.classList.contains(className); ``` + Toggle class ```js // jQuery $el.toggleClass(className); // Native el.classList.toggle(className); ``` - [2.2](#2.2) Width & Height Width and Height are theoretically identical, take Height as example: + Window height ```js // window height $(window).height(); // without scrollbar, behaves like jQuery window.document.documentElement.clientHeight; // with scrollbar window.innerHeight; ``` + Document height ```js // jQuery $(document).height(); // Native document.documentElement.scrollHeight; ``` + Element height ```js // jQuery $el.height(); // Native function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) el.clientHeight; // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Position & Offset + Position ```js // jQuery $el.position(); // Native { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Native function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Scroll Top ```js // jQuery $(window).scrollTop(); // Native (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ back to top](#table-of-contents)** ## DOM Manipulation - [3.1](#3.1) Remove ```js // jQuery $el.remove(); // Native el.parentNode.removeChild(el); ``` - [3.2](#3.2) Text + Get text ```js // jQuery $el.text(); // Native el.textContent; ``` + Set text ```js // jQuery $el.text(string); // Native el.textContent = string; ``` - [3.3](#3.3) HTML + Get HTML ```js // jQuery $el.html(); // Native el.innerHTML; ``` + Set HTML ```js // jQuery $el.html(htmlString); // Native el.innerHTML = htmlString; ``` - [3.4](#3.4) Append append child element after the last child of parent element ```js // jQuery $el.append("
          hello
          "); // Native let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.appendChild(newEl); ``` - [3.5](#3.5) Prepend ```js // jQuery $el.prepend("
          hello
          "); // Native let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.insertBefore(newEl, el.firstChild); ``` - [3.6](#3.6) insertBefore Insert a new node before the selected elements ```js // jQuery $newEl.insertBefore(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter Insert a new node after the selected elements ```js // jQuery $newEl.insertAfter(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` **[⬆ back to top](#table-of-contents)** ## Ajax Replace with [fetch](https://github.com/camsong/fetch-ie8) and [fetch-jsonp](https://github.com/camsong/fetch-jsonp) **[⬆ back to top](#table-of-contents)** ## Events For a complete replacement with namespace and delegation, refer to https://github.com/oneuijs/oui-dom-events - [5.1](#5.1) Bind an event with on ```js // jQuery $el.on(eventName, eventHandler); // Native el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) Unbind an event with off ```js // jQuery $el.off(eventName, eventHandler); // Native el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Native if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ back to top](#table-of-contents)** ## Utility - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Native Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Native String.trim(string); ``` - [6.3](#6.3) Object Assign Extend, use object.assign polyfill https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Native Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Native el !== child && el.contains(child); ``` **[⬆ back to top](#table-of-contents)** ## Terjemahan * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [English](./README.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) ## Sokongan Pelayar ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # Lesen MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-ru.md ================================================ ## Вам не нужен jQuery В наше время среда фронт энд разработки быстро развивается, современные браузеры уже реализовали значимую часть DOM/BOM APIs и это хорошо. Вам не нужно изучать jQuery с нуля для манипуляцией DOM'ом или обектами событий. В то же время, благодаря лидирующим фронт энд библиотекам, таким как React, Angular и Vue, манипуляция DOM'ом напрямую становится противо шаблонной, jQuery никогда не был менее важен. Этот проект суммирует большинство альтернатив методов jQuery в нативном исполнении с поддержкой IE 10+. ## Содержание 1. [Query Selector](#query-selector) 1. [CSS & Style](#css--style) 1. [Манипуляция DOM](#Манипуляции-dom) 1. [Ajax](#ajax) 1. [События](#События) 1. [Утилиты](#Утилиты) 1. [Альтернативы](#Альтернативы) 1. [Переводы](#Переводы) 1. [Поддержка браузеров](#Поддержка-браузеров) ## Query Selector Для часто используемых селекторов, таких как class, id или attribute мы можем использовать `document.querySelector` или `document.querySelectorAll` для замены. Разница такова: * `document.querySelector` возвращает первый совпавший элемент * `document.querySelectorAll` возвращает все совспавшие элементы как коллекцию узлов(NodeList). Его можно конвертировать в массив используя `[].slice.call(document.querySelectorAll(selector) || []);` * Если никакие элементы не совпадут, jQuery вернет `[]` где DOM API вернет `null`. Обратите внимание на указатель исключения Null (Null Pointer Exception). Вы так же можете использовать `||` для установки значения по умолчанию если не было найдемо совпадений `document.querySelectorAll(selector) || []` > Заметка: `document.querySelector` и `document.querySelectorAll` достаточно **МЕДЛЕННЫ**, старайтесь использовать `getElementById`, `document.getElementsByClassName` или `document.getElementsByTagName` если хотите улучшить производительность. - [1.0](#1.0) Query by selector ```js // jQuery $('selector'); // Нативно document.querySelectorAll('selector'); ``` - [1.1](#1.1) Запрос по классу ```js // jQuery $('.class'); // Нативно document.querySelectorAll('.class'); // или document.getElementsByClassName('class'); ``` - [1.2](#1.2) Запрос по ID ```js // jQuery $('#id'); // Нативно document.querySelector('#id'); // или document.getElementById('id'); ``` - [1.3](#1.3) Запрос по атрибуту ```js // jQuery $('a[target=_blank]'); // Нативно document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Найти среди потомков + Найти nodes ```js // jQuery $el.find('li'); // Нативно el.querySelectorAll('li'); ``` + Найти body ```js // jQuery $('body'); // Нативно document.body; ``` + Найти атрибуты ```js // jQuery $el.attr('foo'); // Нативно e.getAttribute('foo'); ``` + Найти data attribute ```js // jQuery $el.data('foo'); // Нативно // используя getAttribute el.getAttribute('data-foo'); // также можно использовать `dataset`, если не требуется поддержка ниже IE 11. el.dataset['foo']; ``` - [1.5](#1.5) Родственные/Предыдущие/Следующие Элементы + Родственные элементы ```js // jQuery $el.siblings(); // Нативно [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Предыдущие элементы ```js // jQuery $el.prev(); // Нативно el.previousElementSibling; ``` + Следующие элементы ```js // jQuery $el.next(); // Нативно el.nextElementSibling; ``` - [1.6](#1.6) Closest Возвращает первый совпавший элемент по предоставленному селектору, обоходя от текущего элементы до документа. ```js // jQuery $el.closest(queryString); // Нативно - Only latest, NO IE el.closest(selector); // Нативно - IE10+ function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Родители до Получить родителей кажого элемента в текущем сете совпавших элементов, но не включая элемент совпавший с селектором, узел DOM'а, или объект jQuery. ```js // jQuery $el.parentsUntil(selector, filter); // Нативно function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // Совпадать начиная от родителя el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) От + Input/Textarea ```js // jQuery $('#my-input').val(); // Нативно document.querySelector('#my-input').value; ``` + получить индекс e.currentTarget между `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Нативно [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Контент Iframe `$('iframe').contents()` возвращает `contentDocument` для именно этого iframe + Контент Iframe ```js // jQuery $iframe.contents(); // Нативно iframe.contentDocument; ``` + Iframe Query ```js // jQuery $iframe.contents().find('.css'); // Нативно iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ Наверх](#Содержание)** ## CSS & Style - [2.1](#2.1) CSS + Получить стиль ```js // jQuery $el.css("color"); // Нативно // ЗАМЕТКА: Известная ошика, возвращает 'auto' если значение стиля 'auto' const win = el.ownerDocument.defaultView; // null означает не возвращать псевдостили win.getComputedStyle(el, null).color; ``` + Присвоение style ```js // jQuery $el.css({ color: "#ff0011" }); // Нативно el.style.color = '#ff0011'; ``` + Получение/Присвоение стилей Заметьте что если вы хотите присвоить несколько стилей за раз, вы можете сослаться на [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) метод в oui-dom-utils package. + Добавить класс ```js // jQuery $el.addClass(className); // Нативно el.classList.add(className); ``` + Удалить class ```js // jQuery $el.removeClass(className); // Нативно el.classList.remove(className); ``` + Имеет класс ```js // jQuery $el.hasClass(className); // Нативно el.classList.contains(className); ``` + Переключать класс ```js // jQuery $el.toggleClass(className); // Нативно el.classList.toggle(className); ``` - [2.2](#2.2) Ширина и Высота Ширина и высота теоритечески идентичны, например возьмем высоту: + высота окна ```js // Высота окна $(window).height(); // без скроллбара, ведет себя как jQuery window.document.documentElement.clientHeight; // вместе с скроллбаром window.innerHeight; ``` + высота документа ```js // jQuery $(document).height(); // Нативно document.documentElement.scrollHeight; ``` + Высота элемента ```js // jQuery $el.height(); // Нативно function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // С точностью до целого числа(когда `border-box`, это `height`; когда `content-box`, это `height + padding + border`) el.clientHeight; // с точностью до десятых(когда `border-box`, это `height`; когда `content-box`, это `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Позиция и смещение + Позиция ```js // jQuery $el.position(); // Нативно { left: el.offsetLeft, top: el.offsetTop } ``` + Смещение ```js // jQuery $el.offset(); // Нативно function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Прокрутка вверх ```js // jQuery $(window).scrollTop(); // Нативно (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ Наверх](#Содержание)** ## Манипуляции DOM - [3.1](#3.1) Remove ```js // jQuery $el.remove(); // Нативно el.parentNode.removeChild(el); ``` - [3.2](#3.2) Текст + Получить текст ```js // jQuery $el.text(); // Нативно el.textContent; ``` + Присвоить текст ```js // jQuery $el.text(string); // Нативно el.textContent = string; ``` - [3.3](#3.3) HTML + Получить HTML ```js // jQuery $el.html(); // Нативно el.innerHTML; ``` + Присвоить HTML ```js // jQuery $el.html(htmlString); // Нативно el.innerHTML = htmlString; ``` - [3.4](#3.4) Append Добавление элемента ребенка после последнего ребенка элемента родителя ```js // jQuery $el.append("
          hello
          "); // Нативно el.insertAdjacentHTML("beforeend","
          hello
          "); ``` - [3.5](#3.5) Prepend ```js // jQuery $el.prepend("
          hello
          "); // Нативно el.insertAdjacentHTML("afterbegin","
          hello
          "); ``` - [3.6](#3.6) insertBefore Вставка нового элемента перед выбранным элементом ```js // jQuery $newEl.insertBefore(queryString); // Нативно const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter Вставка новго элемента после выбранного элемента ```js // jQuery $newEl.insertAfter(queryString); // Нативно const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` - [3.8](#3.8) is Возвращает `true` если совпадает с селектором запроса ```js // jQuery - заметьте что `is` так же работает с `function` или `elements` которые не имют к этому отношения $el.is(selector); // Нативно el.matches(selector); ``` **[⬆ Наверх](#Содержание)** ## Ajax Заменить с [fetch](https://github.com/camsong/fetch-ie8) и [fetch-jsonp](https://github.com/camsong/fetch-jsonp) **[⬆ Наверх](#Содержание)** ## События Для полной замены с пространством имен и делегация, сослаться на [oui-dom-events](https://github.com/oneuijs/oui-dom-events) - [5.1](#5.1) Связать событие используя on ```js // jQuery $el.on(eventName, eventHandler); // Нативно el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) Отвязать событие используя off ```js // jQuery $el.off(eventName, eventHandler); // Нативно el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Нативно if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ Наверх](#Содержание)** ## Утилиты - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Нативно Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Нативно string.trim(); ``` - [6.3](#6.3) Назначение объекта Дополнительно, используйте полифил object.assign https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Нативно Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Нативно el !== child && el.contains(child); ``` **[⬆ Наверх](#Содержание)** ## Альтернативы * [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - Примеры как исполняются частые события, элементы, ajax и тд с ванильным javascript. * [npm-dom](http://github.com/npm-dom) и [webmodules](http://github.com/webmodules) - Отдельные DOM модули можно найти на NPM ## Переводы * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Bahasa Indonesia](./README-id.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Español](./README-es.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) ## Поддержка браузеров ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # License MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-tr.md ================================================ ## jQuery'e İhtiyacınız Yok [![Build Status](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery.svg)](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery) Önyüz ortamları bugünlerde çok hızlı gelişiyor, öyle ki modern tarayıcılar DOM/DOM APİ'lere ait önemli gereklilikleri çoktan yerine getirdiler. DOM işleme ve olaylar için, en baştan jQuery ögrenmemize gerek kalmadı. Bu arada, üstünlükleri ile jQuery'i önemsizleştiren ve doğrudan DOM değişikliklerinin bir Anti-pattern olduğunu gösteren, React, Angular ve Vue gibi gelişmiş önyüz kütüphanelerine ayrıca teşekkür ederiz. Bu proje, IE10+ desteği ile coğunluğu jQuery yöntemlerine alternatif olan yerleşik uygulamaları içerir. ## İçerik Tablosu 1. [Sorgu seçiciler](#sorgu-seçiciler) 1. [CSS & Stil](#css--stil) 1. [DOM düzenleme](#dom-düzenleme) 1. [Ajax](#ajax) 1. [Olaylar](#olaylar) 1. [Araçlar](#araçlar) 1. [Alternatifler](#alternatifler) 1. [Çeviriler](#Çeviriler) 1. [Tarayıcı desteği](#tarayıcı-desteği) ## Sorgu seçiciler Yaygın olan class, id ve özellik seçiciler yerine, `document.querySelector` yada `document.querySelectorAll` kullanabiliriz. Ayrıldıkları nokta: * `document.querySelector` ilk seçilen öğeyi döndürür * `document.querySelectorAll` Seçilen tüm öğeleri NodeList olarak geri döndürür. `[].slice.call(document.querySelectorAll(selector) || []);` kullanarak bir diziye dönüştürebilirsiniz. * Herhangi bir öğenin seçilememesi durumda ise, jQuery `[]` döndürürken, DOM API `null` döndürecektir. Null Pointer istisnası almamak için `||` ile varsayılan değere atama yapabilirsiniz, örnek: `document.querySelectorAll(selector) || []` > Uyarı: `document.querySelector` ve `document.querySelectorAll` biraz **YAVAŞ** olabilir, Daha hızlısını isterseniz, `getElementById`, `document.getElementsByClassName` yada `document.getElementsByTagName` kullanabilirsiniz. - [1.0](#1.0) Seçici ile sorgu ```js // jQuery $('selector'); // Yerleşik document.querySelectorAll('selector'); ``` - [1.1](#1.1) Sınıf ile sorgu ```js // jQuery $('.class'); // Yerleşik document.querySelectorAll('.class'); // yada document.getElementsByClassName('class'); ``` - [1.2](#1.2) Id ile sorgu ```js // jQuery $('#id'); // Yerleşik document.querySelector('#id'); // yada document.getElementById('id'); ``` - [1.3](#1.3) Özellik ile sorgu ```js // jQuery $('a[target=_blank]'); // Yerleşik document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Öğe erişimi + Node'a erişim ```js // jQuery $el.find('li'); // Yerleşik el.querySelectorAll('li'); ``` + Body'e erişim ```js // jQuery $('body'); // Yerleşik document.body; ``` + Özelliğe erişim ```js // jQuery $el.attr('foo'); // Yerleşik el.getAttribute('foo'); ``` + Data özelliğine erişim ```js // jQuery $el.data('foo'); // Yerleşik // getAttribute kullanarak el.getAttribute('data-foo'); // Eğer IE 11+ kullanıyor iseniz, `dataset` ile de erişebilirsiniz el.dataset['foo']; ``` - [1.5](#1.5) Kardeş/Önceki/Sonraki öğeler + Kardeş öğeler ```js // jQuery $el.siblings(); // Yerleşik [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Önceki öğeler ```js // jQuery $el.prev(); // Yerleşik el.previousElementSibling; ``` + Sonraki öğeler ```js // jQuery $el.next(); // Yerleşik el.nextElementSibling; ``` - [1.6](#1.6) En yakın Verilen seçici ile eşleşen ilk öğeyi döndürür, geçerli öğeden başlayarak document'a kadar geçiş yapar. ```js // jQuery $el.closest(selector); // Yerleşik - Sadece en güncellerde, IE desteklemiyor el.closest(selector); // Yerleşik - IE10+ function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Önceki atalar Verilen seçici ile eşleşen öğe veya DOM node veya jQuery nesnesi hariç, mevcut öğe ile aradaki tüm önceki ataları bir set dahilinde verir. ```js // jQuery $el.parentsUntil(selector, filter); // Yerleşik function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // eşleştirme, atadan başlar el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Yerleşik document.querySelector('#my-input').value; ``` + e.currentTarget ile `.radio` arasındaki dizini verir ```js // jQuery $(e.currentTarget).index('.radio'); // Yerleşik [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Iframe İçeriği Mevcut Iframe için `$('iframe').contents()` yerine `contentDocument` döndürür. + Iframe İçeriği ```js // jQuery $iframe.contents(); // Yerleşik iframe.contentDocument; ``` + Iframe seçici ```js // jQuery $iframe.contents().find('.css'); // Yerleşik iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ üste dön](#İçerik-tablosu)** ## CSS & Stil - [2.1](#2.1) CSS + Stili verir ```js // jQuery $el.css("color"); // Yerleşik // NOT: Bilinen bir hata, eğer stil değeri 'auto' ise 'auto' döndürür const win = el.ownerDocument.defaultView; // null sahte tipleri döndürmemesi için win.getComputedStyle(el, null).color; ``` + Stil değiştir ```js // jQuery $el.css({ color: "#ff0011" }); // Yerleşik el.style.color = '#ff0011'; ``` + Stil değeri al/değiştir Eğer aynı anda birden fazla stili değiştirmek istiyor iseniz, oui-dom-utils paketi içindeki [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) metoduna göz atınız. + Sınıf ekle ```js // jQuery $el.addClass(className); // Yerleşik el.classList.add(className); ``` + Sınıf çıkart ```js // jQuery $el.removeClass(className); // Yerleşik el.classList.remove(className); ``` + sınfı var mı? ```js // jQuery $el.hasClass(className); // Yerleşik el.classList.contains(className); ``` + Sınfı takas et ```js // jQuery $el.toggleClass(className); // Yerleşik el.classList.toggle(className); ``` - [2.2](#2.2) Genişlik ve Yükseklik Genişlik ve Yükseklik teorik olarak aynı şekilde, örnek olarak Yükseklik veriliyor + Window Yüksekliği ```js // window yüksekliği $(window).height(); // kaydırma çubuğu olmaksızın, jQuery ile aynı window.document.documentElement.clientHeight; // kaydırma çubuğu ile birlikte window.innerHeight; ``` + Document yüksekliği ```js // jQuery $(document).height(); // Yerleşik document.documentElement.scrollHeight; ``` + Öğe yüksekliği ```js // jQuery $el.height(); // Yerleşik function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // Tamsayı olarak daha doğru olanı(`border-box` iken, `height` esas; `content-box` ise, `height + padding + border` esas alınır) el.clientHeight; // Ondalık olarak daha doğru olanı(`border-box` iken, `height` esas; `content-box` ise, `height + padding + border` esas alınır) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Pozisyon ve Ara-Açıklığı + Pozisyon ```js // jQuery $el.position(); // Yerleşik { left: el.offsetLeft, top: el.offsetTop } ``` + Ara-Açıklığı ```js // jQuery $el.offset(); // Yerleşik function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Üste kaydır ```js // jQuery $(window).scrollTop(); // Yerleşik (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ üste dön](#İçerik-tablosu)** ## DOM düzenleme - [3.1](#3.1) Çıkartma ```js // jQuery $el.remove(); // Yerleşik el.parentNode.removeChild(el); ``` - [3.2](#3.2) Metin + Get text ```js // jQuery $el.text(); // Yerleşik el.textContent; ``` + Set text ```js // jQuery $el.text(string); // Yerleşik el.textContent = string; ``` - [3.3](#3.3) HTML + HTML'i alma ```js // jQuery $el.html(); // Yerleşik el.innerHTML; ``` + HTML atama ```js // jQuery $el.html(htmlString); // Yerleşik el.innerHTML = htmlString; ``` - [3.4](#3.4) Sona ekleme Ata öğenin son çocuğundan sonra öğe ekleme ```js // jQuery $el.append("
          hello
          "); // Yerleşik el.insertAdjacentHTML("beforeend","
          hello
          "); ``` - [3.5](#3.5) Öne ekleme ```js // jQuery $el.prepend("
          hello
          "); // Yerleşik el.insertAdjacentHTML("afterbegin","
          hello
          "); ``` - [3.6](#3.6) Öncesine Ekleme Seçili öğeden önceki yere yeni öğe ekleme ```js // jQuery $newEl.insertBefore(queryString); // Yerleşik const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) Sonrasına ekleme Seçili öğeden sonraki yere yeni öğe ekleme ```js // jQuery $newEl.insertAfter(queryString); // Yerleşik const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` - [3.8](#3.8) eşit mi? Sorgu seçici ile eşleşiyor ise `true` döner ```js // jQuery için not: `is` aynı zamanda `function` veya `elements` için de geçerlidir fakat burada bir önemi bulunmuyor $el.is(selector); // Yerleşik el.matches(selector); ``` - [3.9](#3.9) Klonlama Mevcut öğenin bir derin kopyasını oluşturur ```js // jQuery $el.clone(); // Yerleşik el.cloneNode(); // Derin kopya için, `true` parametresi kullanınız ``` **[⬆ üste dön](#İçerik-tablosu)** ## Ajax [Fetch API](https://fetch.spec.whatwg.org/) ajax için XMLHttpRequest yerine kullanan yeni standarttır. Chrome ve Firefox destekler, eski tarayıcılar için polyfill kullanabilirsiniz. IE9+ ve üstü için [github/fetch](http://github.com/github/fetch) yada IE8+ ve üstü için [fetch-ie8](https://github.com/camsong/fetch-ie8/), JSONP istekler için [fetch-jsonp](https://github.com/camsong/fetch-jsonp) deneyiniz. **[⬆ üste dön](#İçerik-tablosu)** ## Olaylar Namespace ve Delegasyon ile tam olarak değiştirmek için, https://github.com/oneuijs/oui-dom-events sayfasına bakınız - [5.1](#5.1) on ile bir öğeye bağlama ```js // jQuery $el.on(eventName, eventHandler); // Yerleşik el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) off ile bir bağlamayı sonlandırma ```js // jQuery $el.off(eventName, eventHandler); // Yerleşik el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Tetikleyici ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Yerleşik if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ üste dön](#İçerik-tablosu)** ## Araçlar - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Yerleşik Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Yerleşik string.trim(); ``` - [6.3](#6.3) Nesne atama Türetmek için, object.assign polyfill'ini deneyiniz https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Yerleşik Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) İçerme ```js // jQuery $.contains(el, child); // Yerleşik el !== child && el.contains(child); ``` **[⬆ üste dön](#İçerik-tablosu)** ## Alternatifler * [jQuery'e İhtiyacınız Yok](http://youmightnotneedjquery.com/) - Yaygın olan olay, öğe ve ajax işlemlerinin yalın Javascript'teki karşılıklarına ait örnekler * [npm-dom](http://github.com/npm-dom) ve [webmodules](http://github.com/webmodules) - NPM için ayrı DOM modül organizasyonları ## Çeviriler * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Bahasa Indonesia](./README-id.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Español](./README-es.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) ## Tarayıcı Desteği ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # Lisans MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README-vi.md ================================================ ## Bạn không cần jQuery nữa đâu Ngày nay, môi trường lập trình front-end phát triển rất nhanh chóng, các trình duyệt hiện đại đã cung cấp các API đủ tốt để làm việc với DOM/BOM. Bạn không còn cần phải học về jQuery nữa. Đồng thời, nhờ sự ra đời của các thư viện như React, Angular và Vue đã khiến cho việc can thiệp trực tiếp vào DOM trở thành một việc không tốt. jQuery đã không còn quan trọng như trước nữa. Bài viết này tổng hợp những cách để thay thế các hàm của jQuery bằng các hàm được hỗ trợ bởi trình duyệt, và hó cũng hoạt động trên IE 10+ ## Danh mục 1. [Query Selector](#query-selector) 1. [CSS & Style](#css--style) 1. [Thao tác với DOM](#thao-tác-với-dom) 1. [Ajax](#ajax) 1. [Events](#events) 1. [Hàm tiện ích](#hàm-tiện-ích) 1. [Ngôn ngữ khác](#ngôn-ngữ-khác) 1. [Các trình duyệt hỗ trợ](#các-trình-duyệt-hỗ-trợ) ## Query Selector Đối với những selector phổ biến như class, id hoặc thuộc tính thì chúng ta có thể sử dụng `document.querySelector` hoặc `document.querySelectorAll` để thay thế cho jQuery selector. Sự khác biệt của hai hàm này là ở chỗ: * `document.querySelector` trả về element đầu tiên được tìm thấy * `document.querySelectorAll` trả về tất cả các element được tìm thấy dưới dạng một instance của NodeList. Nó có thể được convert qua array bằng cách `[].slice.call(document.querySelectorAll(selector) || []);` * Nếu không có element nào được tìm thấy, thì jQuery sẽ trả về một array rỗng `[]` trong khi đó DOM API sẽ trả về `null`. Hãy chú ý đến Null Pointer Exception. Bạn có thể sử dụng toán tử `||` để đặt giá trị default nếu như không có element nào được tìm thấy, ví dụ như `document.querySelectorAll(selector) || []` > Chú ý : `document.querySelector` và `document.querySelectorAll` hoạt động khá **CHẬM**, hãy thử dùng `getElementById`, `document.getElementsByClassName` hoặc `document.getElementsByTagName` nếu bạn muốn đạt hiệu suất tốt hơn. - [1.0](#1.0) Query bằng selector ```js // jQuery $('selector'); // Native document.querySelectorAll('selector'); ``` - [1.1](#1.1) Query bằng class ```js // jQuery $('.class'); // Native document.querySelectorAll('.class'); // hoặc document.getElementsByClassName('class'); ``` - [1.2](#1.2) Query bằng id ```js // jQuery $('#id'); // Native document.querySelector('#id'); // hoặc document.getElementById('id'); ``` - [1.3](#1.3) Query bằng thuộc tính ```js // jQuery $('a[target=_blank]'); // Native document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Tìm bất cứ gì. + Tìm node ```js // jQuery $el.find('li'); // Native el.querySelectorAll('li'); ``` + Tìm body ```js // jQuery $('body'); // Native document.body; ``` + lấy thuộc tính ```js // jQuery $el.attr('foo'); // Native e.getAttribute('foo'); ``` + Lấy giá trị của thuộc tính `data` ```js // jQuery $el.data('foo'); // Native // using getAttribute el.getAttribute('data-foo'); // you can also use `dataset` if only need to support IE 11+ el.dataset['foo']; ``` - [1.5](#1.5) Tìm element cùng level/trước/sau + Element cùng level ```js // jQuery $el.siblings(); // Native [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Element ở phía trước ```js // jQuery $el.prev(); // Native el.previousElementSibling; ``` + Element ở phía sau ```js // next $el.next(); el.nextElementSibling; ``` - [1.6](#1.6) Element gần nhất Trả về element đầu tiên có selector khớp với yêu cầu khi duyệt từ element hiện tại trở lên tới document. ```js // jQuery $el.closest(queryString); // Native function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Tìm parent Truy ngược một cách đệ quy tổ tiên của element hiện tại, cho đến khi tìm được một element tổ tiên ( element cần tìm ) mà element đó là con trực tiếp của element khớp với selector được cung cấp, Return lại element cần tìm đó. ```js // jQuery $el.parentsUntil(selector, filter); // Native function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // match start from parent el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Native document.querySelector('#my-input').value; ``` + Lấy index của e.currentTarget trong danh sách các element khớp với selector `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Native [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Nội dung Iframe `$('iframe').contents()` trả về thuộc tính `contentDocument` của iframe được tìm thấy + Nọi dung iframe ```js // jQuery $iframe.contents(); // Native iframe.contentDocument; ``` + Query Iframe ```js // jQuery $iframe.contents().find('.css'); // Native iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ Trở về đầu](#danh-mục)** ## CSS & Style - [2.1](#2.1) CSS + Lấy style ```js // jQuery $el.css("color"); // Native // NOTE: Bug đã được biết, sẽ trả về 'auto' nếu giá trị của style là 'auto' const win = el.ownerDocument.defaultView; // null means not return presudo styles win.getComputedStyle(el, null).color; ``` + Đặt style ```js // jQuery $el.css({ color: "#ff0011" }); // Native el.style.color = '#ff0011'; ``` + Lấy/Đặt Nhiều style Nếu bạn muốn đặt nhiều style một lần, bạn có thể sẽ thích phương thức [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) trong thư viện oui-dom-utils. + Thêm class và element ```js // jQuery $el.addClass(className); // Native el.classList.add(className); ``` + Loại bỏ class class ra khỏi element ```js // jQuery $el.removeClass(className); // Native el.classList.remove(className); ``` + Kiểm tra xem element có class nào đó hay không ```js // jQuery $el.hasClass(className); // Native el.classList.contains(className); ``` + Toggle class ```js // jQuery $el.toggleClass(className); // Native el.classList.toggle(className); ``` - [2.2](#2.2) Chiều rộng, chiều cao Về mặt lý thuyết thì chiều rộng và chiều cao giống như nhau trong cả jQuery và DOM API: + Chiều rộng của window ```js // window height $(window).height(); // trừ đi scrollbar window.document.documentElement.clientHeight; // Tính luôn scrollbar window.innerHeight; ``` + Chiều cao của Document ```js // jQuery $(document).height(); // Native document.documentElement.scrollHeight; ``` + Chiều cao của element ```js // jQuery $el.height(); // Native function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // chính xác tới số nguyên(khi có thuộc tính `box-sizing` là `border-box`, nó là `height`; khi box-sizing là `content-box`, nó là `height + padding + border`) el.clientHeight; // Chính xác tới số thập phân(khi `box-sizing` là `border-box`, nó là `height`; khi `box-sizing` là `content-box`, nó là `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Position & Offset + Position ```js // jQuery $el.position(); // Native { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Native function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Scroll Top ```js // jQuery $(window).scrollTop(); // Native (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ Trở về đầu](#danh-mục)** ## Thao tác với DOM - [3.1](#3.1) Loại bỏ ```js // jQuery $el.remove(); // Native el.parentNode.removeChild(el); ``` - [3.2](#3.2) Text + Lấy text ```js // jQuery $el.text(); // Native el.textContent; ``` + Đặt giá trị text ```js // jQuery $el.text(string); // Native el.textContent = string; ``` - [3.3](#3.3) HTML + Lấy HTML ```js // jQuery $el.html(); // Native el.innerHTML; ``` + Đặt giá trị HTML ```js // jQuery $el.html(htmlString); // Native el.innerHTML = htmlString; ``` - [3.4](#3.4) Append append một element sau element con cuối cùng của element cha ```js // jQuery $el.append("
          hello
          "); // Native let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.appendChild(newEl); ``` - [3.5](#3.5) Prepend ```js // jQuery $el.prepend("
          hello
          "); // Native let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.insertBefore(newEl, el.firstChild); ``` - [3.6](#3.6) insertBefore Chèn một node vào trước element được query. ```js // jQuery $newEl.insertBefore(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter Chèn node vào sau element được query ```js // jQuery $newEl.insertAfter(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` **[⬆ Trở về đầu](#danh-mục)** ## Ajax Thay thế bằng [fetch](https://github.com/camsong/fetch-ie8) và [fetch-jsonp](https://github.com/camsong/fetch-jsonp) **[⬆ Trở về đầu](#danh-mục)** ## Events Để có một sự thay thế đầy đủ nhất, bạn nên sử dụng https://github.com/oneuijs/oui-dom-events - [5.1](#5.1) Bind event bằng on ```js // jQuery $el.on(eventName, eventHandler); // Native el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) Unbind event bằng off ```js // jQuery $el.off(eventName, eventHandler); // Native el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Native if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ Trở về đầu](#danh-mục)** ## Hàm tiện ích - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Native Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Native string.trim(); ``` - [6.3](#6.3) Object Assign Mở rộng, sử dụng object.assign https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Native Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Native el !== child && el.contains(child); ``` **[⬆ Trở về đầu](#danh-mục)** ## Ngôn ngữ khác * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) ## Các trình duyệt hỗ trợ ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # Giấy phép MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.ko-KR.md ================================================ ## You Don't Need jQuery 오늘날 프론트엔드 개발 환경은 급격히 진화하고 있고, 모던 브라우저들은 이미 충분히 많은 DOM/BOM API들을 구현했습니다. 우리는 jQuery를 DOM 처리나 이벤트를 위해 처음부터 배울 필요가 없습니다. React, Angular, Vue같은 프론트엔드 라이브러리들이 주도권을 차지하는 동안 DOM을 바로 처리하는 것은 안티패턴이 되었고, jQuery의 중요성은 줄어들었습니다. 이 프로젝트는 대부분의 jQuery 메소드의 대안을 IE 10+ 이상을 지원하는 네이티브 구현으로 소개합니다. ## 목차 1. [Query Selector](#query-selector) 1. [CSS & Style](#css--style) 1. [DOM 조작](#dom-조작) 1. [Ajax](#ajax) 1. [이벤트](#이벤트) 1. [유틸리티](#유틸리티) 1. [대안방법](#대안방법) 1. [번역](#번역) 1. [브라우저 지원](#브라우저-지원) ## Query Selector 평범한 class, id, attribute같은 selecotor는 `document.querySelector`나 `document.querySelectorAll`으로 대체할 수 있습니다. * `document.querySelector`는 처음 매칭된 엘리먼트를 반환합니다. * `document.querySelectorAll`는 모든 매칭된 엘리먼트를 NodeList로 반환합니다. `[].slice.call`을 사용해서 Array로 변환할 수 있습니다. * 만약 매칭된 엘리멘트가 없으면 jQuery는 `[]` 를 반환하지만 DOM API는 `null`을 반환합니다. Null Pointer Exception에 주의하세요. > 안내: `document.querySelector`와 `document.querySelectorAll`는 꽤 **느립니다**, `getElementById`나 `document.getElementsByClassName`, `document.getElementsByTagName`를 사용하면 퍼포먼스가 향상을 기대할 수 있습니다. - [1.0](#1.0) selector로 찾기 ```js // jQuery $('selector'); // Native document.querySelectorAll('selector'); ``` - [1.1](#1.1) class로 찾기 ```js // jQuery $('.class'); // Native document.querySelectorAll('.class'); // or document.getElementsByClassName('class'); ``` - [1.2](#1.2) id로 찾기 ```js // jQuery $('#id'); // Native document.querySelector('#id'); // or document.getElementById('id'); ``` - [1.3](#1.3) 속성(attribute)으로 찾기 ```js // jQuery $('a[target=_blank]'); // Native document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) 자식에서 찾기 ```js // jQuery $el.find('li'); // Native el.querySelectorAll('li'); ``` - [1.5](#1.5) 형제/이전/다음 엘리먼트 찾기 + 형제 엘리먼트 ```js // jQuery $el.siblings(); // Native [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + 이전 엘리먼트 ```js // jQuery $el.prev(); // Native el.previousElementSibling; ``` + 다음 엘리먼트 ```js // jQuery $el.next(); // Native el.nextElementSibling; ``` - [1.6](#1.6) Closest 현재 엘리먼트부터 document로 이동하면서 주어진 셀렉터와 일치하는 가장 가까운 엘리먼트를 반환합니다. ```js // jQuery $el.closest(selector); // Native - 최신 브라우저만, IE는 미지원 el.closest(selector); // Native - IE10 이상 function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Parents Until 주어진 셀렉터에 매칭되는 엘리먼트를 찾기까지 부모 태그들을 위로 올라가며 탐색하여 저장해두었다가 DOM 노드 또는 jQuery object로 반환합니다. ```js // jQuery $el.parentsUntil(selector, filter); // Native function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // match start from parent el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Native document.querySelector('#my-input').value; ``` + e.currentTarget이 `.radio`의 몇번째인지 구하기 ```js // jQuery $(e.currentTarget).index('.radio'); // Native [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Iframe Contents `$('iframe').contents()`는 iframe에 한정해서 `contentDocument`를 반환합니다. + Iframe contents ```js // jQuery $iframe.contents(); // Native iframe.contentDocument; ``` + Iframe에서 찾기 ```js // jQuery $iframe.contents().find('.css'); // Native iframe.contentDocument.querySelectorAll('.css'); ``` - [1.10](#1.10) body 얻기 ```js // jQuery $('body'); // Native document.body; ``` - [1.11](#1.11) 속성 얻기 및 설정 + 속성 얻기 ```js // jQuery $el.attr('foo'); // Native el.getAttribute('foo'); ``` + 속성 설정하기 ```js // jQuery, DOM 변형 없이 메모리에서 작동됩니다. $el.attr('foo', 'bar'); // Native el.setAttribute('foo', 'bar'); ``` + `data-` 속성 얻기 ```js // jQuery $el.data('foo'); // Native (`getAttribute` 사용) el.getAttribute('data-foo'); // Native (IE 11 이상의 지원만 필요하다면 `dataset`을 사용) el.dataset['foo']; ``` **[⬆ 목차로 돌아가기](#목차)** ## CSS & Style - [2.1](#2.1) CSS + style값 얻기 ```js // jQuery $el.css("color"); // Native // NOTE: 알려진 버그로, style값이 'auto'이면 'auto'를 반환합니다. const win = el.ownerDocument.defaultView; // null은 가상 스타일은 반환하지 않음을 의미합니다. win.getComputedStyle(el, null).color; ``` + style값 설정하기 ```js // jQuery $el.css({ color: "#ff0011" }); // Native el.style.color = '#ff0011'; ``` + Style값들을 동시에 얻거나 설정하기 만약 한번에 여러 style값을 바꾸고 싶다면 oui-dom-utils 패키지의 [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194)를 사용해보세요. + class 추가하기 ```js // jQuery $el.addClass(className); // Native el.classList.add(className); ``` + class 제거하기 ```js // jQuery $el.removeClass(className); // Native el.classList.remove(className); ``` + class를 포함하고 있는지 검사하기 ```js // jQuery $el.hasClass(className); // Native el.classList.contains(className); ``` + class 토글하기 ```js // jQuery $el.toggleClass(className); // Native el.classList.toggle(className); ``` - [2.2](#2.2) 폭과 높이 폭과 높이는 이론상 동일합니다. 높이로 예를 들겠습니다. + Window의 높이 ```js // window 높이 $(window).height(); // jQuery처럼 스크롤바를 제외하기 window.document.documentElement.clientHeight; // 스크롤바 포함 window.innerHeight; ``` + 문서 높이 ```js // jQuery $(document).height(); // Native document.documentElement.scrollHeight; ``` + Element 높이 ```js // jQuery $el.height(); // Native function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // 정수로 정확하게(`border-box`일 때 이 값은 `height`이고, `content-box`일 때, 이 값은 `height + padding + border`) el.clientHeight; // 실수로 정확하게(`border-box`일 때 이 값은 `height`이고, `content-box`일 때, 이 값은 `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Position & Offset + Position ```js // jQuery $el.position(); // Native { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Native function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Scroll Top ```js // jQuery $(window).scrollTop(); // Native (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ 목차로 돌아가기](#목차)** ## DOM 조작 - [3.1](#3.1) 제거 ```js // jQuery $el.remove(); // Native el.parentNode.removeChild(el); ``` - [3.2](#3.2) Text + text 가져오기 ```js // jQuery $el.text(); // Native el.textContent; ``` + text 설정하기 ```js // jQuery $el.text(string); // Native el.textContent = string; ``` - [3.3](#3.3) HTML + HTML 가져오기 ```js // jQuery $el.html(); // Native el.innerHTML; ``` + HTML 설정하기 ```js // jQuery $el.html(htmlString); // Native el.innerHTML = htmlString; ``` - [3.4](#3.4) 해당 엘리먼트의 자식들 뒤에 넣기(Append) 부모 엘리먼트의 마지막 자식으로 엘리먼트를 추가합니다. ```js // jQuery $el.append("
          hello
          "); // Native el.insertAdjacentHTML("beforeend","
          hello
          "); ``` - [3.5](#3.5) 해당 엘리먼트의 자식들 앞에 넣기(Prepend) ```js // jQuery $el.prepend("
          hello
          "); // Native el.insertAdjacentHTML("afterbegin","
          hello
          "); ``` - [3.6](#3.6) 해당 엘리먼트 앞에 넣기(insertBefore) 새 노드를 선택한 엘리먼트 앞에 넣습니다. ```js // jQuery $newEl.insertBefore(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) 해당 엘리먼트 뒤에 넣기(insertAfter) 새 노드를 선택한 엘리먼트 뒤에 넣습니다. ```js // jQuery $newEl.insertAfter(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` - [3.8](#3.8) is query selector와 일치하면 `true` 를 반환합니다. ```js // jQuery $el.is(selector); // Native el.matches(selector); ``` - [3.9](#3.9) clone 엘리먼트의 복제본을 만듭니다. ```js // jQuery $el.clone(); // Native el.cloneNode(); // Deep clone은 파라미터를 `true` 로 설정하세요. ``` - [3.10](#3.10) empty 모든 자식 노드를 제거합니다. ```js // jQuery $el.empty(); // Native el.innerHTML = ''; ``` **[⬆ 목차로 돌아가기](#목차)** ## Ajax [Fetch API](https://fetch.spec.whatwg.org/) 는 XMLHttpRequest를 ajax로 대체하는 새로운 표준 입니다. Chrome과 Firefox에서 작동하며, polyfill을 이용해서 구형 브라우저에서 작동되도록 만들 수도 있습니다. IE9 이상에서 지원하는 [github/fetch](http://github.com/github/fetch) 혹은 IE8 이상에서 지원하는 [fetch-ie8](https://github.com/camsong/fetch-ie8/), JSONP 요청을 만드는 [fetch-jsonp](https://github.com/camsong/fetch-jsonp)를 이용해보세요. **[⬆ 목차로 돌아가기](#목차)** ## 이벤트 namespace와 delegation을 포함해서 완전히 갈아 엎길 원하시면 https://github.com/oneuijs/oui-dom-events 를 고려해보세요. - [5.1](#5.1) 이벤트 Bind 걸기 ```js // jQuery $el.on(eventName, eventHandler); // Native el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) 이벤트 Bind 풀기 ```js // jQuery $el.off(eventName, eventHandler); // Native el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) 이벤트 발생시키기(Trigger) ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Native if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ 목차로 돌아가기](#목차)** ## 유틸리티 - [6.1](#6.1) 배열인지 검사(isArray) ```js // jQuery $.isArray(range); // Native Array.isArray(range); ``` - [6.2](#6.2) 앞뒤 공백 지우기(Trim) ```js // jQuery $.trim(string); // Native string.trim(); ``` - [6.3](#6.3) Object Assign 사용하려면 object.assign polyfill을 사용하세요. https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Native Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Native el !== child && el.contains(child); ``` - [6.5](#6.5) inArray ```js // jQuery $.inArray(item, array); // Native array.indexOf(item); ``` - [6.6](#6.6) map ```js // jQuery $.map(array, function(value, index) { }); // Native Array.map(function(value, index) { }); ``` **[⬆ 목차로 돌아가기](#목차)** ## 대안방법 * [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - 일반 자바스크립트로 공통이벤트, 엘리먼트, ajax 등을 다루는 방법 예제. * [npm-dom](http://github.com/npm-dom) 과 [webmodules](http://github.com/webmodules) - 개별 DOM모듈을 NPM에서 찾을 수 있습니다. ## 번역 * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Bahasa Indonesia](./README-id.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Español](./README-es.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) * [Italian](./README-it.md) ## 브라우저 지원 ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # License MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.md ================================================ ## You Don't Need jQuery [![Build Status](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery.svg)](https://travis-ci.org/oneuijs/You-Dont-Need-jQuery) Frontend environments evolve rapidly nowadays, modern browsers have already implemented a great deal of DOM/BOM APIs which are good enough. We don't have to learn jQuery from scratch for DOM manipulation or events. In the meantime, thanks to the prevailment of frontend libraries such as React, Angular and Vue, manipulating DOM directly becomes anti-pattern, jQuery has never been less important. This project summarizes most of the jQuery method alternatives in native implementation, with IE 10+ support. ## Table of Contents 1. [Query Selector](#query-selector) 1. [CSS & Style](#css--style) 1. [DOM Manipulation](#dom-manipulation) 1. [Ajax](#ajax) 1. [Events](#events) 1. [Utilities](#utilities) 1. [Promises](#promises) 1. [Animation](#animation) 1. [Alternatives](#alternatives) 1. [Translations](#translations) 1. [Browser Support](#browser-support) ## Query Selector In place of common selectors like class, id or attribute we can use `document.querySelector` or `document.querySelectorAll` for substitution. The differences lie in: * `document.querySelector` returns the first matched element * `document.querySelectorAll` returns all matched elements as NodeList. It can be converted to Array using `[].slice.call(document.querySelectorAll(selector) || []);` * If no elements matched, jQuery would return `[]` whereas the DOM API will return `null`. Pay attention to Null Pointer Exception. You can also use `||` to set default value if not found, like `document.querySelectorAll(selector) || []` > Notice: `document.querySelector` and `document.querySelectorAll` are quite **SLOW**, try to use `getElementById`, `document.getElementsByClassName` or `document.getElementsByTagName` if you want to get a performance bonus. - [1.0](#1.0) Query by selector ```js // jQuery $('selector'); // Native document.querySelectorAll('selector'); ``` - [1.1](#1.1) Query by class ```js // jQuery $('.class'); // Native document.querySelectorAll('.class'); // or document.getElementsByClassName('class'); ``` - [1.2](#1.2) Query by id ```js // jQuery $('#id'); // Native document.querySelector('#id'); // or document.getElementById('id'); ``` - [1.3](#1.3) Query by attribute ```js // jQuery $('a[target=_blank]'); // Native document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Query in descendents ```js // jQuery $el.find('li'); // Native el.querySelectorAll('li'); ``` - [1.5](#1.5) Sibling/Previous/Next Elements + Sibling elements ```js // jQuery $el.siblings(); // Native [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Previous elements ```js // jQuery $el.prev(); // Native el.previousElementSibling; ``` + Next elements ```js // jQuery $el.next(); // Native el.nextElementSibling; ``` - [1.6](#1.6) Closest Return the first matched element by provided selector, traversing from current element to document. ```js // jQuery $el.closest(selector); // Native - Only latest, NO IE el.closest(selector); // Native - IE10+ function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Parents Until Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. ```js // jQuery $el.parentsUntil(selector, filter); // Native function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // match start from parent el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Native document.querySelector('#my-input').value; ``` + Get index of e.currentTarget between `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Native [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Iframe Contents `$('iframe').contents()` returns `contentDocument` for this specific iframe + Iframe contents ```js // jQuery $iframe.contents(); // Native iframe.contentDocument; ``` + Iframe Query ```js // jQuery $iframe.contents().find('.css'); // Native iframe.contentDocument.querySelectorAll('.css'); ``` - [1.10](#1.10) Get body ```js // jQuery $('body'); // Native document.body; ``` - [1.11](#1.11) Attribute getter and setter + Get an attribute ```js // jQuery $el.attr('foo'); // Native el.getAttribute('foo'); ``` + Set an attribute ```js // jQuery, note that this works in memory without change the DOM $el.attr('foo', 'bar'); // Native el.setAttribute('foo', 'bar'); ``` + Get a `data-` attribute ```js // jQuery $el.data('foo'); // Native (use `getAttribute`) el.getAttribute('data-foo'); // Native (use `dataset` if only need to support IE 11+) el.dataset['foo']; ``` **[⬆ back to top](#table-of-contents)** ## CSS & Style - [2.1](#2.1) CSS + Get style ```js // jQuery $el.css("color"); // Native // NOTE: Known bug, will return 'auto' if style value is 'auto' const win = el.ownerDocument.defaultView; // null means not return pseudo styles win.getComputedStyle(el, null).color; ``` + Set style ```js // jQuery $el.css({ color: "#ff0011" }); // Native el.style.color = '#ff0011'; ``` + Get/Set Styles Note that if you want to set multiple styles once, you could refer to [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) method in oui-dom-utils package. + Add class ```js // jQuery $el.addClass(className); // Native el.classList.add(className); ``` + Remove class ```js // jQuery $el.removeClass(className); // Native el.classList.remove(className); ``` + has class ```js // jQuery $el.hasClass(className); // Native el.classList.contains(className); ``` + Toggle class ```js // jQuery $el.toggleClass(className); // Native el.classList.toggle(className); ``` - [2.2](#2.2) Width & Height Width and Height are theoretically identical, take Height as example: + Window height ```js // window height $(window).height(); // without scrollbar, behaves like jQuery window.document.documentElement.clientHeight; // with scrollbar window.innerHeight; ``` + Document height ```js // jQuery $(document).height(); // Native document.documentElement.scrollHeight; ``` + Element height ```js // jQuery $el.height(); // Native function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // accurate to integer(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) el.clientHeight; // accurate to decimal(when `border-box`, it's `height`; when `content-box`, it's `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Position & Offset + Position ```js // jQuery $el.position(); // Native { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Native function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Scroll Top ```js // jQuery $(window).scrollTop(); // Native (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ back to top](#table-of-contents)** ## DOM Manipulation - [3.1](#3.1) Remove ```js // jQuery $el.remove(); // Native el.parentNode.removeChild(el); ``` - [3.2](#3.2) Text + Get text ```js // jQuery $el.text(); // Native el.textContent; ``` + Set text ```js // jQuery $el.text(string); // Native el.textContent = string; ``` - [3.3](#3.3) HTML + Get HTML ```js // jQuery $el.html(); // Native el.innerHTML; ``` + Set HTML ```js // jQuery $el.html(htmlString); // Native el.innerHTML = htmlString; ``` - [3.4](#3.4) Append Append child element after the last child of parent element ```js // jQuery $el.append("
          hello
          "); // Native el.insertAdjacentHTML("beforeend","
          hello
          "); ``` - [3.5](#3.5) Prepend ```js // jQuery $el.prepend("
          hello
          "); // Native el.insertAdjacentHTML("afterbegin","
          hello
          "); ``` - [3.6](#3.6) insertBefore Insert a new node before the selected elements ```js // jQuery $newEl.insertBefore(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter Insert a new node after the selected elements ```js // jQuery $newEl.insertAfter(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` - [3.8](#3.8) is Return `true` if it matches the query selector ```js // jQuery - Notice `is` also work with `function` or `elements` which is not concerned here $el.is(selector); // Native el.matches(selector); ``` - [3.9](#3.9) clone Create a deep copy of that element ```js // jQuery $el.clone(); // Native el.cloneNode(); // For Deep clone , set param as `true` ``` - [3.10](#3.10) empty Remove all child nodes ```js // jQuery $el.empty(); // Native el.innerHTML = ''; ``` - [3.11](#3.11) wrap Wrap an HTML structure around each element ```js // jQuery $('.inner').wrap('
          '); // Native [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ var wrapper = document.createElement('div'); wrapper.className = 'wrapper'; el.parentNode.insertBefore(wrapper, el); el.parentNode.removeChild(el); wrapper.appendChild(el); }); ``` - [3.12](#3.12) unwrap Remove the parents of the set of matched elements from the DOM ```js // jQuery $('.inner').unwrap(); // Native [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ [].slice.call(el.childNodes).forEach(function(child){ el.parentNode.insertBefore(child, el); }); el.parentNode.removeChild(el); }); ``` - [3.13](#3.13) replaceWith Replace each element in the set of matched elements with the provided new content ```js // jQuery $('.inner').replaceWith('
          '); // Native [].slice.call(document.querySelectorAll('.inner')).forEach(function(el){ var outer = document.createElement('div'); outer.className = 'outer'; el.parentNode.insertBefore(outer, el); el.parentNode.removeChild(el); }); ``` **[⬆ back to top](#table-of-contents)** ## Ajax [Fetch API](https://fetch.spec.whatwg.org/) is the new standard to replace XMLHttpRequest to do ajax. It works on Chrome and Firefox, you can use polyfills to make it work on legacy browsers. Try [github/fetch](http://github.com/github/fetch) on IE9+ or [fetch-ie8](https://github.com/camsong/fetch-ie8/) on IE8+, [fetch-jsonp](https://github.com/camsong/fetch-jsonp) to make JSONP requests. **[⬆ back to top](#table-of-contents)** ## Events For a complete replacement with namespace and delegation, refer to https://github.com/oneuijs/oui-dom-events - [5.1](#5.1) Bind an event with on ```js // jQuery $el.on(eventName, eventHandler); // Native el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) Unbind an event with off ```js // jQuery $el.off(eventName, eventHandler); // Native el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Native if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ back to top](#table-of-contents)** ## Utilities Most of utilities are found by native API. Others advanced functions could be choosed better utilities library focus on consistency and performance. Recommend [lodash](https://lodash.com) to replace. - [6.1](#6.1) Basic utilities + isArray Determine whether the argument is an array. ```js // jQuery $.isArray(array); // Native Array.isArray(array); ``` + isWindow Determine whether the argument is a window. ```js // jQuery $.isArray(obj); // Native function isWindow(obj) { return obj != null && obj === obj.window; } ``` + inArray Search for a specified value within an array and return its index (or -1 if not found). ```js // jQuery $.inArray(item, array); // Native Array.indexOf(item); ``` + isNumbic Determines whether its argument is a number. Use `typeof` to decide type. if necessary to use library, sometimes `typeof` isn't accurate. ```js // jQuery $.isNumbic(item); // Native function isNumbic(item) { return typeof value === 'number'; } ``` + isFunction Determine if the argument passed is a JavaScript function object. ```js // jQuery $.isFunction(item); // Native function isFunction(item) { return typeof value === 'function'; } ``` + isEmptyObject Check to see if an object is empty (contains no enumerable properties). ```js // jQuery $.isEmptyObject(obj); // Native function isEmptyObject(obj) { for (let key in obj) { return false; } return true; } ``` + isPlanObject Check to see if an object is a plain object (created using “{}” or “new Object”). ```js // jQuery $.isPlanObject(obj); // Native function isPlainObject(obj) { if (typeof (obj) !== 'object' || obj.nodeType || obj != null && obj === obj.window) { return false; } if (obj.constructor && !{}.hasOwnPropert.call(obj.constructor.prototype, 'isPrototypeOf')) { return false; } return true; } ``` + extend Merge the contents of two or more objects together into the first object. object.assign is ES6 API, and you could use [polyfill](https://github.com/ljharb/object.assign) also. ```js // jQuery $.extend({}, defaultOpts, opts); // Native Object.assign({}, defaultOpts, opts); ``` + trim Remove the whitespace from the beginning and end of a string. ```js // jQuery $.trim(string); // Native string.trim(); ``` + map Translate all items in an array or object to new array of items. ```js // jQuery $.map(array, function(value, index) { }); // Native array.map(function(value, index) { }); ``` + each A generic iterator function, which can be used to seamlessly iterate over both objects and arrays. ```js // jQuery $.each(array, function(value, index) { }); // Native array.forEach(function(value, index) { }); ``` + grep Finds the elements of an array which satisfy a filter function. ```js // jQuery $.grep(array, function(value, index) { }); // Native array.filter(function(value, index) { }); ``` + type Determine the internal JavaScript [[Class]] of an object. ```js // jQuery $.type(obj); // Native Object.prototype.toString.call(obj).replace(/^\[object (.+)\]$/, '$1').toLowerCase(); ``` + merge Merge the contents of two arrays together into the first array. ```js // jQuery $.merge(array1, array2); // Native // But concat function don't remove duplicate items. function merge() { return Array.prototype.concat.apply([], arguments) } ``` + now Return a number representing the current time. ```js // jQuery $.now(); // Native Date.now(); ``` + proxy Takes a function and returns a new one that will always have a particular context. ```js // jQuery $.proxy(fn, context); // Native fn.bind(context); ``` + makeArray Convert an array-like object into a true JavaScript array. ```js // jQuery $.makeArray(array); // Native [].slice.call(array); ``` - [6.2](#6.2) DOM utilities + unique Sorts an array of DOM elements, in place, with the duplicates removed. Note that this only works on arrays of DOM elements, not strings or numbers. Sizzle's API + contains Check to see if a DOM element is a descendant of another DOM element. ```js // jQuery $.contains(el, child); // Native el !== child && el.contains(child); ``` - [6.3](#6.3) Globaleval ```js // jQuery $.globaleval(code); // Native function Globaleval(code) { let script = document.createElement('script'); script.text = code; document.head.appendChild(script).parentNode.removeChild(script); } // Use eval, but context of eval is current, context of $.Globaleval is global. eval(code); ``` - [6.4](#6.4) parse + parseHTML Parses a string into an array of DOM nodes. ```js // jQuery $.parseHTML(htmlString); // Native function parseHTML(string) { const tmp = document.implementation.createHTMLDocument(); tmp.body.innerHTML = string; return tmp.body.children; } ``` + parseJSON Takes a well-formed JSON string and returns the resulting JavaScript value. ```js // jQuery $.parseJSON(str); // Native JSON.parse(str); ``` **[⬆ back to top](#table-of-contents)** ## Promises A promise represents the eventual result of an asynchronous operation. jQuery has its own way to handle promises. Native JavaScript implements a thin and minimal API to handle promises according to the [Promises/A+](http://promises-aplus.github.io/promises-spec/) specification. - [7.1](#7.1) done, fail, always `done` is called when promise is resolved, `fail` is called when promise is rejected, `always` is called when promise is either resolved or rejected. ```js // jQuery $promise.done(doneCallback).fail(failCallback).always(alwaysCallback) // Native promise.then(doneCallback, failCallback).then(alwaysCallback, alwaysCallback) ``` - [7.2](#7.2) when `when` is used to handle multiple promises. It will resolve when all promises are resolved, and reject if either one is rejected. ```js // jQuery $.when($promise1, $promise2).done((promise1Result, promise2Result) => {}) // Native Promise.all([$promise1, $promise2]).then([promise1Result, promise2Result] => {}); ``` - [7.3](#7.3) Deferred Deferred is a way to create promises. ```js // jQuery function asyncFunc() { var d = new $.Deferred(); setTimeout(function() { if(true) { d.resolve('some_value_compute_asynchronously'); } else { d.reject('failed'); } }, 1000); return d.promise(); } // Native function asyncFunc() { return new Promise((resolve, reject) => { setTimeout(function() { if (true) { resolve('some_value_compute_asynchronously'); } else { reject('failed'); } }, 1000); }); } ``` **[⬆ back to top](#table-of-contents)** ## Animation - [8.1](#8.1) Show & Hide ```js // jQuery $el.show(); $el.hide(); // Native // More detail about show method, please refer to https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L363 el.style.display = ''|'inline'|'inline-block'|'inline-table'|'block'; el.style.display = 'none'; ``` - [8.2](#8.2) Toggle ```js // jQuery $el.toggle(); // Native if (el.ownerDocument.defaultView.getComputedStyle(el, null).display === 'none') { el.style.display = ''|'inline'|'inline-block'|'inline-table'|'block'; } else { el.style.display = 'none'; } ``` - [8.3](#8.3) FadeIn & FadeOut ```js // jQuery $el.fadeIn(3000); $el.fadeOut(3000); // Native el.style.transition = 'opacity 3s'; // fadeIn el.style.opacity = '1'; // fadeOut el.style.opacity = '0'; ``` - [8.4](#8.4) FadeTo ```js // jQuery $el.fadeTo('slow',0.15); // Native el.style.transition = 'opacity 3s'; // assume 'slow' equals 3 seconds el.style.opacity = '0.15'; ``` - [8.5](#8.5) FadeToggle ```js // jQuery $el.fadeToggle(); // Native el.style.transition = 'opacity 3s'; let { opacity } = el.ownerDocument.defaultView.getComputedStyle(el, null); if (opacity === '1') { el.style.opacity = '0'; } else { el.style.opacity = '1'; } ``` - [8.6](#8.6) SlideUp & SlideDown ```js // jQuery $el.slideUp(); $el.slideDown(); // Native let originHeight = '100px'; el.style.transition = 'height 3s'; // slideUp el.style.height = '0px'; // slideDown el.style.height = originHeight; ``` - [8.7](#8.7) SlideToggle ```js // jQuery $el.slideToggle(); // Native let originHeight = '100px'; el.style.transition = 'height 3s'; let { height } = el.ownerDocument.defaultView.getComputedStyle(el, null); if (parseInt(height, 10) === 0) { el.style.height = originHeight; } else { el.style.height = '0px'; } ``` - [8.8](#8.8) Animate ```js // jQuery $el.animate({params}, speed); // Native el.style.transition = 'all' + speed; Object.keys(params).forEach(function(key) { el.style[key] = params[key]; }) ``` ## Alternatives * [You Might Not Need jQuery](http://youmightnotneedjquery.com/) - Examples of how to do common event, element, ajax etc with plain javascript. * [npm-dom](http://github.com/npm-dom) and [webmodules](http://github.com/webmodules) - Organizations you can find individual DOM modules on NPM ## Translations * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Bahasa Indonesia](./README-id.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Español](./README-es.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) * [Italian](./README-it.md) ## Browser Support ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # License MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.pt-BR.md ================================================ > #### You Don't Need jQuery Você não precisa de jQuery --- Ambientes Frontend evoluem rapidamente nos dias de hoje, navegadores modernos já implementaram uma grande parte das APIs DOM/BOM que são boas o suficiente. Nós não temos que aprender jQuery a partir do zero para manipulação do DOM ou eventos. Nesse meio tempo, graças a bibliotecas frontend como React, Angular e Vue, a manipulação direta do DOM torna-se um anti-padrão, jQuery é menos importante do que nunca. Este projeto resume a maioria das alternativas dos métodos jQuery em implementação nativa, com suporte ao IE 10+. ## Tabela de conteúdos 1. [Query Selector](#query-selector) 1. [CSS & Estilo](#css--estilo) 1. [Manipulação do DOM](#manipulação-do-dom) 1. [Ajax](#ajax) 1. [Eventos](#eventos) 1. [Utilitários](#utilitários) 1. [Suporte dos Navegadores](#suporte-dos-navegadores) ## Query Selector No lugar de seletores comuns como classe, id ou atributo podemos usar `document.querySelector` ou `document.querySelectorAll` para substituição. As diferenças são: * `document.querySelector` retorna o primeiro elemento correspondente * `document.querySelectorAll` retorna todos os elementos correspondentes como NodeList. Pode ser convertido para Array usando `[].slice.call(document.querySelectorAll(selector) || []);` * Se não tiver elementos correspondentes, jQuery retornaria `[]` considerando que a DOM API irá retornar `null`. Preste atenção ao Null Pointer Exception. Você também pode usar `||` para definir um valor padrão caso nenhum elemento seja encontrado, como `document.querySelectorAll(selector) || []` > Aviso: `document.querySelector` e `document.querySelectorAll` são bastante **LENTOS**, tente usar `getElementById`, `document.getElementsByClassName` ou `document.getElementsByTagName` se você quer ter uma maior performance. - [1.0](#1.0) Query por seletor ```js // jQuery $('selector'); // Nativo document.querySelectorAll('selector'); ``` - [1.1](#1.1) Query por classe ```js // jQuery $('.class'); // Nativo document.querySelectorAll('.class'); // ou document.getElementsByClassName('class'); ``` - [1.2](#1.2) Query por id ```js // jQuery $('#id'); // Nativo document.querySelector('#id'); // ou document.getElementById('id'); ``` - [1.3](#1.3) Query por atributo ```js // jQuery $('a[target=_blank]'); // Nativo document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Find sth. + Busca por nós ```js // jQuery $el.find('li'); // Nativo el.querySelectorAll('li'); ``` + Buscar `body` ```js // jQuery $('body'); // Nativo document.body; ``` + Buscar atributos ```js // jQuery $el.attr('foo'); // Nativo e.getAttribute('foo'); ``` + Buscar atributos `data-` ```js // jQuery $el.data('foo'); // Nativo // usando getAttribute el.getAttribute('data-foo'); // você também pode usar `dataset` se você precisar suportar apenas IE 11+ el.dataset['foo']; ``` - [1.5](#1.5) Sibling/Previous/Next Elements + Sibling elements ```js // jQuery $el.siblings(); // Nativo [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Previous elements ```js // jQuery $el.prev(); // Nativo el.previousElementSibling; ``` + Next elements ```js // jQuery $el.next(); // Nativo el.nextElementSibling; ``` - [1.6](#1.6) Closest Retorna o primeiro elemento que corresponda ao seletor, partindo do elemento atual para o document. ```js // jQuery $el.closest(queryString); // Nativo function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Parents Until Obtém os ancestrais de cada elemento no atual conjunto de elementos combinados, mas não inclui o elemento correspondente pelo seletor, nó do DOM, ou objeto jQuery. ```js // jQuery $el.parentsUntil(selector, filter); // Nativo function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // match start from parent el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Nativo document.querySelector('#my-input').value; ``` + Obter o índice do e.currentTarget entre `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Nativo [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Iframe Contents `$('iframe').contents()` retorna `contentDocument` para este iframe específico + Iframe contents ```js // jQuery $iframe.contents(); // Nativo iframe.contentDocument; ``` + Iframe Query ```js // jQuery $iframe.contents().find('.css'); // Nativo iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ ir para o topo](#tabela-de-conteúdos)** ## CSS & Estilo - [2.1](#2.1) CSS + Obter estilo ```js // jQuery $el.css("color"); // Nativo // AVISO: Bug conhecido, irá retornar 'auto' se o valor do estilo for 'auto' const win = el.ownerDocument.defaultView; // null significa não retornar estilos win.getComputedStyle(el, null).color; ``` + Definir Estilo ```js // jQuery $el.css({ color: "#ff0011" }); // Nativo el.style.color = '#ff0011'; ``` + Get/Set Styles Observe que se você deseja setar vários estilos de uma vez, você pode optar por [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) método no pacote oui-dom-utils. + Adicionar classe ```js // jQuery $el.addClass(className); // Nativo el.classList.add(className); ``` + Remover classe ```js // jQuery $el.removeClass(className); // Nativo el.classList.remove(className); ``` + Verificar classe ```js // jQuery $el.hasClass(className); // Nativo el.classList.contains(className); ``` + Toggle class ```js // jQuery $el.toggleClass(className); // Nativo el.classList.toggle(className); ``` - [2.2](#2.2) Largura e Altura `width` e `height` são teoricamente idênticos, vamos pegar `height` como exemplo: + Altura da janela ```jsc // window height $(window).height(); // sem scrollbar, se comporta como jQuery window.document.documentElement.clientHeight; // com scrollbar window.innerHeight; ``` + Altura do Documento ```js // jQuery $(document).height(); // Nativo document.documentElement.scrollHeight; ``` + Altura do Elemento ```js // jQuery $el.height(); // Nativo function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // preciso para inteiro(quando `border-box`, é `height`; quando `content-box`, é `height + padding + border`) el.clientHeight; // preciso para decimal(quando `border-box`, é `height`; quando `content-box`, é `height + padding + border`) el.getBoundingClientRect().height; ``` - [2.3](#2.3) Position & Offset + Position ```js // jQuery $el.position(); // Nativo { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Nativo function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Rolar para o topo ```js // jQuery $(window).scrollTop(); // Nativo (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ ir para o topo](#tabela-de-conteúdos)** ## Manipulação do Dom - [3.1](#3.1) Remover ```js // jQuery $el.remove(); // Nativo el.parentNode.removeChild(el); ``` - [3.2](#3.2) Texto + Obter texto ```js // jQuery $el.text(); // Nativo el.textContent; ``` + Definir texto ```js // jQuery $el.text(string); // Nativo el.textContent = string; ``` - [3.3](#3.3) HTML + Obter HTML ```js // jQuery $el.html(); // Nativo el.innerHTML; ``` + Definir HTML ```js // jQuery $el.html(htmlString); // Nativo el.innerHTML = htmlString; ``` - [3.4](#3.4) Append Incluir elemento filho após o último filho do elemento pai. ```js // jQuery $el.append("
          hello
          "); // Nativo let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.appendChild(newEl); ``` - [3.5](#3.5) Prepend ```js // jQuery $el.prepend("
          hello
          "); // Nativo let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.insertBefore(newEl, el.firstChild); ``` - [3.6](#3.6) insertBefore Insere um novo nó antes dos elementos selecionados. ```js // jQuery $newEl.insertBefore(queryString); // Nativo const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter Insere um novo nó após os elementos selecionados. ```js // jQuery $newEl.insertAfter(queryString); // Nativo const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` **[⬆ ir para o topo](#tabela-de-conteúdos)** ## Ajax Substitua por [fetch](https://github.com/camsong/fetch-ie8) e [fetch-jsonp](https://github.com/camsong/fetch-jsonp) **[⬆ ir para o topo](#tabela-de-conteúdos)** ## Eventos Para uma substituição completa com namespace e delegation, consulte https://github.com/oneuijs/oui-dom-events - [5.1](#5.1) `Bind` num evento com `on` ```js // jQuery $el.on(eventName, eventHandler); // Nativo el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) `Unbind` num evento com `off` ```js // jQuery $el.off(eventName, eventHandler); // Nativo el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Nativo if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ ir para o topo](#tabela-de-conteúdos)** ## Utilitários - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Nativo Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Nativo string.trim(); ``` - [6.3](#6.3) Object Assign Use o polyfill `object.assign` para eetender um Object: https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Nativo Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Nativo el !== child && el.contains(child); ``` **[⬆ ir para o topo](#tabela-de-conteúdos)** ## Suporte dos Navegadores ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # Licença MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/README.zh-CN.md ================================================ ## You Don't Need jQuery 前端发展很快,现代浏览器原生 API 已经足够好用。我们并不需要为了操作 DOM、Event 等再学习一下 jQuery 的 API。同时由于 React、Angular、Vue 等框架的流行,直接操作 DOM 不再是好的模式,jQuery 使用场景大大减少。本项目总结了大部分 jQuery API 替代的方法,暂时只支持 IE10+ 以上浏览器。 ## 目录 1. [Query Selector](#query-selector) 1. [CSS & Style](#css--style) 1. [DOM Manipulation](#dom-manipulation) 1. [Ajax](#ajax) 1. [Events](#events) 1. [Utilities](#utilities) 1. [Alternatives](#alternatives) 1. [Translations](#translations) 1. [Browser Support](#browser-support) ## Query Selector 常用的 class、id、属性 选择器都可以使用 `document.querySelector` 或 `document.querySelectorAll` 替代。区别是 * `document.querySelector` 返回第一个匹配的 Element * `document.querySelectorAll` 返回所有匹配的 Element 组成的 NodeList。它可以通过 `[].slice.call()` 把它转成 Array * 如果匹配不到任何 Element,jQuery 返回空数组 `[]`,但 `document.querySelector` 返回 `null`,注意空指针异常。当找不到时,也可以使用 `||` 设置默认的值,如 `document.querySelectorAll(selector) || []` > 注意:`document.querySelector` 和 `document.querySelectorAll` 性能很**差**。如果想提高性能,尽量使用 `document.getElementById`、`document.getElementsByClassName` 或 `document.getElementsByTagName`。 - [1.0](#1.0) Query by selector ```js // jQuery $('selector'); // Native document.querySelectorAll('selector'); ``` - [1.1](#1.1) Query by class ```js // jQuery $('.css'); // Native document.querySelectorAll('.css'); // or document.getElementsByClassName('css'); ``` - [1.2](#1.2) Query by id ```js // jQuery $('#id'); // Native document.querySelector('#id'); // or document.getElementById('id'); ``` - [1.3](#1.3) Query by attribute ```js // jQuery $('a[target=_blank]'); // Native document.querySelectorAll('a[target=_blank]'); ``` - [1.4](#1.4) Find sth. + Find nodes ```js // jQuery $el.find('li'); // Native el.querySelectorAll('li'); ``` + Find body ```js // jQuery $('body'); // Native document.body; ``` + Find Attribute ```js // jQuery $el.attr('foo'); // Native e.getAttribute('foo'); ``` + Find data attribute ```js // jQuery $el.data('foo'); // Native // using getAttribute el.getAttribute('data-foo'); // you can also use `dataset` if only need to support IE 11+ el.dataset['foo']; ``` - [1.5](#1.5) Sibling/Previous/Next Elements + Sibling elements ```js // jQuery $el.siblings(); // Native [].filter.call(el.parentNode.children, function(child) { return child !== el; }); ``` + Previous elements ```js // jQuery $el.prev(); // Native el.previousElementSibling; ``` + Next elements ```js // next $el.next(); el.nextElementSibling; ``` - [1.6](#1.6) Closest Closest 获得匹配选择器的第一个祖先元素,从当前元素开始沿 DOM 树向上。 ```js // jQuery $el.closest(queryString); // Native function closest(el, selector) { const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; while (el) { if (matchesSelector.call(el, selector)) { return el; } else { el = el.parentElement; } } return null; } ``` - [1.7](#1.7) Parents Until 获取当前每一个匹配元素集的祖先,不包括匹配元素的本身。 ```js // jQuery $el.parentsUntil(selector, filter); // Native function parentsUntil(el, selector, filter) { const result = []; const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector; // match start from parent el = el.parentElement; while (el && !matchesSelector.call(el, selector)) { if (!filter) { result.push(el); } else { if (matchesSelector.call(el, filter)) { result.push(el); } } el = el.parentElement; } return result; } ``` - [1.8](#1.8) Form + Input/Textarea ```js // jQuery $('#my-input').val(); // Native document.querySelector('#my-input').value; ``` + Get index of e.currentTarget between `.radio` ```js // jQuery $(e.currentTarget).index('.radio'); // Native [].indexOf.call(document.querySelectAll('.radio'), e.currentTarget); ``` - [1.9](#1.9) Iframe Contents jQuery 对象的 iframe `contents()` 返回的是 iframe 内的 `document` + Iframe contents ```js // jQuery $iframe.contents(); // Native iframe.contentDocument; ``` + Iframe Query ```js // jQuery $iframe.contents().find('.css'); // Native iframe.contentDocument.querySelectorAll('.css'); ``` **[⬆ 回到顶部](#目录)** ## CSS & Style - [2.1](#2.1) CSS + Get style ```js // jQuery $el.css("color"); // Native // 注意:此处为了解决当 style 值为 auto 时,返回 auto 的问题 const win = el.ownerDocument.defaultView; // null 的意思是不返回伪类元素 win.getComputedStyle(el, null).color; ``` + Set style ```js // jQuery $el.css({ color: "#ff0011" }); // Native el.style.color = '#ff0011'; ``` + Get/Set Styles 注意,如果想一次设置多个 style,可以参考 oui-dom-utils 中 [setStyles](https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L194) 方法 + Add class ```js // jQuery $el.addClass(className); // Native el.classList.add(className); ``` + Remove class ```js // jQuery $el.removeClass(className); // Native el.classList.remove(className); ``` + has class ```js // jQuery $el.hasClass(className); // Native el.classList.contains(className); ``` + Toggle class ```js // jQuery $el.toggleClass(className); // Native el.classList.toggle(className); ``` - [2.2](#2.2) Width & Height Width 与 Height 获取方法相同,下面以 Height 为例: + Window height ```js // jQuery $(window).height(); // Native // 不含 scrollbar,与 jQuery 行为一致 window.document.documentElement.clientHeight; // 含 scrollbar window.innerHeight; ``` + Document height ```js // jQuery $(document).height(); // Native document.documentElement.scrollHeight; ``` + Element height ```js // jQuery $el.height(); // Native // 与 jQuery 一致(一直为 content 区域的高度) function getHeight(el) { const styles = this.getComputedStyles(el); const height = el.offsetHeight; const borderTopWidth = parseFloat(styles.borderTopWidth); const borderBottomWidth = parseFloat(styles.borderBottomWidth); const paddingTop = parseFloat(styles.paddingTop); const paddingBottom = parseFloat(styles.paddingBottom); return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; } // 精确到整数(border-box 时为 height 值,content-box 时为 height + padding + border 值) el.clientHeight; // 精确到小数(border-box 时为 height 值,content-box 时为 height + padding + border 值) el.getBoundingClientRect().height; ``` + Iframe height $iframe .contents() 方法返回 iframe 的 contentDocument ```js // jQuery $('iframe').contents().height(); // Native iframe.contentDocument.documentElement.scrollHeight; ``` - [2.3](#2.3) Position & Offset + Position ```js // jQuery $el.position(); // Native { left: el.offsetLeft, top: el.offsetTop } ``` + Offset ```js // jQuery $el.offset(); // Native function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft } } ``` - [2.4](#2.4) Scroll Top ```js // jQuery $(window).scrollTop(); // Native (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop; ``` **[⬆ 回到顶部](#目录)** ## DOM Manipulation - [3.1](#3.1) Remove ```js // jQuery $el.remove(); // Native el.parentNode.removeChild(el); ``` - [3.2](#3.2) Text + Get text ```js // jQuery $el.text(); // Native el.textContent; ``` + Set text ```js // jQuery $el.text(string); // Native el.textContent = string; ``` - [3.3](#3.3) HTML + Get HTML ```js // jQuery $el.html(); // Native el.innerHTML; ``` + Set HTML ```js // jQuery $el.html(htmlString); // Native el.innerHTML = htmlString; ``` - [3.4](#3.4) Append Append 插入到子节点的末尾 ```js // jQuery $el.append("
          hello
          "); // Native let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.appendChild(newEl); ``` - [3.5](#3.5) Prepend ```js // jQuery $el.prepend("
          hello
          "); // Native let newEl = document.createElement('div'); newEl.setAttribute('id', 'container'); newEl.innerHTML = 'hello'; el.insertBefore(newEl, el.firstChild); ``` - [3.6](#3.6) insertBefore 在选中元素前插入新节点 ```js // jQuery $newEl.insertBefore(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target); ``` - [3.7](#3.7) insertAfter 在选中元素后插入新节点 ```js // jQuery $newEl.insertAfter(queryString); // Native const target = document.querySelector(queryString); target.parentNode.insertBefore(newEl, target.nextSibling); ``` **[⬆ 回到顶部](#目录)** ## Ajax 用 [fetch](https://github.com/camsong/fetch-ie8) 和 [fetch-jsonp](https://github.com/camsong/fetch-jsonp) 替代 **[⬆ 回到顶部](#目录)** ## Events 完整地替代命名空间和事件代理,链接到 https://github.com/oneuijs/oui-dom-events - [5.1](#5.1) Bind an event with on ```js // jQuery $el.on(eventName, eventHandler); // Native el.addEventListener(eventName, eventHandler); ``` - [5.2](#5.2) Unbind an event with off ```js // jQuery $el.off(eventName, eventHandler); // Native el.removeEventListener(eventName, eventHandler); ``` - [5.3](#5.3) Trigger ```js // jQuery $(el).trigger('custom-event', {key1: 'data'}); // Native if (window.CustomEvent) { const event = new CustomEvent('custom-event', {detail: {key1: 'data'}}); } else { const event = document.createEvent('CustomEvent'); event.initCustomEvent('custom-event', true, true, {key1: 'data'}); } el.dispatchEvent(event); ``` **[⬆ 回到顶部](#目录)** ## Utilities - [6.1](#6.1) isArray ```js // jQuery $.isArray(range); // Native Array.isArray(range); ``` - [6.2](#6.2) Trim ```js // jQuery $.trim(string); // Native string.trim(); ``` - [6.3](#6.3) Object Assign 继承,使用 object.assign polyfill https://github.com/ljharb/object.assign ```js // jQuery $.extend({}, defaultOpts, opts); // Native Object.assign({}, defaultOpts, opts); ``` - [6.4](#6.4) Contains ```js // jQuery $.contains(el, child); // Native el !== child && el.contains(child); ``` **[⬆ 回到顶部](#目录)** ## Alternatives * [你可能不需要 jQuery (You Might Not Need jQuery)](http://youmightnotneedjquery.com/) - 如何使用原生 JavaScript 实现通用事件,元素,ajax 等用法。 * [npm-dom](http://github.com/npm-dom) 以及 [webmodules](http://github.com/webmodules) - 在 NPM 上提供独立 DOM 模块的组织 ## Translations * [한국어](./README.ko-KR.md) * [简体中文](./README.zh-CN.md) * [Bahasa Melayu](./README-my.md) * [Bahasa Indonesia](./README-id.md) * [Português(PT-BR)](./README.pt-BR.md) * [Tiếng Việt Nam](./README-vi.md) * [Español](./README-es.md) * [Русский](./README-ru.md) * [Türkçe](./README-tr.md) * [Italian](./README-it.md) ## Browser Support ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) --- | --- | --- | --- | --- | Latest ✔ | Latest ✔ | 10+ ✔ | Latest ✔ | 6.1+ ✔ | # License MIT ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/git.git ================================================ gitdir: ../.git/modules/You-Dont-Need-jQuery ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/karma.conf.js ================================================ // Karma configuration // Generated on Sun Nov 22 2015 22:10:47 GMT+0800 (CST) require('babel-core/register'); module.exports = function(config) { config.set({ // base path that will be used to resolve all patterns (eg. files, exclude) basePath: '.', // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['mocha'], // list of files / patterns to load in the browser files: [ './test/**/*.spec.js' ], // list of files to exclude exclude: [ ], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { 'test/**/*.spec.js': ['webpack', 'sourcemap'] }, // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ['progress'], coverageReporter: { reporters: [ {type: 'text'}, {type: 'html', dir: 'coverage'}, ] }, webpackMiddleware: { stats: 'minimal' }, webpack: { cache: true, devtool: 'inline-source-map', module: { loaders: [{ test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/ }], postLoaders: [{ test: /\.js/, exclude: /(test|node_modules)/, loader: 'istanbul-instrumenter' }], }, resolve: { extensions: ['', '.js', '.jsx'] } }, // web server port port: 9876, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_INFO, // enable / disable watching file and executing tests whenever any file changes autoWatch: true, // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: ['Firefox'], // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits // singleRun: false, // Concurrency level // how many browser should be started simultanous // concurrency: Infinity, // plugins: ['karma-phantomjs-launcher', 'karma-sourcemap-loader', 'karma-webpack'] }) } ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/package.json ================================================ { "name": "You-Dont-Need-jQuery", "version": "1.0.0", "description": "Examples of how to do query, style, dom, ajax, event etc like jQuery with plain javascript.", "scripts": { "test": "karma start --single-run", "tdd": "karma start --auto-watch --no-single-run", "test-cov": "karma start --auto-watch --single-run --reporters progress,coverage", "lint": "eslint src test" }, "dependencies": {}, "devDependencies": { "babel-cli": "^6.2.0", "babel-core": "^6.1.21", "babel-eslint": "^4.1.5", "babel-loader": "^6.2.0", "babel-preset-es2015": "^6.1.18", "babel-preset-stage-0": "^6.1.18", "chai": "^3.4.1", "eslint": "^1.9.0", "eslint-config-airbnb": "^1.0.0", "eslint-plugin-react": "^3.10.0", "isparta": "^4.0.0", "istanbul-instrumenter-loader": "^0.1.3", "jquery": "^2.1.4", "karma": "^0.13.15", "karma-coverage": "^0.5.3", "karma-firefox-launcher": "^0.1.7", "karma-mocha": "^0.2.1", "karma-sourcemap-loader": "^0.3.6", "karma-webpack": "^1.7.0", "mocha": "^2.3.4", "webpack": "^1.12.9" }, "repository": { "type": "git", "url": "https://github.com/oneuijs/You-Dont-Need-jQuery.git" }, "keywords": [ "convertion guide", "jQuery", "es6", "es2015", "babel", "OneUI Group" ], "author": "OneUI Group", "license": "MIT", "bugs": { "url": "https://github.com/oneuijs/You-Dont-Need-jQuery/issues" }, "homepage": "https://github.com/oneuijs/You-Dont-Need-jQuery" } ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/README.md ================================================ # Test cases for all the tips ## Usage run all tests once ``` npm run test ``` run tests on TDD(Test Driven Development) mode ``` npm run tdd ``` ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/css.spec.js ================================================ // test for CSS related ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/dom.spec.js ================================================ // test for CSS and style related ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/query.spec.js ================================================ // tests for Query Selector related import { expect } from 'chai'; import $ from 'jquery'; describe('query selector', () => { describe('basic', () => { beforeEach(() => { document.body.innerHTML = `
          • I
          • II
          • III
            • III.I
            • III.II
          `; }); afterEach(() => { const el = document.querySelector('#query-selector-test1'); el.parentNode.removeChild(el); }); it('1.0 Query by selector', () => { const $els = $('li.item[data-role="red"]'); const els = document.querySelectorAll('li.item[data-role="red"]'); expect($els.length).to.equal(2); [].forEach.call($els, function($el, i) { expect($el).to.equal(els[i]); }); }); it('1.1 Query by class', () => { const $els = $('.item'); const els = document.getElementsByClassName('item'); [].forEach.call($els, function($el, i) { expect($el).to.equal(els[i]); }); }); it('1.2 Query by id', () => { expect($('#nested-ul')[0]).to.equal(document.getElementById('nested-ul')); }); it('1.3 Query by attribute', () => { const $els = $('[data-role="blue"]'); const els = document.querySelectorAll('[data-role="blue"]'); expect($els.length).to.equal(2); [].forEach.call($els, function($el, i) { expect($el).to.equal(els[i]); }); }); it('1.4 Query in descendents', () => { const $els = $('#query-selector-test1').find('.item'); const els = document.getElementById('query-selector-test1').querySelectorAll('.item'); expect($els.length).to.equal(4); [].forEach.call($els, function($el, i) { expect($el).to.equal(els[i]); }); }); }); }); ================================================ FILE: spec/fixtures/git/repo-with-submodules/You-Dont-Need-jQuery/test/utilities.spec.js ================================================ // test for Utilities related ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/COMMIT_EDITMSG ================================================ submodules # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: # new file: .gitmodules # new file: You-Dont-Need-jQuery # new file: jstips # ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/HEAD ================================================ ref: refs/heads/master ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/config ================================================ [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [branch "master"] remote = origin merge = refs/heads/master [remote "origin"] url = git@github.com:atom/some-repo-i-guess.git fetch = +refs/heads/*:refs/remotes/origin/* [submodule "jstips"] url = https://github.com/loverajoel/jstips [submodule "You-Dont-Need-jQuery"] url = https://github.com/oneuijs/You-Dont-Need-jQuery ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/description ================================================ Unnamed repository; edit this file 'description' to name the repository. ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/applypatch-msg.sample ================================================ #!/bin/sh # # An example hook script to check the commit log message taken by # applypatch from an e-mail message. # # The hook should exit with non-zero status after issuing an # appropriate message if it wants to stop the commit. The hook is # allowed to edit the commit message file. # # To enable this hook, rename this file to "applypatch-msg". . git-sh-setup commitmsg="$(git rev-parse --git-path hooks/commit-msg)" test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} : ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/commit-msg.sample ================================================ #!/bin/sh # # An example hook script to check the commit log message. # Called by "git commit" with one argument, the name of the file # that has the commit message. The hook should exit with non-zero # status after issuing an appropriate message if it wants to stop the # commit. The hook is allowed to edit the commit message file. # # To enable this hook, rename this file to "commit-msg". # Uncomment the below to add a Signed-off-by line to the message. # Doing this in a hook is a bad idea in general, but the prepare-commit-msg # hook is more suited to it. # # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" # This example catches duplicate Signed-off-by lines. test "" = "$(grep '^Signed-off-by: ' "$1" | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { echo >&2 Duplicate Signed-off-by lines. exit 1 } ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/post-update.sample ================================================ #!/bin/sh # # An example hook script to prepare a packed repository for use over # dumb transports. # # To enable this hook, rename this file to "post-update". exec git update-server-info ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-applypatch.sample ================================================ #!/bin/sh # # An example hook script to verify what is about to be committed # by applypatch from an e-mail message. # # The hook should exit with non-zero status after issuing an # appropriate message if it wants to stop the commit. # # To enable this hook, rename this file to "pre-applypatch". . git-sh-setup precommit="$(git rev-parse --git-path hooks/pre-commit)" test -x "$precommit" && exec "$precommit" ${1+"$@"} : ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-commit.sample ================================================ #!/bin/sh # # An example hook script to verify what is about to be committed. # Called by "git commit" with no arguments. The hook should # exit with non-zero status after issuing an appropriate message if # it wants to stop the commit. # # To enable this hook, rename this file to "pre-commit". if git rev-parse --verify HEAD >/dev/null 2>&1 then against=HEAD else # Initial commit: diff against an empty tree object against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi # If you want to allow non-ASCII filenames set this variable to true. allownonascii=$(git config --bool hooks.allownonascii) # Redirect output to stderr. exec 1>&2 # Cross platform projects tend to avoid non-ASCII filenames; prevent # them from being added to the repository. We exploit the fact that the # printable range starts at the space character and ends with tilde. if [ "$allownonascii" != "true" ] && # Note that the use of brackets around a tr range is ok here, (it's # even required, for portability to Solaris 10's /usr/bin/tr), since # the square bracket bytes happen to fall in the designated range. test $(git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 then cat <<\EOF Error: Attempt to add a non-ASCII file name. This can cause problems if you want to work with people on other platforms. To be portable it is advisable to rename the file. If you know what you are doing you can disable this check using: git config hooks.allownonascii true EOF exit 1 fi # If there are whitespace errors, print the offending file names and fail. exec git diff-index --check --cached $against -- ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-push.sample ================================================ #!/bin/sh # An example hook script to verify what is about to be pushed. Called by "git # push" after it has checked the remote status, but before anything has been # pushed. If this script exits with a non-zero status nothing will be pushed. # # This hook is called with the following parameters: # # $1 -- Name of the remote to which the push is being done # $2 -- URL to which the push is being done # # If pushing without using a named remote those arguments will be equal. # # Information about the commits which are being pushed is supplied as lines to # the standard input in the form: # # # # This sample shows how to prevent push of commits where the log message starts # with "WIP" (work in progress). remote="$1" url="$2" z40=0000000000000000000000000000000000000000 while read local_ref local_sha remote_ref remote_sha do if [ "$local_sha" = $z40 ] then # Handle delete : else if [ "$remote_sha" = $z40 ] then # New branch, examine all commits range="$local_sha" else # Update to existing branch, examine new commits range="$remote_sha..$local_sha" fi # Check for WIP commit commit=`git rev-list -n 1 --grep '^WIP' "$range"` if [ -n "$commit" ] then echo >&2 "Found WIP commit in $local_ref, not pushing" exit 1 fi fi done exit 0 ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/pre-rebase.sample ================================================ #!/bin/sh # # Copyright (c) 2006, 2008 Junio C Hamano # # The "pre-rebase" hook is run just before "git rebase" starts doing # its job, and can prevent the command from running by exiting with # non-zero status. # # The hook is called with the following parameters: # # $1 -- the upstream the series was forked from. # $2 -- the branch being rebased (or empty when rebasing the current branch). # # This sample shows how to prevent topic branches that are already # merged to 'next' branch from getting rebased, because allowing it # would result in rebasing already published history. publish=next basebranch="$1" if test "$#" = 2 then topic="refs/heads/$2" else topic=`git symbolic-ref HEAD` || exit 0 ;# we do not interrupt rebasing detached HEAD fi case "$topic" in refs/heads/??/*) ;; *) exit 0 ;# we do not interrupt others. ;; esac # Now we are dealing with a topic branch being rebased # on top of master. Is it OK to rebase it? # Does the topic really exist? git show-ref -q "$topic" || { echo >&2 "No such branch $topic" exit 1 } # Is topic fully merged to master? not_in_master=`git rev-list --pretty=oneline ^master "$topic"` if test -z "$not_in_master" then echo >&2 "$topic is fully merged to master; better remove it." exit 1 ;# we could allow it, but there is no point. fi # Is topic ever merged to next? If so you should not be rebasing it. only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` only_next_2=`git rev-list ^master ${publish} | sort` if test "$only_next_1" = "$only_next_2" then not_in_topic=`git rev-list "^$topic" master` if test -z "$not_in_topic" then echo >&2 "$topic is already up-to-date with master" exit 1 ;# we could allow it, but there is no point. else exit 0 fi else not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` /usr/bin/perl -e ' my $topic = $ARGV[0]; my $msg = "* $topic has commits already merged to public branch:\n"; my (%not_in_next) = map { /^([0-9a-f]+) /; ($1 => 1); } split(/\n/, $ARGV[1]); for my $elem (map { /^([0-9a-f]+) (.*)$/; [$1 => $2]; } split(/\n/, $ARGV[2])) { if (!exists $not_in_next{$elem->[0]}) { if ($msg) { print STDERR $msg; undef $msg; } print STDERR " $elem->[1]\n"; } } ' "$topic" "$not_in_next" "$not_in_master" exit 1 fi exit 0 ################################################################ This sample hook safeguards topic branches that have been published from being rewound. The workflow assumed here is: * Once a topic branch forks from "master", "master" is never merged into it again (either directly or indirectly). * Once a topic branch is fully cooked and merged into "master", it is deleted. If you need to build on top of it to correct earlier mistakes, a new topic branch is created by forking at the tip of the "master". This is not strictly necessary, but it makes it easier to keep your history simple. * Whenever you need to test or publish your changes to topic branches, merge them into "next" branch. The script, being an example, hardcodes the publish branch name to be "next", but it is trivial to make it configurable via $GIT_DIR/config mechanism. With this workflow, you would want to know: (1) ... if a topic branch has ever been merged to "next". Young topic branches can have stupid mistakes you would rather clean up before publishing, and things that have not been merged into other branches can be easily rebased without affecting other people. But once it is published, you would not want to rewind it. (2) ... if a topic branch has been fully merged to "master". Then you can delete it. More importantly, you should not build on top of it -- other people may already want to change things related to the topic as patches against your "master", so if you need further changes, it is better to fork the topic (perhaps with the same name) afresh from the tip of "master". Let's look at this example: o---o---o---o---o---o---o---o---o---o "next" / / / / / a---a---b A / / / / / / / / c---c---c---c B / / / / \ / / / / b---b C \ / / / / / \ / ---o---o---o---o---o---o---o---o---o---o---o "master" A, B and C are topic branches. * A has one fix since it was merged up to "next". * B has finished. It has been fully merged up to "master" and "next", and is ready to be deleted. * C has not merged to "next" at all. We would want to allow C to be rebased, refuse A, and encourage B to be deleted. To compute (1): git rev-list ^master ^topic next git rev-list ^master next if these match, topic has not merged in next at all. To compute (2): git rev-list master..topic if this is empty, it is fully merged to "master". ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/prepare-commit-msg.sample ================================================ #!/bin/sh # # An example hook script to prepare the commit log message. # Called by "git commit" with the name of the file that has the # commit message, followed by the description of the commit # message's source. The hook's purpose is to edit the commit # message file. If the hook fails with a non-zero status, # the commit is aborted. # # To enable this hook, rename this file to "prepare-commit-msg". # This hook includes three examples. The first comments out the # "Conflicts:" part of a merge commit. # # The second includes the output of "git diff --name-status -r" # into the message, just before the "git status" output. It is # commented because it doesn't cope with --amend or with squashed # commits. # # The third example adds a Signed-off-by line to the message, that can # still be edited. This is rarely a good idea. case "$2,$3" in merge,) /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; # ,|template,) # /usr/bin/perl -i.bak -pe ' # print "\n" . `git diff --cached --name-status -r` # if /^#/ && $first++ == 0' "$1" ;; *) ;; esac # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/hooks/update.sample ================================================ #!/bin/sh # # An example hook script to blocks unannotated tags from entering. # Called by "git receive-pack" with arguments: refname sha1-old sha1-new # # To enable this hook, rename this file to "update". # # Config # ------ # hooks.allowunannotated # This boolean sets whether unannotated tags will be allowed into the # repository. By default they won't be. # hooks.allowdeletetag # This boolean sets whether deleting tags will be allowed in the # repository. By default they won't be. # hooks.allowmodifytag # This boolean sets whether a tag may be modified after creation. By default # it won't be. # hooks.allowdeletebranch # This boolean sets whether deleting branches will be allowed in the # repository. By default they won't be. # hooks.denycreatebranch # This boolean sets whether remotely creating branches will be denied # in the repository. By default this is allowed. # # --- Command line refname="$1" oldrev="$2" newrev="$3" # --- Safety check if [ -z "$GIT_DIR" ]; then echo "Don't run this script from the command line." >&2 echo " (if you want, you could supply GIT_DIR then run" >&2 echo " $0 )" >&2 exit 1 fi if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then echo "usage: $0 " >&2 exit 1 fi # --- Config allowunannotated=$(git config --bool hooks.allowunannotated) allowdeletebranch=$(git config --bool hooks.allowdeletebranch) denycreatebranch=$(git config --bool hooks.denycreatebranch) allowdeletetag=$(git config --bool hooks.allowdeletetag) allowmodifytag=$(git config --bool hooks.allowmodifytag) # check for no description projectdesc=$(sed -e '1q' "$GIT_DIR/description") case "$projectdesc" in "Unnamed repository"* | "") echo "*** Project description file hasn't been set" >&2 exit 1 ;; esac # --- Check types # if $newrev is 0000...0000, it's a commit to delete a ref. zero="0000000000000000000000000000000000000000" if [ "$newrev" = "$zero" ]; then newrev_type=delete else newrev_type=$(git cat-file -t $newrev) fi case "$refname","$newrev_type" in refs/tags/*,commit) # un-annotated tag short_refname=${refname##refs/tags/} if [ "$allowunannotated" != "true" ]; then echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 exit 1 fi ;; refs/tags/*,delete) # delete tag if [ "$allowdeletetag" != "true" ]; then echo "*** Deleting a tag is not allowed in this repository" >&2 exit 1 fi ;; refs/tags/*,tag) # annotated tag if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 then echo "*** Tag '$refname' already exists." >&2 echo "*** Modifying a tag is not allowed in this repository." >&2 exit 1 fi ;; refs/heads/*,commit) # branch if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then echo "*** Creating a branch is not allowed in this repository" >&2 exit 1 fi ;; refs/heads/*,delete) # delete branch if [ "$allowdeletebranch" != "true" ]; then echo "*** Deleting a branch is not allowed in this repository" >&2 exit 1 fi ;; refs/remotes/*,commit) # tracking branch ;; refs/remotes/*,delete) # delete tracking branch if [ "$allowdeletebranch" != "true" ]; then echo "*** Deleting a tracking branch is not allowed in this repository" >&2 exit 1 fi ;; *) # Anything else (is there anything else?) echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 exit 1 ;; esac # --- Finished exit 0 ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/info/exclude ================================================ # git ls-files --others --exclude-from=.git/info/exclude # Lines that start with '#' are comments. # For a project mostly in C, the following would be a good set of # exclude patterns (uncomment them if you want to use them): # *.[oa] # *~ ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/logs/HEAD ================================================ 0000000000000000000000000000000000000000 d3e073baf592c56614c68ead9e2cd0a3880140cd joshaber 1452185922 -0500 commit (initial): first d3e073baf592c56614c68ead9e2cd0a3880140cd d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 joshaber 1452186239 -0500 commit: submodules ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/logs/refs/heads/master ================================================ 0000000000000000000000000000000000000000 d3e073baf592c56614c68ead9e2cd0a3880140cd joshaber 1452185922 -0500 commit (initial): first d3e073baf592c56614c68ead9e2cd0a3880140cd d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 joshaber 1452186239 -0500 commit: submodules ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/COMMIT_EDITMSG ================================================ whitespace is nicespace # Please enter the commit message for your changes. Lines starting # with '#' will be ignored, and an empty message aborts the commit. # On branch master # Your branch is up-to-date with 'origin/master'. # # Changes to be committed: # modified: README.md # ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/HEAD ================================================ ref: refs/heads/master ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/ORIG_HEAD ================================================ 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/config ================================================ [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true worktree = ../../../You-Dont-Need-jQuery ignorecase = true precomposeunicode = true [remote "origin"] url = https://github.com/oneuijs/You-Dont-Need-jQuery fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/description ================================================ Unnamed repository; edit this file 'description' to name the repository. ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/gitdir ================================================ .git ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/applypatch-msg.sample ================================================ #!/bin/sh # # An example hook script to check the commit log message taken by # applypatch from an e-mail message. # # The hook should exit with non-zero status after issuing an # appropriate message if it wants to stop the commit. The hook is # allowed to edit the commit message file. # # To enable this hook, rename this file to "applypatch-msg". . git-sh-setup commitmsg="$(git rev-parse --git-path hooks/commit-msg)" test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} : ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/commit-msg.sample ================================================ #!/bin/sh # # An example hook script to check the commit log message. # Called by "git commit" with one argument, the name of the file # that has the commit message. The hook should exit with non-zero # status after issuing an appropriate message if it wants to stop the # commit. The hook is allowed to edit the commit message file. # # To enable this hook, rename this file to "commit-msg". # Uncomment the below to add a Signed-off-by line to the message. # Doing this in a hook is a bad idea in general, but the prepare-commit-msg # hook is more suited to it. # # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" # This example catches duplicate Signed-off-by lines. test "" = "$(grep '^Signed-off-by: ' "$1" | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { echo >&2 Duplicate Signed-off-by lines. exit 1 } ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/post-update.sample ================================================ #!/bin/sh # # An example hook script to prepare a packed repository for use over # dumb transports. # # To enable this hook, rename this file to "post-update". exec git update-server-info ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-applypatch.sample ================================================ #!/bin/sh # # An example hook script to verify what is about to be committed # by applypatch from an e-mail message. # # The hook should exit with non-zero status after issuing an # appropriate message if it wants to stop the commit. # # To enable this hook, rename this file to "pre-applypatch". . git-sh-setup precommit="$(git rev-parse --git-path hooks/pre-commit)" test -x "$precommit" && exec "$precommit" ${1+"$@"} : ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-commit.sample ================================================ #!/bin/sh # # An example hook script to verify what is about to be committed. # Called by "git commit" with no arguments. The hook should # exit with non-zero status after issuing an appropriate message if # it wants to stop the commit. # # To enable this hook, rename this file to "pre-commit". if git rev-parse --verify HEAD >/dev/null 2>&1 then against=HEAD else # Initial commit: diff against an empty tree object against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi # If you want to allow non-ASCII filenames set this variable to true. allownonascii=$(git config --bool hooks.allownonascii) # Redirect output to stderr. exec 1>&2 # Cross platform projects tend to avoid non-ASCII filenames; prevent # them from being added to the repository. We exploit the fact that the # printable range starts at the space character and ends with tilde. if [ "$allownonascii" != "true" ] && # Note that the use of brackets around a tr range is ok here, (it's # even required, for portability to Solaris 10's /usr/bin/tr), since # the square bracket bytes happen to fall in the designated range. test $(git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 then cat <<\EOF Error: Attempt to add a non-ASCII file name. This can cause problems if you want to work with people on other platforms. To be portable it is advisable to rename the file. If you know what you are doing you can disable this check using: git config hooks.allownonascii true EOF exit 1 fi # If there are whitespace errors, print the offending file names and fail. exec git diff-index --check --cached $against -- ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-push.sample ================================================ #!/bin/sh # An example hook script to verify what is about to be pushed. Called by "git # push" after it has checked the remote status, but before anything has been # pushed. If this script exits with a non-zero status nothing will be pushed. # # This hook is called with the following parameters: # # $1 -- Name of the remote to which the push is being done # $2 -- URL to which the push is being done # # If pushing without using a named remote those arguments will be equal. # # Information about the commits which are being pushed is supplied as lines to # the standard input in the form: # # # # This sample shows how to prevent push of commits where the log message starts # with "WIP" (work in progress). remote="$1" url="$2" z40=0000000000000000000000000000000000000000 while read local_ref local_sha remote_ref remote_sha do if [ "$local_sha" = $z40 ] then # Handle delete : else if [ "$remote_sha" = $z40 ] then # New branch, examine all commits range="$local_sha" else # Update to existing branch, examine new commits range="$remote_sha..$local_sha" fi # Check for WIP commit commit=`git rev-list -n 1 --grep '^WIP' "$range"` if [ -n "$commit" ] then echo >&2 "Found WIP commit in $local_ref, not pushing" exit 1 fi fi done exit 0 ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/pre-rebase.sample ================================================ #!/bin/sh # # Copyright (c) 2006, 2008 Junio C Hamano # # The "pre-rebase" hook is run just before "git rebase" starts doing # its job, and can prevent the command from running by exiting with # non-zero status. # # The hook is called with the following parameters: # # $1 -- the upstream the series was forked from. # $2 -- the branch being rebased (or empty when rebasing the current branch). # # This sample shows how to prevent topic branches that are already # merged to 'next' branch from getting rebased, because allowing it # would result in rebasing already published history. publish=next basebranch="$1" if test "$#" = 2 then topic="refs/heads/$2" else topic=`git symbolic-ref HEAD` || exit 0 ;# we do not interrupt rebasing detached HEAD fi case "$topic" in refs/heads/??/*) ;; *) exit 0 ;# we do not interrupt others. ;; esac # Now we are dealing with a topic branch being rebased # on top of master. Is it OK to rebase it? # Does the topic really exist? git show-ref -q "$topic" || { echo >&2 "No such branch $topic" exit 1 } # Is topic fully merged to master? not_in_master=`git rev-list --pretty=oneline ^master "$topic"` if test -z "$not_in_master" then echo >&2 "$topic is fully merged to master; better remove it." exit 1 ;# we could allow it, but there is no point. fi # Is topic ever merged to next? If so you should not be rebasing it. only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` only_next_2=`git rev-list ^master ${publish} | sort` if test "$only_next_1" = "$only_next_2" then not_in_topic=`git rev-list "^$topic" master` if test -z "$not_in_topic" then echo >&2 "$topic is already up-to-date with master" exit 1 ;# we could allow it, but there is no point. else exit 0 fi else not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` /usr/bin/perl -e ' my $topic = $ARGV[0]; my $msg = "* $topic has commits already merged to public branch:\n"; my (%not_in_next) = map { /^([0-9a-f]+) /; ($1 => 1); } split(/\n/, $ARGV[1]); for my $elem (map { /^([0-9a-f]+) (.*)$/; [$1 => $2]; } split(/\n/, $ARGV[2])) { if (!exists $not_in_next{$elem->[0]}) { if ($msg) { print STDERR $msg; undef $msg; } print STDERR " $elem->[1]\n"; } } ' "$topic" "$not_in_next" "$not_in_master" exit 1 fi exit 0 ################################################################ This sample hook safeguards topic branches that have been published from being rewound. The workflow assumed here is: * Once a topic branch forks from "master", "master" is never merged into it again (either directly or indirectly). * Once a topic branch is fully cooked and merged into "master", it is deleted. If you need to build on top of it to correct earlier mistakes, a new topic branch is created by forking at the tip of the "master". This is not strictly necessary, but it makes it easier to keep your history simple. * Whenever you need to test or publish your changes to topic branches, merge them into "next" branch. The script, being an example, hardcodes the publish branch name to be "next", but it is trivial to make it configurable via $GIT_DIR/config mechanism. With this workflow, you would want to know: (1) ... if a topic branch has ever been merged to "next". Young topic branches can have stupid mistakes you would rather clean up before publishing, and things that have not been merged into other branches can be easily rebased without affecting other people. But once it is published, you would not want to rewind it. (2) ... if a topic branch has been fully merged to "master". Then you can delete it. More importantly, you should not build on top of it -- other people may already want to change things related to the topic as patches against your "master", so if you need further changes, it is better to fork the topic (perhaps with the same name) afresh from the tip of "master". Let's look at this example: o---o---o---o---o---o---o---o---o---o "next" / / / / / a---a---b A / / / / / / / / c---c---c---c B / / / / \ / / / / b---b C \ / / / / / \ / ---o---o---o---o---o---o---o---o---o---o---o "master" A, B and C are topic branches. * A has one fix since it was merged up to "next". * B has finished. It has been fully merged up to "master" and "next", and is ready to be deleted. * C has not merged to "next" at all. We would want to allow C to be rebased, refuse A, and encourage B to be deleted. To compute (1): git rev-list ^master ^topic next git rev-list ^master next if these match, topic has not merged in next at all. To compute (2): git rev-list master..topic if this is empty, it is fully merged to "master". ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/prepare-commit-msg.sample ================================================ #!/bin/sh # # An example hook script to prepare the commit log message. # Called by "git commit" with the name of the file that has the # commit message, followed by the description of the commit # message's source. The hook's purpose is to edit the commit # message file. If the hook fails with a non-zero status, # the commit is aborted. # # To enable this hook, rename this file to "prepare-commit-msg". # This hook includes three examples. The first comments out the # "Conflicts:" part of a merge commit. # # The second includes the output of "git diff --name-status -r" # into the message, just before the "git status" output. It is # commented because it doesn't cope with --amend or with squashed # commits. # # The third example adds a Signed-off-by line to the message, that can # still be edited. This is rarely a good idea. case "$2,$3" in merge,) /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; # ,|template,) # /usr/bin/perl -i.bak -pe ' # print "\n" . `git diff --cached --name-status -r` # if /^#/ && $first++ == 0' "$1" ;; *) ;; esac # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/hooks/update.sample ================================================ #!/bin/sh # # An example hook script to blocks unannotated tags from entering. # Called by "git receive-pack" with arguments: refname sha1-old sha1-new # # To enable this hook, rename this file to "update". # # Config # ------ # hooks.allowunannotated # This boolean sets whether unannotated tags will be allowed into the # repository. By default they won't be. # hooks.allowdeletetag # This boolean sets whether deleting tags will be allowed in the # repository. By default they won't be. # hooks.allowmodifytag # This boolean sets whether a tag may be modified after creation. By default # it won't be. # hooks.allowdeletebranch # This boolean sets whether deleting branches will be allowed in the # repository. By default they won't be. # hooks.denycreatebranch # This boolean sets whether remotely creating branches will be denied # in the repository. By default this is allowed. # # --- Command line refname="$1" oldrev="$2" newrev="$3" # --- Safety check if [ -z "$GIT_DIR" ]; then echo "Don't run this script from the command line." >&2 echo " (if you want, you could supply GIT_DIR then run" >&2 echo " $0 )" >&2 exit 1 fi if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then echo "usage: $0 " >&2 exit 1 fi # --- Config allowunannotated=$(git config --bool hooks.allowunannotated) allowdeletebranch=$(git config --bool hooks.allowdeletebranch) denycreatebranch=$(git config --bool hooks.denycreatebranch) allowdeletetag=$(git config --bool hooks.allowdeletetag) allowmodifytag=$(git config --bool hooks.allowmodifytag) # check for no description projectdesc=$(sed -e '1q' "$GIT_DIR/description") case "$projectdesc" in "Unnamed repository"* | "") echo "*** Project description file hasn't been set" >&2 exit 1 ;; esac # --- Check types # if $newrev is 0000...0000, it's a commit to delete a ref. zero="0000000000000000000000000000000000000000" if [ "$newrev" = "$zero" ]; then newrev_type=delete else newrev_type=$(git cat-file -t $newrev) fi case "$refname","$newrev_type" in refs/tags/*,commit) # un-annotated tag short_refname=${refname##refs/tags/} if [ "$allowunannotated" != "true" ]; then echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 exit 1 fi ;; refs/tags/*,delete) # delete tag if [ "$allowdeletetag" != "true" ]; then echo "*** Deleting a tag is not allowed in this repository" >&2 exit 1 fi ;; refs/tags/*,tag) # annotated tag if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 then echo "*** Tag '$refname' already exists." >&2 echo "*** Modifying a tag is not allowed in this repository." >&2 exit 1 fi ;; refs/heads/*,commit) # branch if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then echo "*** Creating a branch is not allowed in this repository" >&2 exit 1 fi ;; refs/heads/*,delete) # delete branch if [ "$allowdeletebranch" != "true" ]; then echo "*** Deleting a branch is not allowed in this repository" >&2 exit 1 fi ;; refs/remotes/*,commit) # tracking branch ;; refs/remotes/*,delete) # delete tracking branch if [ "$allowdeletebranch" != "true" ]; then echo "*** Deleting a tracking branch is not allowed in this repository" >&2 exit 1 fi ;; *) # Anything else (is there anything else?) echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 exit 1 ;; esac # --- Finished exit 0 ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/info/exclude ================================================ # git ls-files --others --exclude-from=.git/info/exclude # Lines that start with '#' are comments. # For a project mostly in C, the following would be a good set of # exclude patterns (uncomment them if you want to use them): # *.[oa] # *~ ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/HEAD ================================================ 0000000000000000000000000000000000000000 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a joshaber 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a a78b35a896b890f0a2a4f1f924c5739776415250 joshaber 1452202510 -0500 commit: whitespace is nicespace ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/heads/master ================================================ 0000000000000000000000000000000000000000 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a joshaber 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a a78b35a896b890f0a2a4f1f924c5739776415250 joshaber 1452202510 -0500 commit: whitespace is nicespace ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/logs/refs/remotes/origin/HEAD ================================================ 0000000000000000000000000000000000000000 2e9bbc77d60f20eb462ead5b2ac7405b62b9b90a joshaber 1452186236 -0500 clone: from https://github.com/oneuijs/You-Dont-Need-jQuery ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/You-Dont-Need-jQuery/objects/a7/8b35a896b890f0a2a4f1f924c5739776415250 ================================================ xQ 0D)remDfk#)m73aF\).*$/Signed-off-by: \1/p') # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" # This example catches duplicate Signed-off-by lines. test "" = "$(grep '^Signed-off-by: ' "$1" | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { echo >&2 Duplicate Signed-off-by lines. exit 1 } ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/post-update.sample ================================================ #!/bin/sh # # An example hook script to prepare a packed repository for use over # dumb transports. # # To enable this hook, rename this file to "post-update". exec git update-server-info ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-applypatch.sample ================================================ #!/bin/sh # # An example hook script to verify what is about to be committed # by applypatch from an e-mail message. # # The hook should exit with non-zero status after issuing an # appropriate message if it wants to stop the commit. # # To enable this hook, rename this file to "pre-applypatch". . git-sh-setup precommit="$(git rev-parse --git-path hooks/pre-commit)" test -x "$precommit" && exec "$precommit" ${1+"$@"} : ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-commit.sample ================================================ #!/bin/sh # # An example hook script to verify what is about to be committed. # Called by "git commit" with no arguments. The hook should # exit with non-zero status after issuing an appropriate message if # it wants to stop the commit. # # To enable this hook, rename this file to "pre-commit". if git rev-parse --verify HEAD >/dev/null 2>&1 then against=HEAD else # Initial commit: diff against an empty tree object against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi # If you want to allow non-ASCII filenames set this variable to true. allownonascii=$(git config --bool hooks.allownonascii) # Redirect output to stderr. exec 1>&2 # Cross platform projects tend to avoid non-ASCII filenames; prevent # them from being added to the repository. We exploit the fact that the # printable range starts at the space character and ends with tilde. if [ "$allownonascii" != "true" ] && # Note that the use of brackets around a tr range is ok here, (it's # even required, for portability to Solaris 10's /usr/bin/tr), since # the square bracket bytes happen to fall in the designated range. test $(git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 then cat <<\EOF Error: Attempt to add a non-ASCII file name. This can cause problems if you want to work with people on other platforms. To be portable it is advisable to rename the file. If you know what you are doing you can disable this check using: git config hooks.allownonascii true EOF exit 1 fi # If there are whitespace errors, print the offending file names and fail. exec git diff-index --check --cached $against -- ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-push.sample ================================================ #!/bin/sh # An example hook script to verify what is about to be pushed. Called by "git # push" after it has checked the remote status, but before anything has been # pushed. If this script exits with a non-zero status nothing will be pushed. # # This hook is called with the following parameters: # # $1 -- Name of the remote to which the push is being done # $2 -- URL to which the push is being done # # If pushing without using a named remote those arguments will be equal. # # Information about the commits which are being pushed is supplied as lines to # the standard input in the form: # # # # This sample shows how to prevent push of commits where the log message starts # with "WIP" (work in progress). remote="$1" url="$2" z40=0000000000000000000000000000000000000000 while read local_ref local_sha remote_ref remote_sha do if [ "$local_sha" = $z40 ] then # Handle delete : else if [ "$remote_sha" = $z40 ] then # New branch, examine all commits range="$local_sha" else # Update to existing branch, examine new commits range="$remote_sha..$local_sha" fi # Check for WIP commit commit=`git rev-list -n 1 --grep '^WIP' "$range"` if [ -n "$commit" ] then echo >&2 "Found WIP commit in $local_ref, not pushing" exit 1 fi fi done exit 0 ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/pre-rebase.sample ================================================ #!/bin/sh # # Copyright (c) 2006, 2008 Junio C Hamano # # The "pre-rebase" hook is run just before "git rebase" starts doing # its job, and can prevent the command from running by exiting with # non-zero status. # # The hook is called with the following parameters: # # $1 -- the upstream the series was forked from. # $2 -- the branch being rebased (or empty when rebasing the current branch). # # This sample shows how to prevent topic branches that are already # merged to 'next' branch from getting rebased, because allowing it # would result in rebasing already published history. publish=next basebranch="$1" if test "$#" = 2 then topic="refs/heads/$2" else topic=`git symbolic-ref HEAD` || exit 0 ;# we do not interrupt rebasing detached HEAD fi case "$topic" in refs/heads/??/*) ;; *) exit 0 ;# we do not interrupt others. ;; esac # Now we are dealing with a topic branch being rebased # on top of master. Is it OK to rebase it? # Does the topic really exist? git show-ref -q "$topic" || { echo >&2 "No such branch $topic" exit 1 } # Is topic fully merged to master? not_in_master=`git rev-list --pretty=oneline ^master "$topic"` if test -z "$not_in_master" then echo >&2 "$topic is fully merged to master; better remove it." exit 1 ;# we could allow it, but there is no point. fi # Is topic ever merged to next? If so you should not be rebasing it. only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` only_next_2=`git rev-list ^master ${publish} | sort` if test "$only_next_1" = "$only_next_2" then not_in_topic=`git rev-list "^$topic" master` if test -z "$not_in_topic" then echo >&2 "$topic is already up-to-date with master" exit 1 ;# we could allow it, but there is no point. else exit 0 fi else not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` /usr/bin/perl -e ' my $topic = $ARGV[0]; my $msg = "* $topic has commits already merged to public branch:\n"; my (%not_in_next) = map { /^([0-9a-f]+) /; ($1 => 1); } split(/\n/, $ARGV[1]); for my $elem (map { /^([0-9a-f]+) (.*)$/; [$1 => $2]; } split(/\n/, $ARGV[2])) { if (!exists $not_in_next{$elem->[0]}) { if ($msg) { print STDERR $msg; undef $msg; } print STDERR " $elem->[1]\n"; } } ' "$topic" "$not_in_next" "$not_in_master" exit 1 fi exit 0 ################################################################ This sample hook safeguards topic branches that have been published from being rewound. The workflow assumed here is: * Once a topic branch forks from "master", "master" is never merged into it again (either directly or indirectly). * Once a topic branch is fully cooked and merged into "master", it is deleted. If you need to build on top of it to correct earlier mistakes, a new topic branch is created by forking at the tip of the "master". This is not strictly necessary, but it makes it easier to keep your history simple. * Whenever you need to test or publish your changes to topic branches, merge them into "next" branch. The script, being an example, hardcodes the publish branch name to be "next", but it is trivial to make it configurable via $GIT_DIR/config mechanism. With this workflow, you would want to know: (1) ... if a topic branch has ever been merged to "next". Young topic branches can have stupid mistakes you would rather clean up before publishing, and things that have not been merged into other branches can be easily rebased without affecting other people. But once it is published, you would not want to rewind it. (2) ... if a topic branch has been fully merged to "master". Then you can delete it. More importantly, you should not build on top of it -- other people may already want to change things related to the topic as patches against your "master", so if you need further changes, it is better to fork the topic (perhaps with the same name) afresh from the tip of "master". Let's look at this example: o---o---o---o---o---o---o---o---o---o "next" / / / / / a---a---b A / / / / / / / / c---c---c---c B / / / / \ / / / / b---b C \ / / / / / \ / ---o---o---o---o---o---o---o---o---o---o---o "master" A, B and C are topic branches. * A has one fix since it was merged up to "next". * B has finished. It has been fully merged up to "master" and "next", and is ready to be deleted. * C has not merged to "next" at all. We would want to allow C to be rebased, refuse A, and encourage B to be deleted. To compute (1): git rev-list ^master ^topic next git rev-list ^master next if these match, topic has not merged in next at all. To compute (2): git rev-list master..topic if this is empty, it is fully merged to "master". ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/prepare-commit-msg.sample ================================================ #!/bin/sh # # An example hook script to prepare the commit log message. # Called by "git commit" with the name of the file that has the # commit message, followed by the description of the commit # message's source. The hook's purpose is to edit the commit # message file. If the hook fails with a non-zero status, # the commit is aborted. # # To enable this hook, rename this file to "prepare-commit-msg". # This hook includes three examples. The first comments out the # "Conflicts:" part of a merge commit. # # The second includes the output of "git diff --name-status -r" # into the message, just before the "git status" output. It is # commented because it doesn't cope with --amend or with squashed # commits. # # The third example adds a Signed-off-by line to the message, that can # still be edited. This is rarely a good idea. case "$2,$3" in merge,) /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; # ,|template,) # /usr/bin/perl -i.bak -pe ' # print "\n" . `git diff --cached --name-status -r` # if /^#/ && $first++ == 0' "$1" ;; *) ;; esac # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/hooks/update.sample ================================================ #!/bin/sh # # An example hook script to blocks unannotated tags from entering. # Called by "git receive-pack" with arguments: refname sha1-old sha1-new # # To enable this hook, rename this file to "update". # # Config # ------ # hooks.allowunannotated # This boolean sets whether unannotated tags will be allowed into the # repository. By default they won't be. # hooks.allowdeletetag # This boolean sets whether deleting tags will be allowed in the # repository. By default they won't be. # hooks.allowmodifytag # This boolean sets whether a tag may be modified after creation. By default # it won't be. # hooks.allowdeletebranch # This boolean sets whether deleting branches will be allowed in the # repository. By default they won't be. # hooks.denycreatebranch # This boolean sets whether remotely creating branches will be denied # in the repository. By default this is allowed. # # --- Command line refname="$1" oldrev="$2" newrev="$3" # --- Safety check if [ -z "$GIT_DIR" ]; then echo "Don't run this script from the command line." >&2 echo " (if you want, you could supply GIT_DIR then run" >&2 echo " $0 )" >&2 exit 1 fi if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then echo "usage: $0 " >&2 exit 1 fi # --- Config allowunannotated=$(git config --bool hooks.allowunannotated) allowdeletebranch=$(git config --bool hooks.allowdeletebranch) denycreatebranch=$(git config --bool hooks.denycreatebranch) allowdeletetag=$(git config --bool hooks.allowdeletetag) allowmodifytag=$(git config --bool hooks.allowmodifytag) # check for no description projectdesc=$(sed -e '1q' "$GIT_DIR/description") case "$projectdesc" in "Unnamed repository"* | "") echo "*** Project description file hasn't been set" >&2 exit 1 ;; esac # --- Check types # if $newrev is 0000...0000, it's a commit to delete a ref. zero="0000000000000000000000000000000000000000" if [ "$newrev" = "$zero" ]; then newrev_type=delete else newrev_type=$(git cat-file -t $newrev) fi case "$refname","$newrev_type" in refs/tags/*,commit) # un-annotated tag short_refname=${refname##refs/tags/} if [ "$allowunannotated" != "true" ]; then echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 exit 1 fi ;; refs/tags/*,delete) # delete tag if [ "$allowdeletetag" != "true" ]; then echo "*** Deleting a tag is not allowed in this repository" >&2 exit 1 fi ;; refs/tags/*,tag) # annotated tag if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 then echo "*** Tag '$refname' already exists." >&2 echo "*** Modifying a tag is not allowed in this repository." >&2 exit 1 fi ;; refs/heads/*,commit) # branch if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then echo "*** Creating a branch is not allowed in this repository" >&2 exit 1 fi ;; refs/heads/*,delete) # delete branch if [ "$allowdeletebranch" != "true" ]; then echo "*** Deleting a branch is not allowed in this repository" >&2 exit 1 fi ;; refs/remotes/*,commit) # tracking branch ;; refs/remotes/*,delete) # delete tracking branch if [ "$allowdeletebranch" != "true" ]; then echo "*** Deleting a tracking branch is not allowed in this repository" >&2 exit 1 fi ;; *) # Anything else (is there anything else?) echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 exit 1 ;; esac # --- Finished exit 0 ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/info/exclude ================================================ # git ls-files --others --exclude-from=.git/info/exclude # Lines that start with '#' are comments. # For a project mostly in C, the following would be a good set of # exclude patterns (uncomment them if you want to use them): # *.[oa] # *~ ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/HEAD ================================================ 0000000000000000000000000000000000000000 9f0218b7652b622afea799a4723490c43e1af1fe joshaber 1452186187 -0500 clone: from https://github.com/loverajoel/jstips 9f0218b7652b622afea799a4723490c43e1af1fe 9f0218b7652b622afea799a4723490c43e1af1fe joshaber 1452201852 -0500 checkout: moving from master to test 9f0218b7652b622afea799a4723490c43e1af1fe 0525ef667328cb1f86b1ddf523db4a064e1590fa joshaber 1452201881 -0500 commit: whitespace is nicespace ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/refs/heads/master ================================================ 0000000000000000000000000000000000000000 9f0218b7652b622afea799a4723490c43e1af1fe joshaber 1452186187 -0500 clone: from https://github.com/loverajoel/jstips ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/refs/heads/test ================================================ 0000000000000000000000000000000000000000 9f0218b7652b622afea799a4723490c43e1af1fe joshaber 1452201852 -0500 branch: Created from HEAD 9f0218b7652b622afea799a4723490c43e1af1fe 0525ef667328cb1f86b1ddf523db4a064e1590fa joshaber 1452201881 -0500 commit: whitespace is nicespace ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/logs/refs/remotes/origin/HEAD ================================================ 0000000000000000000000000000000000000000 9f0218b7652b622afea799a4723490c43e1af1fe joshaber 1452186187 -0500 clone: from https://github.com/loverajoel/jstips ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/objects/05/25ef667328cb1f86b1ddf523db4a064e1590fa ================================================ x] }{ ,?bliZחݷ7IuK6fQ`u8h2RVlI dY2 2͊,ݦEzc:@( }K{gQ|x_)1~F|QBI ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/packed-refs ================================================ # pack-refs with: peeled fully-peeled 9f0218b7652b622afea799a4723490c43e1af1fe refs/remotes/origin/master ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/refs/heads/master ================================================ 9f0218b7652b622afea799a4723490c43e1af1fe ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/refs/heads/test ================================================ 0525ef667328cb1f86b1ddf523db4a064e1590fa ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/modules/jstips/refs/remotes/origin/HEAD ================================================ ref: refs/remotes/origin/master ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/objects/d3/e073baf592c56614c68ead9e2cd0a3880140cd ================================================ xA E]sJbL0h1MݽZe2z@r̒< %Ljzm] $i+olç[q|o'GhIk5י?G*}5 ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/refs/heads/master ================================================ d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 ================================================ FILE: spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master ================================================ d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5 ================================================ FILE: spec/fixtures/git/repo-with-submodules/jstips/CONTRIBUTING.md ================================================ # How to submit your tip To submit a tip to the list, fork the repository and add your tip to the top of the list of tips in the `README.md` file. You may want to use a [topic branch](https://github.com/dchelimsky/rspec/wiki/Topic-Branches) for each tip. Use the format below when writing your tip. Your tip should be readable in less than two minutes. You may add links to other sites or videos that give more insight if you wish. Once your tip is ready, [issue a pull request](https://help.github.com/articles/using-pull-requests/) and your tip will be reviewed. Every day one tip will be merged from the available pull requests. # Tip format ## #01(number) - Title > yyyy-mm-dd(date) by @username This is my awesome tip! # Notes Leave the date and the tip number empty. When we decide merge the pull request you will add them and squash your commits. Remember: New tips must be added to the *top* of the list of [tips](https://github.com/loverajoel/jstips#tips-list) in the `README.md` file. ================================================ FILE: spec/fixtures/git/repo-with-submodules/jstips/README.md ================================================ ![header](https://raw.githubusercontent.com/loverajoel/jstips/master/resources/jstips-header-blog.gif) # Introducing Javascript Tips > New year, new project. **A JS tip per day!** With great excitement, I introduce these short and useful daily Javascript tips that will allow you to improve your code writing. With less than 2 minutes each day, you will be able to read about performance, frameworks, conventions, hacks, interview questions and all the items that the future of this awesome language holds for us. At midday, no matter if it is a weekend or a holiday, a tip will be posted and tweeted. ### Can you help us enrich it? Please feel free to send us a PR with your own Javascript tip to be published here. Any improvements or suggestions are more than welcome! [Click to see the instructions](https://github.com/loverajoel/jstips/blob/master/CONTRIBUTING.md) ### Let’s keep in touch To get updates, watch the repo and follow the [Twitter account](https://twitter.com/tips_js), only one tweet will be sent per day. It is a deal! > Don't forget to Star the repo, as this will help to promote the project! # Tips list ## #06 - Writing a single method for arrays or single elements > 2016-01-06 by [@mattfxyz](https://twitter.com/mattfxyz) Rather than writing separate methods to handle an array and a single element parameter, write your functions so they can handle both. This is similar to how some of jQuery's functions work (`css` will modify everything matched by the selector). You just have to concat everything into an array first. `Array.concat` will accept an array or a single element. ```javascript function printUpperCase(words) { var elements = [].concat(words); for (var i = 0; i < elements.length; i++) { console.log(elements[i].toUpperCase()); } } ``` `printUpperCase` is now ready to accept a single node or an array of nodes as it's parameter. ```javascript printUpperCase("cactus"); // => CACTUS printUpperCase(["cactus", "bear", "potato"]); // => CACTUS // BEAR // POTATO ``` ## #05 - Differences between `undefined` and `null` > 2016-01-05 by [@loverajoel](https://twitter.com/loverajoel) - `undefined` means a variable has not been declared, or has been declared but has not yet been assigned a value - `null` is an assignment value that means "no value" - Javascript sets unassigned variables with a default value of `undefined` - Javascript never sets a value to `null`. It is used by programmers to indicate that a `var` has no value. - `undefined` is not valid in JSON while `null` is - `undefined` typeof is `undefined` - `null` typeof is an `object` - Both are primitives - You can know if a variable is [undefined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined) ```javascript typeof variable === "undefined" ``` - You can check if a variable is [null](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null) ```javascript variable === null ``` - The **equality** operator considers them equal, but the **identity** doesn't ```javascript null == undefined // true null === undefined // false ``` ## #04 - Sorting strings with accented characters > 2016-01-04 by [@loverajoel](https://twitter.com/loverajoel) Javascript has a native method **[sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)** that allows sorting arrays. Doing a simple `array.sort()` will treat each array entry as a string and sort it alphabetically. Also you can provide your [own custom sorting](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters) function. ```javascript ['Shanghai', 'New York', 'Mumbai', 'Buenos Aires'].sort(); // ["Buenos Aires", "Mumbai", "New York", "Shanghai"] ``` But when you try order an array of non ASCII characters like this `['é', 'a', 'ú', 'c']`, you will obtain a strange result `['c', 'e', 'á', 'ú']`. That happens because sort works only with english language. See the next example: ```javascript // Spanish ['único','árbol', 'cosas', 'fútbol'].sort(); // ["cosas", "fútbol", "árbol", "único"] // bad order // German ['Woche', 'wöchentlich', 'wäre', 'Wann'].sort(); // ["Wann", "Woche", "wäre", "wöchentlich"] // bad order ``` Fortunately, there are two ways to overcome this behavior [localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) and [Intl.Collator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator) provided by ECMAScript Internationalization API. > Both methods have their own custom parameters in order to configure it to work adequately. ### Using `localeCompare()` ```javascript ['único','árbol', 'cosas', 'fútbol'].sort(function (a, b) { return a.localeCompare(b); }); // ["árbol", "cosas", "fútbol", "único"] ['Woche', 'wöchentlich', 'wäre', 'Wann'].sort(function (a, b) { return a.localeCompare(b); }); // ["Wann", "wäre", "Woche", "wöchentlich"] ``` ### Using `Intl.Collator()` ```javascript ['único','árbol', 'cosas', 'fútbol'].sort(Intl.Collator().compare); // ["árbol", "cosas", "fútbol", "único"] ['Woche', 'wöchentlich', 'wäre', 'Wann'].sort(Intl.Collator().compare); // ["Wann", "wäre", "Woche", "wöchentlich"] ``` - For each method you can customize the location. - According to [Firefox](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare#Performance) Intl.Collator is faster when comparing large numbers of strings. So when you are working with arrays of strings in a language other than English, remember to use this method to avoid unexpected sorting. ## #03 - Improve Nested Conditionals > 2016-01-03 by [AlbertoFuente](https://github.com/AlbertoFuente) How can we improve and make more efficient nested `if` statement on javascript. ```javascript if (color) { if (color === 'black') { printBlackBackground(); } else if (color === 'red') { printRedBackground(); } else if (color === 'blue') { printBlueBackground(); } else if (color === 'green') { printGreenBackground(); } else { printYellowBackground(); } } ``` One way to improve the nested `if` statement would be using the `switch` statement. Although it is less verbose and is more ordered, It's not recommended to use it because it's so difficult to debug errors, here's [why](https://toddmotto.com/deprecating-the-switch-statement-for-object-literals/). ```javascript switch(color) { case 'black': printBlackBackground(); break; case 'red': printRedBackground(); break; case 'blue': printBlueBackground(); break; case 'green': printGreenBackground(); break; default: printYellowBackground(); } ``` But what if we have a conditional with several checks in each statement? In this case, if we like to do less verbose and more ordered, we can use the conditional `switch`. If we pass `true` as parameter to the `switch` statement, It allows us to put a conditional in each case. ```javascript switch(true) { case (typeof color === 'string' && color === 'black'): printBlackBackground(); break; case (typeof color === 'string' && color === 'red'): printRedBackground(); break; case (typeof color === 'string' && color === 'blue'): printBlueBackground(); break; case (typeof color === 'string' && color === 'green'): printGreenBackground(); break; case (typeof color === 'string' && color === 'yellow'): printYellowBackground(); break; } ``` But we must always avoid having several checks in every condition, avoiding use of `switch` as far as possible and take into account that the most efficient way to do this is through an `object`. ```javascript var colorObj = { 'black': printBlackBackground, 'red': printRedBackground, 'blue': printBlueBackground, 'green': printGreenBackground, 'yellow': printYellowBackground }; if (color && colorObj.hasOwnProperty(color)) { colorObj[color](); } ``` Here you can find more information about [this](http://www.nicoespeon.com/en/2015/01/oop-revisited-switch-in-js/). ## #02 - ReactJs - Keys in children components are important > 2016-01-02 by [@loverajoel](https://twitter.com/loverajoel) The [key](https://facebook.github.io/react/docs/multiple-components.html#dynamic-children) is an attribute that you must pass to all components created dynamically from an array. It's unique and constant id that React use for identify each component in the DOM and know that it's a different component and not the same one. Using keys will ensure that the child component is preserved and not recreated and prevent that weird things happens. > Key is not really about performance, it's more about identity (which in turn leads to better performance). randomly assigned and changing values are not identity [Paul O’Shannessy](https://github.com/facebook/react/issues/1342#issuecomment-39230939) - Use an exisiting unique value of the object. - Define the keys in the parent components, not in child components ```javascript //bad ... render() {
          {{item.name}}
          } ... //good ``` - [Use the array index is a bad practice.](https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318#.76co046o9) - `random()` will not work ```javascript //bad ``` - You can create your own unique id, be sure that the method be fast and attach it to your object. - When the amount of child are big or involve expensive components, use keys has performance improvements. - [You must provide the key attribute for all children of ReactCSSTransitionGroup.](http://docs.reactjs-china.com/react/docs/animation.html) ## #1 - AngularJs: `$digest` vs `$apply` > 2016-01-01 by [@loverajoel](https://twitter.com/loverajoel) One of the most appreciated features of AngularJs is the two way data binding. In order to make this work AngularJs evaluate the changes between the model and the view through of cycles(`$digest`). You need to understand this concept in order to understand how the framework works under the hood. Angular evaluate each watcher whenever one event was fired, this is the known `$digest` cycle. Sometimes you have to force to run a new cycle manually and you must choose the correct option because this phase is one of the most influential in terms of performance. ### `$apply` This core method lets you to start the digestion cycle explicitly, that means that all watchers are checked, the entire application starts the `$digest loop`. Internally after execute an optional function parameter, call internally to `$rootScope.$digest();`. ### `$digest` In this case the `$digest` method starts the `$digest` cycle for the current scope and its children. You should notice that the parents scopes will not be checked and not be affected. ### Recommendations - Use `$apply` or `$digest` only when browser DOM events have triggered outside of AngularJS. - Pass a function expression to `$apply`, this have a error handling mechanism and allow integrate changes in the digest cycle ```javascript $scope.$apply(() => { $scope.tip = 'Javascript Tip'; }); ``` - If only needs update the current scope or its children use `$digest`, and prevent a new digest cycle for the whole application. The performance benefit it's self evident - `$apply()` is hard process for the machine and can lead to performance issues when having a lot of binding. - If you are using >AngularJS 1.2.X, use `$evalAsync` is a core method that will evaluate the expression during the current cycle or the next. This can improve your application's performance. ## #0 - Insert item inside an Array > 2015-12-29 Insert an item into an existing array is a daily common task. You can add elements to the end of an array using push, to the beginning using unshift, or the middle using splice. But those are known methods, doesn't mean there isn't a more performant way, here we go... Add a element at the end of the array is easy with push(), but there is a way more performant. ```javascript var arr = [1,2,3,4,5]; arr.push(6); arr[arr.length] = 6; // 43% faster in Chrome 47.0.2526.106 on macOS 10.11.1 ``` Both methods modify the original array. Don't believe me? Check the [jsperf](http://jsperf.com/push-item-inside-an-array) Now we are trying to add an item to the beginning of the array ```javascript var arr = [1,2,3,4,5]; arr.unshift(0); [0].concat(arr); // 98% faster in Chrome 47.0.2526.106 on macOS 10.11.1 ``` Here is a little bit detail, unshift edit the original array, concat return a new array. [jsperf](http://jsperf.com/unshift-item-inside-an-array) Add items at the middle of an array is easy with splice and is the most performant way to do it. ```javascript var items = ['one', 'two', 'three', 'four']; items.splice(items.length / 2, 0, 'hello'); ``` I tried to run these tests in various Browsers and OS and the results were similar. I hope this tips will be useful for you and encourage to perform your own tests! ### License [![CC0](http://i.creativecommons.org/p/zero/1.0/88x31.png)](http://creativecommons.org/publicdomain/zero/1.0/) ================================================ FILE: spec/fixtures/git/repo-with-submodules/jstips/git.git ================================================ gitdir: ../.git/modules/jstips ================================================ FILE: spec/fixtures/git/repo-with-submodules/jstips/resources/log.js ================================================ (() => { console.log('Hello world'); })(); ================================================ FILE: spec/fixtures/git/working-dir/.gitignore ================================================ poop ignored.txt ================================================ FILE: spec/fixtures/git/working-dir/a.txt ================================================ ================================================ FILE: spec/fixtures/git/working-dir/git.git/HEAD ================================================ ref: refs/heads/master ================================================ FILE: spec/fixtures/git/working-dir/git.git/config ================================================ [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true ================================================ FILE: spec/fixtures/git/working-dir/git.git/objects/8a/9c86f1cb1f14b8f436eb91f4b052c8802ca99e ================================================ xInB1)σf vbcQne=, ķ9r&V!quH.)i6Ί|tu޺}-(+lUΉf_pUQIcxˀxo`wz}~ulmY(+QJ vN1iHL ================================================ FILE: spec/fixtures/git/working-dir/git.git/refs/heads/master ================================================ 8a9c86f1cb1f14b8f436eb91f4b052c8802ca99e ================================================ FILE: spec/fixtures/indentation/classes.js ================================================ class MyClass extends OtherComponent { state = { test: 1 } constructor() { test(); } otherfunction = (a, b = { default: false }) => { more(); } } ================================================ FILE: spec/fixtures/indentation/expressions.js ================================================ /* multi-line expressions */ req .shouldBeOne(); too. more. shouldBeOneToo; const a = long_expression; b = long; b = 3 + 5; b = 3 + 5; b = 3 + 5 + 7 + 8 * 8 * 9 / 17 * 8 / 20 - 34 + 3 * 9 - 8; ifthis && thendo() || otherwise && dothis /** A comment, should be at 1 */ ================================================ FILE: spec/fixtures/indentation/function_call.js ================================================ foo({ sd, sdf }, 4 ); foo( 2, { sd, sdf }, 4 ); foo( 2, { sd, sdf }); foo( 2, { sd, sdf }); foo(2, 4); foo({ symetric_opening_and_closing_scopes: 'indent me at 1' }); foo(myWrapper(mysecondWrapper({ a: 1 // should be at 1 }))); ================================================ FILE: spec/fixtures/indentation/if_then_else.js ================================================ /** if-then-else loops */ if (true) foo(); else bar(); if (true) { foo(); bar(); } else { foo(); } // https://github.com/atom/atom/issues/6691 if (true) { foo(); bar(); } else { foo(); } if (true) { if (yes) doit(); // 2 bar(); } else if (more()) { foo(); // 1 } if (true) foo(); else if (more()) { // 1 foo(); // 1 } if (true) foo(); else if (more()) // 1 foo(); // 2 if (we ()) { go(); } if (true) { foo(); bar(); } else if (false) { more(); } else { foo(); } ================================================ FILE: spec/fixtures/indentation/jsx.jsx ================================================ /** JSX */ const jsx = (
          good link sdfg
          sdf
          ); const two = (
          test test
          ); const a = ( ); const b = ( ); const two = (
          { test && 'test' }
          ); ================================================ FILE: spec/fixtures/indentation/objects_and_array.js ================================================ var x = [ 3, 4 ]; const y = [ 1 ]; const j = [{ a: 1 }]; let h = { a: [ 1, 2 ], b: { j: [ { l: 1 }] }, c: { j: [ { l: 1 }] }, }; const a = { b: 1 }; const x = { g: { a: 1, b: 2 }, h: { c: 3 } } ================================================ FILE: spec/fixtures/indentation/switch.js ================================================ switch (e) { case 5: something(); more(); case 6: somethingElse(); case 7: default: done(); } ================================================ FILE: spec/fixtures/indentation/while.js ================================================ /** While loops */ while (condition) inLoop(); while (condition) inLoop(); after(); while (mycondition) { sdfsdfg(); } while (mycondition) { sdfsdfg(); } while (mycond) if (more) doit; after(); while (mycond) if (more) doit; after(); while (mycondition) { sdfsdfg(); if (test) { more() }} while (mycondition) if (test) { more() } ================================================ FILE: spec/fixtures/lorem.txt ================================================ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ultricies nulla id nibh aliquam, vitae euismod ipsum scelerisque. Vestibulum vulputate facilisis nisi, eu rhoncus turpis pretium ut. Curabitur facilisis urna in diam efficitur, vel maximus tellus consectetur. Suspendisse pulvinar felis sed metus tristique, a posuere dui suscipit. Ut vehicula, tellus ac blandit consequat, libero dui hendrerit elit, non pretium metus odio sed dolor. Vivamus quis volutpat ipsum. In convallis magna nec nunc tristique malesuada. Sed sed hendrerit lacus. Etiam arcu dui, consequat vel neque vitae, iaculis egestas justo. Donec lacinia odio nulla, condimentum porta erat accumsan at. Nunc vulputate nulla vel nunc fermentum egestas. Duis ultricies libero elit, nec facilisis mi rhoncus ornare. Aliquam aliquet libero vitae arcu porttitor mattis. Vestibulum ultricies consectetur arcu, non gravida magna eleifend vel. Phasellus varius mattis ultricies. Vestibulum placerat lacus non consectetur fringilla. Duis congue, arcu iaculis vehicula hendrerit, purus odio faucibus ipsum, et fermentum massa tellus euismod nulla. Vivamus pellentesque blandit massa, sit amet hendrerit turpis congue eu. Suspendisse diam dui, vestibulum nec semper varius, maximus eu nunc. Vivamus facilisis pulvinar viverra. Praesent luctus lectus id est porttitor volutpat. Suspendisse est augue, mattis a tincidunt id, condimentum in turpis. Curabitur at erat commodo orci interdum tincidunt. Sed sodales elit odio, a placerat ipsum luctus nec. Sed maximus, justo ut pharetra pellentesque, orci mi faucibus enim, quis viverra arcu dui sed nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent quis velit libero. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus a rutrum tortor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce bibendum odio et neque vestibulum rutrum. Vestibulum commodo, nibh non sodales lobortis, dui ex consectetur leo, a finibus libero lectus ac diam. Etiam dui nunc, bibendum a tempor vel, vestibulum lacinia neque. Mauris consectetur odio sit amet maximus pretium. Sed rutrum nunc at ante ullamcorper fermentum. Proin at quam a mauris pellentesque viverra. Nunc pretium pulvinar ipsum. Vestibulum eu nibh ut ex gravida tempus. Praesent ut elit ut ligula tristique dapibus ut sit amet leo. Proin non molestie erat. ================================================ FILE: spec/fixtures/module-cache/file.json ================================================ { "foo": "bar" } ================================================ FILE: spec/fixtures/native-cache/file-1.js ================================================ module.exports = function () { return 1; } ================================================ FILE: spec/fixtures/native-cache/file-2.js ================================================ module.exports = function () { return 2; } ================================================ FILE: spec/fixtures/native-cache/file-3.js ================================================ module.exports = function () { return 3; } ================================================ FILE: spec/fixtures/native-cache/file-4.js ================================================ module.exports = function () { return "file-4" } ================================================ FILE: spec/fixtures/packages/folder/package-symlinked/package.json ================================================ { "name": "package-symlinked", "version": "0.1.0" } ================================================ FILE: spec/fixtures/packages/package-that-throws-an-exception/index.coffee ================================================ throw new Error("This package throws an exception") ================================================ FILE: spec/fixtures/packages/package-that-throws-on-activate/index.coffee ================================================ module.exports = activate: -> throw new Error('Top that') deactivate: -> serialize: -> ================================================ FILE: spec/fixtures/packages/package-that-throws-on-deactivate/index.coffee ================================================ module.exports = activate: -> deactivate: -> throw new Error('Top that') serialize: -> ================================================ FILE: spec/fixtures/packages/package-with-activation-commands/index.coffee ================================================ module.exports = activateCallCount: 0 activationCommandCallCount: 0 activate: -> @activateCallCount++ atom.commands.add 'atom-workspace', 'activation-command', => @activationCommandCallCount++ ================================================ FILE: spec/fixtures/packages/package-with-activation-commands/package.cson ================================================ 'activationCommands': '.workspace': 'activation-command' ================================================ FILE: spec/fixtures/packages/package-with-activation-commands-and-deserializers/index.js ================================================ module.exports = { activateCallCount: 0, activationCommandCallCount: 0, initialize() {}, activate () { this.activateCallCount++ atom.commands.add('atom-workspace', 'activation-command-2', () => this.activationCommandCallCount++) }, deserializeMethod1 (state) { return { wasDeserializedBy: 'deserializeMethod1', state: state } }, deserializeMethod2 (state) { return { wasDeserializedBy: 'deserializeMethod2', state: state } } } ================================================ FILE: spec/fixtures/packages/package-with-activation-commands-and-deserializers/package.json ================================================ { "name": "package-with-activation-commands-and-deserializers", "version": "1.0.0", "main": "./index", "activationCommands": { "atom-workspace": [ "activation-command-2" ] }, "deserializers": { "Deserializer1": "deserializeMethod1", "Deserializer2": "deserializeMethod2" } } ================================================ FILE: spec/fixtures/packages/package-with-activation-hooks/index.coffee ================================================ module.exports = activateCallCount: 0 activate: -> @activateCallCount++ ================================================ FILE: spec/fixtures/packages/package-with-activation-hooks/package.cson ================================================ { "name": "package-with-activation-hooks", "version": "0.1.0", "activationHooks": ['language-fictitious:grammar-used'] } ================================================ FILE: spec/fixtures/packages/package-with-broken-keymap/keymaps/broken.json ================================================ INVALID ================================================ FILE: spec/fixtures/packages/package-with-broken-package-json/package.json ================================================ INVALID ================================================ FILE: spec/fixtures/packages/package-with-cached-incompatible-native-module/main.js ================================================ ================================================ FILE: spec/fixtures/packages/package-with-cached-incompatible-native-module/package.json ================================================ { "name": "package-with-cached-incompatible-native-module", "version": "1.0.0", "main": "./main.js", "_atomModuleCache": { "extensions": { ".node": [ "node_modules/native-module/build/Release/native.node" ] } } } ================================================ FILE: spec/fixtures/packages/package-with-config-defaults/index.coffee ================================================ module.exports = configDefaults: numbers: { one: 1, two: 2 } activate: -> # no-op ================================================ FILE: spec/fixtures/packages/package-with-config-schema/index.coffee ================================================ module.exports = config: numbers: type: 'object' properties: one: type: 'integer' default: 1 two: type: 'integer' default: 2 activate: -> # no-op ================================================ FILE: spec/fixtures/packages/package-with-consumed-services/index.coffee ================================================ module.exports = activate: -> deactivate: -> consumeFirstServiceV3: -> consumeFirstServiceV4: -> consumeSecondService: -> ================================================ FILE: spec/fixtures/packages/package-with-consumed-services/package.json ================================================ { "name": "package-with-consumed-services", "consumedServices": { "service-1": { "versions": { ">=0.2 <=0.3.6": "consumeFirstServiceV3", "^0.4.1": "consumeFirstServiceV4" } }, "service-2": { "versions": { "0.2.1 || 0.2.2": "consumeSecondService" } } } } ================================================ FILE: spec/fixtures/packages/package-with-deactivate/index.coffee ================================================ module.exports = activate: -> deactivate: -> ================================================ FILE: spec/fixtures/packages/package-with-deprecated-pane-item-method/index.coffee ================================================ class TestItem getUri: -> "test" exports.activate = -> atom.workspace.addOpener -> new TestItem ================================================ FILE: spec/fixtures/packages/package-with-deserializers/index.js ================================================ module.exports = { initialize() {}, activate () {}, deserializeMethod1 (state) { return { wasDeserializedBy: 'deserializeMethod1', state: state } }, deserializeMethod2 (state) { return { wasDeserializedBy: 'deserializeMethod2', state: state } } } ================================================ FILE: spec/fixtures/packages/package-with-deserializers/package.json ================================================ { "name": "package-with-deserializers", "version": "1.0.0", "main": "./index", "deserializers": { "Deserializer1": "deserializeMethod1", "Deserializer2": "deserializeMethod2" } } ================================================ FILE: spec/fixtures/packages/package-with-different-directory-name/package.json ================================================ { "name": "package-with-a-totally-different-name", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-directory-provider/index.js ================================================ 'use strict' class FakeRemoteDirectory { constructor (uri) { this.uri = uri } relativize (uri) { return uri } getPath () { return this.uri } isRoot () { return true } getSubdirectory () { return { existsSync () { return false } } } existsSync () { return true } contains () { return false } } exports.provideDirectoryProvider = function () { return { name: 'directory provider from package-with-directory-provider', directoryForURISync (uri) { if (uri.startsWith('remote://')) { return new FakeRemoteDirectory(uri) } } } } ================================================ FILE: spec/fixtures/packages/package-with-directory-provider/package.json ================================================ { "name": "package-with-directory-provider", "providedServices": { "atom.directory-provider": { "description": "Provides custom Directory instances", "versions": { "0.1.1": "provideDirectoryProvider" } } } } ================================================ FILE: spec/fixtures/packages/package-with-empty-activation-commands/index.coffee ================================================ module.exports = activate: -> ================================================ FILE: spec/fixtures/packages/package-with-empty-activation-commands/package.json ================================================ { "name": "package-with-empty-activation-commands", "version": "0.1.0", "activationCommands": {"atom-workspace": []} } ================================================ FILE: spec/fixtures/packages/package-with-empty-activation-hooks/index.coffee ================================================ module.exports = activate: -> ================================================ FILE: spec/fixtures/packages/package-with-empty-activation-hooks/package.json ================================================ { "name": "package-with-empty-activation-hooks", "version": "0.1.0", "activationHooks": [] } ================================================ FILE: spec/fixtures/packages/package-with-empty-keymap/keymaps/keymap.cson ================================================ ================================================ FILE: spec/fixtures/packages/package-with-empty-keymap/package.json ================================================ { "name": "package-with-empty-keymap", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-empty-menu/menus/menu.cson ================================================ ================================================ FILE: spec/fixtures/packages/package-with-empty-menu/package.json ================================================ { "name": "package-with-empty-menu", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-empty-workspace-openers/index.coffee ================================================ module.exports = activate: -> ================================================ FILE: spec/fixtures/packages/package-with-empty-workspace-openers/package.json ================================================ { "name": "package-with-empty-workspace-openers", "version": "0.1.0", "workspaceOpeners": [] } ================================================ FILE: spec/fixtures/packages/package-with-eval-time-api-calls/index.js ================================================ atom.deserializers.add('MyDeserializer', function (state) { return {state: state, a: 'b'} }) exports.activate = function () {} ================================================ FILE: spec/fixtures/packages/package-with-eval-time-api-calls/package.json ================================================ { "name": "package-with-eval-time-api-calls", "version": "1.2.3", "main": "./index" } ================================================ FILE: spec/fixtures/packages/package-with-grammars/grammars/alittle.cson ================================================ 'fileTypes': ['alittle'] 'name': 'Alittle' 'scopeName': 'source.alittle' 'patterns': [ { 'captures': '0': 'name': 'keyword.alittle' 'match': 'alittle' } ] ================================================ FILE: spec/fixtures/packages/package-with-grammars/grammars/alot.cson ================================================ 'fileTypes': ['alot', 'foobizbang'] 'name': 'Alot' 'scopeName': 'source.alot' 'patterns': [ { 'captures': '0': 'name': 'keyword.alot' 'match': 'alot' } ] ================================================ FILE: spec/fixtures/packages/package-with-ignored-incompatible-native-module/main.js ================================================ ================================================ FILE: spec/fixtures/packages/package-with-ignored-incompatible-native-module/package.json ================================================ { "name": "package-with-ignored-incompatible-native-module", "version": "1.0.0", "main": "./main.js", "_atomModuleCache": { "extensions": { ".node": [ "node_modules/compatible-native-module/build/Release/native.node" ] } } } ================================================ FILE: spec/fixtures/packages/package-with-incompatible-native-module/main.js ================================================ ================================================ FILE: spec/fixtures/packages/package-with-incompatible-native-module/package.json ================================================ { "name": "package-with-incompatible-native-module", "version": "1.0.0", "main": "./main.js" } ================================================ FILE: spec/fixtures/packages/package-with-incompatible-native-module-loaded-conditionally/main.js ================================================ const condition = false; if (condition) { const { native } = require("./node_modules/native-module"); native(condition); } ================================================ FILE: spec/fixtures/packages/package-with-incompatible-native-module-loaded-conditionally/package.json ================================================ { "name": "package-with-incompatible-native-module", "version": "1.0.0", "main": "./main.js" } ================================================ FILE: spec/fixtures/packages/package-with-index/index.coffee ================================================ module.exports = activate: -> ================================================ FILE: spec/fixtures/packages/package-with-injection-selector/grammars/grammar.cson ================================================ 'name': 'test' 'scopeName': 'source.test' 'injectionSelector': 'comment' 'patterns': [{'include': 'source.sql'}] ================================================ FILE: spec/fixtures/packages/package-with-invalid-activation-commands/package.json ================================================ { "name": "package-with-invalid-activation-commands", "version": "1.0.0", "activationCommands": { "<>": [ "foo:bar" ] } } ================================================ FILE: spec/fixtures/packages/package-with-invalid-context-menu/menus/menu.json ================================================ { "context-menu": { "<>": [ { "label": "Hello", "command:": "world" } ] } } ================================================ FILE: spec/fixtures/packages/package-with-invalid-context-menu/package.json ================================================ { "name": "package-with-invalid-context-menu", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-invalid-grammar/grammars/grammar.json ================================================ >< ================================================ FILE: spec/fixtures/packages/package-with-invalid-grammar/package.json ================================================ { "name": "package-with-invalid-grammar", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-invalid-settings/package.json ================================================ { "name": "package-with-invalid-settings", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-invalid-settings/settings/settings.json ================================================ >< ================================================ FILE: spec/fixtures/packages/package-with-invalid-styles/package.json ================================================ { "name": "package-with-invalid-styles", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-invalid-styles/styles/index.less ================================================ { ================================================ FILE: spec/fixtures/packages/package-with-invalid-url-package-json/package.json ================================================ { "name": "package-with-invalid-url-package-json", "repository": "foo" } ================================================ FILE: spec/fixtures/packages/package-with-json-config-schema/package.json ================================================ { "name": "package-with-json-config-schema", "configSchema": { "a": { "type": "number", "default": 5 }, "b": { "type": "string", "default": "five" } } } ================================================ FILE: spec/fixtures/packages/package-with-keymaps/keymaps/keymap-1.cson ================================================ ".test-1": "ctrl-z": "test-1" ================================================ FILE: spec/fixtures/packages/package-with-keymaps/keymaps/keymap-2.cson ================================================ ".test-2": "ctrl-z": "test-2" ================================================ FILE: spec/fixtures/packages/package-with-keymaps/keymaps/keymap-3.cjson ================================================ ".test-3": "ctrl-z": "test-3" ================================================ FILE: spec/fixtures/packages/package-with-keymaps-manifest/keymaps/keymap-1.json ================================================ { ".test-1": { "ctrl-z": "keymap-1" } } ================================================ FILE: spec/fixtures/packages/package-with-keymaps-manifest/keymaps/keymap-2.cson ================================================ ".test-1": "ctrl-z": "keymap-2" "ctrl-n": "keymap-2" ================================================ FILE: spec/fixtures/packages/package-with-keymaps-manifest/keymaps/keymap-3.cson ================================================ ".test-3": "ctrl-y": "keymap-3" ================================================ FILE: spec/fixtures/packages/package-with-keymaps-manifest/package.cson ================================================ keymaps: ["keymap-2", "keymap-1"] ================================================ FILE: spec/fixtures/packages/package-with-main/main-module.coffee ================================================ module.exports = activate: -> ================================================ FILE: spec/fixtures/packages/package-with-main/package.cson ================================================ 'main': 'main-module.coffee' 'version': '2.3.4' ================================================ FILE: spec/fixtures/packages/package-with-menus/menus/menu-1.cson ================================================ 'menu': [ {'label': 'Second to Last'} ] 'context-menu': '.test-1': [ {label: 'Menu item 1', command: 'command-1'} ] ================================================ FILE: spec/fixtures/packages/package-with-menus/menus/menu-2.cson ================================================ 'menu': [ { 'label': 'Last' } ] 'context-menu': '.test-1': [ {label: 'Menu item 2', command: 'command-2'} ] ================================================ FILE: spec/fixtures/packages/package-with-menus/menus/menu-3.cson ================================================ 'menu': [ { 'label': 'Second to Last' } ] 'context-menu': '.test-1': [ {label: 'Menu item 3', command: 'command-3'} ] ================================================ FILE: spec/fixtures/packages/package-with-menus-manifest/menus/menu-1.cson ================================================ 'menu': [ { 'label': 'Last' } ] 'context-menu': '.test-1': [ {label: 'Menu item 1', command: 'command-1'} ] ================================================ FILE: spec/fixtures/packages/package-with-menus-manifest/menus/menu-2.cson ================================================ 'menu': [ { 'label': 'Second to Last' } ] 'context-menu': '.test-1': [ {label: 'Menu item 2', command: 'command-2'} ] ================================================ FILE: spec/fixtures/packages/package-with-menus-manifest/menus/menu-3.cson ================================================ 'context-menu': '.test-1': [ {label: 'Menu item 3', command: 'command-3'} ] ================================================ FILE: spec/fixtures/packages/package-with-menus-manifest/package.cson ================================================ menus: ["menu-2", "menu-1"] ================================================ FILE: spec/fixtures/packages/package-with-missing-consumed-services/index.coffee ================================================ module.exports = activate: -> deactivate: -> ================================================ FILE: spec/fixtures/packages/package-with-missing-consumed-services/package.json ================================================ { "name": "package-with-missing-consumed-services", "consumedServices": { "service-1": { "versions": { ">=0.1": "consumeMissingService" } } } } ================================================ FILE: spec/fixtures/packages/package-with-missing-provided-services/index.coffee ================================================ module.exports = activate: -> deactivate: -> ================================================ FILE: spec/fixtures/packages/package-with-missing-provided-services/package.json ================================================ { "name": "package-with-missing-provided-services", "providedServices": { "service-1": { "description": "The first service", "versions": { "0.2.9": "provideMissingService" } } } } ================================================ FILE: spec/fixtures/packages/package-with-no-activate/index.js ================================================ module.exports = {} ================================================ FILE: spec/fixtures/packages/package-with-no-activate/package.json ================================================ { "name": "package-with-no-activate", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json ================================================ { "name": "package-with-a-git-prefixed-git-repo-url", "repository": { "type": "git", "url": "git+https://github.com/example/repo.git" }, "_id": "this is here to simulate the URL being already normalized by npm. we still need to stript git+ from the beginning and .git from the end." } ================================================ FILE: spec/fixtures/packages/package-with-provided-services/index.coffee ================================================ module.exports = activate: -> deactivate: -> provideFirstServiceV2: -> 'first-service-v2' provideFirstServiceV3: -> 'first-service-v3' provideFirstServiceV4: -> 'first-service-v4' provideSecondService: -> 'second-service' ================================================ FILE: spec/fixtures/packages/package-with-provided-services/package.json ================================================ { "name": "package-with-provided-services", "providedServices": { "service-1": { "description": "The first service", "versions": { "0.2.9": "provideFirstServiceV2", "0.3.1": "provideFirstServiceV3", "0.4.1": "provideFirstServiceV4" } }, "service-2": { "description": "The second service", "versions": { "0.2.1": "provideSecondService" } } } } ================================================ FILE: spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson ================================================ 'name': 'Test Ruby' 'type': 'tree-sitter' 'scopeName': 'test.rb' 'parser': 'tree-sitter-ruby' 'firstLineRegex': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)' 'fileTypes': [ 'rb' ] ================================================ FILE: spec/fixtures/packages/package-with-rb-filetype/package.json ================================================ { "name": "package-with-rb-filetype", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/package-with-serialization/index.coffee ================================================ module.exports = activate: ({@someNumber}) -> @someNumber ?= 1 serialize: -> {@someNumber} ================================================ FILE: spec/fixtures/packages/package-with-serialize-error/index.coffee ================================================ module.exports = activate: -> deactivate: -> serialize: -> throw new Error("I'm no good at this.") ================================================ FILE: spec/fixtures/packages/package-with-serialize-error/package.cson ================================================ 'main': 'index.coffee' ================================================ FILE: spec/fixtures/packages/package-with-settings/settings/omg.cson ================================================ '.source.omg': 'editor': 'increaseIndentPattern': '^a' ================================================ FILE: spec/fixtures/packages/package-with-short-url-package-json/package.json ================================================ { "name": "package-with-short-url-package-json", "repository": "example/repo" } ================================================ FILE: spec/fixtures/packages/package-with-style-sheets-manifest/package.cson ================================================ styleSheets: ['2', '1'] ================================================ FILE: spec/fixtures/packages/package-with-style-sheets-manifest/styles/1.css ================================================ #jasmine-content { font-size: 1px; } ================================================ FILE: spec/fixtures/packages/package-with-style-sheets-manifest/styles/2.less ================================================ @size: 2px; #jasmine-content { font-size: @size; } ================================================ FILE: spec/fixtures/packages/package-with-style-sheets-manifest/styles/3.css ================================================ #jasmine-content { font-size: 3px; } ================================================ FILE: spec/fixtures/packages/package-with-styles/styles/1.css ================================================ #jasmine-content { font-size: 1px; } ================================================ FILE: spec/fixtures/packages/package-with-styles/styles/2.less ================================================ @size: 2px; #jasmine-content { font-size: @size; } ================================================ FILE: spec/fixtures/packages/package-with-styles/styles/3.test-context.css ================================================ #jasmine-content { font-size: 3px; } ================================================ FILE: spec/fixtures/packages/package-with-styles/styles/4.css ================================================ a { color: red } ================================================ FILE: spec/fixtures/packages/package-with-stylesheets-manifest/package.cson ================================================ styleSheets: ['2', '1'] ================================================ FILE: spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js ================================================ exports.isFakeTreeSitterParser = true ================================================ FILE: spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson ================================================ name: 'Some Language' scopeName: 'some-language' type: 'tree-sitter' parser: './fake-parser' fileTypes: [ 'somelang' ] scopes: 'class > identifier': 'entity.name.type.class' ================================================ FILE: spec/fixtures/packages/package-with-uri-handler/index.js ================================================ module.exports = { activate: () => null, deactivate: () => null, handleURI: () => null, } ================================================ FILE: spec/fixtures/packages/package-with-uri-handler/package.json ================================================ { "name": "package-with-uri-handler", "uriHandler": { "method": "handleURI" } } ================================================ FILE: spec/fixtures/packages/package-with-url-main/index.js ================================================ module.exports = function initialize() { global.reachedUrlMain = true; return Promise.resolve(); }; ================================================ FILE: spec/fixtures/packages/package-with-url-main/package.json ================================================ { "name": "package-with-url-main", "version": "1.0.0", "urlMain": "./index.js" } ================================================ FILE: spec/fixtures/packages/package-with-view-providers/index.js ================================================ 'use strict' module.exports = { activate () {}, theDeserializerMethod (state) { return {state: state} }, viewProviderMethod1 (model) { if (model.worksWithViewProvider1) { let element = document.createElement('div') element.dataset['createdBy'] = 'view-provider-1' return element } }, viewProviderMethod2 (model) { if (model.worksWithViewProvider2) { let element = document.createElement('div') element.dataset['createdBy'] = 'view-provider-2' return element } } } ================================================ FILE: spec/fixtures/packages/package-with-view-providers/package.json ================================================ { "name": "package-with-view-providers", "main": "./index", "version": "1.0.0", "deserializers": { "DeserializerFromPackageWithViewProviders": "theDeserializerMethod" }, "viewProviders": [ "viewProviderMethod1", "viewProviderMethod2" ] } ================================================ FILE: spec/fixtures/packages/package-with-workspace-openers/index.coffee ================================================ module.exports = activateCallCount: 0 openerCount: 0 activate: -> @activateCallCount++ atom.workspace.addOpener (filePath) => if filePath is 'atom://fictitious' @openerCount++ ================================================ FILE: spec/fixtures/packages/package-with-workspace-openers/package.cson ================================================ { "name": "package-with-workspace-openers", "version": "0.1.0", "workspaceOpeners": ['atom://fictitious'] } ================================================ FILE: spec/fixtures/packages/package-without-module/package.cson ================================================ "name": "perfect" ================================================ FILE: spec/fixtures/packages/sublime-tabs/package.json ================================================ { "name": "sublime-tabs", "version": "1.0.0" } ================================================ FILE: spec/fixtures/packages/theme-with-incomplete-ui-variables/package.json ================================================ { "theme": "ui", "styleSheets": ["editor.less"] } ================================================ FILE: spec/fixtures/packages/theme-with-incomplete-ui-variables/styles/editor.less ================================================ @import "ui-variables"; atom-text-editor { padding-top: @component-padding; padding-right: @component-padding; padding-bottom: @component-padding; color: @input-background-color; background-color: @background-color-info; // From the fallback variables, not overridden } ================================================ FILE: spec/fixtures/packages/theme-with-incomplete-ui-variables/styles/ui-variables.less ================================================ // This does not contain all of the ui-variables available. @app-background-color: #00f; // Changed @input-background-color: #f00; // Changed ================================================ FILE: spec/fixtures/packages/theme-with-index-css/index.css ================================================ atom-text-editor { padding-top: 1234px; } ================================================ FILE: spec/fixtures/packages/theme-with-index-css/package.json ================================================ { "theme": "ui" } ================================================ FILE: spec/fixtures/packages/theme-with-index-less/index.less ================================================ @padding: 4321px; atom-text-editor { padding-top: @padding; } ================================================ FILE: spec/fixtures/packages/theme-with-index-less/package.json ================================================ { "theme": "ui" } ================================================ FILE: spec/fixtures/packages/theme-with-invalid-styles/index.less ================================================ <> ================================================ FILE: spec/fixtures/packages/theme-with-invalid-styles/package.json ================================================ { "name": "theme-with-invalid-styles", "theme": "ui" } ================================================ FILE: spec/fixtures/packages/theme-with-package-file/package.json ================================================ { "theme": "ui", "styleSheets": ["first.css", "second.less", "last.css"] } ================================================ FILE: spec/fixtures/packages/theme-with-package-file/styles/first.css ================================================ atom-text-editor { padding-top: 101px; padding-right: 101px; padding-bottom: 101px; color: red; } ================================================ FILE: spec/fixtures/packages/theme-with-package-file/styles/last.css ================================================ atom-text-editor { /* padding-top: 103px; padding-right: 103px;*/ padding-bottom: 103px; } ================================================ FILE: spec/fixtures/packages/theme-with-package-file/styles/second.less ================================================ @number: 102px; atom-text-editor { /* padding-top: 102px;*/ padding-right: @number; padding-bottom: @number; } ================================================ FILE: spec/fixtures/packages/theme-with-syntax-variables/package.json ================================================ { "theme": "syntax", "styleSheets": ["editor.less"] } ================================================ FILE: spec/fixtures/packages/theme-with-syntax-variables/styles/editor.less ================================================ ================================================ FILE: spec/fixtures/packages/theme-with-ui-variables/package.json ================================================ { "theme": "ui", "styleSheets": ["editor.less"] } ================================================ FILE: spec/fixtures/packages/theme-with-ui-variables/styles/editor.less ================================================ @import "ui-variables"; atom-text-editor { padding-top: @component-padding; padding-right: @component-padding; padding-bottom: @component-padding; color: @input-background-color; } ================================================ FILE: spec/fixtures/packages/theme-with-ui-variables/styles/ui-variables.less ================================================ // Variables different from the original are marked 'Changed' @text-color: #333; @text-color-subtle: #777; @text-color-highlight: #111; @text-color-selected: @text-color-highlight; @text-color-info: #5293d8; @text-color-success: #1fe977; @text-color-warning: #f78a46; @text-color-error: #c00; @background-color-info: #0098ff; @background-color-success: #17ca65; @background-color-warning: #ff4800; @background-color-error: #c00; @background-color-highlight: rgba(255, 255, 255, 0.10); @background-color-selected: @background-color-highlight; @app-background-color: #00f; // Changed @base-background-color: #fff; @base-border-color: #eee; @pane-item-background-color: @base-background-color; @pane-item-border-color: @base-border-color; @input-background-color: #f00; // Changed @input-border-color: @base-border-color; @tool-panel-background-color: #f4f4f4; @tool-panel-border-color: @base-border-color; @inset-panel-background-color: #eee; @inset-panel-border-color: @base-border-color; @panel-heading-background-color: #ddd; @panel-heading-border-color: transparent; @overlay-background-color: #f4f4f4; @overlay-border-color: @base-border-color; @button-background-color: #ccc; @button-background-color-hover: lighten(@button-background-color, 5%); @button-background-color-selected: @button-background-color-hover; @button-border-color: #aaa; @tab-bar-background-color: #fff; @tab-bar-border-color: darken(@tab-background-color-active, 10%); @tab-background-color: #f4f4f4; @tab-background-color-active: #fff; @tab-border-color: @base-border-color; @tree-view-background-color: @tool-panel-background-color; @tree-view-border-color: @tool-panel-border-color; @ui-site-color-1: @background-color-success; // green @ui-site-color-2: @background-color-info; // blue @ui-site-color-3: @background-color-warning; // orange @ui-site-color-4: #db2ff4; // purple @ui-site-color-5: #f5e11d; // yellow @font-size: 12px; @disclosure-arrow-size: 12px; @component-padding: 150px; @component-icon-padding: 5px; @component-icon-size: 16px; @component-line-height: 25px; @component-border-radius: 2px; @tab-height: 30px; @font-family: Arial; ================================================ FILE: spec/fixtures/packages/theme-without-package-file/styles/a.css ================================================ atom-text-editor { padding-top: 10px; padding-right: 10px; padding-bottom: 10px; } ================================================ FILE: spec/fixtures/packages/theme-without-package-file/styles/b.css ================================================ atom-text-editor { padding-right: 20px; padding-bottom: 20px; } ================================================ FILE: spec/fixtures/packages/theme-without-package-file/styles/c.less ================================================ @number: 30px; atom-text-editor { padding-bottom: @number; } ================================================ FILE: spec/fixtures/packages/theme-without-package-file/styles/d.csv ================================================ atom-text-editor { padding-top: 100px; padding-right: 100px; padding-bottom: 100px; } ================================================ FILE: spec/fixtures/packages/wordcount/package.json ================================================ { "name": "wordcount", "version": "2.0.9" } ================================================ FILE: spec/fixtures/sample-with-comments.js ================================================ var quicksort = function () { /* this is a multiline comment it is, I promise */ var sort = function(items) { // comment at the end of a foldable line // This is a collection of // single line comments. // Wowza if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = []; /* This is a multiline comment block with an empty line inside of it. Awesome. */ while(items.length > 0) { current = items.shift(); current < pivot ? left.push(current) : right.push(current); } // This is a collection of // single line comments // ...with an empty line // among it, geez! return sort(left).concat(pivot).concat(sort(right)); }; // this is a single-line comment return sort(Array.apply(this, arguments)); }; ================================================ FILE: spec/fixtures/sample-with-many-folds.js ================================================ 1; 2; function f3() { return 4; }; 6; 7; function f8() { return 9; }; 11; 12; ================================================ FILE: spec/fixtures/sample-with-tabs-and-leading-comment.coffee ================================================ # This is a comment if this.studyingEconomics buy() while supply > demand sell() until supply > demand ================================================ FILE: spec/fixtures/sample-with-tabs.coffee ================================================ # Econ 101 if this.studyingEconomics buy() while supply > demand sell() until supply > demand ================================================ FILE: spec/fixtures/sample.js ================================================ var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = []; while(items.length > 0) { current = items.shift(); current < pivot ? left.push(current) : right.push(current); } return sort(left).concat(pivot).concat(sort(right)); }; return sort(Array.apply(this, arguments)); }; ================================================ FILE: spec/fixtures/sample.less ================================================ @color: #4D926F; #header { color: @color; } h2 { color: @color; } ================================================ FILE: spec/fixtures/sample.txt ================================================ Some textSome text. ================================================ FILE: spec/fixtures/script-with-deprecations.js ================================================ require('fs').existsSync('hi'); process.stdout.write('hi'); ================================================ FILE: spec/fixtures/script.js ================================================ process.stdout.write(process.argv[2]); ================================================ FILE: spec/fixtures/shebang ================================================ #!/usr/bin/ruby puts "Atom fixture test" ================================================ FILE: spec/fixtures/task-handler-with-deprecations.coffee ================================================ {deprecate} = require 'grim' deprecate('Fake task deprecation') module.exports = -> ================================================ FILE: spec/fixtures/task-spec-handler.coffee ================================================ module.exports = -> emit("some-event", 1, 2, 3) 'hello' ================================================ FILE: spec/fixtures/testdir/sample-theme-1/readme ================================================ ================================================ FILE: spec/fixtures/testdir/sample-theme-1/src/js/main.js ================================================ ================================================ FILE: spec/fixtures/testdir/sample-theme-2/readme ================================================ ================================================ FILE: spec/fixtures/testdir/sample-theme-2/src/js/main.js ================================================ ================================================ FILE: spec/fixtures/testdir/sample-theme-2/src/js/plugin/main.js ================================================ ================================================ FILE: spec/fixtures/two-hundred.txt ================================================ 0 1 2 3 4---------------------------------------------------------------------------------------------------- 5 6 7 8 9---------------------------------------------------------------------------------------------------- 10 11 12 13 14---------------------------------------------------------------------------------------------------- 15 16 17 18 19---------------------------------------------------------------------------------------------------- 20 21 22 23 24---------------------------------------------------------------------------------------------------- 25 26 27 28 29---------------------------------------------------------------------------------------------------- 30 31 32 33 34---------------------------------------------------------------------------------------------------- 35 36 37 38 39---------------------------------------------------------------------------------------------------- 40 41 42 43 44---------------------------------------------------------------------------------------------------- 45 46 47 48 49---------------------------------------------------------------------------------------------------- 50 51 52 53 54---------------------------------------------------------------------------------------------------- 55 56 57 58 59---------------------------------------------------------------------------------------------------- 60 61 62 63 64---------------------------------------------------------------------------------------------------- 65 66 67 68 69---------------------------------------------------------------------------------------------------- 70 71 72 73 74---------------------------------------------------------------------------------------------------- 75 76 77 78 79---------------------------------------------------------------------------------------------------- 80 81 82 83 84---------------------------------------------------------------------------------------------------- 85 86 87 88 89---------------------------------------------------------------------------------------------------- 90 91 92 93 94---------------------------------------------------------------------------------------------------- 95 96 97 98 99---------------------------------------------------------------------------------------------------- 100 101 102 103 104---------------------------------------------------------------------------------------------------- 105 106 107 108 109---------------------------------------------------------------------------------------------------- 110 111 112 113 114---------------------------------------------------------------------------------------------------- 115 116 117 118 119---------------------------------------------------------------------------------------------------- 120 121 122 123 124---------------------------------------------------------------------------------------------------- 125 126 127 128 129---------------------------------------------------------------------------------------------------- 130 131 132 133 134---------------------------------------------------------------------------------------------------- 135 136 137 138 139---------------------------------------------------------------------------------------------------- 140 141 142 143 144---------------------------------------------------------------------------------------------------- 145 146 147 148 149---------------------------------------------------------------------------------------------------- 150 151 152 153 154---------------------------------------------------------------------------------------------------- 155 156 157 158 159---------------------------------------------------------------------------------------------------- 160 161 162 163 164---------------------------------------------------------------------------------------------------- 165 166 167 168 169---------------------------------------------------------------------------------------------------- 170 171 172 173 174---------------------------------------------------------------------------------------------------- 175 176 177 178 179---------------------------------------------------------------------------------------------------- 180 181 182 183 184---------------------------------------------------------------------------------------------------- 185 186 187 188 189---------------------------------------------------------------------------------------------------- 190 191 192 193 194---------------------------------------------------------------------------------------------------- 195 196 197 198 199---------------------------------------------------------------------------------------------------- ================================================ FILE: spec/fixtures/typescript/invalid.ts ================================================ var foo = 123 123; // Syntax error ================================================ FILE: spec/fixtures/typescript/valid.ts ================================================ var inc = v => v + 1 export = inc ================================================ FILE: spec/git-repository-provider-spec.js ================================================ 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'); 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 with a commondir file for a worktree', () => { it('returns a Promise that resolves to a GitRepository', async () => { const directory = new Directory( path.join( __dirname, 'fixtures', 'git', 'master.git', 'worktrees', 'worktree-dir' ) ); 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 exists()', () => { 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 a Promise that resolves to null', async () => { const repo = await provider.repositoryForDirectory(directory); expect(repo).toBe(null); expect(directory.getSubdirectory).toHaveBeenCalledWith('.git'); }); }); }); describe('.repositoryForDirectorySync(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 = provider.repositoryForDirectorySync(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', () => { const firstRepo = provider.repositoryForDirectorySync( new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git')) ); const secondRepo = provider.repositoryForDirectorySync( 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', () => { const directory = new Directory(temp.mkdirSync('dir')); const repo = provider.repositoryForDirectorySync(directory); expect(repo).toBe(null); }); }); describe('when specified a Directory with an invalid Git repository', () => { it('resolves with null', () => { 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 = provider.repositoryForDirectorySync(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', () => { 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 = provider.repositoryForDirectorySync(directory); expect(result).toBeInstanceOf(GitRepository); expect(provider.pathToRepository[result.getPath()]).toBeTruthy(); expect(result.getType()).toBe('git'); }); }); describe('when specified a Directory with a commondir file for a worktree', () => { it('returns a Promise that resolves to a GitRepository', () => { const directory = new Directory( path.join( __dirname, 'fixtures', 'git', 'master.git', 'worktrees', 'worktree-dir' ) ); const result = provider.repositoryForDirectorySync(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'); }); }); }); }); ================================================ FILE: spec/git-repository-spec.js ================================================ const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); const GitRepository = require('../src/git-repository'); const Project = require('../src/project'); describe('GitRepository', () => { let repo; beforeEach(() => { const gitPath = path.join(temp.dir, '.git'); if (fs.isDirectorySync(gitPath)) fs.removeSync(gitPath); }); afterEach(() => { if (repo && !repo.isDestroyed()) repo.destroy(); }); describe('@open(path)', () => { it('returns null when no repository is found', () => { expect(GitRepository.open(path.join(temp.dir, 'nogit.txt'))).toBeNull(); }); }); describe('new GitRepository(path)', () => { it('throws an exception when no repository is found', () => { expect( () => new GitRepository(path.join(temp.dir, 'nogit.txt')) ).toThrow(); }); }); describe('.getPath()', () => { it('returns the repository path for a .git directory path with a directory', () => { repo = new GitRepository( path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects') ); expect(repo.getPath()).toBe( path.join(__dirname, 'fixtures', 'git', 'master.git') ); }); it('returns the repository path for a repository path', () => { repo = new GitRepository( path.join(__dirname, 'fixtures', 'git', 'master.git') ); expect(repo.getPath()).toBe( path.join(__dirname, 'fixtures', 'git', 'master.git') ); }); }); describe('.isPathIgnored(path)', () => { it('returns true for an ignored path', () => { repo = new GitRepository( path.join(__dirname, 'fixtures', 'git', 'ignore.git') ); expect(repo.isPathIgnored('a.txt')).toBeTruthy(); }); it('returns false for a non-ignored path', () => { repo = new GitRepository( path.join(__dirname, 'fixtures', 'git', 'ignore.git') ); expect(repo.isPathIgnored('b.txt')).toBeFalsy(); }); }); describe('.isPathModified(path)', () => { let filePath, newPath; beforeEach(() => { const workingDirPath = copyRepository(); repo = new GitRepository(workingDirPath); filePath = path.join(workingDirPath, 'a.txt'); newPath = path.join(workingDirPath, 'new-path.txt'); }); describe('when the path is unstaged', () => { it('returns false if the path has not been modified', () => { expect(repo.isPathModified(filePath)).toBeFalsy(); }); it('returns true if the path is modified', () => { fs.writeFileSync(filePath, 'change'); expect(repo.isPathModified(filePath)).toBeTruthy(); }); it('returns true if the path is deleted', () => { fs.removeSync(filePath); expect(repo.isPathModified(filePath)).toBeTruthy(); }); it('returns false if the path is new', () => { expect(repo.isPathModified(newPath)).toBeFalsy(); }); }); }); describe('.isPathNew(path)', () => { let filePath, newPath; beforeEach(() => { const workingDirPath = copyRepository(); repo = new GitRepository(workingDirPath); filePath = path.join(workingDirPath, 'a.txt'); newPath = path.join(workingDirPath, 'new-path.txt'); fs.writeFileSync(newPath, "i'm new here"); }); describe('when the path is unstaged', () => { it('returns true if the path is new', () => { expect(repo.isPathNew(newPath)).toBeTruthy(); }); it("returns false if the path isn't new", () => { expect(repo.isPathNew(filePath)).toBeFalsy(); }); }); }); describe('.checkoutHead(path)', () => { let filePath; beforeEach(() => { const workingDirPath = copyRepository(); repo = new GitRepository(workingDirPath); filePath = path.join(workingDirPath, 'a.txt'); }); it('no longer reports a path as modified after checkout', () => { expect(repo.isPathModified(filePath)).toBeFalsy(); fs.writeFileSync(filePath, 'ch ch changes'); expect(repo.isPathModified(filePath)).toBeTruthy(); expect(repo.checkoutHead(filePath)).toBeTruthy(); expect(repo.isPathModified(filePath)).toBeFalsy(); }); it('restores the contents of the path to the original text', () => { fs.writeFileSync(filePath, 'ch ch changes'); expect(repo.checkoutHead(filePath)).toBeTruthy(); expect(fs.readFileSync(filePath, 'utf8')).toBe(''); }); it('fires a status-changed event if the checkout completes successfully', () => { fs.writeFileSync(filePath, 'ch ch changes'); repo.getPathStatus(filePath); const statusHandler = jasmine.createSpy('statusHandler'); repo.onDidChangeStatus(statusHandler); repo.checkoutHead(filePath); expect(statusHandler.callCount).toBe(1); expect(statusHandler.argsForCall[0][0]).toEqual({ path: filePath, pathStatus: 0 }); repo.checkoutHead(filePath); expect(statusHandler.callCount).toBe(1); }); }); describe('.checkoutHeadForEditor(editor)', () => { let filePath, editor; beforeEach(async () => { spyOn(atom, 'confirm'); const workingDirPath = copyRepository(); repo = new GitRepository(workingDirPath, { project: atom.project, config: atom.config, confirm: atom.confirm }); filePath = path.join(workingDirPath, 'a.txt'); fs.writeFileSync(filePath, 'ch ch changes'); editor = await atom.workspace.open(filePath); }); it('displays a confirmation dialog by default', () => { // Permissions issues with this test on Windows if (process.platform === 'win32') return; atom.confirm.andCallFake(({ buttons }) => buttons.OK()); atom.config.set('editor.confirmCheckoutHeadRevision', true); repo.checkoutHeadForEditor(editor); expect(fs.readFileSync(filePath, 'utf8')).toBe(''); }); it('does not display a dialog when confirmation is disabled', () => { // Flakey EPERM opening a.txt on Win32 if (process.platform === 'win32') return; atom.config.set('editor.confirmCheckoutHeadRevision', false); repo.checkoutHeadForEditor(editor); expect(fs.readFileSync(filePath, 'utf8')).toBe(''); expect(atom.confirm).not.toHaveBeenCalled(); }); }); describe('.destroy()', () => { it('throws an exception when any method is called after it is called', () => { repo = new GitRepository( path.join(__dirname, 'fixtures', 'git', 'master.git') ); repo.destroy(); expect(() => repo.getShortHead()).toThrow(); }); }); describe('.getPathStatus(path)', () => { let filePath; beforeEach(() => { const workingDirectory = copyRepository(); repo = new GitRepository(workingDirectory); filePath = path.join(workingDirectory, 'file.txt'); }); it('trigger a status-changed event when the new status differs from the last cached one', () => { const statusHandler = jasmine.createSpy('statusHandler'); repo.onDidChangeStatus(statusHandler); fs.writeFileSync(filePath, ''); let status = repo.getPathStatus(filePath); expect(statusHandler.callCount).toBe(1); expect(statusHandler.argsForCall[0][0]).toEqual({ path: filePath, pathStatus: status }); fs.writeFileSync(filePath, 'abc'); status = repo.getPathStatus(filePath); expect(statusHandler.callCount).toBe(1); }); }); describe('.getDirectoryStatus(path)', () => { let directoryPath, filePath; beforeEach(() => { const workingDirectory = copyRepository(); repo = new GitRepository(workingDirectory); directoryPath = path.join(workingDirectory, 'dir'); filePath = path.join(directoryPath, 'b.txt'); }); it('gets the status based on the files inside the directory', () => { expect( repo.isStatusModified(repo.getDirectoryStatus(directoryPath)) ).toBe(false); fs.writeFileSync(filePath, 'abc'); repo.getPathStatus(filePath); expect( repo.isStatusModified(repo.getDirectoryStatus(directoryPath)) ).toBe(true); }); }); describe('.refreshStatus()', () => { let newPath, modifiedPath, cleanPath, workingDirectory; beforeEach(() => { workingDirectory = copyRepository(); repo = new GitRepository(workingDirectory, { project: atom.project, config: atom.config }); modifiedPath = path.join(workingDirectory, 'file.txt'); newPath = path.join(workingDirectory, 'untracked.txt'); cleanPath = path.join(workingDirectory, 'other.txt'); fs.writeFileSync(cleanPath, 'Full of text'); fs.writeFileSync(newPath, ''); newPath = fs.absolute(newPath); }); it('returns status information for all new and modified files', async () => { const statusHandler = jasmine.createSpy('statusHandler'); repo.onDidChangeStatuses(statusHandler); fs.writeFileSync(modifiedPath, 'making this path modified'); await repo.refreshStatus(); expect(statusHandler.callCount).toBe(1); expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined(); expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy(); expect( repo.isStatusModified(repo.getCachedPathStatus(modifiedPath)) ).toBeTruthy(); }); it('caches the proper statuses when a subdir is open', async () => { const subDir = path.join(workingDirectory, 'dir'); fs.mkdirSync(subDir); const filePath = path.join(subDir, 'b.txt'); fs.writeFileSync(filePath, ''); atom.project.setPaths([subDir]); await atom.workspace.open('b.txt'); repo = atom.project.getRepositories()[0]; await repo.refreshStatus(); const status = repo.getCachedPathStatus(filePath); expect(repo.isStatusModified(status)).toBe(false); expect(repo.isStatusNew(status)).toBe(false); }); it('works correctly when the project has multiple folders (regression)', async () => { atom.project.addPath(workingDirectory); atom.project.addPath(path.join(__dirname, 'fixtures', 'dir')); await repo.refreshStatus(); expect(repo.getCachedPathStatus(cleanPath)).toBeUndefined(); expect(repo.isStatusNew(repo.getCachedPathStatus(newPath))).toBeTruthy(); expect( repo.isStatusModified(repo.getCachedPathStatus(modifiedPath)) ).toBeTruthy(); }); it('caches statuses that were looked up synchronously', async () => { const originalContent = 'undefined'; fs.writeFileSync(modifiedPath, 'making this path modified'); repo.getPathStatus('file.txt'); fs.writeFileSync(modifiedPath, originalContent); await repo.refreshStatus(); expect( repo.isStatusModified(repo.getCachedPathStatus(modifiedPath)) ).toBeFalsy(); }); }); describe('buffer events', () => { let editor; beforeEach(async () => { atom.project.setPaths([copyRepository()]); const refreshPromise = new Promise(resolve => atom.project.getRepositories()[0].onDidChangeStatuses(resolve) ); editor = await atom.workspace.open('other.txt'); await refreshPromise; }); it('emits a status-changed event when a buffer is saved', async () => { editor.insertNewline(); const statusHandler = jasmine.createSpy('statusHandler'); atom.project.getRepositories()[0].onDidChangeStatus(statusHandler); await editor.save(); expect(statusHandler.callCount).toBe(1); expect(statusHandler).toHaveBeenCalledWith({ path: editor.getPath(), pathStatus: 256 }); }); it('emits a status-changed event when a buffer is reloaded', async () => { fs.writeFileSync(editor.getPath(), 'changed'); const statusHandler = jasmine.createSpy('statusHandler'); atom.project.getRepositories()[0].onDidChangeStatus(statusHandler); await editor.getBuffer().reload(); expect(statusHandler.callCount).toBe(1); expect(statusHandler).toHaveBeenCalledWith({ path: editor.getPath(), pathStatus: 256 }); await editor.getBuffer().reload(); expect(statusHandler.callCount).toBe(1); }); it("emits a status-changed event when a buffer's path changes", () => { fs.writeFileSync(editor.getPath(), 'changed'); const statusHandler = jasmine.createSpy('statusHandler'); atom.project.getRepositories()[0].onDidChangeStatus(statusHandler); editor.getBuffer().emitter.emit('did-change-path'); expect(statusHandler.callCount).toBe(1); expect(statusHandler).toHaveBeenCalledWith({ path: editor.getPath(), pathStatus: 256 }); editor.getBuffer().emitter.emit('did-change-path'); expect(statusHandler.callCount).toBe(1); }); it('stops listening to the buffer when the repository is destroyed (regression)', () => { atom.project.getRepositories()[0].destroy(); expect(() => editor.save()).not.toThrow(); }); }); describe('when a project is deserialized', () => { let buffer, project2, statusHandler; afterEach(() => { if (project2) project2.destroy(); }); it('subscribes to all the serialized buffers in the project', async () => { atom.project.setPaths([copyRepository()]); await atom.workspace.open('file.txt'); project2 = new Project({ notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, grammarRegistry: atom.grammars, applicationDelegate: atom.applicationDelegate }); await project2.deserialize( atom.project.serialize({ isUnloading: false }) ); buffer = project2.getBuffers()[0]; buffer.append('changes'); statusHandler = jasmine.createSpy('statusHandler'); project2.getRepositories()[0].onDidChangeStatus(statusHandler); await buffer.save(); expect(statusHandler.callCount).toBe(1); expect(statusHandler).toHaveBeenCalledWith({ path: buffer.getPath(), pathStatus: 256 }); }); }); }); function copyRepository() { const workingDirPath = temp.mkdirSync('atom-spec-git'); fs.copySync( path.join(__dirname, 'fixtures', 'git', 'working-dir'), workingDirPath ); fs.renameSync( path.join(workingDirPath, 'git.git'), path.join(workingDirPath, '.git') ); return workingDirPath; } ================================================ FILE: spec/grammar-registry-spec.js ================================================ const dedent = require('dedent'); const path = require('path'); const fs = require('fs-plus'); const temp = require('temp').track(); const TextBuffer = require('text-buffer'); const GrammarRegistry = require('../src/grammar-registry'); const TreeSitterGrammar = require('../src/tree-sitter-grammar'); const FirstMate = require('first-mate'); const { OnigRegExp } = require('oniguruma'); describe('GrammarRegistry', () => { let grammarRegistry; beforeEach(() => { grammarRegistry = new GrammarRegistry({ config: atom.config }); expect(subscriptionCount(grammarRegistry)).toBe(1); }); describe('.assignLanguageMode(buffer, languageId)', () => { it('assigns to the buffer a language mode with the given language id', async () => { grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-css/grammars/css.cson') ); const buffer = new TextBuffer(); expect(grammarRegistry.assignLanguageMode(buffer, 'source.js')).toBe( true ); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js'); expect(grammarRegistry.getAssignedLanguageId(buffer)).toBe('source.js'); // Returns true if we found the grammar, even if it didn't change expect(grammarRegistry.assignLanguageMode(buffer, 'source.js')).toBe( true ); // Language names are not case-sensitive expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe( true ); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css'); // Returns false if no language is found expect(grammarRegistry.assignLanguageMode(buffer, 'blub')).toBe(false); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css'); }); describe('when no languageId is passed', () => { it('makes the buffer use the null grammar', () => { grammarRegistry.loadGrammarSync( require.resolve('language-css/grammars/css.cson') ); const buffer = new TextBuffer(); expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe( true ); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css'); expect(grammarRegistry.assignLanguageMode(buffer, null)).toBe(true); expect(buffer.getLanguageMode().getLanguageId()).toBe( 'text.plain.null-grammar' ); expect(grammarRegistry.getAssignedLanguageId(buffer)).toBe(null); }); }); }); describe('.assignGrammar(buffer, grammar)', () => { it('allows a TextMate grammar to be assigned directly, even when Tree-sitter is permitted', () => { grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); const tmGrammar = grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); const buffer = new TextBuffer(); expect(grammarRegistry.assignGrammar(buffer, tmGrammar)).toBe(true); expect(buffer.getLanguageMode().getGrammar()).toBe(tmGrammar); }); }); describe('.grammarForId(languageId)', () => { it('returns a text-mate grammar when `core.useTreeSitterParsers` is false', () => { atom.config.set('core.useTreeSitterParsers', false, { scopeSelector: '.source.js' }); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); const grammar = grammarRegistry.grammarForId('source.js'); expect(grammar instanceof FirstMate.Grammar).toBe(true); expect(grammar.scopeName).toBe('source.js'); grammarRegistry.removeGrammar(grammar); expect(grammarRegistry.grammarForId('javascript')).toBe(undefined); }); it('returns a tree-sitter grammar when `core.useTreeSitterParsers` is true', () => { atom.config.set('core.useTreeSitterParsers', true); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); const grammar = grammarRegistry.grammarForId('source.js'); expect(grammar instanceof TreeSitterGrammar).toBe(true); expect(grammar.scopeName).toBe('source.js'); grammarRegistry.removeGrammar(grammar); expect( grammarRegistry.grammarForId('source.js') instanceof FirstMate.Grammar ).toBe(true); }); }); describe('.autoAssignLanguageMode(buffer)', () => { it('assigns to the buffer a language mode based on the best available grammar', () => { grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-css/grammars/css.cson') ); const buffer = new TextBuffer(); buffer.setPath('foo.js'); expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe( true ); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css'); grammarRegistry.autoAssignLanguageMode(buffer); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js'); }); }); describe('.maintainLanguageMode(buffer)', () => { it('assigns a grammar to the buffer based on its path', async () => { const buffer = new TextBuffer(); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-c/grammars/c.cson') ); buffer.setPath('test.js'); grammarRegistry.maintainLanguageMode(buffer); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js'); buffer.setPath('test.c'); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.c'); }); it("updates the buffer's grammar when a more appropriate text-mate grammar is added for its path", async () => { atom.config.set('core.useTreeSitterParsers', false); const buffer = new TextBuffer(); expect(buffer.getLanguageMode().getLanguageId()).toBe(null); buffer.setPath('test.js'); grammarRegistry.maintainLanguageMode(buffer); const textMateGrammar = grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); expect(buffer.getLanguageMode().grammar).toBe(textMateGrammar); grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); expect(buffer.getLanguageMode().grammar).toBe(textMateGrammar); }); it("updates the buffer's grammar when a more appropriate tree-sitter grammar is added for its path", async () => { atom.config.set('core.useTreeSitterParsers', true); const buffer = new TextBuffer(); expect(buffer.getLanguageMode().getLanguageId()).toBe(null); buffer.setPath('test.js'); grammarRegistry.maintainLanguageMode(buffer); const treeSitterGrammar = grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); expect(buffer.getLanguageMode().grammar).toBe(treeSitterGrammar); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); expect(buffer.getLanguageMode().grammar).toBe(treeSitterGrammar); }); it('can be overridden by calling .assignLanguageMode', () => { const buffer = new TextBuffer(); buffer.setPath('test.js'); grammarRegistry.maintainLanguageMode(buffer); grammarRegistry.loadGrammarSync( require.resolve('language-css/grammars/css.cson') ); expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe( true ); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css'); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css'); }); it('returns a disposable that can be used to stop the registry from updating the buffer', async () => { const buffer = new TextBuffer(); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); const previousSubscriptionCount = buffer.emitter.getTotalListenerCount(); const disposable = grammarRegistry.maintainLanguageMode(buffer); expect(buffer.emitter.getTotalListenerCount()).toBeGreaterThan( previousSubscriptionCount ); expect(retainedBufferCount(grammarRegistry)).toBe(1); buffer.setPath('test.js'); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js'); buffer.setPath('test.txt'); expect(buffer.getLanguageMode().getLanguageId()).toBe( 'text.plain.null-grammar' ); disposable.dispose(); expect(buffer.emitter.getTotalListenerCount()).toBe( previousSubscriptionCount ); expect(retainedBufferCount(grammarRegistry)).toBe(0); buffer.setPath('test.js'); expect(buffer.getLanguageMode().getLanguageId()).toBe( 'text.plain.null-grammar' ); expect(retainedBufferCount(grammarRegistry)).toBe(0); }); it("doesn't do anything when called a second time with the same buffer", async () => { const buffer = new TextBuffer(); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); const disposable1 = grammarRegistry.maintainLanguageMode(buffer); const disposable2 = grammarRegistry.maintainLanguageMode(buffer); buffer.setPath('test.js'); expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js'); disposable2.dispose(); buffer.setPath('test.txt'); expect(buffer.getLanguageMode().getLanguageId()).toBe( 'text.plain.null-grammar' ); disposable1.dispose(); buffer.setPath('test.js'); expect(buffer.getLanguageMode().getLanguageId()).toBe( 'text.plain.null-grammar' ); }); it('does not retain the buffer after the buffer is destroyed', () => { const buffer = new TextBuffer(); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); const disposable = grammarRegistry.maintainLanguageMode(buffer); expect(retainedBufferCount(grammarRegistry)).toBe(1); expect(subscriptionCount(grammarRegistry)).toBe(3); buffer.destroy(); expect(retainedBufferCount(grammarRegistry)).toBe(0); expect(subscriptionCount(grammarRegistry)).toBe(1); expect(buffer.emitter.getTotalListenerCount()).toBe(0); disposable.dispose(); expect(retainedBufferCount(grammarRegistry)).toBe(0); expect(subscriptionCount(grammarRegistry)).toBe(1); }); it('does not retain the buffer when the grammar registry is destroyed', () => { const buffer = new TextBuffer(); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.maintainLanguageMode(buffer); expect(retainedBufferCount(grammarRegistry)).toBe(1); expect(subscriptionCount(grammarRegistry)).toBe(3); grammarRegistry.clear(); expect(retainedBufferCount(grammarRegistry)).toBe(0); expect(subscriptionCount(grammarRegistry)).toBe(1); expect(buffer.emitter.getTotalListenerCount()).toBe(0); }); }); describe('.selectGrammar(filePath)', () => { it('always returns a grammar', () => { const registry = new GrammarRegistry({ config: atom.config }); expect(registry.selectGrammar().scopeName).toBe( 'text.plain.null-grammar' ); }); it('selects the text.plain grammar over the null grammar', async () => { await atom.packages.activatePackage('language-text'); expect(atom.grammars.selectGrammar('test.txt').scopeName).toBe( 'text.plain' ); }); it('selects a grammar based on the file path case insensitively', async () => { await atom.packages.activatePackage('language-coffee-script'); expect(atom.grammars.selectGrammar('/tmp/source.coffee').scopeName).toBe( 'source.coffee' ); expect(atom.grammars.selectGrammar('/tmp/source.COFFEE').scopeName).toBe( 'source.coffee' ); }); describe('on Windows', () => { let originalPlatform; beforeEach(() => { originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'win32' }); }); afterEach(() => { Object.defineProperty(process, 'platform', { value: originalPlatform }); }); it('normalizes back slashes to forward slashes when matching the fileTypes', async () => { await atom.packages.activatePackage('language-git'); expect( atom.grammars.selectGrammar('something\\.git\\config').scopeName ).toBe('source.git-config'); }); }); it("can use the filePath to load the correct grammar based on the grammar's filetype", async () => { await atom.packages.activatePackage('language-git'); await atom.packages.activatePackage('language-javascript'); await atom.packages.activatePackage('language-ruby'); expect(atom.grammars.selectGrammar('file.js').name).toBe('JavaScript'); // based on extension (.js) expect( atom.grammars.selectGrammar(path.join(temp.dir, '.git', 'config')).name ).toBe('Git Config'); // based on end of the path (.git/config) expect(atom.grammars.selectGrammar('Rakefile').name).toBe('Ruby'); // based on the file's basename (Rakefile) expect(atom.grammars.selectGrammar('curb').name).toBe('Null Grammar'); expect(atom.grammars.selectGrammar('/hu.git/config').name).toBe( 'Null Grammar' ); }); it("uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", async () => { await atom.packages.activatePackage('language-javascript'); await atom.packages.activatePackage('language-ruby'); const filePath = require.resolve('./fixtures/shebang'); expect(atom.grammars.selectGrammar(filePath).name).toBe('Ruby'); }); it('uses the number of newlines in the first line regex to determine the number of lines to test against', async () => { await atom.packages.activatePackage('language-property-list'); await atom.packages.activatePackage('language-coffee-script'); let fileContent = 'first-line\n'; expect( atom.grammars.selectGrammar('dummy.coffee', fileContent).name ).toBe('CoffeeScript'); fileContent = ''; expect( atom.grammars.selectGrammar('grammar.tmLanguage', fileContent).name ).toBe('Null Grammar'); fileContent += '\n'; expect( atom.grammars.selectGrammar('grammar.tmLanguage', fileContent).name ).toBe('Property List (XML)'); }); it("doesn't read the file when the file contents are specified", async () => { await atom.packages.activatePackage('language-ruby'); const filePath = require.resolve('./fixtures/shebang'); const filePathContents = fs.readFileSync(filePath, 'utf8'); spyOn(fs, 'read').andCallThrough(); expect(atom.grammars.selectGrammar(filePath, filePathContents).name).toBe( 'Ruby' ); expect(fs.read).not.toHaveBeenCalled(); }); describe('when multiple grammars have matching fileTypes', () => { it('selects the grammar with the longest fileType match', () => { const grammarPath1 = temp.path({ suffix: '.json' }); fs.writeFileSync( grammarPath1, JSON.stringify({ name: 'test1', scopeName: 'source1', fileTypes: ['test'] }) ); const grammar1 = atom.grammars.loadGrammarSync(grammarPath1); expect(atom.grammars.selectGrammar('more.test', '')).toBe(grammar1); fs.removeSync(grammarPath1); const grammarPath2 = temp.path({ suffix: '.json' }); fs.writeFileSync( grammarPath2, JSON.stringify({ name: 'test2', scopeName: 'source2', fileTypes: ['test', 'more.test'] }) ); const grammar2 = atom.grammars.loadGrammarSync(grammarPath2); expect(atom.grammars.selectGrammar('more.test', '')).toBe(grammar2); return fs.removeSync(grammarPath2); }); }); it('favors non-bundled packages when breaking scoring ties', async () => { await atom.packages.activatePackage('language-ruby'); await atom.packages.activatePackage( path.join(__dirname, 'fixtures', 'packages', 'package-with-rb-filetype') ); 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', () => { it('does not throw an exception (regression)', () => { expect(() => atom.grammars.selectGrammar(null, '#!/usr/bin/ruby') ).not.toThrow(); expect(() => atom.grammars.selectGrammar(null, '')).not.toThrow(); expect(() => atom.grammars.selectGrammar(null, null)).not.toThrow(); }); }); describe('when the user has custom grammar file types', () => { it('considers the custom file types as well as those defined in the grammar', async () => { await atom.packages.activatePackage('language-ruby'); atom.config.set('core.customFileTypes', { 'source.ruby': ['Cheffile'] }); expect( atom.grammars.selectGrammar('build/Cheffile', 'cookbook "postgres"') .scopeName ).toBe('source.ruby'); }); it('favors user-defined file types over built-in ones of equal length', async () => { await atom.packages.activatePackage('language-ruby'); await atom.packages.activatePackage('language-coffee-script'); atom.config.set('core.customFileTypes', { 'source.coffee': ['Rakefile'], 'source.ruby': ['Cakefile'] }); expect(atom.grammars.selectGrammar('Rakefile', '').scopeName).toBe( 'source.coffee' ); expect(atom.grammars.selectGrammar('Cakefile', '').scopeName).toBe( 'source.ruby' ); }); it('favors user-defined file types over grammars with matching first-line-regexps', async () => { await atom.packages.activatePackage('language-ruby'); await atom.packages.activatePackage('language-javascript'); atom.config.set('core.customFileTypes', { 'source.ruby': ['bootstrap'] }); expect( atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node') .scopeName ).toBe('source.ruby'); }); }); it('favors a grammar with a matching file type over one with m matching first line pattern', async () => { await atom.packages.activatePackage('language-ruby'); await atom.packages.activatePackage('language-javascript'); expect( atom.grammars.selectGrammar('foo.rb', '#!/usr/bin/env node').scopeName ).toBe('source.ruby'); }); describe('tree-sitter vs text-mate', () => { it('favors a text-mate grammar over a tree-sitter grammar when `core.useTreeSitterParsers` is false', () => { atom.config.set('core.useTreeSitterParsers', false, { scopeSelector: '.source.js' }); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); const grammar = grammarRegistry.selectGrammar('test.js'); expect(grammar.scopeName).toBe('source.js'); expect(grammar instanceof FirstMate.Grammar).toBe(true); }); it('favors a tree-sitter grammar over a text-mate grammar when `core.useTreeSitterParsers` is true', () => { atom.config.set('core.useTreeSitterParsers', true); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); const grammar = grammarRegistry.selectGrammar('test.js'); expect(grammar instanceof TreeSitterGrammar).toBe(true); }); it('only favors a tree-sitter grammar if it actually matches in some way (regression)', () => { atom.config.set('core.useTreeSitterParsers', true); grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); const grammar = grammarRegistry.selectGrammar('test', ''); expect(grammar.name).toBe('Null Grammar'); }); }); describe('tree-sitter grammars with content regexes', () => { it('recognizes C++ header files', () => { atom.config.set('core.useTreeSitterParsers', true); grammarRegistry.loadGrammarSync( require.resolve('language-c/grammars/tree-sitter-c.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-c/grammars/tree-sitter-cpp.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-coffee-script/grammars/coffeescript.cson') ); let grammar = grammarRegistry.selectGrammar( 'test.h', dedent` #include typedef struct { void verb(); } Noun; ` ); expect(grammar.name).toBe('C'); grammar = grammarRegistry.selectGrammar( 'test.h', dedent` #include class Noun { public: void verb(); }; ` ); expect(grammar.name).toBe('C++'); // The word `class` only indicates C++ in `.h` files, not in all files. grammar = grammarRegistry.selectGrammar( 'test.coffee', dedent` module.exports = class Noun verb: -> true ` ); expect(grammar.name).toBe('CoffeeScript'); }); it('recognizes C++ files that do not match the content regex (regression)', () => { atom.config.set('core.useTreeSitterParsers', true); grammarRegistry.loadGrammarSync( require.resolve('language-c/grammars/tree-sitter-c.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-c/grammars/c++.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-c/grammars/tree-sitter-cpp.cson') ); let grammar = grammarRegistry.selectGrammar( 'test.cc', dedent` int a(); ` ); expect(grammar.name).toBe('C++'); }); it('does not apply content regexes from grammars without filetype or first line matches', () => { atom.config.set('core.useTreeSitterParsers', true); grammarRegistry.loadGrammarSync( require.resolve('language-c/grammars/tree-sitter-cpp.cson') ); let grammar = grammarRegistry.selectGrammar( '', dedent` class Foo # this is ruby, not C++ end ` ); expect(grammar.name).toBe('Null Grammar'); }); it('recognizes shell scripts with shebang lines', () => { atom.config.set('core.useTreeSitterParsers', true); grammarRegistry.loadGrammarSync( require.resolve('language-shellscript/grammars/shell-unix-bash.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-shellscript/grammars/tree-sitter-bash.cson') ); let grammar = grammarRegistry.selectGrammar( 'test.h', dedent` #!/bin/bash echo "hi" ` ); expect(grammar.name).toBe('Shell Script'); expect(grammar instanceof TreeSitterGrammar).toBeTruthy(); grammar = grammarRegistry.selectGrammar( 'test.h', dedent` # vim: set ft=bash echo "hi" ` ); expect(grammar.name).toBe('Shell Script'); expect(grammar instanceof TreeSitterGrammar).toBeTruthy(); atom.config.set('core.useTreeSitterParsers', false); grammar = grammarRegistry.selectGrammar( 'test.h', dedent` #!/bin/bash echo "hi" ` ); expect(grammar.name).toBe('Shell Script'); expect(grammar instanceof TreeSitterGrammar).toBeFalsy(); }); it('recognizes JavaScript files that use Flow', () => { atom.config.set('core.useTreeSitterParsers', true); grammarRegistry.loadGrammarSync( require.resolve( 'language-javascript/grammars/tree-sitter-javascript.cson' ) ); grammarRegistry.loadGrammarSync( require.resolve('language-typescript/grammars/tree-sitter-flow.cson') ); let grammar = grammarRegistry.selectGrammar( 'test.js', dedent` // Copyright something // @flow module.exports = function () { return 1 + 1 } ` ); expect(grammar.name).toBe('Flow JavaScript'); grammar = grammarRegistry.selectGrammar( 'test.js', dedent` module.exports = function () { return 1 + 1 } ` ); expect(grammar.name).toBe('JavaScript'); }); }); describe('text-mate grammars with content regexes', () => { it('favors grammars that match the content regex', () => { const grammar1 = { name: 'foo', fileTypes: ['foo'] }; grammarRegistry.addGrammar(grammar1); const grammar2 = { name: 'foo++', contentRegex: new OnigRegExp('.*bar'), fileTypes: ['foo'] }; grammarRegistry.addGrammar(grammar2); const grammar = grammarRegistry.selectGrammar( 'test.foo', dedent` ${'\n'.repeat(50)}bar${'\n'.repeat(50)} ` ); expect(grammar).toBe(grammar2); }); }); }); describe('.removeGrammar(grammar)', () => { it("removes the grammar, so it won't be returned by selectGrammar", async () => { await atom.packages.activatePackage('language-css'); const grammar = atom.grammars.selectGrammar('foo.css'); atom.grammars.removeGrammar(grammar); expect(atom.grammars.selectGrammar('foo.css').name).not.toBe( grammar.name ); }); }); describe('.addInjectionPoint(languageId, {type, language, content})', () => { const injectionPoint = { type: 'some_node_type', language() { return 'some_language_name'; }, content(node) { return node; } }; beforeEach(() => { atom.config.set('core.useTreeSitterParsers', true); }); it('adds an injection point to the grammar with the given id', async () => { await atom.packages.activatePackage('language-javascript'); atom.grammars.addInjectionPoint('javascript', injectionPoint); const grammar = atom.grammars.grammarForId('javascript'); expect(grammar.injectionPoints).toContain(injectionPoint); }); describe('when called before a grammar with the given id is loaded', () => { it('adds the injection point once the grammar is loaded', async () => { atom.grammars.addInjectionPoint('javascript', injectionPoint); await atom.packages.activatePackage('language-javascript'); const grammar = atom.grammars.grammarForId('javascript'); expect(grammar.injectionPoints).toContain(injectionPoint); }); }); }); describe('serialization', () => { it("persists editors' grammar overrides", async () => { const buffer1 = new TextBuffer(); const buffer2 = new TextBuffer(); grammarRegistry.loadGrammarSync( require.resolve('language-c/grammars/c.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-html/grammars/html.cson') ); grammarRegistry.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); grammarRegistry.maintainLanguageMode(buffer1); grammarRegistry.maintainLanguageMode(buffer2); grammarRegistry.assignLanguageMode(buffer1, 'source.c'); grammarRegistry.assignLanguageMode(buffer2, 'source.js'); const buffer1Copy = await TextBuffer.deserialize(buffer1.serialize()); const buffer2Copy = await TextBuffer.deserialize(buffer2.serialize()); const grammarRegistryCopy = new GrammarRegistry({ config: atom.config }); grammarRegistryCopy.deserialize( JSON.parse(JSON.stringify(grammarRegistry.serialize())) ); grammarRegistryCopy.loadGrammarSync( require.resolve('language-c/grammars/c.cson') ); grammarRegistryCopy.loadGrammarSync( require.resolve('language-html/grammars/html.cson') ); expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe(null); expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe(null); grammarRegistryCopy.maintainLanguageMode(buffer1Copy); grammarRegistryCopy.maintainLanguageMode(buffer2Copy); expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe('source.c'); expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe(null); grammarRegistryCopy.loadGrammarSync( require.resolve('language-javascript/grammars/javascript.cson') ); expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe('source.c'); expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe('source.js'); }); }); describe('when working with grammars', () => { beforeEach(async () => { await atom.packages.activatePackage('language-javascript'); }); it('returns only Tree-sitter grammars by default', async () => { const tmGrammars = atom.grammars.getGrammars(); const allGrammars = atom.grammars.getGrammars({ includeTreeSitter: true }); expect(allGrammars.length).toBeGreaterThan(tmGrammars.length); }); it('executes the foreach callback on both Tree-sitter and TextMate grammars', async () => { const numAllGrammars = atom.grammars.getGrammars({ includeTreeSitter: true }).length; let i = 0; atom.grammars.forEachGrammar(() => i++); expect(i).toBe(numAllGrammars); }); }); }); function retainedBufferCount(grammarRegistry) { return grammarRegistry.grammarScoresByBuffer.size; } function subscriptionCount(grammarRegistry) { return grammarRegistry.subscriptions.disposables.size; } ================================================ FILE: spec/gutter-container-spec.js ================================================ 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([]); })); }); ================================================ FILE: spec/gutter-spec.js ================================================ 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(); }); }); }); ================================================ FILE: spec/helpers/random.js ================================================ 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(''); } ================================================ FILE: spec/helpers/words.js ================================================ /** * 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' ]; ================================================ FILE: spec/history-manager-spec.js ================================================ const { HistoryManager, HistoryProject } = require('../src/history-manager'); const StateStore = require('../src/state-store'); describe('HistoryManager', () => { let historyManager, commandRegistry, project, stateStore; let commandDisposable, projectDisposable; beforeEach(async () => { commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']); commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']); commandRegistry.add.andReturn(commandDisposable); stateStore = new StateStore('history-manager-test', 1); await stateStore.save('history-manager', { projects: [ { paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23) }, { paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13) } ] }); projectDisposable = jasmine.createSpyObj('Disposable', ['dispose']); project = jasmine.createSpyObj('Project', ['onDidChangePaths']); project.onDidChangePaths.andCallFake(f => { project.didChangePathsListener = f; return projectDisposable; }); historyManager = new HistoryManager({ stateStore, project, commands: commandRegistry }); await historyManager.loadState(); }); afterEach(async () => { await stateStore.clear(); }); describe('constructor', () => { it("registers the 'clear-project-history' command function", () => { expect(commandRegistry.add).toHaveBeenCalled(); const cmdCall = commandRegistry.add.calls[0]; expect(cmdCall.args.length).toBe(3); expect(cmdCall.args[0]).toBe('atom-workspace'); expect(typeof cmdCall.args[1]['application:clear-project-history']).toBe( 'function' ); }); describe('getProjects', () => { it('returns an array of HistoryProjects', () => { expect(historyManager.getProjects()).toEqual([ new HistoryProject( ['/1', 'c:\\2'], new Date(2016, 9, 17, 17, 16, 23) ), new HistoryProject(['/test'], new Date(2016, 9, 17, 11, 12, 13)) ]); }); it('returns an array of HistoryProjects that is not mutable state', () => { const firstProjects = historyManager.getProjects(); firstProjects.pop(); firstProjects[0].path = 'modified'; const secondProjects = historyManager.getProjects(); expect(secondProjects.length).toBe(2); expect(secondProjects[0].path).not.toBe('modified'); }); }); describe('clearProjects', () => { it('clears the list of projects', async () => { expect(historyManager.getProjects().length).not.toBe(0); await historyManager.clearProjects(); expect(historyManager.getProjects().length).toBe(0); }); it('saves the state', async () => { await historyManager.clearProjects(); const historyManager2 = new HistoryManager({ stateStore, project, commands: commandRegistry }); await historyManager2.loadState(); expect(historyManager.getProjects().length).toBe(0); }); it('fires the onDidChangeProjects event', async () => { const didChangeSpy = jasmine.createSpy(); historyManager.onDidChangeProjects(didChangeSpy); await historyManager.clearProjects(); expect(historyManager.getProjects().length).toBe(0); expect(didChangeSpy).toHaveBeenCalled(); }); }); it('listens to project.onDidChangePaths adding a new project', () => { const start = new Date(); project.didChangePathsListener(['/a/new', '/path/or/two']); const projects = historyManager.getProjects(); expect(projects.length).toBe(3); expect(projects[0].paths).toEqual(['/a/new', '/path/or/two']); expect(projects[0].lastOpened).not.toBeLessThan(start); }); it('listens to project.onDidChangePaths updating an existing project', () => { const start = new Date(); project.didChangePathsListener(['/test']); const projects = historyManager.getProjects(); expect(projects.length).toBe(2); expect(projects[0].paths).toEqual(['/test']); expect(projects[0].lastOpened).not.toBeLessThan(start); }); }); describe('loadState', () => { it('defaults to an empty array if no state', async () => { await stateStore.clear(); await historyManager.loadState(); expect(historyManager.getProjects()).toEqual([]); }); it('defaults to an empty array if no projects', async () => { await stateStore.save('history-manager', {}); await historyManager.loadState(); expect(historyManager.getProjects()).toEqual([]); }); }); describe('addProject', () => { it('adds a new project to the end', async () => { const date = new Date(2010, 10, 9, 8, 7, 6); await historyManager.addProject(['/a/b'], date); const projects = historyManager.getProjects(); expect(projects.length).toBe(3); expect(projects[2].paths).toEqual(['/a/b']); expect(projects[2].lastOpened).toBe(date); }); it('adds a new project to the start', async () => { const date = new Date(); await historyManager.addProject(['/so/new'], date); const projects = historyManager.getProjects(); expect(projects.length).toBe(3); expect(projects[0].paths).toEqual(['/so/new']); expect(projects[0].lastOpened).toBe(date); }); it('updates an existing project and moves it to the start', async () => { const date = new Date(); await historyManager.addProject(['/test'], date); const projects = historyManager.getProjects(); expect(projects.length).toBe(2); expect(projects[0].paths).toEqual(['/test']); expect(projects[0].lastOpened).toBe(date); }); it('fires the onDidChangeProjects event when adding a project', async () => { const didChangeSpy = jasmine.createSpy(); const beforeCount = historyManager.getProjects().length; historyManager.onDidChangeProjects(didChangeSpy); await historyManager.addProject(['/test-new'], new Date()); expect(didChangeSpy).toHaveBeenCalled(); expect(historyManager.getProjects().length).toBe(beforeCount + 1); }); it('fires the onDidChangeProjects event when updating a project', async () => { const didChangeSpy = jasmine.createSpy(); const beforeCount = historyManager.getProjects().length; historyManager.onDidChangeProjects(didChangeSpy); await historyManager.addProject(['/test'], new Date()); expect(didChangeSpy).toHaveBeenCalled(); expect(historyManager.getProjects().length).toBe(beforeCount); }); }); describe('getProject', () => { it('returns a project that matches the paths', () => { const project = historyManager.getProject(['/1', 'c:\\2']); expect(project).not.toBeNull(); expect(project.paths).toEqual(['/1', 'c:\\2']); }); it("returns null when it can't find the project", () => { const project = historyManager.getProject(['/1']); expect(project).toBeNull(); }); }); describe('saveState', () => { let savedHistory; beforeEach(() => { // historyManager.saveState is spied on globally to prevent specs from // modifying the shared project history. Since these tests depend on // saveState, we unspy it but in turn spy on the state store instead // so that no data is actually stored to it. jasmine.unspy(historyManager, 'saveState'); spyOn(historyManager.stateStore, 'save').andCallFake((name, history) => { savedHistory = history; return Promise.resolve(); }); }); it('saves the state', async () => { await historyManager.addProject(['/save/state']); await historyManager.saveState(); const historyManager2 = new HistoryManager({ stateStore, project, commands: commandRegistry }); spyOn(historyManager2.stateStore, 'load').andCallFake(name => Promise.resolve(savedHistory) ); await historyManager2.loadState(); expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']); }); }); }); ================================================ FILE: spec/integration/helpers/atom-launcher.sh ================================================ #!/bin/bash # This script wraps the `Atom` binary, allowing the `chromedriver` server to # execute it with positional arguments and environment variables. `chromedriver` # only allows 'switches' to be specified when starting a browser, not positional # arguments, so this script accepts the following special switches: # # * `atom-path`: The path to the `Atom` binary. # * `atom-args`: A space-separated list of positional arguments to pass to Atom. # * `atom-env`: A space-separated list of key=value pairs representing environment # variables to set for Atom. # # Any other switches will be passed through to `Atom`. atom_path="" atom_switches=() atom_args=() for arg in "$@"; do case $arg in --atom-path=*) atom_path="${arg#*=}" ;; --atom-args=*) atom_arg_string="${arg#*=}" for atom_arg in $atom_arg_string; do atom_args+=($atom_arg) done ;; --atom-env=*) atom_env_string="${arg#*=}" for atom_env_pair in $atom_env_string; do export $atom_env_pair done ;; *) atom_switches+=($arg) ;; esac done echo "Launching Atom" >&2 echo "${atom_path}" ${atom_args[@]} ${atom_switches[@]} >&2 exec "${atom_path}" ${atom_args[@]} ${atom_switches[@]} ================================================ FILE: spec/integration/helpers/start-atom.js ================================================ const path = require('path'); const http = require('http'); const temp = require('temp').track(); const { remote } = require('electron'); const { once } = require('underscore-plus'); const { spawn } = require('child_process'); const webdriverio = require('../../../script/node_modules/webdriverio'); const AtomPath = remote.process.argv[0]; const AtomLauncherPath = path.join( __dirname, '..', 'helpers', 'atom-launcher.sh' ); const ChromedriverPath = path.resolve( __dirname, '..', '..', '..', 'script', 'node_modules', 'electron-chromedriver', 'bin', 'chromedriver' ); const ChromedriverPort = 8082; const ChromedriverURLBase = '/wd/hub'; const ChromedriverStatusURL = `http://localhost:${ChromedriverPort}${ChromedriverURLBase}/status`; const chromeDriverUp = done => { const checkStatus = () => http .get(ChromedriverStatusURL, response => { if (response.statusCode === 200) { done(); } else { chromeDriverUp(done); } }) .on('error', () => chromeDriverUp(done)); setTimeout(checkStatus, 100); }; const chromeDriverDown = done => { const checkStatus = () => http .get(ChromedriverStatusURL, response => chromeDriverDown(done)) .on('error', done); setTimeout(checkStatus, 100); }; const buildAtomClient = async (args, env) => { const userDataDir = temp.mkdirSync('atom-user-data-dir'); const client = await webdriverio.remote({ host: 'localhost', port: ChromedriverPort, capabilities: { browserName: 'chrome', // Webdriverio will figure it out on it's own, but I will leave it in case it's helpful in the future https://webdriver.io/docs/configurationfile.html 'goog:chromeOptions': { binary: AtomLauncherPath, args: [ `atom-path=${AtomPath}`, `atom-args=${args.join(' ')}`, `atom-env=${Object.entries(env) .map(([key, value]) => `${key}=${value}`) .join(' ')}`, 'dev', 'safe', `user-data-dir=${userDataDir}` ] } } }); client.addCommand('waitForPaneItemCount', async function(count, timeout) { await this.waitUntil( () => this.execute(() => atom.workspace.getActivePane().getItems().length), timeout ); }); client.addCommand('treeViewRootDirectories', async function() { const treeViewElement = await this.$('.tree-view'); await treeViewElement.waitForExist(10000); return this.execute(() => Array.from( document.querySelectorAll('.tree-view .project-root > .header .name') ).map(element => element.dataset.path) ); }); client.addCommand('dispatchCommand', async function(command) { return this.execute( command => atom.commands.dispatch(document.activeElement, command), command ); }); return client; }; module.exports = function(args, env, fn) { let chromedriver, chromedriverLogs, chromedriverExit; runs(() => { chromedriver = spawn(ChromedriverPath, [ '--verbose', `--port=${ChromedriverPort}`, `--url-base=${ChromedriverURLBase}` ]); chromedriverLogs = []; chromedriverExit = new Promise(resolve => { let errorCode = null; chromedriver.on('exit', (code, signal) => { if (signal == null) { errorCode = code; } }); chromedriver.stderr.on('data', log => chromedriverLogs.push(log.toString()) ); chromedriver.stderr.on('close', () => resolve(errorCode)); }); }); waitsFor('webdriver to start', chromeDriverUp, 15000); waitsFor( 'tests to run', async done => { const finish = once(async () => { await client.deleteSession(); chromedriver.kill(); const errorCode = await chromedriverExit; if (errorCode != null) { jasmine.getEnv().currentSpec .fail(`Chromedriver exited with code ${errorCode}. Logs:\n${chromedriverLogs.join('\n')}`); } done(); }); let client; try { client = await buildAtomClient(args, env); } catch (error) { jasmine .getEnv() .currentSpec.fail(`Unable to build Atom client.\n${error}`); finish(); return; } try { await client.waitUntil(async function() { const handles = await this.getWindowHandles(); return handles.length > 0; }, 10000); } catch (error) { jasmine .getEnv() .currentSpec.fail(`Unable to locate windows.\n\n${error}`); finish(); return; } try { const workspaceElement = await client.$('atom-workspace'); await workspaceElement.waitForExist(10000); } catch (error) { jasmine .getEnv() .currentSpec.fail(`Unable to find workspace element.\n\n${error}`); finish(); return; } try { await fn(client); } catch (error) { jasmine.getEnv().currentSpec.fail(error); finish(); return; } finish(); }, 60000 ); waitsFor('webdriver to stop', chromeDriverDown, 15000); }; ================================================ FILE: spec/integration/smoke-spec.js ================================================ const fs = require('fs-plus'); const path = require('path'); const season = require('season'); const temp = require('temp').track(); const runAtom = require('./helpers/start-atom'); describe('Smoke Test', () => { // Fails on win32 if (process.platform !== 'darwin') { return; } const atomHome = temp.mkdirSync('atom-home'); beforeEach(() => { jasmine.useRealClock(); season.writeFileSync(path.join(atomHome, 'config.cson'), { '*': { welcome: { showOnStartup: false }, core: { telemetryConsent: 'no', disabledPackages: ['github'] } } }); }); it('can open a file in Atom and perform basic operations on it', async () => { const tempDirPath = temp.mkdirSync('empty-dir'); const filePath = path.join(tempDirPath, 'new-file'); fs.writeFileSync(filePath, '', { encoding: 'utf8' }); runAtom([tempDirPath], { ATOM_HOME: atomHome }, async client => { const roots = await client.treeViewRootDirectories(); expect(roots).toEqual([tempDirPath]); await client.execute(filePath => atom.workspace.open(filePath), filePath); const textEditorElement = await client.$('atom-text-editor'); await textEditorElement.waitForExist(5000); await client.waitForPaneItemCount(1, 1000); await textEditorElement.click(); const closestElement = await client.execute(() => document.activeElement.closest('atom-text-editor') ); expect(closestElement).not.toBeNull(); await client.keys('Hello!'); const text = await client.execute(() => atom.workspace.getActiveTextEditor().getText() ); expect(text).toBe('Hello!'); await client.dispatchCommand('editor:delete-line'); }); }); }); ================================================ FILE: spec/jasmine-junit-reporter.js ================================================ require('jasmine-reporters'); class JasmineJUnitReporter extends jasmine.JUnitXmlReporter { fullDescription(spec) { let fullDescription = spec.description; let currentSuite = spec.suite; while (currentSuite) { fullDescription = currentSuite.description + ' ' + fullDescription; currentSuite = currentSuite.parentSuite; } return fullDescription; } reportSpecResults(spec) { spec.description = this.fullDescription(spec); return super.reportSpecResults(spec); } } module.exports = { JasmineJUnitReporter }; ================================================ FILE: spec/jasmine-list-reporter.js ================================================ const { TerminalReporter } = require('jasmine-tagged'); class JasmineListReporter extends TerminalReporter { fullDescription(spec) { let fullDescription = 'it ' + spec.description; let currentSuite = spec.suite; while (currentSuite) { fullDescription = currentSuite.description + ' > ' + fullDescription; currentSuite = currentSuite.parentSuite; } return fullDescription; } reportSpecStarting(spec) { this.print_(this.fullDescription(spec) + ' '); } reportSpecResults(spec) { const result = spec.results(); if (result.skipped) { return; } let msg = ''; if (result.passed()) { msg = this.stringWithColor_('[pass]', this.color_.pass()); } else { msg = this.stringWithColor_('[FAIL]', this.color_.fail()); this.addFailureToFailures_(spec); } this.printLine_(msg); } } module.exports = { JasmineListReporter }; ================================================ FILE: spec/jasmine-test-runner.coffee ================================================ Grim = require 'grim' fs = require 'fs-plus' temp = require 'temp' path = require 'path' {ipcRenderer} = require 'electron' temp.track() module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) -> window[key] = value for key, value of require '../vendor/jasmine' require 'jasmine-tagged' # Rewrite global jasmine functions to have support for async tests. # This way packages can create async specs without having to import these from the # async-spec-helpers file. global.it = asyncifyJasmineFn global.it, 1 global.fit = asyncifyJasmineFn global.fit, 1 global.ffit = asyncifyJasmineFn global.ffit, 1 global.fffit = asyncifyJasmineFn global.fffit, 1 global.beforeEach = asyncifyJasmineFn global.beforeEach, 0 global.afterEach = asyncifyJasmineFn global.afterEach, 0 # Allow document.title to be assigned in specs without screwing up spec window title documentTitle = null Object.defineProperty document, 'title', get: -> documentTitle set: (title) -> documentTitle = title userHome = process.env.ATOM_HOME or path.join(fs.getHomeDirectory(), '.atom') atomHome = temp.mkdirSync prefix: 'atom-test-home-' if process.env.APM_TEST_PACKAGES testPackages = process.env.APM_TEST_PACKAGES.split /\s+/ fs.makeTreeSync path.join(atomHome, 'packages') for packName in testPackages userPack = path.join(userHome, 'packages', packName) loadablePack = path.join(atomHome, 'packages', packName) try fs.symlinkSync userPack, loadablePack, 'dir' catch fs.copySync userPack, loadablePack ApplicationDelegate = require '../src/application-delegate' applicationDelegate = new ApplicationDelegate() applicationDelegate.setRepresentedFilename = -> applicationDelegate.setWindowDocumentEdited = -> window.atom = buildAtomEnvironment({ applicationDelegate, window, document, configDirPath: atomHome enablePersistence: false }) require './spec-helper' disableFocusMethods() if process.env.JANKY_SHA1 or process.env.CI requireSpecs(testPath) for testPath in testPaths setSpecType('user') resolveWithExitCode = null promise = new Promise (resolve, reject) -> resolveWithExitCode = resolve jasmineEnv = jasmine.getEnv() jasmineEnv.addReporter(buildReporter({logFile, headless, resolveWithExitCode})) if process.env.TEST_JUNIT_XML_PATH {JasmineJUnitReporter} = require './jasmine-junit-reporter' process.stdout.write "Outputting JUnit XML to <#{process.env.TEST_JUNIT_XML_PATH}>\n" outputDir = path.dirname(process.env.TEST_JUNIT_XML_PATH) fileBase = path.basename(process.env.TEST_JUNIT_XML_PATH, '.xml') jasmineEnv.addReporter new JasmineJUnitReporter(outputDir, true, false, fileBase, true) jasmineEnv.setIncludedTags([process.platform]) jasmineContent = document.createElement('div') jasmineContent.setAttribute('id', 'jasmine-content') document.body.appendChild(jasmineContent) jasmineEnv.execute() promise asyncifyJasmineFn = (fn, callbackPosition) -> (args...) -> if typeof args[callbackPosition] is 'function' callback = args[callbackPosition] args[callbackPosition] = (args...) -> result = callback.apply this, args if result instanceof Promise waitsForPromise(-> result) fn.apply this, args waitsForPromise = (fn) -> promise = fn() global.waitsFor('spec promise to resolve', (done) -> promise.then(done, (error) -> jasmine.getEnv().currentSpec.fail error done() ) ) disableFocusMethods = -> ['fdescribe', 'ffdescribe', 'fffdescribe', 'fit', 'ffit', 'fffit'].forEach (methodName) -> focusMethod = window[methodName] window[methodName] = (description) -> error = new Error('Focused spec is running on CI') focusMethod description, -> throw error requireSpecs = (testPath, specType) -> if fs.isDirectorySync(testPath) for testFilePath in fs.listTreeSync(testPath) when /-spec\.(coffee|js)$/.test testFilePath require(testFilePath) # Set spec directory on spec for setting up the project in spec-helper setSpecDirectory(testPath) else require(testPath) setSpecDirectory(path.dirname(testPath)) setSpecField = (name, value) -> specs = jasmine.getEnv().currentRunner().specs() return if specs.length is 0 for index in [specs.length-1..0] break if specs[index][name]? specs[index][name] = value setSpecType = (specType) -> setSpecField('specType', specType) setSpecDirectory = (specDirectory) -> setSpecField('specDirectory', specDirectory) buildReporter = ({logFile, headless, resolveWithExitCode}) -> if headless buildTerminalReporter(logFile, resolveWithExitCode) else AtomReporter = require './atom-reporter' reporter = new AtomReporter() buildTerminalReporter = (logFile, resolveWithExitCode) -> logStream = fs.openSync(logFile, 'w') if logFile? log = (str) -> if logStream? fs.writeSync(logStream, str) else ipcRenderer.send 'write-to-stderr', str options = print: (str) -> log(str) onComplete: (runner) -> fs.closeSync(logStream) if logStream? if Grim.getDeprecationsLength() > 0 Grim.logDeprecations() resolveWithExitCode(1) return if runner.results().failedCount > 0 resolveWithExitCode(1) else resolveWithExitCode(0) if process.env.ATOM_JASMINE_REPORTER is 'list' {JasmineListReporter} = require './jasmine-list-reporter' new JasmineListReporter(options) else {TerminalReporter} = require 'jasmine-tagged' new TerminalReporter(options) ================================================ FILE: spec/keymap-extensions-spec.js ================================================ const temp = require('temp').track(); const fs = require('fs-plus'); describe('keymap-extensions', function() { beforeEach(function() { atom.keymaps.configDirPath = temp.path('atom-spec-keymap-ext'); fs.writeFileSync(atom.keymaps.getUserKeymapPath(), '#'); this.userKeymapLoaded = function() {}; atom.keymaps.onDidLoadUserKeymap(() => this.userKeymapLoaded()); }); afterEach(function() { fs.removeSync(atom.keymaps.configDirPath); atom.keymaps.destroy(); }); describe('did-load-user-keymap', () => it('fires when user keymap is loaded', function() { spyOn(this, 'userKeymapLoaded'); atom.keymaps.loadUserKeymap(); expect(this.userKeymapLoaded).toHaveBeenCalled(); })); }); ================================================ FILE: spec/main-process/atom-application.test.js ================================================ /* globals assert */ const path = require('path'); const { EventEmitter } = require('events'); const temp = require('temp').track(); const fs = require('fs-plus'); const electron = require('electron'); const sandbox = require('sinon').createSandbox(); const AtomApplication = require('../../src/main-process/atom-application'); const parseCommandLine = require('../../src/main-process/parse-command-line'); const { emitterEventPromise, conditionPromise } = require('../async-spec-helpers'); // These tests use a utility class called LaunchScenario, defined below, to manipulate AtomApplication instances that // (1) are stubbed to only simulate AtomWindow creation and (2) allow you to use a shorthand notation to assert the // application state after certain launch actions. // // Each scenario instance has access to a small set of directories and files created within a dedicated temporary // directory. For convenience, you may use short names to refer to any of its contents (their basenames, basically). // Check `LaunchScenario::init()` to see what directories and files are available. // // To create an application and its first window, call `await scenario.launch({})`. "Launch" may open multiple windows, // so it returns a Promise that resolves to an array of StubWindows. Its options argument may be created by // `parseCommandLine()` from a simulated argv string, or built by hand to include `{pathsToOpen}` and so on. // // To create additional windows, call `await scenario.open({})` with similar arguments. `LaunchScenario::open()` returns // a Promise that resolves to the opened or re-used StubWindows. The one exception is if `urlsToOpen` are provided in the open // arguments; then it resolves to an Array of StubWindows, because AtomApplication processes each URL individually. // // To ensure that the expected windows have been created, call `await scenario.assert('')` with a string specifying the // expected window contents. The specification shorthand language is as follows: // // * '[_ _]' describes a single window with no project roots and no open editors. // * '[_ 1.md]' describes a single window with no project roots and a single editor open on the file `./a/1.md` within // the LaunchScenario temporary directory. // * '[a _]' describes a single window with one project root - the directory `./a` within the LaunchScenario temporary // directory - and no open editors. // * '[a,b 1.md,2.md]' describes a single window with two project roots - the directories `./a` and `./b` - and two // open editors - `./a/1.md` and `./b/2.md`. // * '[a _] [b,c 2.md]' describes two windows, one with a project root of `./a` and no open editors, and another with // two project roots, `./b` and `./c`, and one open editor on `./b/2.md`. The windows are listed in their expected // creation order. describe('AtomApplication', function() { let scenario, sinon; if (process.env.CI) { this.timeout(10 * 1000); } beforeEach(async function() { sinon = sandbox; scenario = await LaunchScenario.create(sinon); }); afterEach(async function() { await scenario.destroy(); sinon.restore(); }); describe('command-line interface behavior', function() { describe('with no open windows', function() { // This is also the case when a user selects the application from the OS shell it('opens an empty window', async function() { await scenario.launch(parseCommandLine([])); await scenario.assert('[_ _]'); }); // This is also the case when a user clicks on a file in their file manager it('opens a file', async function() { await scenario.open(parseCommandLine(['a/1.md'])); await scenario.assert('[_ 1.md]'); }); // This is also the case when a user clicks on a folder in their file manager // (or, on macOS, drags the folder to Atom in their doc) it('opens a directory', async function() { await scenario.open(parseCommandLine(['a'])); await scenario.assert('[a _]'); }); it('opens a file with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a/1.md'])); await scenario.assert('[_ 1.md]'); }); it('opens a directory with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a'])); await scenario.assert('[a _]'); }); it('opens a file with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])); await scenario.assert('[_ 1.md]'); }); it('opens a directory with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a'])); await scenario.assert('[a _]'); }); describe('with previous window state', function() { let app; beforeEach(function() { app = scenario.addApplication({ applicationJson: { version: '1', windows: [ { projectRoots: [scenario.convertRootPath('b')] }, { projectRoots: [scenario.convertRootPath('c')] } ] } }); }); describe('with core.restorePreviousWindowsOnStart set to "no"', function() { beforeEach(function() { app.config.set('core.restorePreviousWindowsOnStart', 'no'); }); it("doesn't restore windows when launched with no arguments", async function() { await scenario.launch({ app }); await scenario.assert('[_ _]'); }); it("doesn't restore windows when launched with paths to open", async function() { await scenario.launch({ app, pathsToOpen: ['a/1.md'] }); await scenario.assert('[_ 1.md]'); }); it("doesn't restore windows when --new-window is provided", async function() { await scenario.launch({ app, newWindow: true }); await scenario.assert('[_ _]'); }); }); describe('with core.restorePreviousWindowsOnStart set to "yes"', function() { beforeEach(function() { app.config.set('core.restorePreviousWindowsOnStart', 'yes'); }); it('restores windows when launched with no arguments', async function() { await scenario.launch({ app }); await scenario.assert('[b _] [c _]'); }); it("doesn't restore windows when launched with paths to open", async function() { await scenario.launch({ app, pathsToOpen: ['a/1.md'] }); await scenario.assert('[_ 1.md]'); }); it("doesn't restore windows when --new-window is provided", async function() { await scenario.launch({ app, newWindow: true }); await scenario.assert('[_ _]'); }); }); describe('with core.restorePreviousWindowsOnStart set to "always"', function() { beforeEach(function() { app.config.set('core.restorePreviousWindowsOnStart', 'always'); }); it('restores windows when launched with no arguments', async function() { await scenario.launch({ app }); await scenario.assert('[b _] [c _]'); }); it('restores windows when launched with a project path to open', async function() { await scenario.launch({ app, pathsToOpen: ['a'] }); await scenario.assert('[b _] [c _] [a _]'); }); it('restores windows when launched with a file path to open', async function() { await scenario.launch({ app, pathsToOpen: ['a/1.md'] }); await scenario.assert('[b _] [c 1.md]'); }); it('collapses new paths into restored windows when appropriate', async function() { await scenario.launch({ app, pathsToOpen: ['b/2.md'] }); await scenario.assert('[b 2.md] [c _]'); }); it("doesn't restore windows when --new-window is provided", async function() { await scenario.launch({ app, newWindow: true }); await scenario.assert('[_ _]'); }); it("doesn't restore windows on open, just launch", async function() { await scenario.launch({ app, pathsToOpen: ['a'], newWindow: true }); await scenario.open(parseCommandLine(['b'])); await scenario.assert('[a _] [b _]'); }); }); }); describe('with unversioned application state', function() { it('reads "initialPaths" as project roots', async function() { const app = scenario.addApplication({ applicationJson: [ { initialPaths: [scenario.convertRootPath('a')] }, { initialPaths: [ scenario.convertRootPath('b'), scenario.convertRootPath('c') ] } ] }); app.config.set('core.restorePreviousWindowsOnStart', 'always'); await scenario.launch({ app }); await scenario.assert('[a _] [b,c _]'); }); it('filters file paths from project root lists', async function() { const app = scenario.addApplication({ applicationJson: [ { initialPaths: [ scenario.convertRootPath('b'), scenario.convertEditorPath('a/1.md') ] } ] }); app.config.set('core.restorePreviousWindowsOnStart', 'always'); await scenario.launch({ app }); await scenario.assert('[b _]'); }); }); }); describe('with one empty window', function() { beforeEach(async function() { await scenario.preconditions('[_ _]'); }); // This is also the case when a user selects the application from the OS shell it('opens a new, empty window', async function() { await scenario.open(parseCommandLine([])); await scenario.assert('[_ _] [_ _]'); }); // This is also the case when a user clicks on a file in their file manager it('opens a file', async function() { await scenario.open(parseCommandLine(['a/1.md'])); await scenario.assert('[_ 1.md]'); }); // This is also the case when a user clicks on a folder in their file manager it('opens a directory', async function() { await scenario.open(parseCommandLine(['a'])); await scenario.assert('[a _]'); }); it('opens a file with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a/1.md'])); await scenario.assert('[_ 1.md]'); }); it('opens a directory with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a'])); await scenario.assert('[a _]'); }); it('opens a file with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])); await scenario.assert('[_ _] [_ 1.md]'); }); it('opens a directory with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a'])); await scenario.assert('[_ _] [a _]'); }); }); describe('with one window that has a project root', function() { beforeEach(async function() { await scenario.preconditions('[a _]'); }); // This is also the case when a user selects the application from the OS shell it('opens a new, empty window', async function() { await scenario.open(parseCommandLine([])); await scenario.assert('[a _] [_ _]'); }); // This is also the case when a user clicks on a file within the project root in their file manager it('opens a file within the project root', async function() { await scenario.open(parseCommandLine(['a/1.md'])); await scenario.assert('[a 1.md]'); }); // This is also the case when a user clicks on a project root folder in their file manager it('opens a directory that matches the project root', async function() { await scenario.open(parseCommandLine(['a'])); await scenario.assert('[a _]'); }); // This is also the case when a user clicks on a file outside the project root in their file manager it('opens a file outside the project root', async function() { await scenario.open(parseCommandLine(['b/2.md'])); await scenario.assert('[a 2.md]'); }); // This is also the case when a user clicks on a new folder in their file manager it('opens a directory other than the project root', async function() { await scenario.open(parseCommandLine(['b'])); await scenario.assert('[a _] [b _]'); }); it('opens a file within the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a/1.md'])); await scenario.assert('[a 1.md]'); }); it('opens a directory that matches the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a'])); await scenario.assert('[a _]'); }); it('opens a file outside the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'b/2.md'])); await scenario.assert('[a 2.md]'); }); it('opens a directory other than the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'b'])); await scenario.assert('[a,b _]'); }); it('opens a file within the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])); await scenario.assert('[a _] [_ 1.md]'); }); it('opens a directory that matches the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a'])); await scenario.assert('[a _] [a _]'); }); it('opens a file outside the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'b/2.md'])); await scenario.assert('[a _] [_ 2.md]'); }); it('opens a directory other than the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'b'])); await scenario.assert('[a _] [b _]'); }); }); describe('with two windows, one with a project root and one empty', function() { beforeEach(async function() { await scenario.preconditions('[a _] [_ _]'); }); // This is also the case when a user selects the application from the OS shell it('opens a new, empty window', async function() { await scenario.open(parseCommandLine([])); await scenario.assert('[a _] [_ _] [_ _]'); }); // This is also the case when a user clicks on a file within the project root in their file manager it('opens a file within the project root', async function() { await scenario.open(parseCommandLine(['a/1.md'])); await scenario.assert('[a 1.md] [_ _]'); }); // This is also the case when a user clicks on a project root folder in their file manager it('opens a directory that matches the project root', async function() { await scenario.open(parseCommandLine(['a'])); await scenario.assert('[a _] [_ _]'); }); // This is also the case when a user clicks on a file outside the project root in their file manager it('opens a file outside the project root', async function() { await scenario.open(parseCommandLine(['b/2.md'])); await scenario.assert('[a _] [_ 2.md]'); }); // This is also the case when a user clicks on a new folder in their file manager it('opens a directory other than the project root', async function() { await scenario.open(parseCommandLine(['b'])); await scenario.assert('[a _] [b _]'); }); it('opens a file within the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a/1.md'])); await scenario.assert('[a 1.md] [_ _]'); }); it('opens a directory that matches the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a'])); await scenario.assert('[a _] [_ _]'); }); it('opens a file outside the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'b/2.md'])); await scenario.assert('[a _] [_ 2.md]'); }); it('opens a directory other than the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'b'])); await scenario.assert('[a _] [b _]'); }); it('opens a file within the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])); await scenario.assert('[a _] [_ _] [_ 1.md]'); }); it('opens a directory that matches the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a'])); await scenario.assert('[a _] [_ _] [a _]'); }); it('opens a file outside the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'b/2.md'])); await scenario.assert('[a _] [_ _] [_ 2.md]'); }); it('opens a directory other than the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'b'])); await scenario.assert('[a _] [_ _] [b _]'); }); }); describe('with two windows, one empty and one with a project root', function() { beforeEach(async function() { await scenario.preconditions('[_ _] [a _]'); }); // This is also the case when a user selects the application from the OS shell it('opens a new, empty window', async function() { await scenario.open(parseCommandLine([])); await scenario.assert('[_ _] [a _] [_ _]'); }); // This is also the case when a user clicks on a file within the project root in their file manager it('opens a file within the project root', async function() { await scenario.open(parseCommandLine(['a/1.md'])); await scenario.assert('[_ _] [a 1.md]'); }); // This is also the case when a user clicks on a project root folder in their file manager it('opens a directory that matches the project root', async function() { await scenario.open(parseCommandLine(['a'])); await scenario.assert('[_ _] [a _]'); }); // This is also the case when a user clicks on a file outside the project root in their file manager it('opens a file outside the project root', async function() { await scenario.open(parseCommandLine(['b/2.md'])); await scenario.assert('[_ 2.md] [a _]'); }); // This is also the case when a user clicks on a new folder in their file manager it('opens a directory other than the project root', async function() { await scenario.open(parseCommandLine(['b'])); await scenario.assert('[b _] [a _]'); }); it('opens a file within the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a/1.md'])); await scenario.assert('[_ _] [a 1.md]'); }); it('opens a directory that matches the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'a'])); await scenario.assert('[_ _] [a _]'); }); it('opens a file outside the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'b/2.md'])); await scenario.assert('[_ _] [a 2.md]'); }); it('opens a directory other than the project root with --add', async function() { await scenario.open(parseCommandLine(['--add', 'b'])); await scenario.assert('[_ _] [a,b _]'); }); it('opens a file within the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a/1.md'])); await scenario.assert('[_ _] [a _] [_ 1.md]'); }); it('opens a directory that matches the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'a'])); await scenario.assert('[_ _] [a _] [a _]'); }); it('opens a file outside the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'b/2.md'])); await scenario.assert('[_ _] [a _] [_ 2.md]'); }); it('opens a directory other than the project root with --new-window', async function() { await scenario.open(parseCommandLine(['--new-window', 'b'])); await scenario.assert('[_ _] [a _] [b _]'); }); }); describe('--wait', function() { it('kills the specified pid after a newly-opened window is closed', async function() { const [w0] = await scenario.launch( parseCommandLine(['--new-window', '--wait', '--pid', '101']) ); const w1 = await scenario.open( parseCommandLine(['--new-window', '--wait', '--pid', '202']) ); assert.lengthOf(scenario.killedPids, 0); w0.browserWindow.emit('closed'); assert.deepEqual(scenario.killedPids, [101]); w1.browserWindow.emit('closed'); assert.deepEqual(scenario.killedPids, [101, 202]); }); it('kills the specified pid after all newly-opened files in an existing window are closed', async function() { const [w] = await scenario.launch( parseCommandLine(['--new-window', 'a']) ); await scenario.open( parseCommandLine([ '--add', '--wait', '--pid', '303', 'a/1.md', 'b/2.md' ]) ); await scenario.assert('[a 1.md,2.md]'); assert.lengthOf(scenario.killedPids, 0); scenario .getApplication(0) .windowDidClosePathWithWaitSession( w, scenario.convertEditorPath('b/2.md') ); assert.lengthOf(scenario.killedPids, 0); scenario .getApplication(0) .windowDidClosePathWithWaitSession( w, scenario.convertEditorPath('a/1.md') ); assert.deepEqual(scenario.killedPids, [303]); }); it('kills the specified pid after a newly-opened directory in an existing window is closed', async function() { const [w] = await scenario.launch( parseCommandLine(['--new-window', 'a']) ); await scenario.open( parseCommandLine(['--add', '--wait', '--pid', '404', 'b']) ); await scenario.assert('[a,b _]'); assert.lengthOf(scenario.killedPids, 0); scenario .getApplication(0) .windowDidClosePathWithWaitSession(w, scenario.convertRootPath('b')); assert.deepEqual(scenario.killedPids, [404]); }); }); describe('atom:// URLs', function() { describe('with a package-name host', function() { it("loads the package's urlMain in a new window", async function() { await scenario.launch({}); const app = scenario.getApplication(0); app.packages = { getAvailablePackageMetadata: () => [ { name: 'package-with-url-main', urlMain: 'some/url-main' } ], resolvePackagePath: () => path.resolve('dot-atom/package-with-url-main') }; const [w1, w2] = await scenario.open( parseCommandLine([ 'atom://package-with-url-main/test1', 'atom://package-with-url-main/test2' ]) ); assert.strictEqual( w1.loadSettings.windowInitializationScript, path.resolve('dot-atom/package-with-url-main/some/url-main') ); assert.strictEqual( w1.loadSettings.urlToOpen, 'atom://package-with-url-main/test1' ); assert.strictEqual( w2.loadSettings.windowInitializationScript, path.resolve('dot-atom/package-with-url-main/some/url-main') ); assert.strictEqual( w2.loadSettings.urlToOpen, 'atom://package-with-url-main/test2' ); }); it('sends a URI message to the most recently focused non-spec window', async function() { const [w0] = await scenario.launch({}); const w1 = await scenario.open(parseCommandLine(['--new-window'])); const w2 = await scenario.open(parseCommandLine(['--new-window'])); const w3 = await scenario.open( parseCommandLine(['--test', 'a/1.md']) ); const app = scenario.getApplication(0); app.packages = { getAvailablePackageMetadata: () => [] }; const [uw] = await scenario.open( parseCommandLine(['atom://package-without-url-main/test']) ); assert.strictEqual(uw, w2); assert.isTrue( w2.sendURIMessage.calledWith('atom://package-without-url-main/test') ); assert.strictEqual(w2.focus.callCount, 2); for (const other of [w0, w1, w3]) { assert.isFalse(other.sendURIMessage.called); } }); it('creates a new window and sends a URI message to it once it loads', async function() { const [w0] = await scenario.launch( parseCommandLine(['--test', 'a/1.md']) ); const app = scenario.getApplication(0); app.packages = { getAvailablePackageMetadata: () => [] }; const [uw] = await scenario.open( parseCommandLine(['atom://package-without-url-main/test']) ); assert.notStrictEqual(uw, w0); assert.strictEqual( uw.loadSettings.windowInitializationScript, path.resolve( __dirname, '../../src/initialize-application-window.js' ) ); uw.emit('window:loaded'); assert.isTrue( uw.sendURIMessage.calledWith('atom://package-without-url-main/test') ); }); }); describe('with a "core" host', function() { it('sends a URI message to the most recently focused non-spec window that owns the open locations', async function() { const [w0] = await scenario.launch(parseCommandLine(['a'])); const w1 = await scenario.open( parseCommandLine(['--new-window', 'a']) ); const w2 = await scenario.open( parseCommandLine(['--new-window', 'b']) ); const uri = `atom://core/open/file?filename=${encodeURIComponent( scenario.convertEditorPath('a/1.md') )}`; const [uw] = await scenario.open(parseCommandLine([uri])); assert.strictEqual(uw, w1); assert.isTrue(w1.sendURIMessage.calledWith(uri)); for (const other of [w0, w2]) { assert.isFalse(other.sendURIMessage.called); } }); it('creates a new window and sends a URI message to it once it loads', async function() { const [w0] = await scenario.launch( parseCommandLine(['--test', 'a/1.md']) ); const uri = `atom://core/open/file?filename=${encodeURIComponent( scenario.convertEditorPath('b/2.md') )}`; const [uw] = await scenario.open(parseCommandLine([uri])); assert.notStrictEqual(uw, w0); uw.emit('window:loaded'); assert.isTrue(uw.sendURIMessage.calledWith(uri)); }); }); }); it('opens a file to a specific line number', async function() { await scenario.open(parseCommandLine(['a/1.md:10'])); await scenario.assert('[_ 1.md]'); const w = scenario.getWindow(0); assert.lengthOf(w._locations, 1); assert.strictEqual(w._locations[0].initialLine, 9); assert.isNull(w._locations[0].initialColumn); }); it('opens a file to a specific line number and column', async function() { await scenario.open(parseCommandLine(['b/2.md:12:5'])); await scenario.assert('[_ 2.md]'); const w = scenario.getWindow(0); assert.lengthOf(w._locations, 1); assert.strictEqual(w._locations[0].initialLine, 11); assert.strictEqual(w._locations[0].initialColumn, 4); }); it('opens a directory with a non-file protocol', async function() { await scenario.open( parseCommandLine(['remote://server:3437/some/directory/path']) ); const w = scenario.getWindow(0); assert.lengthOf(w._locations, 1); assert.strictEqual( w._locations[0].pathToOpen, 'remote://server:3437/some/directory/path' ); assert.isFalse(w._locations[0].exists); assert.isFalse(w._locations[0].isDirectory); assert.isFalse(w._locations[0].isFile); }); it('truncates trailing whitespace and colons', async function() { await scenario.open(parseCommandLine(['b/2.md:: '])); await scenario.assert('[_ 2.md]'); const w = scenario.getWindow(0); assert.lengthOf(w._locations, 1); assert.isNull(w._locations[0].initialLine); assert.isNull(w._locations[0].initialColumn); }); it('disregards test and benchmark windows', async function() { await scenario.launch(parseCommandLine(['--test', 'b'])); await scenario.open(parseCommandLine(['--new-window'])); await scenario.open(parseCommandLine(['--test', 'c'])); await scenario.open(parseCommandLine(['--benchmark', 'b'])); await scenario.open(parseCommandLine(['a/1.md'])); // Test and benchmark StubWindows are visible as empty editor windows here await scenario.assert('[_ _] [_ 1.md] [_ _] [_ _]'); }); }); if (process.platform === 'darwin' || process.platform === 'win32') { it('positions new windows at an offset from the previous window', async function() { const [w0] = await scenario.launch(parseCommandLine(['a'])); w0.setSize(400, 400); const d0 = w0.getDimensions(); const w1 = await scenario.open(parseCommandLine(['b'])); const d1 = w1.getDimensions(); assert.isAbove(d1.x, d0.x); assert.isAbove(d1.y, d0.y); }); } if (process.platform === 'darwin') { describe('with no windows open', function() { let app; beforeEach(async function() { const [w] = await scenario.launch(parseCommandLine([])); app = scenario.getApplication(0); app.removeWindow(w); sinon.stub(app, 'promptForPathToOpen'); global.atom = { workspace: { getActiveTextEditor() {} } }; }); it('opens a new file', function() { app.emit('application:open-file'); assert.isTrue( app.promptForPathToOpen.calledWith('file', { devMode: false, safeMode: false, window: null }) ); }); it('opens a new directory', function() { app.emit('application:open-folder'); assert.isTrue( app.promptForPathToOpen.calledWith('folder', { devMode: false, safeMode: false, window: null }) ); }); it('opens a new file or directory', function() { app.emit('application:open'); assert.isTrue( app.promptForPathToOpen.calledWith('all', { devMode: false, safeMode: false, window: null }) ); }); it('reopens a project in a new window', async function() { const paths = scenario.convertPaths(['a', 'b']); app.emit('application:reopen-project', { paths }); await conditionPromise(() => app.getAllWindows().length > 0); assert.deepEqual( app.getAllWindows().map(w => Array.from(w._rootPaths)), [paths] ); }); }); } describe('existing application re-use', function() { let createApplication; const version = electron.app.getVersion(); beforeEach(function() { createApplication = async options => { options.version = version; const app = scenario.addApplication(options); await app.listenForArgumentsFromNewProcess(options); await app.launch(options); return app; }; }); it('creates a new application when no socket is present', async function() { const app0 = await AtomApplication.open({ createApplication, version }); await app0.deleteSocketSecretFile(); const app1 = await AtomApplication.open({ createApplication, version }); assert.isNotNull(app1); assert.notStrictEqual(app0, app1); }); it('creates a new application for spec windows', async function() { const app0 = await AtomApplication.open({ createApplication, version }); const app1 = await AtomApplication.open({ createApplication, version, ...parseCommandLine(['--test', 'a']) }); assert.isNotNull(app1); assert.notStrictEqual(app0, app1); }); it('sends a request to an existing application when a socket is present', async function() { const app0 = await AtomApplication.open({ createApplication, version }); assert.lengthOf(app0.getAllWindows(), 1); const app1 = await AtomApplication.open({ createApplication, version, ...parseCommandLine(['--new-window']) }); assert.isNull(app1); assert.isTrue(electron.app.quit.called); await conditionPromise(() => app0.getAllWindows().length === 2); await scenario.assert('[_ _] [_ _]'); }); }); describe('IPC handling', function() { let w0, w1, w2, app; beforeEach(async function() { w0 = (await scenario.launch(parseCommandLine(['a'])))[0]; w1 = await scenario.open(parseCommandLine(['--new-window'])); w2 = await scenario.open(parseCommandLine(['--new-window', 'b'])); app = scenario.getApplication(0); sinon.spy(app, 'openPaths'); sinon .stub(app, 'promptForPath') .callsFake((_type, callback, defaultPath) => callback([defaultPath])); }); // This is the IPC message used to handle: // * application:reopen-project // * choosing "open in new window" when adding a folder that has previously saved state // * drag and drop // * deprecated call links in deprecation-cop // * other direct callers of `atom.open()` it('"open" opens a fixed path by the standard opening rules', async function() { sinon.stub(app, 'atomWindowForEvent').callsFake(() => w1); electron.ipcMain.emit( 'open', {}, { pathsToOpen: [scenario.convertEditorPath('a/1.md')] } ); await app.openPaths.lastCall.returnValue; await scenario.assert('[a 1.md] [_ _] [b _]'); electron.ipcMain.emit( 'open', {}, { pathsToOpen: [scenario.convertRootPath('c')] } ); await app.openPaths.lastCall.returnValue; await scenario.assert('[a 1.md] [c _] [b _]'); electron.ipcMain.emit( 'open', {}, { pathsToOpen: [scenario.convertRootPath('d')], here: true } ); await app.openPaths.lastCall.returnValue; await scenario.assert('[a 1.md] [c,d _] [b _]'); }); it('"open" without any option open the prompt for selecting a path', async function() { sinon.stub(app, 'atomWindowForEvent').callsFake(() => w1); electron.ipcMain.emit('open', {}); assert.strictEqual(app.promptForPath.lastCall.args[0], 'all'); }); it('"open-chosen-any" opens a file in the sending window', async function() { sinon.stub(app, 'atomWindowForEvent').callsFake(() => w2); electron.ipcMain.emit( 'open-chosen-any', {}, scenario.convertEditorPath('a/1.md') ); await conditionPromise(() => app.openPaths.called); await app.openPaths.lastCall.returnValue; await scenario.assert('[a _] [_ _] [b 1.md]'); assert.isTrue(app.promptForPath.called); assert.strictEqual(app.promptForPath.lastCall.args[0], 'all'); }); it('"open-chosen-any" opens a directory by the standard opening rules', async function() { sinon.stub(app, 'atomWindowForEvent').callsFake(() => w1); // Open unrecognized directory in empty window electron.ipcMain.emit( 'open-chosen-any', {}, scenario.convertRootPath('c') ); await conditionPromise(() => app.openPaths.callCount > 0); await app.openPaths.lastCall.returnValue; await scenario.assert('[a _] [c _] [b _]'); assert.strictEqual(app.promptForPath.callCount, 1); assert.strictEqual(app.promptForPath.lastCall.args[0], 'all'); // Open unrecognized directory in new window electron.ipcMain.emit( 'open-chosen-any', {}, scenario.convertRootPath('d') ); await conditionPromise(() => app.openPaths.callCount > 1); await app.openPaths.lastCall.returnValue; await scenario.assert('[a _] [c _] [b _] [d _]'); assert.strictEqual(app.promptForPath.callCount, 2); assert.strictEqual(app.promptForPath.lastCall.args[0], 'all'); // Open recognized directory in existing window electron.ipcMain.emit( 'open-chosen-any', {}, scenario.convertRootPath('a') ); await conditionPromise(() => app.openPaths.callCount > 2); await app.openPaths.lastCall.returnValue; await scenario.assert('[a _] [c _] [b _] [d _]'); assert.strictEqual(app.promptForPath.callCount, 3); assert.strictEqual(app.promptForPath.lastCall.args[0], 'all'); }); it('"open-chosen-file" opens a file chooser and opens the chosen file in the sending window', async function() { sinon.stub(app, 'atomWindowForEvent').callsFake(() => w0); electron.ipcMain.emit( 'open-chosen-file', {}, scenario.convertEditorPath('b/2.md') ); await app.openPaths.lastCall.returnValue; await scenario.assert('[a 2.md] [_ _] [b _]'); assert.isTrue(app.promptForPath.called); assert.strictEqual(app.promptForPath.lastCall.args[0], 'file'); }); it('"open-chosen-folder" opens a directory chooser and opens the chosen directory', async function() { sinon.stub(app, 'atomWindowForEvent').callsFake(() => w0); electron.ipcMain.emit( 'open-chosen-folder', {}, scenario.convertRootPath('c') ); await app.openPaths.lastCall.returnValue; await scenario.assert('[a _] [c _] [b _]'); assert.isTrue(app.promptForPath.called); assert.strictEqual(app.promptForPath.lastCall.args[0], 'folder'); }); }); describe('window state serialization', function() { it('occurs immediately when adding a window', async function() { await scenario.launch(parseCommandLine(['a'])); const promise = emitterEventPromise( scenario.getApplication(0), 'application:did-save-state' ); await scenario.open(parseCommandLine(['c', 'b'])); await promise; assert.isTrue( scenario .getApplication(0) .storageFolder.store.calledWith('application.json', { version: '1', windows: [ { projectRoots: [scenario.convertRootPath('a')] }, { projectRoots: [ scenario.convertRootPath('b'), scenario.convertRootPath('c') ] } ] }) ); }); it('occurs immediately when removing a window', async function() { await scenario.launch(parseCommandLine(['a'])); const w = await scenario.open(parseCommandLine(['b'])); const promise = emitterEventPromise( scenario.getApplication(0), 'application:did-save-state' ); scenario.getApplication(0).removeWindow(w); await promise; assert.isTrue( scenario .getApplication(0) .storageFolder.store.calledWith('application.json', { version: '1', windows: [{ projectRoots: [scenario.convertRootPath('a')] }] }) ); }); it('occurs when the window is blurred', async function() { const [w] = await scenario.launch(parseCommandLine(['a'])); const promise = emitterEventPromise( scenario.getApplication(0), 'application:did-save-state' ); w.browserWindow.emit('blur'); await promise; }); }); describe('when closing the last window', function() { if (process.platform === 'linux' || process.platform === 'win32') { it('quits the application', async function() { const [w] = await scenario.launch(parseCommandLine(['a'])); scenario.getApplication(0).removeWindow(w); assert.isTrue(electron.app.quit.called); }); } else if (process.platform === 'darwin') { it('leaves the application open', async function() { const [w] = await scenario.launch(parseCommandLine(['a'])); scenario.getApplication(0).removeWindow(w); assert.isFalse(electron.app.quit.called); }); } }); describe('quitting', function() { it('waits until all windows have saved their state before quitting', async function() { const [w0] = await scenario.launch(parseCommandLine(['a'])); const w1 = await scenario.open(parseCommandLine(['b'])); assert.notStrictEqual(w0, w1); sinon.spy(w0, 'close'); let resolveUnload0; w0.prepareToUnload = () => new Promise(resolve => { resolveUnload0 = resolve; }); sinon.spy(w1, 'close'); let resolveUnload1; w1.prepareToUnload = () => new Promise(resolve => { resolveUnload1 = resolve; }); const evt = { preventDefault: sinon.spy() }; electron.app.emit('before-quit', evt); await new Promise(process.nextTick); assert.isTrue(evt.preventDefault.called); assert.isFalse(electron.app.quit.called); resolveUnload1(true); await new Promise(process.nextTick); assert.isFalse(electron.app.quit.called); resolveUnload0(true); await scenario.getApplication(0).lastBeforeQuitPromise; assert.isTrue(electron.app.quit.called); assert.isTrue(w0.close.called); assert.isTrue(w1.close.called); }); it('prevents a quit if a user cancels when prompted to save', async function() { const [w] = await scenario.launch(parseCommandLine(['a'])); let resolveUnload; w.prepareToUnload = () => new Promise(resolve => { resolveUnload = resolve; }); const evt = { preventDefault: sinon.spy() }; electron.app.emit('before-quit', evt); await new Promise(process.nextTick); assert.isTrue(evt.preventDefault.called); resolveUnload(false); await scenario.getApplication(0).lastBeforeQuitPromise; assert.isFalse(electron.app.quit.called); }); it('closes successfully unloaded windows', async function() { const [w0] = await scenario.launch(parseCommandLine(['a'])); const w1 = await scenario.open(parseCommandLine(['b'])); sinon.spy(w0, 'close'); let resolveUnload0; w0.prepareToUnload = () => new Promise(resolve => { resolveUnload0 = resolve; }); sinon.spy(w1, 'close'); let resolveUnload1; w1.prepareToUnload = () => new Promise(resolve => { resolveUnload1 = resolve; }); const evt = { preventDefault() {} }; electron.app.emit('before-quit', evt); resolveUnload0(false); resolveUnload1(true); await scenario.getApplication(0).lastBeforeQuitPromise; assert.isFalse(electron.app.quit.called); assert.isFalse(w0.close.called); assert.isTrue(w1.close.called); }); }); }); class StubWindow extends EventEmitter { constructor(sinon, loadSettings, options) { super(); this.loadSettings = loadSettings; this._dimensions = Object.assign({}, loadSettings.windowDimensions) || { x: 100, y: 100 }; this._position = { x: 0, y: 0 }; this._locations = []; this._rootPaths = new Set(); this._editorPaths = new Set(); let resolveClosePromise; this.closedPromise = new Promise(resolve => { resolveClosePromise = resolve; }); this.minimize = sinon.spy(); this.maximize = sinon.spy(); this.center = sinon.spy(); this.focus = sinon.spy(); this.show = sinon.spy(); this.hide = sinon.spy(); this.prepareToUnload = sinon.spy(); this.close = resolveClosePromise; this.replaceEnvironment = sinon.spy(); this.disableZoom = sinon.spy(); this.isFocused = sinon .stub() .returns(options.isFocused !== undefined ? options.isFocused : false); this.isMinimized = sinon .stub() .returns(options.isMinimized !== undefined ? options.isMinimized : false); this.isMaximized = sinon .stub() .returns(options.isMaximized !== undefined ? options.isMaximized : false); this.sendURIMessage = sinon.spy(); this.didChangeUserSettings = sinon.spy(); this.didFailToReadUserSettings = sinon.spy(); this.isSpec = loadSettings.isSpec !== undefined ? loadSettings.isSpec : false; this.devMode = loadSettings.devMode !== undefined ? loadSettings.devMode : false; this.safeMode = loadSettings.safeMode !== undefined ? loadSettings.safeMode : false; this.browserWindow = new EventEmitter(); this.browserWindow.webContents = new EventEmitter(); const locationsToOpen = this.loadSettings.locationsToOpen || []; if ( !( locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null ) && !this.isSpec ) { this.openLocations(locationsToOpen); } } openPath(pathToOpen, initialLine, initialColumn) { return this.openLocations([{ pathToOpen, initialLine, initialColumn }]); } openLocations(locations) { this._locations.push(...locations); for (const location of locations) { if (location.pathToOpen) { if (location.isDirectory) { this._rootPaths.add(location.pathToOpen); } else if (location.isFile) { this._editorPaths.add(location.pathToOpen); } } } this.projectRoots = Array.from(this._rootPaths); this.projectRoots.sort(); this.emit('window:locations-opened'); } setSize(x, y) { this._dimensions = { x, y }; } setPosition(x, y) { this._position = { x, y }; } isSpecWindow() { return this.isSpec; } hasProjectPaths() { return this._rootPaths.size > 0; } containsLocations(locations) { return locations.every(location => this.containsLocation(location)); } containsLocation(location) { if (!location.pathToOpen) return false; return Array.from(this._rootPaths).some(projectPath => { if (location.pathToOpen === projectPath) return true; if (location.pathToOpen.startsWith(path.join(projectPath, path.sep))) { if (!location.exists) return true; if (!location.isDirectory) return true; } return false; }); } getDimensions() { return Object.assign({}, this._dimensions); } } class LaunchScenario { static async create(sandbox) { const scenario = new this(sandbox); await scenario.init(); return scenario; } constructor(sandbox) { this.sinon = sandbox; this.applications = new Set(); this.windows = new Set(); this.root = null; this.atomHome = null; this.projectRootPool = new Map(); this.filePathPool = new Map(); this.killedPids = []; this.originalAtomHome = null; } async init() { if (this.root !== null) { return this.root; } this.root = await new Promise((resolve, reject) => { temp.mkdir('launch-', (err, rootPath) => { if (err) { reject(err); } else { resolve(rootPath); } }); }); this.atomHome = path.join(this.root, '.atom'); await new Promise((resolve, reject) => { fs.makeTree(this.atomHome, err => { if (err) { reject(err); } else { resolve(); } }); }); this.originalAtomHome = process.env.ATOM_HOME; process.env.ATOM_HOME = this.atomHome; await Promise.all( ['a', 'b', 'c', 'd'].map( dirPath => new Promise((resolve, reject) => { const fullDirPath = path.join(this.root, dirPath); fs.makeTree(fullDirPath, err => { if (err) { reject(err); } else { this.projectRootPool.set(dirPath, fullDirPath); resolve(); } }); }) ) ); await Promise.all( ['a/1.md', 'b/2.md'].map( filePath => new Promise((resolve, reject) => { const fullFilePath = path.join(this.root, filePath); fs.writeFile( fullFilePath, `file: ${filePath}\n`, { encoding: 'utf8' }, err => { if (err) { reject(err); } else { this.filePathPool.set(filePath, fullFilePath); this.filePathPool.set(path.basename(filePath), fullFilePath); resolve(); } } ); }) ) ); this.sinon.stub(electron.app, 'quit'); } async preconditions(source) { const app = this.addApplication(); const windowPromises = []; for (const windowSpec of this.parseWindowSpecs(source)) { if (windowSpec.editors.length === 0) { windowSpec.editors.push(null); } windowPromises.push( ((theApp, foldersToOpen, pathsToOpen) => { return theApp.openPaths({ newWindow: true, foldersToOpen, pathsToOpen }); })(app, windowSpec.roots, windowSpec.editors) ); } await Promise.all(windowPromises); } launch(options) { const app = options.app || this.addApplication(); delete options.app; if (options.pathsToOpen) { options.pathsToOpen = this.convertPaths(options.pathsToOpen); } return app.launch(options); } open(options) { if (this.applications.size === 0) { return this.launch(options); } let app = options.app; if (!app) { const apps = Array.from(this.applications); app = apps[apps.length - 1]; } else { delete options.app; } if (options.pathsToOpen) { options.pathsToOpen = this.convertPaths(options.pathsToOpen); } options.preserveFocus = true; return app.openWithOptions(options); } async assert(source) { const windowSpecs = this.parseWindowSpecs(source); let specIndex = 0; const windowPromises = []; for (const window of this.windows) { windowPromises.push( (async (theWindow, theSpec) => { const { _rootPaths: rootPaths, _editorPaths: editorPaths } = theWindow; const comparison = { ok: true, extraWindow: false, missingWindow: false, extraRoots: [], missingRoots: [], extraEditors: [], missingEditors: [], roots: rootPaths, editors: editorPaths }; if (!theSpec) { comparison.ok = false; comparison.extraWindow = true; comparison.extraRoots = rootPaths; comparison.extraEditors = editorPaths; } else { const [missingRoots, extraRoots] = this.compareSets( theSpec.roots, rootPaths ); const [missingEditors, extraEditors] = this.compareSets( theSpec.editors, editorPaths ); comparison.ok = missingRoots.length === 0 && extraRoots.length === 0 && missingEditors.length === 0 && extraEditors.length === 0; comparison.extraRoots = extraRoots; comparison.missingRoots = missingRoots; comparison.extraEditors = extraEditors; comparison.missingEditors = missingEditors; } return comparison; })(window, windowSpecs[specIndex++]) ); } const comparisons = await Promise.all(windowPromises); for (; specIndex < windowSpecs.length; specIndex++) { const spec = windowSpecs[specIndex]; comparisons.push({ ok: false, extraWindow: false, missingWindow: true, extraRoots: [], missingRoots: spec.roots, extraEditors: [], missingEditors: spec.editors, roots: null, editors: null }); } const shorthandParts = []; const descriptionParts = []; for (const comparison of comparisons) { if (comparison.roots !== null && comparison.editors !== null) { const shortRoots = Array.from(comparison.roots, r => path.basename(r) ).join(','); const shortPaths = Array.from(comparison.editors, e => path.basename(e) ).join(','); shorthandParts.push(`[${shortRoots} ${shortPaths}]`); } if (comparison.ok) { continue; } let parts = []; if (comparison.extraWindow) { parts.push('extra window\n'); } else if (comparison.missingWindow) { parts.push('missing window\n'); } else { parts.push('incorrect window\n'); } const shorten = fullPaths => fullPaths.map(fullPath => path.basename(fullPath)).join(', '); if (comparison.extraRoots.length > 0) { parts.push(`* extra roots ${shorten(comparison.extraRoots)}\n`); } if (comparison.missingRoots.length > 0) { parts.push(`* missing roots ${shorten(comparison.missingRoots)}\n`); } if (comparison.extraEditors.length > 0) { parts.push(`* extra editors ${shorten(comparison.extraEditors)}\n`); } if (comparison.missingEditors.length > 0) { parts.push(`* missing editors ${shorten(comparison.missingEditors)}\n`); } descriptionParts.push(parts.join('')); } if (descriptionParts.length !== 0) { descriptionParts.unshift(shorthandParts.join(' ') + '\n'); descriptionParts.unshift('Launched windows did not match spec\n'); } assert.isTrue(descriptionParts.length === 0, descriptionParts.join('')); } async destroy() { await Promise.all(Array.from(this.applications, app => app.destroy())); if (this.originalAtomHome) { process.env.ATOM_HOME = this.originalAtomHome; } } addApplication(options = {}) { const app = new AtomApplication({ resourcePath: path.resolve(__dirname, '../..'), atomHomeDirPath: this.atomHome, preserveFocus: true, killProcess: pid => { this.killedPids.push(pid); }, ...options }); this.sinon.stub(app, 'createWindow').callsFake(loadSettings => { const newWindow = new StubWindow(this.sinon, loadSettings, options); this.windows.add(newWindow); return newWindow; }); this.sinon .stub(app.storageFolder, 'load') .callsFake(() => Promise.resolve( options.applicationJson || { version: '1', windows: [] } ) ); this.sinon .stub(app.storageFolder, 'store') .callsFake(() => Promise.resolve()); this.applications.add(app); return app; } getApplication(index) { const app = Array.from(this.applications)[index]; if (!app) { throw new Error(`Application ${index} does not exist`); } return app; } getWindow(index) { const window = Array.from(this.windows)[index]; if (!window) { throw new Error(`Window ${index} does not exist`); } return window; } compareSets(expected, actual) { const expectedItems = new Set(expected); const extra = []; const missing = []; for (const actualItem of actual) { if (!expectedItems.delete(actualItem)) { // actualItem was present, but not expected extra.push(actualItem); } } for (const remainingItem of expectedItems) { // remainingItem was expected, but not present missing.push(remainingItem); } return [missing, extra]; } convertRootPath(shortRootPath) { if ( shortRootPath.startsWith('atom://') || shortRootPath.startsWith('remote://') ) { return shortRootPath; } const fullRootPath = this.projectRootPool.get(shortRootPath); if (!fullRootPath) { throw new Error(`Unexpected short project root path: ${shortRootPath}`); } return fullRootPath; } convertEditorPath(shortEditorPath) { const [truncatedPath, ...suffix] = shortEditorPath.split(/(?=:)/); const fullEditorPath = this.filePathPool.get(truncatedPath); if (!fullEditorPath) { throw new Error(`Unexpected short editor path: ${shortEditorPath}`); } return fullEditorPath + suffix.join(''); } convertPaths(paths) { return paths.map(shortPath => { if ( shortPath.startsWith('atom://') || shortPath.startsWith('remote://') ) { return shortPath; } const fullRoot = this.projectRootPool.get(shortPath); if (fullRoot) { return fullRoot; } const [truncatedPath, ...suffix] = shortPath.split(/(?=:)/); const fullEditor = this.filePathPool.get(truncatedPath); if (fullEditor) { return fullEditor + suffix.join(''); } throw new Error(`Unexpected short path: ${shortPath}`); }); } parseWindowSpecs(source) { const specs = []; const rx = /\s*\[(?:_|(\S+)) (?:_|(\S+))\]/g; let match = rx.exec(source); while (match) { const roots = match[1] ? match[1].split(',').map(shortPath => this.convertRootPath(shortPath)) : []; const editors = match[2] ? match[2] .split(',') .map(shortPath => this.convertEditorPath(shortPath)) : []; specs.push({ roots, editors }); match = rx.exec(source); } return specs; } } ================================================ FILE: spec/main-process/atom-window.test.js ================================================ /* globals assert */ const path = require('path'); const fs = require('fs-plus'); const url = require('url'); const { EventEmitter } = require('events'); const temp = require('temp').track(); const sandbox = require('sinon').createSandbox(); const dedent = require('dedent'); const AtomWindow = require('../../src/main-process/atom-window'); const { emitterEventPromise } = require('../async-spec-helpers'); describe('AtomWindow', function() { let sinon, app, service; beforeEach(function() { sinon = sandbox; app = new StubApplication(sinon); service = new StubRecoveryService(sinon); }); afterEach(function() { sinon.restore(); }); describe('creating a real window', function() { let resourcePath, windowInitializationScript, atomHome; let original; this.timeout(10 * 1000); beforeEach(async function() { original = { ATOM_HOME: process.env.ATOM_HOME, ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: process.env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT }; resourcePath = path.resolve(__dirname, '../..'); windowInitializationScript = require.resolve( path.join(resourcePath, 'src/initialize-application-window') ); atomHome = await new Promise((resolve, reject) => { temp.mkdir('launch-', (err, rootPath) => { if (err) { reject(err); } else { resolve(rootPath); } }); }); await new Promise((resolve, reject) => { const config = dedent` '*': core: automaticallyUpdate: false telemetryConsent: "no" welcome: showOnStartup: false `; fs.writeFile( path.join(atomHome, 'config.cson'), config, { encoding: 'utf8' }, err => { if (err) { reject(err); } else { resolve(); } } ); }); process.env.ATOM_HOME = atomHome; process.env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT = 'true'; }); afterEach(async function() { process.env.ATOM_HOME = original.ATOM_HOME; process.env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT = original.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT; }); it('creates a real, properly configured BrowserWindow', async function() { const w = new AtomWindow(app, service, { resourcePath, windowInitializationScript, headless: true, extra: 'extra-load-setting' }); const { browserWindow } = w; assert.isFalse(browserWindow.isVisible()); assert.isTrue(browserWindow.getTitle().startsWith('Atom')); const settings = JSON.parse(browserWindow.loadSettingsJSON); assert.strictEqual(settings.userSettings, 'stub-config'); assert.strictEqual(settings.extra, 'extra-load-setting'); assert.strictEqual(settings.resourcePath, resourcePath); assert.strictEqual(settings.atomHome, atomHome); assert.isFalse(settings.devMode); assert.isFalse(settings.safeMode); assert.isFalse(settings.clearWindowState); await emitterEventPromise(browserWindow, 'ready-to-show'); assert.strictEqual( browserWindow.webContents.getURL(), url.format({ protocol: 'file', pathname: `${resourcePath.replace(/\\/g, '/')}/static/index.html`, slashes: true }) ); }); }); describe('launch behavior', function() { if (process.platform === 'darwin') { it('sets titleBarStyle to "hidden" for a custom title bar on non-spec windows', function() { app.config['core.titleBar'] = 'custom'; const { browserWindow: w0 } = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow }); assert.strictEqual(w0.options.titleBarStyle, 'hidden'); const { browserWindow: w1 } = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, isSpec: true }); assert.isUndefined(w1.options.titleBarStyle); }); it('sets titleBarStyle to "hiddenInset" for a custom inset title bar on non-spec windows', function() { app.config['core.titleBar'] = 'custom-inset'; const { browserWindow: w0 } = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow }); assert.strictEqual(w0.options.titleBarStyle, 'hiddenInset'); const { browserWindow: w1 } = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, isSpec: true }); assert.isUndefined(w1.options.titleBarStyle); }); it('sets frame to "false" for a hidden title bar on non-spec windows', function() { app.config['core.titleBar'] = 'hidden'; const { browserWindow: w0 } = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow }); assert.isFalse(w0.options.frame); const { browserWindow: w1 } = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, isSpec: true }); assert.isUndefined(w1.options.frame); }); } else { it('sets frame to "false" for a hidden title bar on non-spec windows', function() { app.config['core.titleBar'] = 'hidden'; const { browserWindow: w0 } = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow }); assert.isFalse(w0.options.frame); const { browserWindow: w1 } = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, isSpec: true }); assert.isUndefined(w1.options.frame); }); } it('opens initial locations', async function() { const locationsToOpen = [ { pathToOpen: 'file.txt', initialLine: 1, initialColumn: 2, isDirectory: false, hasWaitSession: false }, { pathToOpen: '/directory', initialLine: null, initialColumn: null, isDirectory: true, hasWaitSession: false } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.deepEqual(w.projectRoots, ['/directory']); const loadPromise = emitterEventPromise(w, 'window:loaded'); w.browserWindow.emit('window:loaded'); await loadPromise; assert.deepEqual(w.browserWindow.sent, [ ['message', 'open-locations', locationsToOpen] ]); }); it('does not open an initial null location', async function() { const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen: [{ pathToOpen: null }] }); const loadPromise = emitterEventPromise(w, 'window:loaded'); w.browserWindow.emit('window:loaded'); await loadPromise; assert.lengthOf(w.browserWindow.sent, 0); }); it('does not open initial locations in spec mode', async function() { const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen: [{ pathToOpen: 'file.txt' }], isSpec: true }); const loadPromise = emitterEventPromise(w, 'window:loaded'); w.browserWindow.emit('window:loaded'); await loadPromise; assert.lengthOf(w.browserWindow.sent, 0); }); it('focuses the webView for specs', function() { const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, isSpec: true }); assert.isTrue(w.browserWindow.behavior.focusOnWebView); }); }); describe('project root tracking', function() { it('knows when it has no roots', function() { const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow }); assert.isFalse(w.hasProjectPaths()); }); it('is initialized from directories in the initial locationsToOpen', function() { const locationsToOpen = [ { pathToOpen: 'file.txt', exists: true, isFile: true }, { pathToOpen: 'directory0', exists: true, isDirectory: true }, { pathToOpen: 'directory1', exists: true, isDirectory: true }, { pathToOpen: 'new-file.txt' }, { pathToOpen: null } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.deepEqual(w.projectRoots, ['directory0', 'directory1']); assert.isTrue(w.loadSettings.hasOpenFiles); assert.deepEqual(w.loadSettings.initialProjectRoots, [ 'directory0', 'directory1' ]); assert.isTrue(w.hasProjectPaths()); }); it('is updated synchronously by openLocations', async function() { const locationsToOpen = [ { pathToOpen: 'file.txt', isFile: true }, { pathToOpen: 'directory1', isDirectory: true }, { pathToOpen: 'directory0', isDirectory: true }, { pathToOpen: 'directory0', isDirectory: true }, { pathToOpen: 'new-file.txt' } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow }); assert.deepEqual(w.projectRoots, []); const promise = w.openLocations(locationsToOpen); assert.deepEqual(w.projectRoots, ['directory0', 'directory1']); w.resolveLoadedPromise(); await promise; }); it('is updated by setProjectRoots', function() { const locationsToOpen = [ { pathToOpen: 'directory0', exists: true, isDirectory: true } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.deepEqual(w.projectRoots, ['directory0']); assert.deepEqual(w.loadSettings.initialProjectRoots, ['directory0']); w.setProjectRoots(['directory1', 'directory0', 'directory2']); assert.deepEqual(w.projectRoots, [ 'directory0', 'directory1', 'directory2' ]); assert.deepEqual(w.loadSettings.initialProjectRoots, [ 'directory0', 'directory1', 'directory2' ]); }); it('never reports that it owns the empty path', function() { const locationsToOpen = [ { pathToOpen: 'directory0', exists: true, isDirectory: true }, { pathToOpen: 'directory1', exists: true, isDirectory: true }, { pathToOpen: null } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.isFalse(w.containsLocation({ pathToOpen: null })); }); it('discovers an exact path match', function() { const locationsToOpen = [ { pathToOpen: 'directory0', exists: true, isDirectory: true }, { pathToOpen: 'directory1', exists: true, isDirectory: true } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.isTrue(w.containsLocation({ pathToOpen: 'directory0' })); assert.isFalse(w.containsLocation({ pathToOpen: 'directory2' })); }); it('discovers the path of a file within any project root', function() { const locationsToOpen = [ { pathToOpen: 'directory0', exists: true, isDirectory: true }, { pathToOpen: 'directory1', exists: true, isDirectory: true } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.isTrue( w.containsLocation({ pathToOpen: path.join('directory0/file-0.txt'), exists: true, isFile: true }) ); assert.isTrue( w.containsLocation({ pathToOpen: path.join('directory0/deep/file-0.txt'), exists: true, isFile: true }) ); assert.isFalse( w.containsLocation({ pathToOpen: path.join('directory2/file-9.txt'), exists: true, isFile: true }) ); assert.isFalse( w.containsLocation({ pathToOpen: path.join('directory2/deep/file-9.txt'), exists: true, isFile: true }) ); }); it('reports that it owns nonexistent paths within a project root', function() { const locationsToOpen = [ { pathToOpen: 'directory0', exists: true, isDirectory: true }, { pathToOpen: 'directory1', exists: true, isDirectory: true } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.isTrue( w.containsLocation({ pathToOpen: path.join('directory0/file-1.txt'), exists: false }) ); assert.isTrue( w.containsLocation({ pathToOpen: path.join('directory1/subdir/file-0.txt'), exists: false }) ); }); it('never reports that it owns directories within a project root', function() { const locationsToOpen = [ { pathToOpen: 'directory0', exists: true, isDirectory: true }, { pathToOpen: 'directory1', exists: true, isDirectory: true } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.isFalse( w.containsLocation({ pathToOpen: path.join('directory0/subdir-0'), exists: true, isDirectory: true }) ); }); it('checks a full list of paths and reports if it owns all of them', function() { const locationsToOpen = [ { pathToOpen: 'directory0', exists: true, isDirectory: true }, { pathToOpen: 'directory1', exists: true, isDirectory: true } ]; const w = new AtomWindow(app, service, { browserWindowConstructor: StubBrowserWindow, locationsToOpen }); assert.isTrue( w.containsLocations([ { pathToOpen: 'directory0' }, { pathToOpen: path.join('directory1/file-0.txt'), exists: true, isFile: true } ]) ); assert.isFalse( w.containsLocations([ { pathToOpen: 'directory2' }, { pathToOpen: 'directory0' } ]) ); assert.isFalse( w.containsLocations([ { pathToOpen: 'directory2' }, { pathToOpen: 'directory1' } ]) ); }); }); }); class StubApplication { constructor(sinon) { this.config = { 'core.titleBar': 'native', get: key => this.config[key] || null }; this.configFile = { get() { return 'stub-config'; } }; this.removeWindow = sinon.spy(); this.saveCurrentWindowOptions = sinon.spy(); } } class StubRecoveryService { constructor(sinon) { this.didCloseWindow = sinon.spy(); this.didCrashWindow = sinon.spy(); } } class StubBrowserWindow extends EventEmitter { constructor(options) { super(); this.options = options; this.sent = []; this.behavior = { focusOnWebView: false }; this.webContents = new EventEmitter(); this.webContents.send = (...args) => { this.sent.push(args); }; this.webContents.setVisualZoomLevelLimits = () => {}; } loadURL() {} focusOnWebView() { this.behavior.focusOnWebView = true; } } ================================================ FILE: spec/main-process/file-recovery-service.test.js ================================================ const { dialog } = require('electron'); const FileRecoveryService = require('../../src/main-process/file-recovery-service'); const fs = require('fs-plus'); const fsreal = require('fs'); const EventEmitter = require('events').EventEmitter; const { assert } = require('chai'); const sinon = require('sinon'); const { escapeRegExp } = require('underscore-plus'); const temp = require('temp').track(); describe('FileRecoveryService', function() { let recoveryService, recoveryDirectory, spies; this.timeout(10 * 1000); beforeEach(() => { recoveryDirectory = temp.mkdirSync('atom-spec-file-recovery'); recoveryService = new FileRecoveryService(recoveryDirectory); spies = sinon.createSandbox(); }); afterEach(() => { spies.restore(); try { temp.cleanupSync(); } catch (e) { // Ignore } }); describe('when no crash happens during a save', () => { it('creates a recovery file and deletes it after saving', async () => { const mockWindow = {}; const filePath = temp.path(); fs.writeFileSync(filePath, 'some content'); await recoveryService.willSavePath(mockWindow, filePath); assert.equal(fs.listTreeSync(recoveryDirectory).length, 1); fs.writeFileSync(filePath, 'changed'); await recoveryService.didSavePath(mockWindow, filePath); assert.equal(fs.listTreeSync(recoveryDirectory).length, 0); assert.equal(fs.readFileSync(filePath, 'utf8'), 'changed'); fs.removeSync(filePath); }); it('creates only one recovery file when many windows attempt to save the same file, deleting it when the last one finishes saving it', async () => { const mockWindow = {}; const anotherMockWindow = {}; const filePath = temp.path(); fs.writeFileSync(filePath, 'some content'); await recoveryService.willSavePath(mockWindow, filePath); await recoveryService.willSavePath(anotherMockWindow, filePath); assert.equal(fs.listTreeSync(recoveryDirectory).length, 1); fs.writeFileSync(filePath, 'changed'); await recoveryService.didSavePath(mockWindow, filePath); assert.equal(fs.listTreeSync(recoveryDirectory).length, 1); assert.equal(fs.readFileSync(filePath, 'utf8'), 'changed'); await recoveryService.didSavePath(anotherMockWindow, filePath); assert.equal(fs.listTreeSync(recoveryDirectory).length, 0); assert.equal(fs.readFileSync(filePath, 'utf8'), 'changed'); fs.removeSync(filePath); }); }); describe('when a crash happens during a save', () => { it('restores the created recovery file and deletes it', async () => { const mockWindow = {}; const filePath = temp.path(); fs.writeFileSync(filePath, 'some content'); await recoveryService.willSavePath(mockWindow, filePath); assert.equal(fs.listTreeSync(recoveryDirectory).length, 1); fs.writeFileSync(filePath, 'changed'); await recoveryService.didCrashWindow(mockWindow); assert.equal(fs.listTreeSync(recoveryDirectory).length, 0); assert.equal(fs.readFileSync(filePath, 'utf8'), 'some content'); fs.removeSync(filePath); }); it('restores the created recovery file when many windows attempt to save the same file and one of them crashes', async () => { const mockWindow = {}; const anotherMockWindow = {}; const filePath = temp.path(); fs.writeFileSync(filePath, 'A'); await recoveryService.willSavePath(mockWindow, filePath); fs.writeFileSync(filePath, 'B'); await recoveryService.willSavePath(anotherMockWindow, filePath); assert.equal(fs.listTreeSync(recoveryDirectory).length, 1); fs.writeFileSync(filePath, 'C'); await recoveryService.didCrashWindow(mockWindow); assert.equal(fs.readFileSync(filePath, 'utf8'), 'A'); assert.equal(fs.listTreeSync(recoveryDirectory).length, 0); fs.writeFileSync(filePath, 'D'); await recoveryService.willSavePath(mockWindow, filePath); fs.writeFileSync(filePath, 'E'); await recoveryService.willSavePath(anotherMockWindow, filePath); assert.equal(fs.listTreeSync(recoveryDirectory).length, 1); fs.writeFileSync(filePath, 'F'); await recoveryService.didCrashWindow(anotherMockWindow); assert.equal(fs.readFileSync(filePath, 'utf8'), 'D'); assert.equal(fs.listTreeSync(recoveryDirectory).length, 0); fs.removeSync(filePath); }); it("emits a warning when a file can't be recovered", async () => { const mockWindow = {}; const filePath = temp.path(); fs.writeFileSync(filePath, 'content'); let logs = []; spies.stub(console, 'log').callsFake(message => logs.push(message)); spies.stub(dialog, 'showMessageBox'); // Copy files to be recovered before mocking fs.createWriteStream await recoveryService.willSavePath(mockWindow, filePath); // Stub out fs.createWriteStream so that we can return a fake error when // attempting to copy the recovered file to its original location var fakeEmitter = new EventEmitter(); var onStub = spies.stub(fakeEmitter, 'on'); onStub .withArgs('error') .yields(new Error('Nope')) .returns(fakeEmitter); onStub.withArgs('open').returns(fakeEmitter); spies .stub(fsreal, 'createWriteStream') .withArgs(filePath) .returns(fakeEmitter); await recoveryService.didCrashWindow(mockWindow); let recoveryFiles = fs.listTreeSync(recoveryDirectory); assert.equal(recoveryFiles.length, 1); assert.equal(logs.length, 1); assert.match(logs[0], new RegExp(escapeRegExp(filePath))); assert.match(logs[0], new RegExp(escapeRegExp(recoveryFiles[0]))); fs.removeSync(filePath); }); }); it("doesn't create a recovery file when the file that's being saved doesn't exist yet", async () => { const mockWindow = {}; await recoveryService.willSavePath(mockWindow, 'a-file-that-doesnt-exist'); assert.equal(fs.listTreeSync(recoveryDirectory).length, 0); await recoveryService.didSavePath(mockWindow, 'a-file-that-doesnt-exist'); assert.equal(fs.listTreeSync(recoveryDirectory).length, 0); }); }); ================================================ FILE: spec/main-process/mocha-test-runner.js ================================================ const Mocha = require('mocha'); const fs = require('fs-plus'); const { assert } = require('chai'); module.exports = function(testPaths) { global.assert = assert; let reporterOptions = { reporterEnabled: 'list' }; if (process.env.TEST_JUNIT_XML_PATH) { reporterOptions = { reporterEnabled: 'list, mocha-junit-reporter', mochaJunitReporterReporterOptions: { mochaFile: process.env.TEST_JUNIT_XML_PATH } }; } const mocha = new Mocha({ reporter: 'mocha-multi-reporters', reporterOptions }); for (let testPath of testPaths) { if (fs.isDirectorySync(testPath)) { for (let testFilePath of fs.listTreeSync(testPath)) { if (/\.test\.(coffee|js)$/.test(testFilePath)) { mocha.addFile(testFilePath); } } } else { mocha.addFile(testPath); } } mocha.run(failures => { if (failures === 0) { process.exit(0); } else { process.exit(1); } }); }; ================================================ FILE: spec/main-process/parse-command-line.test.js ================================================ const { assert } = require('chai'); const parseCommandLine = require('../../src/main-process/parse-command-line'); describe('parseCommandLine', () => { describe('when --uri-handler is not passed', () => { it('parses arguments as normal', () => { 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']); }); // The "underscore flag" with no "non-flag argument" after it // is the minimal reproducer for the macOS Gatekeeper startup bug. // By default, it causes the addition of boolean "true"s into yargs' "non-flag argument" array: `argv._` // Whereas we do string-only operations on these arguments, expecting them to be paths or URIs. describe('and --_ or -_ are passed', () => { it('does not attempt to parse booleans as paths or URIs', () => { const args = parseCommandLine([ '--_', '/some/path', '-_', '-_', 'some/other/path', 'atom://test/url', '--_', 'atom://other/url', '-_', './another-path.file', '-_', '-_', '-_' ]); assert.deepEqual(args.urlsToOpen, [ 'atom://test/url', 'atom://other/url' ]); assert.deepEqual(args.pathsToOpen, [ '/some/path', 'some/other/path', './another-path.file' ]); }); }); describe('and a non-flag number is passed as an argument', () => { it('does not attempt to parse numbers as paths or URIs', () => { const args = parseCommandLine([ '43', '/some/path', '22', '97', 'some/other/path', 'atom://test/url', '885', 'atom://other/url', '42', './another-path.file' ]); assert.deepEqual(args.urlsToOpen, [ 'atom://test/url', 'atom://other/url' ]); assert.deepEqual(args.pathsToOpen, [ '/some/path', 'some/other/path', './another-path.file' ]); }); }); }); describe('when --uri-handler is passed', () => { it('ignores other arguments and limits to one URL', () => { const args = parseCommandLine([ '-d', '--uri-handler', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url' ]); assert.isUndefined(args.devMode); assert.isUndefined(args.safeMode); assert.isUndefined(args.test); assert.deepEqual(args.urlsToOpen, ['atom://test/url']); assert.deepEqual(args.pathsToOpen, []); }); }); describe('when evil macOS Gatekeeper flag "-psn_0_[six or seven digits here]" is passed', () => { it('ignores any arguments starting with "-psn_"', () => { const getPsnFlag = () => { return `-psn_0_${Math.floor(Math.random() * 10_000_000)}`; }; const args = parseCommandLine([ getPsnFlag(), '/some/path', getPsnFlag(), getPsnFlag(), 'some/other/path', 'atom://test/url', getPsnFlag(), 'atom://other/url', '-psn_ Any argument starting with "-psn_" should be ignored, even this one.', './another-path.file' ]); assert.deepEqual(args.urlsToOpen, [ 'atom://test/url', 'atom://other/url' ]); assert.deepEqual(args.pathsToOpen, [ '/some/path', 'some/other/path', './another-path.file' ]); }); }); }); ================================================ FILE: spec/menu-manager-spec.js ================================================ const path = require('path'); const MenuManager = require('../src/menu-manager'); describe('MenuManager', function() { let menu = null; beforeEach(function() { menu = new MenuManager({ keymapManager: atom.keymaps, packageManager: atom.packages }); spyOn(menu, 'sendToBrowserProcess'); // Do not modify Atom's actual menus menu.initialize({ resourcePath: atom.getLoadSettings().resourcePath }); }); describe('::add(items)', function() { it('can add new menus that can be removed with the returned disposable', function() { const disposable = menu.add([ { label: 'A', submenu: [{ label: 'B', command: 'b' }] } ]); expect(menu.template).toEqual([ { label: 'A', id: 'A', submenu: [{ label: 'B', id: 'B', command: 'b' }] } ]); disposable.dispose(); expect(menu.template).toEqual([]); }); it('can add submenu items to existing menus that can be removed with the returned disposable', function() { const disposable1 = menu.add([ { label: 'A', submenu: [{ label: 'B', command: 'b' }] } ]); const disposable2 = menu.add([ { label: 'A', submenu: [{ label: 'C', submenu: [{ label: 'D', command: 'd' }] }] } ]); const disposable3 = menu.add([ { label: 'A', submenu: [{ label: 'C', submenu: [{ label: 'E', command: 'e' }] }] } ]); expect(menu.template).toEqual([ { label: 'A', id: 'A', submenu: [ { label: 'B', id: 'B', command: 'b' }, { label: 'C', id: 'C', submenu: [ { label: 'D', id: 'D', command: 'd' }, { label: 'E', id: 'E', command: 'e' } ] } ] } ]); disposable3.dispose(); expect(menu.template).toEqual([ { label: 'A', id: 'A', submenu: [ { label: 'B', id: 'B', command: 'b' }, { label: 'C', id: 'C', submenu: [{ label: 'D', id: 'D', command: 'd' }] } ] } ]); disposable2.dispose(); expect(menu.template).toEqual([ { label: 'A', id: 'A', submenu: [{ label: 'B', id: 'B', command: 'b' }] } ]); disposable1.dispose(); expect(menu.template).toEqual([]); }); it('does not add duplicate labels to the same menu', function() { const originalItemCount = menu.template.length; menu.add([{ label: 'A', submenu: [{ label: 'B', command: 'b' }] }]); menu.add([{ label: 'A', submenu: [{ label: 'B', command: 'b' }] }]); expect(menu.template[originalItemCount]).toEqual({ label: 'A', id: 'A', submenu: [{ label: 'B', id: 'B', command: 'b' }] }); }); }); describe('::update()', function() { const originalPlatform = process.platform; afterEach(() => Object.defineProperty(process, 'platform', { value: originalPlatform }) ); it('sends the current menu template and associated key bindings to the browser process', function() { menu.add([{ label: 'A', submenu: [{ label: 'B', command: 'b' }] }]); atom.keymaps.add('test', { 'atom-workspace': { 'ctrl-b': 'b' } }); menu.update(); advanceClock(1); expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toEqual([ 'ctrl-b' ]); }); it('omits key bindings that are mapped to unset! in any context', function() { // it would be nice to be smarter about omitting, but that would require a much // more dynamic interaction between the currently focused element and the menu menu.add([{ label: 'A', submenu: [{ label: 'B', command: 'b' }] }]); atom.keymaps.add('test', { 'atom-workspace': { 'ctrl-b': 'b' } }); atom.keymaps.add('test', { 'atom-text-editor': { 'ctrl-b': 'unset!' } }); advanceClock(1); expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined(); }); it('omits key bindings that could conflict with AltGraph characters on macOS', function() { Object.defineProperty(process, 'platform', { value: 'darwin' }); menu.add([ { label: 'A', submenu: [ { label: 'B', command: 'b' }, { label: 'C', command: 'c' }, { label: 'D', command: 'd' } ] } ]); atom.keymaps.add('test', { 'atom-workspace': { 'alt-b': 'b', 'alt-shift-C': 'c', 'alt-cmd-d': 'd' } }); advanceClock(1); expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined(); expect(menu.sendToBrowserProcess.argsForCall[0][1]['c']).toBeUndefined(); expect(menu.sendToBrowserProcess.argsForCall[0][1]['d']).toEqual([ 'alt-cmd-d' ]); }); it('omits key bindings that could conflict with AltGraph characters on Windows', function() { Object.defineProperty(process, 'platform', { value: 'win32' }); menu.add([ { label: 'A', submenu: [ { label: 'B', command: 'b' }, { label: 'C', command: 'c' }, { label: 'D', command: 'd' } ] } ]); atom.keymaps.add('test', { 'atom-workspace': { 'ctrl-alt-b': 'b', 'ctrl-alt-shift-C': 'c', 'ctrl-alt-cmd-d': 'd' } }); advanceClock(1); expect(menu.sendToBrowserProcess.argsForCall[0][1]['b']).toBeUndefined(); expect(menu.sendToBrowserProcess.argsForCall[0][1]['c']).toBeUndefined(); expect(menu.sendToBrowserProcess.argsForCall[0][1]['d']).toEqual([ 'ctrl-alt-cmd-d' ]); }); }); it('updates the application menu when a keymap is reloaded', function() { spyOn(menu, 'update'); const keymapPath = path.join( __dirname, 'fixtures', 'packages', 'package-with-keymaps', 'keymaps', 'keymap-1.cson' ); atom.keymaps.reloadKeymap(keymapPath); expect(menu.update).toHaveBeenCalled(); }); }); ================================================ FILE: spec/menu-sort-helpers-spec.js ================================================ const { sortMenuItems } = require('../src/menu-sort-helpers'); describe('contextMenu', () => { describe('dedupes separators', () => { it('preserves existing submenus', () => { const items = [{ submenu: [] }]; expect(sortMenuItems(items)).toEqual(items); }); }); describe('dedupes separators', () => { it('trims leading separators', () => { const items = [{ type: 'separator' }, { command: 'core:one' }]; const expected = [{ command: 'core:one' }]; expect(sortMenuItems(items)).toEqual(expected); }); it('preserves separators at the beginning of set two', () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' } ]; const expected = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' } ]; expect(sortMenuItems(items)).toEqual(expected); }); it('trims trailing separators', () => { const items = [{ command: 'core:one' }, { type: 'separator' }]; const expected = [{ command: 'core:one' }]; expect(sortMenuItems(items)).toEqual(expected); }); it('removes duplicate separators across sets', () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { type: 'separator' }, { command: 'core:two' } ]; const expected = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' } ]; expect(sortMenuItems(items)).toEqual(expected); }); }); describe('can move an item to a different group by merging groups', () => { it('can move a group of one item', () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' }, { type: 'separator' }, { command: 'core:three', after: ['core:one'] }, { type: 'separator' } ]; const expected = [ { command: 'core:one' }, { command: 'core:three', after: ['core:one'] }, { type: 'separator' }, { command: 'core:two' } ]; expect(sortMenuItems(items)).toEqual(expected); }); it("moves all items in the moving item's group", () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' }, { type: 'separator' }, { command: 'core:three', after: ['core:one'] }, { command: 'core:four' }, { type: 'separator' } ]; const expected = [ { command: 'core:one' }, { command: 'core:three', after: ['core:one'] }, { command: 'core:four' }, { type: 'separator' }, { command: 'core:two' } ]; expect(sortMenuItems(items)).toEqual(expected); }); it("ignores positions relative to commands that don't exist", () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' }, { type: 'separator' }, { command: 'core:three', after: ['core:does-not-exist'] }, { command: 'core:four', after: ['core:one'] }, { type: 'separator' } ]; const expected = [ { command: 'core:one' }, { command: 'core:three', after: ['core:does-not-exist'] }, { command: 'core:four', after: ['core:one'] }, { type: 'separator' }, { command: 'core:two' } ]; expect(sortMenuItems(items)).toEqual(expected); }); it('can handle recursive group merging', () => { const items = [ { command: 'core:one', after: ['core:three'] }, { command: 'core:two', before: ['core:one'] }, { command: 'core:three' } ]; const expected = [ { command: 'core:three' }, { command: 'core:two', before: ['core:one'] }, { command: 'core:one', after: ['core:three'] } ]; expect(sortMenuItems(items)).toEqual(expected); }); it('can merge multiple groups when given a list of before/after commands', () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' }, { type: 'separator' }, { command: 'core:three', after: ['core:one', 'core:two'] } ]; const expected = [ { command: 'core:two' }, { command: 'core:one' }, { command: 'core:three', after: ['core:one', 'core:two'] } ]; expect(sortMenuItems(items)).toEqual(expected); }); it('can merge multiple groups based on both before/after commands', () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' }, { type: 'separator' }, { command: 'core:three', after: ['core:one'], before: ['core:two'] } ]; const expected = [ { command: 'core:one' }, { command: 'core:three', after: ['core:one'], before: ['core:two'] }, { command: 'core:two' } ]; expect(sortMenuItems(items)).toEqual(expected); }); }); describe('sorts items within their ultimate group', () => { it('does a simple sort', () => { const items = [ { command: 'core:two', after: ['core:one'] }, { command: 'core:one' } ]; expect(sortMenuItems(items)).toEqual([ { command: 'core:one' }, { command: 'core:two', after: ['core:one'] } ]); }); it('resolves cycles by ignoring things that conflict', () => { const items = [ { command: 'core:two', after: ['core:one'] }, { command: 'core:one', after: ['core:two'] } ]; expect(sortMenuItems(items)).toEqual([ { command: 'core:one', after: ['core:two'] }, { command: 'core:two', after: ['core:one'] } ]); }); }); describe('sorts groups', () => { it('does a simple sort', () => { const items = [ { command: 'core:two', afterGroupContaining: ['core:one'] }, { type: 'separator' }, { command: 'core:one' } ]; expect(sortMenuItems(items)).toEqual([ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two', afterGroupContaining: ['core:one'] } ]); }); it('resolves cycles by ignoring things that conflict', () => { const items = [ { command: 'core:two', afterGroupContaining: ['core:one'] }, { type: 'separator' }, { command: 'core:one', afterGroupContaining: ['core:two'] } ]; expect(sortMenuItems(items)).toEqual([ { command: 'core:one', afterGroupContaining: ['core:two'] }, { type: 'separator' }, { command: 'core:two', afterGroupContaining: ['core:one'] } ]); }); it('ignores references to commands that do not exist', () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two', afterGroupContaining: ['core:does-not-exist'] } ]; expect(sortMenuItems(items)).toEqual([ { command: 'core:one' }, { type: 'separator' }, { command: 'core:two', afterGroupContaining: ['core:does-not-exist'] } ]); }); it('only respects the first matching [before|after]GroupContaining rule in a given group', () => { const items = [ { command: 'core:one' }, { type: 'separator' }, { command: 'core:three', beforeGroupContaining: ['core:one'] }, { command: 'core:four', afterGroupContaining: ['core:two'] }, { type: 'separator' }, { command: 'core:two' } ]; expect(sortMenuItems(items)).toEqual([ { command: 'core:three', beforeGroupContaining: ['core:one'] }, { command: 'core:four', afterGroupContaining: ['core:two'] }, { type: 'separator' }, { command: 'core:one' }, { type: 'separator' }, { command: 'core:two' } ]); }); }); }); ================================================ FILE: spec/module-cache-spec.js ================================================ const path = require('path'); const Module = require('module'); const fs = require('fs-plus'); const temp = require('temp').track(); const ModuleCache = require('../src/module-cache'); describe('ModuleCache', function() { beforeEach(() => spyOn(Module, '_findPath').andCallThrough()); afterEach(function() { try { temp.cleanupSync(); } catch (error) {} }); it('resolves Electron module paths without hitting the filesystem', function() { const { builtins } = ModuleCache.cache; expect(Object.keys(builtins).length).toBeGreaterThan(0); for (let builtinName in builtins) { const builtinPath = builtins[builtinName]; expect(require.resolve(builtinName)).toBe(builtinPath); expect(fs.isFileSync(require.resolve(builtinName))).toBeTruthy(); } expect(Module._findPath.callCount).toBe(0); }); it('resolves relative core paths without hitting the filesystem', function() { ModuleCache.add(atom.getLoadSettings().resourcePath, { _atomModuleCache: { extensions: { '.json': [path.join('spec', 'fixtures', 'module-cache', 'file.json')] } } }); expect(require('./fixtures/module-cache/file.json').foo).toBe('bar'); expect(Module._findPath.callCount).toBe(0); }); it('resolves module paths when a compatible version is provided by core', function() { const packagePath = fs.realpathSync(temp.mkdirSync('atom-package')); ModuleCache.add(packagePath, { _atomModuleCache: { folders: [ { paths: [''], dependencies: { 'underscore-plus': '*' } } ] } }); ModuleCache.add(atom.getLoadSettings().resourcePath, { _atomModuleCache: { dependencies: [ { name: 'underscore-plus', version: require('underscore-plus/package.json').version, path: path.join( 'node_modules', 'underscore-plus', 'lib', 'underscore-plus.js' ) } ] } }); const indexPath = path.join(packagePath, 'index.js'); fs.writeFileSync( indexPath, `\ exports.load = function() { require('underscore-plus'); };\ ` ); const packageMain = require(indexPath); Module._findPath.reset(); packageMain.load(); expect(Module._findPath.callCount).toBe(0); }); it('does not resolve module paths when no compatible version is provided by core', function() { const packagePath = fs.realpathSync(temp.mkdirSync('atom-package')); ModuleCache.add(packagePath, { _atomModuleCache: { folders: [ { paths: [''], dependencies: { 'underscore-plus': '0.0.1' } } ] } }); ModuleCache.add(atom.getLoadSettings().resourcePath, { _atomModuleCache: { dependencies: [ { name: 'underscore-plus', version: require('underscore-plus/package.json').version, path: path.join( 'node_modules', 'underscore-plus', 'lib', 'underscore-plus.js' ) } ] } }); const indexPath = path.join(packagePath, 'index.js'); fs.writeFileSync( indexPath, `\ exports.load = function() { require('underscore-plus'); };\ ` ); spyOn(process, 'cwd').andReturn('/'); // Required when running this test from CLI const packageMain = require(indexPath); Module._findPath.reset(); expect(() => packageMain.load()).toThrow(); expect(Module._findPath.callCount).toBe(1); }); }); ================================================ FILE: spec/native-compile-cache-spec.coffee ================================================ fs = require 'fs' path = require 'path' Module = require 'module' describe "NativeCompileCache", -> nativeCompileCache = require '../src/native-compile-cache' [fakeCacheStore, cachedFiles] = [] beforeEach -> cachedFiles = [] fakeCacheStore = jasmine.createSpyObj("cache store", ["set", "get", "has", "delete"]) fakeCacheStore.has.andCallFake (cacheKey) -> fakeCacheStore.get(cacheKey)? fakeCacheStore.get.andCallFake (cacheKey) -> for entry in cachedFiles by -1 continue if entry.cacheKey isnt cacheKey return entry.cacheBuffer return fakeCacheStore.set.andCallFake (cacheKey, cacheBuffer) -> cachedFiles.push({cacheKey, cacheBuffer}) nativeCompileCache.setCacheStore(fakeCacheStore) nativeCompileCache.setV8Version("a-v8-version") nativeCompileCache.install() it "writes and reads from the cache storage when requiring files", -> fn1 = require('./fixtures/native-cache/file-1') fn2 = require('./fixtures/native-cache/file-2') expect(cachedFiles.length).toBe(2) expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0) expect(fn1()).toBe(1) expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0) expect(fn2()).toBe(2) delete Module._cache[require.resolve('./fixtures/native-cache/file-1')] fn1 = require('./fixtures/native-cache/file-1') expect(cachedFiles.length).toBe(2) expect(fn1()).toBe(1) describe "when v8 version changes", -> it "updates the cache of previously required files", -> nativeCompileCache.setV8Version("version-1") fn4 = require('./fixtures/native-cache/file-4') expect(cachedFiles.length).toBe(1) expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0) expect(fn4()).toBe("file-4") nativeCompileCache.setV8Version("version-2") delete Module._cache[require.resolve('./fixtures/native-cache/file-4')] fn4 = require('./fixtures/native-cache/file-4') expect(cachedFiles.length).toBe(2) expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0) describe "when a previously required and cached file changes", -> beforeEach -> fs.writeFileSync path.resolve(__dirname + '/fixtures/native-cache/file-5'), """ module.exports = function () { return "file-5" } """ afterEach -> fs.unlinkSync path.resolve(__dirname + '/fixtures/native-cache/file-5') it "removes it from the store and re-inserts it with the new cache", -> fn5 = require('./fixtures/native-cache/file-5') expect(cachedFiles.length).toBe(1) expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0) expect(fn5()).toBe("file-5") delete Module._cache[require.resolve('./fixtures/native-cache/file-5')] fs.appendFileSync(require.resolve('./fixtures/native-cache/file-5'), "\n\n") fn5 = require('./fixtures/native-cache/file-5') expect(cachedFiles.length).toBe(2) expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array) expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0) it "deletes previously cached code when the cache is an invalid file", -> fakeCacheStore.has.andReturn(true) fakeCacheStore.get.andCallFake -> Buffer.from("an invalid cache") fn3 = require('./fixtures/native-cache/file-3') expect(fakeCacheStore.delete).toHaveBeenCalled() expect(fn3()).toBe(3) ================================================ FILE: spec/native-watcher-registry-spec.js ================================================ /** @babel */ import path from 'path'; import { Emitter } from 'event-kit'; import { NativeWatcherRegistry } from '../src/native-watcher-registry'; function findRootDirectory() { let current = process.cwd(); while (true) { let next = path.resolve(current, '..'); if (next === current) { return next; } else { current = next; } } } const ROOT = findRootDirectory(); function absolute(...parts) { const candidate = path.join(...parts); return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate); } function parts(fullPath) { return fullPath.split(path.sep).filter(part => part.length > 0); } class MockWatcher { constructor(normalizedPath) { this.normalizedPath = normalizedPath; this.native = null; } getNormalizedPathPromise() { return Promise.resolve(this.normalizedPath); } attachToNative(native, nativePath) { if (this.normalizedPath.startsWith(nativePath)) { if (this.native) { this.native.attached = this.native.attached.filter( each => each !== this ); } this.native = native; this.native.attached.push(this); } } } class MockNative { constructor(name) { this.name = name; this.attached = []; this.disposed = false; this.stopped = false; this.emitter = new Emitter(); } reattachTo(newNative, nativePath) { for (const watcher of this.attached) { watcher.attachToNative(newNative, nativePath); } } onWillStop(callback) { return this.emitter.on('will-stop', callback); } dispose() { this.disposed = true; } stop() { this.stopped = true; this.emitter.emit('will-stop'); } } describe('NativeWatcherRegistry', function() { let createNative, registry; beforeEach(function() { registry = new NativeWatcherRegistry(normalizedPath => createNative(normalizedPath) ); }); it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function() { const watcher = new MockWatcher(absolute('some', 'path')); const NATIVE = new MockNative('created'); createNative = () => NATIVE; await registry.attach(watcher); expect(watcher.native).toBe(NATIVE); }); it('reuses an existing NativeWatcher on the same directory', async function() { this.RETRY_FLAKY_TEST_AND_SLOW_DOWN_THE_BUILD(); const EXISTING = new MockNative('existing'); const existingPath = absolute('existing', 'path'); let firstTime = true; createNative = () => { if (firstTime) { firstTime = false; return EXISTING; } return new MockNative('nope'); }; await registry.attach(new MockWatcher(existingPath)); const watcher = new MockWatcher(existingPath); await registry.attach(watcher); expect(watcher.native).toBe(EXISTING); }); it('attaches to an existing NativeWatcher on a parent directory', async function() { const EXISTING = new MockNative('existing'); const parentDir = absolute('existing', 'path'); const subDir = path.join(parentDir, 'sub', 'directory'); let firstTime = true; createNative = () => { if (firstTime) { firstTime = false; return EXISTING; } return new MockNative('nope'); }; await registry.attach(new MockWatcher(parentDir)); const watcher = new MockWatcher(subDir); await registry.attach(watcher); expect(watcher.native).toBe(EXISTING); }); it('adopts Watchers from NativeWatchers on child directories', async function() { const parentDir = absolute('existing', 'path'); const childDir0 = path.join(parentDir, 'child', 'directory', 'zero'); const childDir1 = path.join(parentDir, 'child', 'directory', 'one'); const otherDir = absolute('another', 'path'); const CHILD0 = new MockNative('existing0'); const CHILD1 = new MockNative('existing1'); const OTHER = new MockNative('existing2'); const PARENT = new MockNative('parent'); createNative = dir => { if (dir === childDir0) { return CHILD0; } else if (dir === childDir1) { return CHILD1; } else if (dir === otherDir) { return OTHER; } else if (dir === parentDir) { return PARENT; } else { throw new Error(`Unexpected path: ${dir}`); } }; const watcher0 = new MockWatcher(childDir0); await registry.attach(watcher0); const watcher1 = new MockWatcher(childDir1); await registry.attach(watcher1); const watcher2 = new MockWatcher(otherDir); await registry.attach(watcher2); expect(watcher0.native).toBe(CHILD0); expect(watcher1.native).toBe(CHILD1); expect(watcher2.native).toBe(OTHER); // Consolidate all three watchers beneath the same native watcher on the parent directory const watcher = new MockWatcher(parentDir); await registry.attach(watcher); expect(watcher.native).toBe(PARENT); expect(watcher0.native).toBe(PARENT); expect(CHILD0.stopped).toBe(true); expect(CHILD0.disposed).toBe(true); expect(watcher1.native).toBe(PARENT); expect(CHILD1.stopped).toBe(true); expect(CHILD1.disposed).toBe(true); expect(watcher2.native).toBe(OTHER); expect(OTHER.stopped).toBe(false); expect(OTHER.disposed).toBe(false); }); describe('removing NativeWatchers', function() { it('happens when they stop', async function() { const STOPPED = new MockNative('stopped'); const RUNNING = new MockNative('running'); const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped'); const stoppedPathParts = stoppedPath .split(path.sep) .filter(part => part.length > 0); const runningPath = absolute( 'watcher', 'that', 'will', 'continue', 'to', 'exist' ); const runningPathParts = runningPath .split(path.sep) .filter(part => part.length > 0); createNative = dir => { if (dir === stoppedPath) { return STOPPED; } else if (dir === runningPath) { return RUNNING; } else { throw new Error(`Unexpected path: ${dir}`); } }; const stoppedWatcher = new MockWatcher(stoppedPath); await registry.attach(stoppedWatcher); const runningWatcher = new MockWatcher(runningPath); await registry.attach(runningWatcher); STOPPED.stop(); const runningNode = registry.tree.root.lookup(runningPathParts).when({ parent: node => node, missing: () => false, children: () => false }); expect(runningNode).toBeTruthy(); expect(runningNode.getNativeWatcher()).toBe(RUNNING); const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({ parent: () => false, missing: () => true, children: () => false }); expect(stoppedNode).toBe(true); }); it('reassigns new child watchers when a parent watcher is stopped', async function() { const CHILD0 = new MockNative('child0'); const CHILD1 = new MockNative('child1'); const PARENT = new MockNative('parent'); const parentDir = absolute('parent'); const childDir0 = path.join(parentDir, 'child0'); const childDir1 = path.join(parentDir, 'child1'); createNative = dir => { if (dir === parentDir) { return PARENT; } else if (dir === childDir0) { return CHILD0; } else if (dir === childDir1) { return CHILD1; } else { throw new Error(`Unexpected directory ${dir}`); } }; const parentWatcher = new MockWatcher(parentDir); const childWatcher0 = new MockWatcher(childDir0); const childWatcher1 = new MockWatcher(childDir1); await registry.attach(parentWatcher); await Promise.all([ registry.attach(childWatcher0), registry.attach(childWatcher1) ]); // All three watchers should share the parent watcher's native watcher. expect(parentWatcher.native).toBe(PARENT); expect(childWatcher0.native).toBe(PARENT); expect(childWatcher1.native).toBe(PARENT); // Stopping the parent should detach and recreate the child watchers. PARENT.stop(); expect(childWatcher0.native).toBe(CHILD0); expect(childWatcher1.native).toBe(CHILD1); expect( registry.tree.root.lookup(parts(parentDir)).when({ parent: () => false, missing: () => false, children: () => true }) ).toBe(true); expect( registry.tree.root.lookup(parts(childDir0)).when({ parent: () => true, missing: () => false, children: () => false }) ).toBe(true); expect( registry.tree.root.lookup(parts(childDir1)).when({ parent: () => true, missing: () => false, children: () => false }) ).toBe(true); }); it('consolidates children when splitting a parent watcher', async function() { const CHILD0 = new MockNative('child0'); const PARENT = new MockNative('parent'); const parentDir = absolute('parent'); const childDir0 = path.join(parentDir, 'child0'); const childDir1 = path.join(parentDir, 'child0', 'child1'); createNative = dir => { if (dir === parentDir) { return PARENT; } else if (dir === childDir0) { return CHILD0; } else { throw new Error(`Unexpected directory ${dir}`); } }; const parentWatcher = new MockWatcher(parentDir); const childWatcher0 = new MockWatcher(childDir0); const childWatcher1 = new MockWatcher(childDir1); await registry.attach(parentWatcher); await Promise.all([ registry.attach(childWatcher0), registry.attach(childWatcher1) ]); // All three watchers should share the parent watcher's native watcher. expect(parentWatcher.native).toBe(PARENT); expect(childWatcher0.native).toBe(PARENT); expect(childWatcher1.native).toBe(PARENT); // Stopping the parent should detach and create the child watchers. Both child watchers should // share the same native watcher. PARENT.stop(); expect(childWatcher0.native).toBe(CHILD0); expect(childWatcher1.native).toBe(CHILD0); expect( registry.tree.root.lookup(parts(parentDir)).when({ parent: () => false, missing: () => false, children: () => true }) ).toBe(true); expect( registry.tree.root.lookup(parts(childDir0)).when({ parent: () => true, missing: () => false, children: () => false }) ).toBe(true); expect( registry.tree.root.lookup(parts(childDir1)).when({ parent: () => true, missing: () => false, children: () => false }) ).toBe(true); }); }); }); ================================================ FILE: spec/notification-manager-spec.js ================================================ 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'); }); }); describe('clearing notifications', () => { it('clears the notifications when ::clear has been called', () => { manager.addSuccess('success'); expect(manager.getNotifications().length).toBe(1); manager.clear(); expect(manager.getNotifications().length).toBe(0); }); describe('adding events', () => { let clearSpy; beforeEach(() => { clearSpy = jasmine.createSpy(); manager.onDidClearNotifications(clearSpy); }); it('emits an event when the notifications have been cleared', () => { manager.clear(); expect(clearSpy).toHaveBeenCalled(); }); }); }); }); ================================================ FILE: spec/notification-spec.js ================================================ 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); })); }); }); ================================================ FILE: spec/package-manager-spec.js ================================================ 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'); 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, dev package path, and Atom repo package path in dev mode and dev resource path is set', () => { const packageManger = new PackageManager({}); const configDirPath = path.join('~', 'someConfig'); const resourcePath = path.join('~', '/atom'); packageManger.initialize({ configDirPath, resourcePath, devMode: true }); expect(packageManger.packageDirPaths.length).toBe(3); expect(packageManger.packageDirPaths).toContain( path.join(configDirPath, 'packages') ); expect(packageManger.packageDirPaths).toContain( path.join(configDirPath, 'dev', 'packages') ); expect(packageManger.packageDirPaths).toContain( path.join(resourcePath, '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', () => { beforeEach(() => { spyOn(Package.prototype, 'requireMainModule').andCallThrough(); }); 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(); workspaceCommandListener = jasmine.createSpy( 'workspaceCommandListener' ); registration = atom.commands.add( 'atom-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 both activation commands and deserializers', () => { let mainModule, promise, workspaceCommandListener, registration; beforeEach(() => { jasmine.attachToDOM(atom.workspace.getElement()); spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn(true); mainModule = require('./fixtures/packages/package-with-activation-commands-and-deserializers/index'); mainModule.activationCommandCallCount = 0; spyOn(mainModule, 'activate').andCallThrough(); workspaceCommandListener = jasmine.createSpy( 'workspaceCommandListener' ); registration = atom.commands.add( '.workspace', 'activation-command-2', workspaceCommandListener ); promise = atom.packages.activatePackage( 'package-with-activation-commands-and-deserializers' ); }); afterEach(() => { if (registration) { registration.dispose(); } mainModule = null; }); it('activates the package when a deserializer is called', async () => { expect(Package.prototype.requireMainModule.callCount).toBe(0); const state1 = { deserializer: 'Deserializer1', a: 'b' }; expect(atom.deserializers.deserialize(state1, atom)).toEqual({ wasDeserializedBy: 'deserializeMethod1', state: state1 }); await promise; expect(Package.prototype.requireMainModule.callCount).toBe(1); }); 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-2', { bubbles: true }) ); await promise; expect(mainModule.activate.callCount).toBe(1); expect(mainModule.activationCommandCallCount).toBe(1); expect(Package.prototype.requireMainModule.callCount).toBe(1); }); }); describe('when the package metadata includes `activationHooks`', () => { let mainModule, promise; beforeEach(() => { mainModule = require('./fixtures/packages/package-with-activation-hooks/index'); spyOn(mainModule, 'activate').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(mainModule.activate.callCount).toBe(1); expect(Package.prototype.requireMainModule.callCount).toBe(1); }); }); describe('when the package metadata includes `workspaceOpeners`', () => { let mainModule, promise; beforeEach(() => { mainModule = require('./fixtures/packages/package-with-workspace-openers/index'); spyOn(mainModule, 'activate').andCallThrough(); }); it('defers requiring/activating the main module until a registered opener is called', async () => { promise = atom.packages.activatePackage( 'package-with-workspace-openers' ); expect(Package.prototype.requireMainModule.callCount).toBe(0); atom.workspace.open('atom://fictitious'); await promise; expect(Package.prototype.requireMainModule.callCount).toBe(1); expect(mainModule.openerCount).toBe(1); }); it('activates the package immediately when the events are empty', async () => { mainModule = require('./fixtures/packages/package-with-empty-workspace-openers/index'); spyOn(mainModule, 'activate').andCallThrough(); atom.packages.activatePackage('package-with-empty-workspace-openers'); expect(mainModule.activate.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'); }); it('loads any tree-sitter grammars defined in the package', async () => { atom.config.set('core.useTreeSitterParsers', true); await atom.packages.activatePackage('package-with-tree-sitter-grammar'); const grammar = atom.grammars.selectGrammar('test.somelang'); expect(grammar.name).toBe('Some Language'); expect(grammar.languageModule.isFakeTreeSitterParser).toBe(true); }); }); 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 ); }); }); }); describe('::getAvailablePackageNames', () => { it('detects a symlinked package', () => { const packageSymLinkedSource = path.join( __dirname, 'fixtures', 'packages', 'folder', 'package-symlinked' ); const destination = path.join( atom.packages.getPackageDirPaths()[0], 'package-symlinked' ); if (!fs.isDirectorySync(destination)) { fs.symlinkSync(packageSymLinkedSource, destination, 'junction'); } const availablePackages = atom.packages.getAvailablePackageNames(); expect(availablePackages.includes('package-symlinked')).toBe(true); fs.removeSync(destination); }); }); }); ================================================ FILE: spec/package-spec.js ================================================ const path = require('path'); const Package = require('../src/package'); const ThemePackage = require('../src/theme-package'); const { mockLocalStorage } = require('./spec-helper'); describe('Package', function() { const build = (constructor, packagePath) => new constructor({ path: packagePath, packageManager: atom.packages, config: atom.config, styleManager: atom.styles, notificationManager: atom.notifications, keymapManager: atom.keymaps, commandRegistry: atom.command, grammarRegistry: atom.grammars, themeManager: atom.themes, menuManager: atom.menu, contextMenuManager: atom.contextMenu, deserializerManager: atom.deserializers, viewRegistry: atom.views }); const buildPackage = packagePath => build(Package, packagePath); const buildThemePackage = themePath => build(ThemePackage, themePath); describe('when the package contains incompatible native modules', function() { beforeEach(function() { atom.packages.devMode = false; mockLocalStorage(); }); afterEach(() => (atom.packages.devMode = true)); it('does not activate it', function() { const packagePath = atom.project .getDirectories()[0] .resolve('packages/package-with-incompatible-native-module'); const pack = buildPackage(packagePath); expect(pack.isCompatible()).toBe(false); expect(pack.incompatibleModules[0].name).toBe('native-module'); expect(pack.incompatibleModules[0].path).toBe( path.join(packagePath, 'node_modules', 'native-module') ); }); it('detects the package as incompatible even if .node file is loaded conditionally', function() { const packagePath = atom.project .getDirectories()[0] .resolve( 'packages/package-with-incompatible-native-module-loaded-conditionally' ); const pack = buildPackage(packagePath); expect(pack.isCompatible()).toBe(false); expect(pack.incompatibleModules[0].name).toBe('native-module'); expect(pack.incompatibleModules[0].path).toBe( path.join(packagePath, 'node_modules', 'native-module') ); }); it("utilizes _atomModuleCache if present to determine the package's native dependencies", function() { let packagePath = atom.project .getDirectories()[0] .resolve('packages/package-with-ignored-incompatible-native-module'); let pack = buildPackage(packagePath); expect(pack.getNativeModuleDependencyPaths().length).toBe(1); // doesn't see the incompatible module expect(pack.isCompatible()).toBe(true); packagePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/package-with-cached-incompatible-native-module') ); pack = buildPackage(packagePath); expect(pack.isCompatible()).toBe(false); }); it('caches the incompatible native modules in local storage', function() { const packagePath = atom.project .getDirectories()[0] .resolve('packages/package-with-incompatible-native-module'); expect(buildPackage(packagePath).isCompatible()).toBe(false); expect(global.localStorage.getItem.callCount).toBe(1); expect(global.localStorage.setItem.callCount).toBe(1); expect(buildPackage(packagePath).isCompatible()).toBe(false); expect(global.localStorage.getItem.callCount).toBe(2); expect(global.localStorage.setItem.callCount).toBe(1); }); it('logs an error to the console describing the problem', function() { const packagePath = atom.project .getDirectories()[0] .resolve('packages/package-with-incompatible-native-module'); spyOn(console, 'warn'); spyOn(atom.notifications, 'addFatalError'); buildPackage(packagePath).activateNow(); expect(atom.notifications.addFatalError).not.toHaveBeenCalled(); expect(console.warn.callCount).toBe(1); expect(console.warn.mostRecentCall.args[0]).toContain( 'it requires one or more incompatible native modules (native-module)' ); }); }); describe('::rebuild()', function() { beforeEach(function() { atom.packages.devMode = false; mockLocalStorage(); }); afterEach(() => (atom.packages.devMode = true)); it('returns a promise resolving to the results of `apm rebuild`', function() { const packagePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/package-with-index') ); const pack = buildPackage(packagePath); const rebuildCallbacks = []; spyOn(pack, 'runRebuildProcess').andCallFake(callback => rebuildCallbacks.push(callback) ); const promise = pack.rebuild(); rebuildCallbacks[0]({ code: 0, stdout: 'stdout output', stderr: 'stderr output' }); waitsFor(done => promise.then(function(result) { expect(result).toEqual({ code: 0, stdout: 'stdout output', stderr: 'stderr output' }); done(); }) ); }); it('persists build failures in local storage', function() { const packagePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/package-with-index') ); const pack = buildPackage(packagePath); expect(pack.isCompatible()).toBe(true); expect(pack.getBuildFailureOutput()).toBeNull(); const rebuildCallbacks = []; spyOn(pack, 'runRebuildProcess').andCallFake(callback => rebuildCallbacks.push(callback) ); pack.rebuild(); rebuildCallbacks[0]({ code: 13, stderr: 'It is broken' }); expect(pack.getBuildFailureOutput()).toBe('It is broken'); expect(pack.getIncompatibleNativeModules()).toEqual([]); expect(pack.isCompatible()).toBe(false); // A different package instance has the same failure output (simulates reload) const pack2 = buildPackage(packagePath); expect(pack2.getBuildFailureOutput()).toBe('It is broken'); expect(pack2.isCompatible()).toBe(false); // Clears the build failure after a successful build pack.rebuild(); rebuildCallbacks[1]({ code: 0, stdout: 'It worked' }); expect(pack.getBuildFailureOutput()).toBeNull(); expect(pack2.getBuildFailureOutput()).toBeNull(); }); it('sets cached incompatible modules to an empty array when the rebuild completes (there may be a build error, but rebuilding *deletes* native modules)', function() { const packagePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/package-with-incompatible-native-module') ); const pack = buildPackage(packagePath); expect(pack.getIncompatibleNativeModules().length).toBeGreaterThan(0); const rebuildCallbacks = []; spyOn(pack, 'runRebuildProcess').andCallFake(callback => rebuildCallbacks.push(callback) ); pack.rebuild(); expect(pack.getIncompatibleNativeModules().length).toBeGreaterThan(0); rebuildCallbacks[0]({ code: 0, stdout: 'It worked' }); expect(pack.getIncompatibleNativeModules().length).toBe(0); }); }); describe('theme', function() { let [editorElement, theme] = []; beforeEach(function() { editorElement = document.createElement('atom-text-editor'); jasmine.attachToDOM(editorElement); }); afterEach(() => waitsForPromise(function() { if (theme != null) { return Promise.resolve(theme.deactivate()); } }) ); describe('when the theme contains a single style file', function() { it('loads and applies css', function() { expect(getComputedStyle(editorElement).paddingBottom).not.toBe( '1234px' ); const themePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/theme-with-index-css') ); theme = buildThemePackage(themePath); theme.activate(); expect(getComputedStyle(editorElement).paddingTop).toBe('1234px'); }); it('parses, loads and applies less', function() { expect(getComputedStyle(editorElement).paddingBottom).not.toBe( '1234px' ); const themePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/theme-with-index-less') ); theme = buildThemePackage(themePath); theme.activate(); expect(getComputedStyle(editorElement).paddingTop).toBe('4321px'); }); }); describe('when the theme contains a package.json file', () => it('loads and applies stylesheets from package.json in the correct order', function() { expect(getComputedStyle(editorElement).paddingTop).not.toBe('101px'); expect(getComputedStyle(editorElement).paddingRight).not.toBe('102px'); expect(getComputedStyle(editorElement).paddingBottom).not.toBe('103px'); const themePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/theme-with-package-file') ); theme = buildThemePackage(themePath); theme.activate(); expect(getComputedStyle(editorElement).paddingTop).toBe('101px'); expect(getComputedStyle(editorElement).paddingRight).toBe('102px'); expect(getComputedStyle(editorElement).paddingBottom).toBe('103px'); })); describe('when the theme does not contain a package.json file and is a directory', () => it('loads all stylesheet files in the directory', function() { expect(getComputedStyle(editorElement).paddingTop).not.toBe('10px'); expect(getComputedStyle(editorElement).paddingRight).not.toBe('20px'); expect(getComputedStyle(editorElement).paddingBottom).not.toBe('30px'); const themePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/theme-without-package-file') ); theme = buildThemePackage(themePath); theme.activate(); expect(getComputedStyle(editorElement).paddingTop).toBe('10px'); expect(getComputedStyle(editorElement).paddingRight).toBe('20px'); expect(getComputedStyle(editorElement).paddingBottom).toBe('30px'); })); describe('reloading a theme', function() { beforeEach(function() { const themePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/theme-with-package-file') ); theme = buildThemePackage(themePath); theme.activate(); }); it('reloads without readding to the stylesheets list', function() { expect(theme.getStylesheetPaths().length).toBe(3); theme.reloadStylesheets(); expect(theme.getStylesheetPaths().length).toBe(3); }); }); describe('events', function() { beforeEach(function() { const themePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/theme-with-package-file') ); theme = buildThemePackage(themePath); theme.activate(); }); it('deactivated event fires on .deactivate()', function() { let spy; theme.onDidDeactivate((spy = jasmine.createSpy())); waitsForPromise(() => Promise.resolve(theme.deactivate())); runs(() => expect(spy).toHaveBeenCalled()); }); }); }); describe('.loadMetadata()', function() { let [packagePath, metadata] = []; beforeEach(function() { packagePath = __guard__(atom.project.getDirectories()[0], x => x.resolve('packages/package-with-different-directory-name') ); metadata = atom.packages.loadPackageMetadata(packagePath, true); }); it('uses the package name defined in package.json', () => expect(metadata.name).toBe('package-with-a-totally-different-name')); }); describe('the initialize() hook', function() { it('gets called when the package is activated', function() { const packagePath = atom.project .getDirectories()[0] .resolve('packages/package-with-deserializers'); const pack = buildPackage(packagePath); pack.requireMainModule(); const { mainModule } = pack; spyOn(mainModule, 'initialize'); expect(mainModule.initialize).not.toHaveBeenCalled(); pack.activate(); expect(mainModule.initialize).toHaveBeenCalled(); expect(mainModule.initialize.callCount).toBe(1); }); it('gets called when a deserializer is used', function() { const packagePath = atom.project .getDirectories()[0] .resolve('packages/package-with-deserializers'); const pack = buildPackage(packagePath); pack.requireMainModule(); const { mainModule } = pack; spyOn(mainModule, 'initialize'); pack.load(); expect(mainModule.initialize).not.toHaveBeenCalled(); atom.deserializers.deserialize({ deserializer: 'Deserializer1', a: 'b' }); expect(mainModule.initialize).toHaveBeenCalled(); }); }); }); function __guard__(value, transform) { return typeof value !== 'undefined' && value !== null ? transform(value) : undefined; } ================================================ FILE: spec/package-transpilation-registry-spec.js ================================================ /** @babel */ import path from 'path'; import PackageTranspilationRegistry from '../src/package-transpilation-registry'; const originalCompiler = { getCachePath: (sourceCode, filePath) => { return 'orig-cache-path'; }, compile: (sourceCode, filePath) => { return sourceCode + '-original-compiler'; }, shouldCompile: (sourceCode, filePath) => { return path.extname(filePath) === '.js'; } }; describe('PackageTranspilationRegistry', () => { let registry; let wrappedCompiler; beforeEach(() => { registry = new PackageTranspilationRegistry(); wrappedCompiler = registry.wrapTranspiler(originalCompiler); }); it('falls through to the original compiler by default', () => { spyOn(originalCompiler, 'getCachePath'); spyOn(originalCompiler, 'compile'); spyOn(originalCompiler, 'shouldCompile'); wrappedCompiler.getCachePath('source', '/path/to/file.js'); wrappedCompiler.compile('source', '/path/to/filejs'); wrappedCompiler.shouldCompile('source', '/path/to/file.js'); expect(originalCompiler.getCachePath).toHaveBeenCalled(); expect(originalCompiler.compile).toHaveBeenCalled(); expect(originalCompiler.shouldCompile).toHaveBeenCalled(); }); describe('when a file is contained in a path that has custom transpilation', () => { const hitPath = path.join('/path/to/lib/file.js'); const hitPathCoffee = path.join('/path/to/file2.coffee'); const missPath = path.join('/path/other/file3.js'); const hitPathMissSubdir = path.join('/path/to/file4.js'); const hitPathMissExt = path.join('/path/to/file5.ts'); const nodeModulesFolder = path.join('/path/to/lib/node_modules/file6.js'); const hitNonStandardExt = path.join('/path/to/file7.omgwhatisthis'); const jsSpec = { glob: 'lib/**/*.js', transpiler: './transpiler-js', options: { type: 'js' } }; const coffeeSpec = { glob: '*.coffee', transpiler: './transpiler-coffee', options: { type: 'coffee' } }; const omgSpec = { glob: '*.omgwhatisthis', transpiler: './transpiler-omg', options: { type: 'omg' } }; const expectedMeta = { name: 'my-package', path: path.join('/path/to'), meta: { some: 'metadata' } }; const jsTranspiler = { transpile: (sourceCode, filePath, options) => { return { code: sourceCode + '-transpiler-js' }; }, getCacheKeyData: (sourceCode, filePath, options) => { return 'js-transpiler-cache-data'; } }; const coffeeTranspiler = { transpile: (sourceCode, filePath, options) => { return { code: sourceCode + '-transpiler-coffee' }; }, getCacheKeyData: (sourceCode, filePath, options) => { return 'coffee-transpiler-cache-data'; } }; const omgTranspiler = { transpile: (sourceCode, filePath, options) => { return { code: sourceCode + '-transpiler-omg' }; }, getCacheKeyData: (sourceCode, filePath, options) => { return 'omg-transpiler-cache-data'; } }; beforeEach(() => { jsSpec._transpilerSource = 'js-transpiler-source'; coffeeSpec._transpilerSource = 'coffee-transpiler-source'; omgTranspiler._transpilerSource = 'omg-transpiler-source'; spyOn(registry, 'getTranspiler').andCallFake(spec => { if (spec.transpiler === './transpiler-js') return jsTranspiler; if (spec.transpiler === './transpiler-coffee') return coffeeTranspiler; if (spec.transpiler === './transpiler-omg') return omgTranspiler; throw new Error('bad transpiler path ' + spec.transpiler); }); registry.addTranspilerConfigForPath( path.join('/path/to'), 'my-package', { some: 'metadata' }, [jsSpec, coffeeSpec, omgSpec] ); }); it('always returns true from shouldCompile for a file in that dir that match a glob', () => { spyOn(originalCompiler, 'shouldCompile').andReturn(false); expect(wrappedCompiler.shouldCompile('source', hitPath)).toBe(true); expect(wrappedCompiler.shouldCompile('source', hitPathCoffee)).toBe(true); expect(wrappedCompiler.shouldCompile('source', hitNonStandardExt)).toBe( true ); expect(wrappedCompiler.shouldCompile('source', hitPathMissExt)).toBe( false ); expect(wrappedCompiler.shouldCompile('source', hitPathMissSubdir)).toBe( false ); expect(wrappedCompiler.shouldCompile('source', missPath)).toBe(false); expect(wrappedCompiler.shouldCompile('source', nodeModulesFolder)).toBe( false ); }); it('calls getCacheKeyData on the transpiler to get additional cache key data', () => { spyOn(registry, 'getTranspilerPath').andReturn('./transpiler-js'); spyOn(jsTranspiler, 'getCacheKeyData').andCallThrough(); wrappedCompiler.getCachePath('source', missPath, jsSpec); expect(jsTranspiler.getCacheKeyData).not.toHaveBeenCalledWith( 'source', missPath, jsSpec.options, expectedMeta ); wrappedCompiler.getCachePath('source', hitPath, jsSpec); expect(jsTranspiler.getCacheKeyData).toHaveBeenCalledWith( 'source', hitPath, jsSpec.options, expectedMeta ); }); it('compiles files matching a glob with the associated transpiler, and the old one otherwise', () => { spyOn(jsTranspiler, 'transpile').andCallThrough(); spyOn(coffeeTranspiler, 'transpile').andCallThrough(); spyOn(omgTranspiler, 'transpile').andCallThrough(); expect(wrappedCompiler.compile('source', hitPath)).toEqual( 'source-transpiler-js' ); expect(jsTranspiler.transpile).toHaveBeenCalledWith( 'source', hitPath, jsSpec.options, expectedMeta ); expect(wrappedCompiler.compile('source', hitPathCoffee)).toEqual( 'source-transpiler-coffee' ); expect(coffeeTranspiler.transpile).toHaveBeenCalledWith( 'source', hitPathCoffee, coffeeSpec.options, expectedMeta ); expect(wrappedCompiler.compile('source', hitNonStandardExt)).toEqual( 'source-transpiler-omg' ); expect(omgTranspiler.transpile).toHaveBeenCalledWith( 'source', hitNonStandardExt, omgSpec.options, expectedMeta ); expect(wrappedCompiler.compile('source', missPath)).toEqual( 'source-original-compiler' ); expect(wrappedCompiler.compile('source', hitPathMissExt)).toEqual( 'source-original-compiler' ); expect(wrappedCompiler.compile('source', hitPathMissSubdir)).toEqual( 'source-original-compiler' ); expect(wrappedCompiler.compile('source', nodeModulesFolder)).toEqual( 'source-original-compiler' ); }); describe('when the packages root path contains node_modules', () => { beforeEach(() => { registry.addTranspilerConfigForPath( path.join('/path/with/node_modules/in/root'), 'my-other-package', { some: 'metadata' }, [jsSpec] ); }); it('returns appropriate values from shouldCompile', () => { spyOn(originalCompiler, 'shouldCompile').andReturn(false); expect( wrappedCompiler.shouldCompile( 'source', '/path/with/node_modules/in/root/lib/test.js' ) ).toBe(true); expect( wrappedCompiler.shouldCompile( 'source', '/path/with/node_modules/in/root/lib/node_modules/test.js' ) ).toBe(false); }); }); }); }); ================================================ FILE: spec/pane-axis-element-spec.js ================================================ const PaneAxis = require('../src/pane-axis'); const PaneContainer = require('../src/pane-container'); const Pane = require('../src/pane'); const buildPane = () => new Pane({ applicationDelegate: atom.applicationDelegate, config: atom.config, deserializerManager: atom.deserializers, notificationManager: atom.notifications, viewRegistry: atom.views }); describe('PaneAxisElement', () => it('correctly subscribes and unsubscribes to the underlying model events on attach/detach', function() { const container = new PaneContainer({ config: atom.config, applicationDelegate: atom.applicationDelegate, viewRegistry: atom.views }); const axis = new PaneAxis({}, atom.views); axis.setContainer(container); const axisElement = axis.getElement(); const panes = [buildPane(), buildPane(), buildPane()]; jasmine.attachToDOM(axisElement); axis.addChild(panes[0]); expect(axisElement.children[0]).toBe(panes[0].getElement()); axisElement.remove(); axis.addChild(panes[1]); expect(axisElement.children[2]).toBeUndefined(); jasmine.attachToDOM(axisElement); expect(axisElement.children[2]).toBe(panes[1].getElement()); axis.addChild(panes[2]); expect(axisElement.children[4]).toBe(panes[2].getElement()); })); ================================================ FILE: spec/pane-container-element-spec.js ================================================ const PaneContainer = require('../src/pane-container'); const PaneAxis = require('../src/pane-axis'); const params = { location: 'center', config: atom.config, confirm: atom.confirm.bind(atom), viewRegistry: atom.views, applicationDelegate: atom.applicationDelegate }; describe('PaneContainerElement', function() { describe('when panes are added or removed', function() { it('inserts or removes resize elements', function() { const childTagNames = () => Array.from(paneAxisElement.children).map(child => child.nodeName.toLowerCase() ); const paneAxis = new PaneAxis({}, atom.views); var paneAxisElement = paneAxis.getElement(); expect(childTagNames()).toEqual([]); paneAxis.addChild(new PaneAxis({}, atom.views)); expect(childTagNames()).toEqual(['atom-pane-axis']); paneAxis.addChild(new PaneAxis({}, atom.views)); expect(childTagNames()).toEqual([ 'atom-pane-axis', 'atom-pane-resize-handle', 'atom-pane-axis' ]); paneAxis.addChild(new PaneAxis({}, atom.views)); expect(childTagNames()).toEqual([ 'atom-pane-axis', 'atom-pane-resize-handle', 'atom-pane-axis', 'atom-pane-resize-handle', 'atom-pane-axis' ]); paneAxis.removeChild(paneAxis.getChildren()[2]); expect(childTagNames()).toEqual([ 'atom-pane-axis', 'atom-pane-resize-handle', 'atom-pane-axis' ]); }); it('transfers focus to the next pane if a focused pane is removed', function() { const container = new PaneContainer(params); const containerElement = container.getElement(); const leftPane = container.getActivePane(); const leftPaneElement = leftPane.getElement(); const rightPane = leftPane.splitRight(); const rightPaneElement = rightPane.getElement(); jasmine.attachToDOM(containerElement); rightPaneElement.focus(); expect(document.activeElement).toBe(rightPaneElement); rightPane.destroy(); expect(containerElement).toHaveClass('panes'); expect(document.activeElement).toBe(leftPaneElement); }); }); describe('when a pane is split', () => it('builds appropriately-oriented atom-pane-axis elements', function() { const container = new PaneContainer(params); const containerElement = container.getElement(); const pane1 = container.getActivePane(); const pane2 = pane1.splitRight(); const pane3 = pane2.splitDown(); const horizontalPanes = containerElement.querySelectorAll( 'atom-pane-container > atom-pane-axis.horizontal > atom-pane' ); expect(horizontalPanes.length).toBe(1); expect(horizontalPanes[0]).toBe(pane1.getElement()); let verticalPanes = containerElement.querySelectorAll( 'atom-pane-container > atom-pane-axis.horizontal > atom-pane-axis.vertical > atom-pane' ); expect(verticalPanes.length).toBe(2); expect(verticalPanes[0]).toBe(pane2.getElement()); expect(verticalPanes[1]).toBe(pane3.getElement()); pane1.destroy(); verticalPanes = containerElement.querySelectorAll( 'atom-pane-container > atom-pane-axis.vertical > atom-pane' ); expect(verticalPanes.length).toBe(2); expect(verticalPanes[0]).toBe(pane2.getElement()); expect(verticalPanes[1]).toBe(pane3.getElement()); })); describe('when the resize element is dragged ', function() { let [container, containerElement] = []; beforeEach(function() { container = new PaneContainer(params); containerElement = container.getElement(); document.querySelector('#jasmine-content').appendChild(containerElement); }); const dragElementToPosition = function(element, clientX) { element.dispatchEvent( new MouseEvent('mousedown', { view: window, bubbles: true, button: 0 }) ); element.dispatchEvent( new MouseEvent('mousemove', { view: window, bubbles: true, clientX }) ); element.dispatchEvent( new MouseEvent('mouseup', { iew: window, bubbles: true, button: 0 }) ); }; const getElementWidth = element => element.getBoundingClientRect().width; const expectPaneScale = (...pairs) => (() => { const result = []; for (let [pane, expectedFlexScale] of pairs) { result.push( expect(pane.getFlexScale()).toBeCloseTo(expectedFlexScale, 0.1) ); } return result; })(); const getResizeElement = i => containerElement.querySelectorAll('atom-pane-resize-handle')[i]; const getPaneElement = i => containerElement.querySelectorAll('atom-pane')[i]; it('adds and removes panes in the direction that the pane is being dragged', function() { const leftPane = container.getActivePane(); expectPaneScale([leftPane, 1]); const middlePane = leftPane.splitRight(); expectPaneScale([leftPane, 1], [middlePane, 1]); dragElementToPosition( getResizeElement(0), getElementWidth(getPaneElement(0)) / 2 ); expectPaneScale([leftPane, 0.5], [middlePane, 1.5]); const rightPane = middlePane.splitRight(); expectPaneScale([leftPane, 0.5], [middlePane, 1.5], [rightPane, 1]); dragElementToPosition( getResizeElement(1), getElementWidth(getPaneElement(0)) + getElementWidth(getPaneElement(1)) / 2 ); expectPaneScale([leftPane, 0.5], [middlePane, 0.75], [rightPane, 1.75]); waitsForPromise(() => middlePane.close()); runs(() => expectPaneScale([leftPane, 0.44], [rightPane, 1.55])); waitsForPromise(() => leftPane.close()); runs(() => expectPaneScale([rightPane, 1])); }); it('splits or closes panes in orthogonal direction that the pane is being dragged', function() { const leftPane = container.getActivePane(); expectPaneScale([leftPane, 1]); const rightPane = leftPane.splitRight(); expectPaneScale([leftPane, 1], [rightPane, 1]); dragElementToPosition( getResizeElement(0), getElementWidth(getPaneElement(0)) / 2 ); expectPaneScale([leftPane, 0.5], [rightPane, 1.5]); // dynamically split pane, pane's flexScale will become to 1 const lowerPane = leftPane.splitDown(); expectPaneScale( [lowerPane, 1], [leftPane, 1], [leftPane.getParent(), 0.5] ); // dynamically close pane, the pane's flexscale will recover to origin value waitsForPromise(() => lowerPane.close()); runs(() => expectPaneScale([leftPane, 0.5], [rightPane, 1.5])); }); it('unsubscribes from mouse events when the pane is detached', function() { container.getActivePane().splitRight(); const element = getResizeElement(0); spyOn(document, 'addEventListener').andCallThrough(); spyOn(document, 'removeEventListener').andCallThrough(); spyOn(element, 'resizeStopped').andCallThrough(); element.dispatchEvent( new MouseEvent('mousedown', { view: window, bubbles: true, button: 0 }) ); waitsFor(() => document.addEventListener.callCount === 2); runs(function() { expect(element.resizeStopped.callCount).toBe(0); container.destroy(); expect(element.resizeStopped.callCount).toBe(1); expect(document.removeEventListener.callCount).toBe(2); }); }); it('does not throw an error when resized to fit content in a detached state', function() { container.getActivePane().splitRight(); const element = getResizeElement(0); element.remove(); expect(() => element.resizeToFitContent()).not.toThrow(); }); }); describe('pane resizing', function() { let [leftPane, rightPane] = []; beforeEach(function() { const container = new PaneContainer(params); leftPane = container.getActivePane(); rightPane = leftPane.splitRight(); }); describe('when pane:increase-size is triggered', () => it('increases the size of the pane', function() { expect(leftPane.getFlexScale()).toBe(1); expect(rightPane.getFlexScale()).toBe(1); atom.commands.dispatch(leftPane.getElement(), 'pane:increase-size'); expect(leftPane.getFlexScale()).toBe(1.1); expect(rightPane.getFlexScale()).toBe(1); atom.commands.dispatch(rightPane.getElement(), 'pane:increase-size'); expect(leftPane.getFlexScale()).toBe(1.1); expect(rightPane.getFlexScale()).toBe(1.1); })); describe('when pane:decrease-size is triggered', () => it('decreases the size of the pane', function() { expect(leftPane.getFlexScale()).toBe(1); expect(rightPane.getFlexScale()).toBe(1); atom.commands.dispatch(leftPane.getElement(), 'pane:decrease-size'); expect(leftPane.getFlexScale()).toBe(1 / 1.1); expect(rightPane.getFlexScale()).toBe(1); atom.commands.dispatch(rightPane.getElement(), 'pane:decrease-size'); expect(leftPane.getFlexScale()).toBe(1 / 1.1); expect(rightPane.getFlexScale()).toBe(1 / 1.1); })); }); describe('when only a single pane is present', function() { let [singlePane] = []; beforeEach(function() { const container = new PaneContainer(params); singlePane = container.getActivePane(); }); describe('when pane:increase-size is triggered', () => it('does not increases the size of the pane', function() { expect(singlePane.getFlexScale()).toBe(1); atom.commands.dispatch(singlePane.getElement(), 'pane:increase-size'); expect(singlePane.getFlexScale()).toBe(1); atom.commands.dispatch(singlePane.getElement(), 'pane:increase-size'); expect(singlePane.getFlexScale()).toBe(1); })); describe('when pane:decrease-size is triggered', () => it('does not decreases the size of the pane', function() { expect(singlePane.getFlexScale()).toBe(1); atom.commands.dispatch(singlePane.getElement(), 'pane:decrease-size'); expect(singlePane.getFlexScale()).toBe(1); atom.commands.dispatch(singlePane.getElement(), 'pane:decrease-size'); expect(singlePane.getFlexScale()).toBe(1); })); }); }); ================================================ FILE: spec/pane-container-spec.js ================================================ const PaneContainer = require('../src/pane-container'); describe('PaneContainer', () => { let confirm, params; beforeEach(() => { confirm = spyOn(atom.applicationDelegate, 'confirm').andCallFake( (options, callback) => callback(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.andCallFake((options, callback) => callback(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.andCallFake((options, callback) => callback(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.length).toBe(6); expect(events[1]).toEqual([ 'did', { item: item1, pane: pane1, index: 0 } ]); expect(events[3]).toEqual([ 'did', { item: item3, pane: pane2, index: 1 } ]); expect(events[5]).toEqual([ 'did', { item: item2, pane: pane2, index: 0 } ]); expect(events[0][0]).toEqual('will'); expect(events[0][1].item).toEqual(item1); expect(events[0][1].pane).toEqual(pane1); expect(events[0][1].index).toEqual(0); expect(typeof events[0][1].prevent).toEqual('function'); expect(events[2][0]).toEqual('will'); expect(events[2][1].item).toEqual(item3); expect(events[2][1].pane).toEqual(pane2); expect(events[2][1].index).toEqual(1); expect(typeof events[2][1].prevent).toEqual('function'); expect(events[4][0]).toEqual('will'); expect(events[4][1].item).toEqual(item2); expect(events[4][1].pane).toEqual(pane2); expect(events[4][1].index).toEqual(0); expect(typeof events[4][1].prevent).toEqual('function'); }); }); 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); })); }); }); ================================================ FILE: spec/pane-element-spec.js ================================================ const PaneContainer = require('../src/pane-container'); describe('PaneElement', function() { let [paneElement, container, containerElement, pane] = []; beforeEach(function() { spyOn(atom.applicationDelegate, 'open'); container = new PaneContainer({ location: 'center', config: atom.config, confirm: atom.confirm.bind(atom), viewRegistry: atom.views, applicationDelegate: atom.applicationDelegate }); containerElement = container.getElement(); pane = container.getActivePane(); paneElement = pane.getElement(); }); describe("when the pane's active status changes", () => it('adds or removes the .active class as appropriate', function() { const pane2 = pane.splitRight(); expect(pane2.isActive()).toBe(true); expect(paneElement.className).not.toMatch(/active/); pane.activate(); expect(paneElement.className).toMatch(/active/); pane2.activate(); expect(paneElement.className).not.toMatch(/active/); })); describe('when the active item changes', function() { it('hides all item elements except the active one', function() { const item1 = document.createElement('div'); const item2 = document.createElement('div'); const item3 = document.createElement('div'); pane.addItem(item1); pane.addItem(item2); pane.addItem(item3); expect(pane.getActiveItem()).toBe(item1); expect(item1.parentElement).toBeDefined(); expect(item1.style.display).toBe(''); expect(item2.parentElement).toBeNull(); expect(item3.parentElement).toBeNull(); pane.activateItem(item2); expect(item2.parentElement).toBeDefined(); expect(item1.style.display).toBe('none'); expect(item2.style.display).toBe(''); expect(item3.parentElement).toBeNull(); pane.activateItem(item3); expect(item3.parentElement).toBeDefined(); expect(item1.style.display).toBe('none'); expect(item2.style.display).toBe('none'); expect(item3.style.display).toBe(''); }); it('transfers focus to the new item if the previous item was focused', function() { const item1 = document.createElement('div'); item1.tabIndex = -1; const item2 = document.createElement('div'); item2.tabIndex = -1; pane.addItem(item1); pane.addItem(item2); jasmine.attachToDOM(paneElement); paneElement.focus(); expect(document.activeElement).toBe(item1); pane.activateItem(item2); expect(document.activeElement).toBe(item2); }); describe('if the active item is a model object', () => it('retrieves the associated view from atom.views and appends it to the itemViews div', function() { class TestModel {} atom.views.addViewProvider(TestModel, function(model) { const view = document.createElement('div'); view.model = model; return view; }); const item1 = new TestModel(); const item2 = new TestModel(); pane.addItem(item1); pane.addItem(item2); expect(paneElement.itemViews.children[0].model).toBe(item1); expect(paneElement.itemViews.children[0].style.display).toBe(''); pane.activateItem(item2); expect(paneElement.itemViews.children[1].model).toBe(item2); expect(paneElement.itemViews.children[0].style.display).toBe('none'); expect(paneElement.itemViews.children[1].style.display).toBe(''); })); describe('when the new active implements .getPath()', function() { it('adds the file path and file name as a data attribute on the pane', function() { const item1 = document.createElement('div'); item1.getPath = () => '/foo/bar.txt'; const item2 = document.createElement('div'); pane.addItem(item1); pane.addItem(item2); expect(paneElement.dataset.activeItemPath).toBe('/foo/bar.txt'); expect(paneElement.dataset.activeItemName).toBe('bar.txt'); pane.activateItem(item2); expect(paneElement.dataset.activeItemPath).toBeUndefined(); expect(paneElement.dataset.activeItemName).toBeUndefined(); pane.activateItem(item1); expect(paneElement.dataset.activeItemPath).toBe('/foo/bar.txt'); expect(paneElement.dataset.activeItemName).toBe('bar.txt'); pane.destroyItems(); expect(paneElement.dataset.activeItemPath).toBeUndefined(); expect(paneElement.dataset.activeItemName).toBeUndefined(); }); describe('when the path of the item changes', function() { let [item1, item2] = []; beforeEach(function() { item1 = document.createElement('div'); item1.path = '/foo/bar.txt'; item1.changePathCallbacks = []; item1.setPath = function(path) { this.path = path; for (let callback of Array.from(this.changePathCallbacks)) { callback(); } }; item1.getPath = function() { return this.path; }; item1.onDidChangePath = function(callback) { this.changePathCallbacks.push(callback); return { dispose: () => { this.changePathCallbacks = this.changePathCallbacks.filter( f => f !== 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', function() { 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', function() { describe('when the destroyed item is an element', () => it('removes the item from the itemViews div', function() { const item1 = document.createElement('div'); const item2 = document.createElement('div'); pane.addItem(item1); pane.addItem(item2); paneElement = pane.getElement(); expect(item1.parentElement).toBe(paneElement.itemViews); pane.destroyItem(item1); expect(item1.parentElement).toBeNull(); expect(item2.parentElement).toBe(paneElement.itemViews); pane.destroyItem(item2); expect(item2.parentElement).toBeNull(); })); describe('when the destroyed item is a model', () => it("removes the model's associated view", function() { class TestModel {} atom.views.addViewProvider(TestModel, function(model) { const view = document.createElement('div'); model.element = view; view.model = model; return view; }); const item1 = new TestModel(); const item2 = new TestModel(); pane.addItem(item1); pane.addItem(item2); expect(item1.element.parentElement).toBe(paneElement.itemViews); pane.destroyItem(item1); expect(item1.element.parentElement).toBeNull(); expect(item2.element.parentElement).toBe(paneElement.itemViews); pane.destroyItem(item2); expect(item2.element.parentElement).toBeNull(); })); }); describe('when the pane element is focused', function() { it('transfers focus to the active view', function() { const item = document.createElement('div'); item.tabIndex = -1; pane.activateItem(item); jasmine.attachToDOM(paneElement); expect(document.activeElement).toBe(document.body); paneElement.focus(); expect(document.activeElement).toBe(item); document.body.focus(); pane.activate(); expect(document.activeElement).toBe(item); }); it('makes the pane active', function() { pane.splitRight(); expect(pane.isActive()).toBe(false); jasmine.attachToDOM(paneElement); paneElement.focus(); expect(pane.isActive()).toBe(true); }); it('does not re-activate the pane when focus changes within the pane', function() { const item = document.createElement('div'); const itemChild = document.createElement('div'); item.tabIndex = -1; itemChild.tabIndex = -1; item.appendChild(itemChild); jasmine.attachToDOM(paneElement); pane.activateItem(item); pane.activate(); let activationCount = 0; pane.onDidActivate(() => activationCount++); itemChild.focus(); expect(activationCount).toBe(0); }); }); describe('when the pane element is attached', () => it('focuses the pane element if isFocused() returns true on its model', function() { pane.focus(); jasmine.attachToDOM(paneElement); expect(document.activeElement).toBe(paneElement); })); describe('drag and drop', function() { const buildDragEvent = function(type, files) { const dataTransfer = { files, data: {}, setData(key, value) { this.data[key] = value; }, getData(key) { return this.data[key]; } }; const event = new CustomEvent('drop'); event.dataTransfer = dataTransfer; return event; }; describe('when a file is dragged to the pane', () => it('opens it', function() { const event = buildDragEvent('drop', [ { path: '/fake1' }, { path: '/fake2' } ]); paneElement.dispatchEvent(event); expect(atom.applicationDelegate.open.callCount).toBe(1); expect(atom.applicationDelegate.open.argsForCall[0][0]).toEqual({ pathsToOpen: ['/fake1', '/fake2'], here: true }); })); describe('when a non-file is dragged to the pane', () => it('does nothing', function() { const event = buildDragEvent('drop', []); paneElement.dispatchEvent(event); expect(atom.applicationDelegate.open).not.toHaveBeenCalled(); })); }); describe('resize', () => it("shrinks independently of its contents' width", function() { jasmine.attachToDOM(containerElement); const item = document.createElement('div'); item.style.width = '2000px'; item.style.height = '30px'; paneElement.insertBefore(item, paneElement.children[0]); paneElement.style.flexGrow = 0.1; expect(paneElement.getBoundingClientRect().width).toBeGreaterThan(0); expect(paneElement.getBoundingClientRect().width).toBeLessThan( item.getBoundingClientRect().width ); paneElement.style.flexGrow = 0; expect(paneElement.getBoundingClientRect().width).toBe(0); })); }); ================================================ FILE: spec/pane-spec.js ================================================ const { extend } = require('underscore-plus'); const { Emitter } = require('event-kit'); const Grim = require('grim'); const Pane = require('../src/pane'); const PaneContainer = require('../src/pane-container'); const { conditionPromise, timeoutPromise } = require('./async-spec-helpers'); describe('Pane', () => { let confirm, showSaveDialog, deserializerDisposable; class Item { static deserialize({ name, uri }) { return new Item(name, uri); } constructor(name, uri) { this.name = name; this.uri = uri; this.emitter = new Emitter(); this.destroyed = false; } getURI() { return this.uri; } getPath() { return this.path; } isEqual(other) { return this.name === (other && other.name); } isPermanentDockItem() { return false; } isDestroyed() { return this.destroyed; } serialize() { return { deserializer: 'Item', name: this.name, uri: this.uri }; } copy() { return new Item(this.name, this.uri); } destroy() { this.destroyed = true; return this.emitter.emit('did-destroy'); } onDidDestroy(fn) { return this.emitter.on('did-destroy', fn); } onDidTerminatePendingState(callback) { return this.emitter.on('terminate-pending-state', callback); } terminatePendingState() { return this.emitter.emit('terminate-pending-state'); } } beforeEach(() => { confirm = spyOn(atom.applicationDelegate, 'confirm'); showSaveDialog = spyOn(atom.applicationDelegate, 'showSaveDialog'); deserializerDisposable = atom.deserializers.add(Item); }); afterEach(() => { deserializerDisposable.dispose(); }); function paneParams(params) { return extend( { applicationDelegate: atom.applicationDelegate, config: atom.config, deserializerManager: atom.deserializers, notificationManager: atom.notifications }, params ); } describe('construction', () => { it('sets the active item to the first item', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B')] }) ); expect(pane.getActiveItem()).toBe(pane.itemAtIndex(0)); }); it('compacts the items array', () => { const pane = new Pane( paneParams({ items: [undefined, new Item('A'), null, new Item('B')] }) ); expect(pane.getItems().length).toBe(2); expect(pane.getActiveItem()).toBe(pane.itemAtIndex(0)); }); }); describe('::activate()', () => { let container, pane1, pane2; beforeEach(() => { container = new PaneContainer({ location: 'center', config: atom.config, applicationDelegate: atom.applicationDelegate }); container.getActivePane().splitRight(); [pane1, pane2] = container.getPanes(); }); it('changes the active pane on the container', () => { expect(container.getActivePane()).toBe(pane2); pane1.activate(); expect(container.getActivePane()).toBe(pane1); pane2.activate(); expect(container.getActivePane()).toBe(pane2); }); it('invokes ::onDidChangeActivePane observers on the container', () => { const observed = []; container.onDidChangeActivePane(activePane => observed.push(activePane)); pane1.activate(); pane1.activate(); pane2.activate(); pane1.activate(); expect(observed).toEqual([pane1, pane2, pane1]); }); it('invokes ::onDidChangeActive observers on the relevant panes', () => { const observed = []; pane1.onDidChangeActive(active => observed.push(active)); pane1.activate(); pane2.activate(); expect(observed).toEqual([true, false]); }); it('invokes ::onDidActivate() observers', () => { let eventCount = 0; pane1.onDidActivate(() => eventCount++); pane1.activate(); pane1.activate(); pane2.activate(); expect(eventCount).toBe(2); }); }); describe('::addItem(item, index)', () => { it('adds the item at the given index', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B')] }) ); const [item1, item2] = pane.getItems(); const item3 = new Item('C'); pane.addItem(item3, { index: 1 }); expect(pane.getItems()).toEqual([item1, item3, item2]); }); it('adds the item after the active item if no index is provided', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); const [item1, item2, item3] = pane.getItems(); pane.activateItem(item2); const item4 = new Item('D'); pane.addItem(item4); expect(pane.getItems()).toEqual([item1, item2, item4, item3]); }); it('sets the active item after adding the first item', () => { const pane = new Pane(paneParams()); const item = new Item('A'); pane.addItem(item); expect(pane.getActiveItem()).toBe(item); }); it('invokes ::onDidAddItem() observers', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B')] }) ); const events = []; pane.onDidAddItem(event => events.push(event)); const item = new Item('C'); pane.addItem(item, { index: 1 }); expect(events).toEqual([{ item, index: 1, moved: false }]); }); it('throws an exception if the item is already present on a pane', () => { const item = new Item('A'); const container = new PaneContainer({ config: atom.config, applicationDelegate: atom.applicationDelegate }); const pane1 = container.getActivePane(); pane1.addItem(item); const pane2 = pane1.splitRight(); expect(() => pane2.addItem(item)).toThrow(); }); it("throws an exception if the item isn't an object", () => { const pane = new Pane(paneParams({ items: [] })); expect(() => pane.addItem(null)).toThrow(); expect(() => pane.addItem('foo')).toThrow(); expect(() => pane.addItem(1)).toThrow(); }); it('destroys any existing pending item', () => { const pane = new Pane(paneParams({ items: [] })); const itemA = new Item('A'); const itemB = new Item('B'); const itemC = new Item('C'); pane.addItem(itemA, { pending: false }); pane.addItem(itemB, { pending: true }); pane.addItem(itemC, { pending: false }); expect(itemB.isDestroyed()).toBe(true); }); it('adds the new item before destroying any existing pending item', () => { const eventOrder = []; const pane = new Pane(paneParams({ items: [] })); const itemA = new Item('A'); const itemB = new Item('B'); pane.addItem(itemA, { pending: true }); pane.onDidAddItem(function({ item }) { if (item === itemB) eventOrder.push('add'); }); pane.onDidRemoveItem(function({ item }) { if (item === itemA) eventOrder.push('remove'); }); pane.addItem(itemB); waitsFor(() => eventOrder.length === 2); runs(() => expect(eventOrder).toEqual(['add', 'remove'])); }); it('subscribes to be notified when item terminates its pending state', () => { const fakeDisposable = { dispose: () => {} }; const spy = jasmine .createSpy('onDidTerminatePendingState') .andReturn(fakeDisposable); const pane = new Pane(paneParams({ items: [] })); const item = { getTitle: () => '', onDidTerminatePendingState: spy }; pane.addItem(item); expect(spy).toHaveBeenCalled(); }); it('subscribes to be notified when item is destroyed', () => { const fakeDisposable = { dispose: () => {} }; const spy = jasmine.createSpy('onDidDestroy').andReturn(fakeDisposable); const pane = new Pane(paneParams({ items: [] })); const item = { getTitle: () => '', onDidDestroy: spy }; pane.addItem(item); expect(spy).toHaveBeenCalled(); }); describe('when using the old API of ::addItem(item, index)', () => { beforeEach(() => spyOn(Grim, 'deprecate')); it('supports the older public API', () => { const pane = new Pane(paneParams({ items: [] })); const itemA = new Item('A'); const itemB = new Item('B'); const itemC = new Item('C'); pane.addItem(itemA, 0); pane.addItem(itemB, 0); pane.addItem(itemC, 0); expect(pane.getItems()).toEqual([itemC, itemB, itemA]); }); it('shows a deprecation warning', () => { const pane = new Pane(paneParams({ items: [] })); pane.addItem(new Item(), 2); expect(Grim.deprecate).toHaveBeenCalledWith( 'Pane::addItem(item, 2) is deprecated in favor of Pane::addItem(item, {index: 2})' ); }); }); }); describe('::activateItem(item)', () => { let pane = null; beforeEach(() => { pane = new Pane(paneParams({ items: [new Item('A'), new Item('B')] })); }); it('changes the active item to the current item', () => { expect(pane.getActiveItem()).toBe(pane.itemAtIndex(0)); pane.activateItem(pane.itemAtIndex(1)); expect(pane.getActiveItem()).toBe(pane.itemAtIndex(1)); }); it("adds the given item if it isn't present in ::items", () => { const item = new Item('C'); pane.activateItem(item); expect(pane.getItems().includes(item)).toBe(true); expect(pane.getActiveItem()).toBe(item); }); it('invokes ::onDidChangeActiveItem() observers', () => { const observed = []; pane.onDidChangeActiveItem(item => observed.push(item)); pane.activateItem(pane.itemAtIndex(1)); expect(observed).toEqual([pane.itemAtIndex(1)]); }); describe('when the item being activated is pending', () => { let itemC = null; let itemD = null; beforeEach(() => { itemC = new Item('C'); itemD = new Item('D'); }); it('replaces the active item if it is pending', () => { pane.activateItem(itemC, { pending: true }); expect(pane.getItems().map(item => item.name)).toEqual(['A', 'C', 'B']); pane.activateItem(itemD, { pending: true }); expect(pane.getItems().map(item => item.name)).toEqual(['A', 'D', 'B']); }); it('adds the item after the active item if it is not pending', () => { pane.activateItem(itemC, { pending: true }); pane.activateItemAtIndex(2); pane.activateItem(itemD, { pending: true }); expect(pane.getItems().map(item => item.name)).toEqual(['A', 'B', 'D']); }); }); }); describe('::setPendingItem', () => { let pane = null; beforeEach(() => { pane = atom.workspace.getActivePane(); }); it('changes the pending item', () => { expect(pane.getPendingItem()).toBeNull(); pane.setPendingItem('fake item'); expect(pane.getPendingItem()).toEqual('fake item'); }); }); describe('::onItemDidTerminatePendingState callback', () => { let pane = null; let callbackCalled = false; beforeEach(() => { pane = atom.workspace.getActivePane(); callbackCalled = false; }); it('is called when the pending item changes', () => { pane.setPendingItem('fake item one'); pane.onItemDidTerminatePendingState(function(item) { callbackCalled = true; expect(item).toEqual('fake item one'); }); pane.setPendingItem('fake item two'); expect(callbackCalled).toBeTruthy(); }); it('has access to the new pending item via ::getPendingItem', () => { pane.setPendingItem('fake item one'); pane.onItemDidTerminatePendingState(function(item) { callbackCalled = true; expect(pane.getPendingItem()).toEqual('fake item two'); }); pane.setPendingItem('fake item two'); expect(callbackCalled).toBeTruthy(); }); it("isn't called when a pending item is replaced with a new one", async () => { pane = null; const pendingSpy = jasmine.createSpy('onItemDidTerminatePendingState'); const destroySpy = jasmine.createSpy('onWillDestroyItem'); await atom.workspace.open('sample.txt', { pending: true }); pane = atom.workspace.getActivePane(); pane.onItemDidTerminatePendingState(pendingSpy); pane.onWillDestroyItem(destroySpy); await atom.workspace.open('sample.js', { pending: true }); expect(destroySpy).toHaveBeenCalled(); expect(pendingSpy).not.toHaveBeenCalled(); }); }); describe('::activateNextRecentlyUsedItem() and ::activatePreviousRecentlyUsedItem()', () => { it('sets the active item to the next/previous item in the itemStack, looping around at either end', () => { const pane = new Pane( paneParams({ items: [ new Item('A'), new Item('B'), new Item('C'), new Item('D'), new Item('E') ] }) ); const [item1, item2, item3, item4, item5] = pane.getItems(); pane.itemStack = [item3, item1, item2, item5, item4]; pane.activateItem(item4); expect(pane.getActiveItem()).toBe(item4); pane.activateNextRecentlyUsedItem(); expect(pane.getActiveItem()).toBe(item5); pane.activateNextRecentlyUsedItem(); expect(pane.getActiveItem()).toBe(item2); pane.activatePreviousRecentlyUsedItem(); expect(pane.getActiveItem()).toBe(item5); pane.activatePreviousRecentlyUsedItem(); expect(pane.getActiveItem()).toBe(item4); pane.activatePreviousRecentlyUsedItem(); expect(pane.getActiveItem()).toBe(item3); pane.activatePreviousRecentlyUsedItem(); expect(pane.getActiveItem()).toBe(item1); pane.activateNextRecentlyUsedItem(); expect(pane.getActiveItem()).toBe(item3); pane.activateNextRecentlyUsedItem(); expect(pane.getActiveItem()).toBe(item4); pane.activateNextRecentlyUsedItem(); pane.moveActiveItemToTopOfStack(); expect(pane.getActiveItem()).toBe(item5); expect(pane.itemStack[4]).toBe(item5); }); }); describe('::activateNextItem() and ::activatePreviousItem()', () => { it('sets the active item to the next/previous item, looping around at either end', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); const [item1, item2, item3] = pane.getItems(); expect(pane.getActiveItem()).toBe(item1); pane.activatePreviousItem(); expect(pane.getActiveItem()).toBe(item3); pane.activatePreviousItem(); expect(pane.getActiveItem()).toBe(item2); pane.activateNextItem(); expect(pane.getActiveItem()).toBe(item3); pane.activateNextItem(); expect(pane.getActiveItem()).toBe(item1); }); }); describe('::activateLastItem()', () => { it('sets the active item to the last item', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); const [item1, , item3] = pane.getItems(); expect(pane.getActiveItem()).toBe(item1); pane.activateLastItem(); expect(pane.getActiveItem()).toBe(item3); }); }); describe('::moveItemRight() and ::moveItemLeft()', () => { it('moves the active item to the right and left, without looping around at either end', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); const [item1, item2, item3] = pane.getItems(); pane.activateItemAtIndex(0); expect(pane.getActiveItem()).toBe(item1); pane.moveItemLeft(); expect(pane.getItems()).toEqual([item1, item2, item3]); pane.moveItemRight(); expect(pane.getItems()).toEqual([item2, item1, item3]); pane.moveItemLeft(); expect(pane.getItems()).toEqual([item1, item2, item3]); pane.activateItemAtIndex(2); expect(pane.getActiveItem()).toBe(item3); pane.moveItemRight(); expect(pane.getItems()).toEqual([item1, item2, item3]); }); }); describe('::activateItemAtIndex(index)', () => { it('activates the item at the given index', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); const [item1, item2, item3] = pane.getItems(); pane.activateItemAtIndex(2); expect(pane.getActiveItem()).toBe(item3); pane.activateItemAtIndex(1); expect(pane.getActiveItem()).toBe(item2); pane.activateItemAtIndex(0); expect(pane.getActiveItem()).toBe(item1); // Doesn't fail with out-of-bounds indices pane.activateItemAtIndex(100); expect(pane.getActiveItem()).toBe(item1); pane.activateItemAtIndex(-1); expect(pane.getActiveItem()).toBe(item1); }); }); describe('::destroyItem(item)', () => { let pane, item1, item2, item3; beforeEach(() => { pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); [item1, item2, item3] = pane.getItems(); }); it('removes the item from the items list and destroys it', () => { expect(pane.getActiveItem()).toBe(item1); pane.destroyItem(item2); expect(pane.getItems().includes(item2)).toBe(false); expect(item2.isDestroyed()).toBe(true); expect(pane.getActiveItem()).toBe(item1); pane.destroyItem(item1); expect(pane.getItems().includes(item1)).toBe(false); expect(item1.isDestroyed()).toBe(true); }); it('removes the item from the itemStack', () => { pane.itemStack = [item2, item3, item1]; pane.activateItem(item1); expect(pane.getActiveItem()).toBe(item1); pane.destroyItem(item3); expect(pane.itemStack).toEqual([item2, item1]); expect(pane.getActiveItem()).toBe(item1); pane.destroyItem(item1); expect(pane.itemStack).toEqual([item2]); expect(pane.getActiveItem()).toBe(item2); pane.destroyItem(item2); expect(pane.itemStack).toEqual([]); expect(pane.getActiveItem()).toBeUndefined(); }); it('does nothing if prevented', () => { const container = new PaneContainer({ config: atom.config, deserializerManager: atom.deserializers, applicationDelegate: atom.applicationDelegate }); pane.setContainer(container); container.onWillDestroyPaneItem(e => e.prevent()); pane.itemStack = [item2, item3, item1]; pane.activateItem(item1); expect(pane.getActiveItem()).toBe(item1); pane.destroyItem(item3); expect(pane.itemStack).toEqual([item2, item3, item1]); expect(pane.getActiveItem()).toBe(item1); pane.destroyItem(item1); expect(pane.itemStack).toEqual([item2, item3, item1]); expect(pane.getActiveItem()).toBe(item1); pane.destroyItem(item2); expect(pane.itemStack).toEqual([item2, item3, item1]); expect(pane.getActiveItem()).toBe(item1); }); 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(async event => { expect(item2.isDestroyed()).toBe(false); await timeoutPromise(50); expect(item2.isDestroyed()).toBe(false); events.push(['will-destroy-item', event]); }); pane.container.onWillDestroyPaneItem(async event => { expect(item2.isDestroyed()).toBe(false); await timeoutPromise(50); expect(item2.isDestroyed()).toBe(false); events.push(['will-destroy-pane-item', event]); }); await pane.destroyItem(item2); expect(item2.isDestroyed()).toBe(true); expect(events[0][0]).toEqual('will-destroy-item'); expect(events[0][1].item).toEqual(item2); expect(events[0][1].index).toEqual(1); expect(events[1][0]).toEqual('will-destroy-pane-item'); expect(events[1][1].item).toEqual(item2); expect(events[1][1].index).toEqual(1); expect(typeof events[1][1].prevent).toEqual('function'); expect(events[1][1].pane).toEqual(pane); }); it('invokes ::onWillRemoveItem() observers', () => { const events = []; pane.onWillRemoveItem(event => events.push(event)); pane.destroyItem(item2); expect(events).toEqual([ { item: item2, index: 1, moved: false, destroyed: true } ]); }); it('invokes ::onDidRemoveItem() observers', () => { const events = []; pane.onDidRemoveItem(event => events.push(event)); pane.destroyItem(item2); expect(events).toEqual([ { item: item2, index: 1, moved: false, destroyed: true } ]); }); describe('when the destroyed item is the active item and is the first item', () => { it('activates the next item', () => { expect(pane.getActiveItem()).toBe(item1); pane.destroyItem(item1); expect(pane.getActiveItem()).toBe(item2); }); }); describe('when the destroyed item is the active item and is not the first item', () => { beforeEach(() => pane.activateItem(item2)); it('activates the previous item', () => { expect(pane.getActiveItem()).toBe(item2); pane.destroyItem(item2); expect(pane.getActiveItem()).toBe(item1); }); }); describe('if the item is modified', () => { let itemURI = null; beforeEach(() => { item1.shouldPromptToSave = () => true; item1.save = jasmine.createSpy('save'); item1.saveAs = jasmine.createSpy('saveAs'); item1.getURI = () => itemURI; }); describe('if the [Save] option is selected', () => { describe('when the item has a uri', () => { it('saves the item before destroying it', async () => { itemURI = 'test'; confirm.andCallFake((options, callback) => callback(0)); const success = await pane.destroyItem(item1); expect(item1.save).toHaveBeenCalled(); expect(pane.getItems().includes(item1)).toBe(false); expect(item1.isDestroyed()).toBe(true); expect(success).toBe(true); }); }); describe('when the item has no uri', () => { it('presents a save-as dialog, then saves the item with the given uri before removing and destroying it', async () => { jasmine.useRealClock(); itemURI = null; showSaveDialog.andCallFake((options, callback) => callback('/selected/path') ); confirm.andCallFake((options, callback) => callback(0)); const success = await pane.destroyItem(item1); expect(showSaveDialog.mostRecentCall.args[0]).toEqual({}); await conditionPromise(() => item1.saveAs.callCount === 1); expect(item1.saveAs).toHaveBeenCalledWith('/selected/path'); expect(pane.getItems().includes(item1)).toBe(false); expect(item1.isDestroyed()).toBe(true); expect(success).toBe(true); }); }); }); describe("if the [Don't Save] option is selected", () => { it('removes and destroys the item without saving it', async () => { confirm.andCallFake((options, callback) => callback(2)); const success = await pane.destroyItem(item1); expect(item1.save).not.toHaveBeenCalled(); expect(pane.getItems().includes(item1)).toBe(false); expect(item1.isDestroyed()).toBe(true); expect(success).toBe(true); }); }); describe('if the [Cancel] option is selected', () => { it('does not save, remove, or destroy the item', async () => { confirm.andCallFake((options, callback) => callback(1)); const success = await pane.destroyItem(item1); expect(item1.save).not.toHaveBeenCalled(); expect(pane.getItems().includes(item1)).toBe(true); expect(item1.isDestroyed()).toBe(false); expect(success).toBe(false); }); }); describe('when force=true', () => { it('destroys the item immediately', async () => { const success = await pane.destroyItem(item1, true); expect(item1.save).not.toHaveBeenCalled(); expect(pane.getItems().includes(item1)).toBe(false); expect(item1.isDestroyed()).toBe(true); expect(success).toBe(true); }); }); }); describe('when the last item is destroyed', () => { describe("when the 'core.destroyEmptyPanes' config option is false (the default)", () => { it('does not destroy the pane, but leaves it in place with empty items', () => { expect(atom.config.get('core.destroyEmptyPanes')).toBe(false); for (let item of pane.getItems()) { pane.destroyItem(item); } expect(pane.isDestroyed()).toBe(false); expect(pane.getActiveItem()).toBeUndefined(); expect(() => pane.saveActiveItem()).not.toThrow(); expect(() => pane.saveActiveItemAs()).not.toThrow(); }); }); describe("when the 'core.destroyEmptyPanes' config option is true", () => { it('destroys the pane', () => { atom.config.set('core.destroyEmptyPanes', true); for (let item of pane.getItems()) { pane.destroyItem(item); } expect(pane.isDestroyed()).toBe(true); }); }); }); describe('when passed a permanent dock item', () => { it("doesn't destroy the item", async () => { spyOn(item1, 'isPermanentDockItem').andReturn(true); const success = await pane.destroyItem(item1); expect(pane.getItems().includes(item1)).toBe(true); expect(item1.isDestroyed()).toBe(false); expect(success).toBe(false); }); it('destroy the item if force=true', async () => { spyOn(item1, 'isPermanentDockItem').andReturn(true); const success = await pane.destroyItem(item1, true); expect(pane.getItems().includes(item1)).toBe(false); expect(item1.isDestroyed()).toBe(true); expect(success).toBe(true); }); }); }); describe('::destroyActiveItem()', () => { it('destroys the active item', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B')] }) ); const activeItem = pane.getActiveItem(); pane.destroyActiveItem(); expect(activeItem.isDestroyed()).toBe(true); expect(pane.getItems().includes(activeItem)).toBe(false); }); it('does not throw an exception if there are no more items', () => { const pane = new Pane(paneParams()); pane.destroyActiveItem(); }); }); describe('::destroyItems()', () => { it('destroys all items', async () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); const [item1, item2, item3] = pane.getItems(); await pane.destroyItems(); expect(item1.isDestroyed()).toBe(true); expect(item2.isDestroyed()).toBe(true); expect(item3.isDestroyed()).toBe(true); expect(pane.getItems()).toEqual([]); }); }); describe('::observeItems()', () => { it('invokes the observer with all current and future items', () => { const pane = new Pane(paneParams({ items: [new Item(), new Item()] })); const [item1, item2] = pane.getItems(); const observed = []; pane.observeItems(item => observed.push(item)); const item3 = new Item(); pane.addItem(item3); expect(observed).toEqual([item1, item2, item3]); }); }); describe('when an item emits a destroyed event', () => { it('removes it from the list of items', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); const [item1, , item3] = pane.getItems(); pane.itemAtIndex(1).destroy(); expect(pane.getItems()).toEqual([item1, item3]); }); }); describe('::destroyInactiveItems()', () => { it('destroys all items but the active item', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C')] }) ); const [, item2] = pane.getItems(); pane.activateItem(item2); pane.destroyInactiveItems(); expect(pane.getItems()).toEqual([item2]); }); }); describe('::saveActiveItem()', () => { let pane; beforeEach(() => { pane = new Pane(paneParams({ items: [new Item('A')] })); showSaveDialog.andCallFake((options, callback) => callback('/selected/path') ); }); describe('when the active item has a uri', () => { beforeEach(() => { pane.getActiveItem().uri = 'test'; }); describe('when the active item has a save method', () => { it('saves the current item', () => { pane.getActiveItem().save = jasmine.createSpy('save'); pane.saveActiveItem(); expect(pane.getActiveItem().save).toHaveBeenCalled(); }); }); describe('when the current item has no save method', () => { it('does nothing', () => { expect(pane.getActiveItem().save).toBeUndefined(); pane.saveActiveItem(); }); }); }); describe('when the current item has no uri', () => { describe('when the current item has a saveAs method', () => { it('opens a save dialog and saves the current item as the selected path', async () => { pane.getActiveItem().saveAs = jasmine.createSpy('saveAs'); await pane.saveActiveItem(); expect(showSaveDialog.mostRecentCall.args[0]).toEqual({}); expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith( '/selected/path' ); }); }); describe('when the current item has no saveAs method', () => { it('does nothing', async () => { expect(pane.getActiveItem().saveAs).toBeUndefined(); await pane.saveActiveItem(); expect(showSaveDialog).not.toHaveBeenCalled(); }); }); it('does nothing if the user cancels choosing a path', async () => { pane.getActiveItem().saveAs = jasmine.createSpy('saveAs'); showSaveDialog.andCallFake((options, callback) => callback(undefined)); await pane.saveActiveItem(); expect(pane.getActiveItem().saveAs).not.toHaveBeenCalled(); }); }); describe("when the item's saveAs rejects with a well-known IO error", () => { it('creates a notification', () => { pane.getActiveItem().saveAs = () => { const error = new Error("EACCES, permission denied '/foo'"); error.path = '/foo'; error.code = 'EACCES'; return Promise.reject(error); }; waitsFor(done => { const subscription = atom.notifications.onDidAddNotification(function( notification ) { expect(notification.getType()).toBe('warning'); expect(notification.getMessage()).toContain('Permission denied'); expect(notification.getMessage()).toContain('/foo'); subscription.dispose(); done(); }); pane.saveActiveItem(); }); }); }); describe("when the item's saveAs throws a well-known IO error", () => { it('creates a notification', () => { pane.getActiveItem().saveAs = () => { const error = new Error("EACCES, permission denied '/foo'"); error.path = '/foo'; error.code = 'EACCES'; throw error; }; waitsFor(done => { const subscription = atom.notifications.onDidAddNotification(function( notification ) { expect(notification.getType()).toBe('warning'); expect(notification.getMessage()).toContain('Permission denied'); expect(notification.getMessage()).toContain('/foo'); subscription.dispose(); done(); }); pane.saveActiveItem(); }); }); }); }); describe('::saveActiveItemAs()', () => { let pane = null; beforeEach(() => { pane = new Pane(paneParams({ items: [new Item('A')] })); showSaveDialog.andCallFake((options, callback) => callback('/selected/path') ); }); describe('when the current item has a saveAs method', () => { it('opens the save dialog and calls saveAs on the item with the selected path', async () => { jasmine.useRealClock(); pane.getActiveItem().path = __filename; pane.getActiveItem().saveAs = jasmine.createSpy('saveAs'); pane.saveActiveItemAs(); expect(showSaveDialog.mostRecentCall.args[0]).toEqual({ defaultPath: __filename }); await conditionPromise( () => pane.getActiveItem().saveAs.callCount === 1 ); expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith( '/selected/path' ); }); }); describe('when the current item does not have a saveAs method', () => { it('does nothing', () => { expect(pane.getActiveItem().saveAs).toBeUndefined(); pane.saveActiveItemAs(); expect(showSaveDialog).not.toHaveBeenCalled(); }); }); describe("when the item's saveAs method throws a well-known IO error", () => { it('creates a notification', () => { pane.getActiveItem().saveAs = () => { const error = new Error("EACCES, permission denied '/foo'"); error.path = '/foo'; error.code = 'EACCES'; return Promise.reject(error); }; waitsFor(done => { const subscription = atom.notifications.onDidAddNotification(function( notification ) { expect(notification.getType()).toBe('warning'); expect(notification.getMessage()).toContain('Permission denied'); expect(notification.getMessage()).toContain('/foo'); subscription.dispose(); done(); }); pane.saveActiveItemAs(); }); }); }); }); describe('::itemForURI(uri)', () => { it('returns the item for which a call to .getURI() returns the given uri', () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C'), new Item('D')] }) ); const [item1, item2] = pane.getItems(); item1.uri = 'a'; item2.uri = 'b'; expect(pane.itemForURI('a')).toBe(item1); expect(pane.itemForURI('b')).toBe(item2); expect(pane.itemForURI('bogus')).toBeUndefined(); }); }); describe('::moveItem(item, index)', () => { let pane, item1, item2, item3, item4; beforeEach(() => { pane = new Pane( paneParams({ items: [new Item('A'), new Item('B'), new Item('C'), new Item('D')] }) ); [item1, item2, item3, item4] = pane.getItems(); }); it('moves the item to the given index and invokes ::onDidMoveItem observers', () => { pane.moveItem(item1, 2); expect(pane.getItems()).toEqual([item2, item3, item1, item4]); pane.moveItem(item2, 3); expect(pane.getItems()).toEqual([item3, item1, item4, item2]); pane.moveItem(item2, 1); expect(pane.getItems()).toEqual([item3, item2, item1, item4]); }); it('invokes ::onDidMoveItem() observers', () => { const events = []; pane.onDidMoveItem(event => events.push(event)); pane.moveItem(item1, 2); pane.moveItem(item2, 3); expect(events).toEqual([ { item: item1, oldIndex: 0, newIndex: 2 }, { item: item2, oldIndex: 0, newIndex: 3 } ]); }); }); describe('::moveItemToPane(item, pane, index)', () => { let container, pane1, pane2; let item1, item2, item3, item4, item5; beforeEach(() => { container = new PaneContainer({ config: atom.config, confirm }); pane1 = container.getActivePane(); pane1.addItems([new Item('A'), new Item('B'), new Item('C')]); pane2 = pane1.splitRight({ items: [new Item('D'), new Item('E')] }); [item1, item2, item3] = pane1.getItems(); [item4, item5] = pane2.getItems(); }); it('moves the item to the given pane at the given index', () => { pane1.moveItemToPane(item2, pane2, 1); expect(pane1.getItems()).toEqual([item1, item3]); expect(pane2.getItems()).toEqual([item4, item2, item5]); }); it('invokes ::onWillRemoveItem() observers', () => { const events = []; pane1.onWillRemoveItem(event => events.push(event)); pane1.moveItemToPane(item2, pane2, 1); expect(events).toEqual([ { item: item2, index: 1, moved: true, destroyed: false } ]); }); it('invokes ::onDidRemoveItem() observers', () => { const events = []; pane1.onDidRemoveItem(event => events.push(event)); pane1.moveItemToPane(item2, pane2, 1); expect(events).toEqual([ { item: item2, index: 1, moved: true, destroyed: false } ]); }); it('does not invoke ::onDidAddPaneItem observers on the container', () => { const addedItems = []; container.onDidAddPaneItem(item => addedItems.push(item)); pane1.moveItemToPane(item2, pane2, 1); expect(addedItems).toEqual([]); }); describe('when the moved item the last item in the source pane', () => { beforeEach(() => item5.destroy()); describe("when the 'core.destroyEmptyPanes' config option is false (the default)", () => { it('does not destroy the pane or the item', () => { pane2.moveItemToPane(item4, pane1, 0); expect(pane2.isDestroyed()).toBe(false); expect(item4.isDestroyed()).toBe(false); }); }); describe("when the 'core.destroyEmptyPanes' config option is true", () => { it('destroys the pane, but not the item', () => { atom.config.set('core.destroyEmptyPanes', true); pane2.moveItemToPane(item4, pane1, 0); expect(pane2.isDestroyed()).toBe(true); expect(item4.isDestroyed()).toBe(false); }); }); }); describe('when the item being moved is pending', () => { it('is made permanent in the new pane', () => { const item6 = new Item('F'); pane1.addItem(item6, { pending: true }); expect(pane1.getPendingItem()).toEqual(item6); pane1.moveItemToPane(item6, pane2, 0); expect(pane2.getPendingItem()).not.toEqual(item6); }); }); describe('when the target pane has a pending item', () => { it('does not destroy the pending item', () => { const item6 = new Item('F'); pane1.addItem(item6, { pending: true }); expect(pane1.getPendingItem()).toEqual(item6); pane2.moveItemToPane(item5, pane1, 0); expect(pane1.getPendingItem()).toEqual(item6); }); }); }); describe('split methods', () => { let pane1, item1, container; beforeEach(() => { container = new PaneContainer({ config: atom.config, confirm, deserializerManager: atom.deserializers }); pane1 = container.getActivePane(); item1 = new Item('A'); pane1.addItem(item1); }); describe('::splitLeft(params)', () => { describe('when the parent is the container root', () => { it('replaces itself with a row and inserts a new pane to the left of itself', () => { const pane2 = pane1.splitLeft({ items: [new Item('B')] }); const pane3 = pane1.splitLeft({ items: [new Item('C')] }); expect(container.root.orientation).toBe('horizontal'); expect(container.root.children).toEqual([pane2, pane3, pane1]); }); }); describe('when `moveActiveItem: true` is passed in the params', () => { it('moves the active item', () => { const pane2 = pane1.splitLeft({ moveActiveItem: true }); expect(pane2.getActiveItem()).toBe(item1); }); }); describe('when `copyActiveItem: true` is passed in the params', () => { it('duplicates the active item', () => { const pane2 = pane1.splitLeft({ copyActiveItem: true }); expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem()); }); it("does nothing if the active item doesn't implement .copy()", () => { item1.copy = null; const pane2 = pane1.splitLeft({ copyActiveItem: true }); expect(pane2.getActiveItem()).toBeUndefined(); }); }); describe('when the parent is a column', () => { it('replaces itself with a row and inserts a new pane to the left of itself', () => { pane1.splitDown(); const pane2 = pane1.splitLeft({ items: [new Item('B')] }); const pane3 = pane1.splitLeft({ items: [new Item('C')] }); const row = container.root.children[0]; expect(row.orientation).toBe('horizontal'); expect(row.children).toEqual([pane2, pane3, pane1]); }); }); }); describe('::splitRight(params)', () => { describe('when the parent is the container root', () => { it('replaces itself with a row and inserts a new pane to the right of itself', () => { const pane2 = pane1.splitRight({ items: [new Item('B')] }); const pane3 = pane1.splitRight({ items: [new Item('C')] }); expect(container.root.orientation).toBe('horizontal'); expect(container.root.children).toEqual([pane1, pane3, pane2]); }); }); describe('when `moveActiveItem: true` is passed in the params', () => { it('moves the active item', () => { const pane2 = pane1.splitRight({ moveActiveItem: true }); expect(pane2.getActiveItem()).toBe(item1); }); }); describe('when `copyActiveItem: true` is passed in the params', () => { it('duplicates the active item', () => { const pane2 = pane1.splitRight({ copyActiveItem: true }); expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem()); }); }); describe('when the parent is a column', () => { it('replaces itself with a row and inserts a new pane to the right of itself', () => { pane1.splitDown(); const pane2 = pane1.splitRight({ items: [new Item('B')] }); const pane3 = pane1.splitRight({ items: [new Item('C')] }); const row = container.root.children[0]; expect(row.orientation).toBe('horizontal'); expect(row.children).toEqual([pane1, pane3, pane2]); }); }); }); describe('::splitUp(params)', () => { describe('when the parent is the container root', () => { it('replaces itself with a column and inserts a new pane above itself', () => { const pane2 = pane1.splitUp({ items: [new Item('B')] }); const pane3 = pane1.splitUp({ items: [new Item('C')] }); expect(container.root.orientation).toBe('vertical'); expect(container.root.children).toEqual([pane2, pane3, pane1]); }); }); describe('when `moveActiveItem: true` is passed in the params', () => { it('moves the active item', () => { const pane2 = pane1.splitUp({ moveActiveItem: true }); expect(pane2.getActiveItem()).toBe(item1); }); }); describe('when `copyActiveItem: true` is passed in the params', () => { it('duplicates the active item', () => { const pane2 = pane1.splitUp({ copyActiveItem: true }); expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem()); }); }); describe('when the parent is a row', () => { it('replaces itself with a column and inserts a new pane above itself', () => { pane1.splitRight(); const pane2 = pane1.splitUp({ items: [new Item('B')] }); const pane3 = pane1.splitUp({ items: [new Item('C')] }); const column = container.root.children[0]; expect(column.orientation).toBe('vertical'); expect(column.children).toEqual([pane2, pane3, pane1]); }); }); }); describe('::splitDown(params)', () => { describe('when the parent is the container root', () => { it('replaces itself with a column and inserts a new pane below itself', () => { const pane2 = pane1.splitDown({ items: [new Item('B')] }); const pane3 = pane1.splitDown({ items: [new Item('C')] }); expect(container.root.orientation).toBe('vertical'); expect(container.root.children).toEqual([pane1, pane3, pane2]); }); }); describe('when `moveActiveItem: true` is passed in the params', () => { it('moves the active item', () => { const pane2 = pane1.splitDown({ moveActiveItem: true }); expect(pane2.getActiveItem()).toBe(item1); }); }); describe('when `copyActiveItem: true` is passed in the params', () => { it('duplicates the active item', () => { const pane2 = pane1.splitDown({ copyActiveItem: true }); expect(pane2.getActiveItem()).toEqual(pane1.getActiveItem()); }); }); describe('when the parent is a row', () => { it('replaces itself with a column and inserts a new pane below itself', () => { pane1.splitRight(); const pane2 = pane1.splitDown({ items: [new Item('B')] }); const pane3 = pane1.splitDown({ items: [new Item('C')] }); const column = container.root.children[0]; expect(column.orientation).toBe('vertical'); expect(column.children).toEqual([pane1, pane3, pane2]); }); }); }); describe('when the pane is empty', () => { describe('when `moveActiveItem: true` is passed in the params', () => { it('gracefully ignores the moveActiveItem parameter', () => { pane1.destroyItem(item1); expect(pane1.getActiveItem()).toBe(undefined); const pane2 = pane1.split('horizontal', 'before', { moveActiveItem: true }); expect(container.root.children).toEqual([pane2, pane1]); expect(pane2.getActiveItem()).toBe(undefined); }); }); describe('when `copyActiveItem: true` is passed in the params', () => { it('gracefully ignores the copyActiveItem parameter', () => { pane1.destroyItem(item1); expect(pane1.getActiveItem()).toBe(undefined); const pane2 = pane1.split('horizontal', 'before', { copyActiveItem: true }); expect(container.root.children).toEqual([pane2, pane1]); expect(pane2.getActiveItem()).toBe(undefined); }); }); }); it('activates the new pane', () => { expect(pane1.isActive()).toBe(true); const pane2 = pane1.splitRight(); expect(pane1.isActive()).toBe(false); expect(pane2.isActive()).toBe(true); }); }); describe('::close()', () => { it('prompts to save unsaved items before destroying the pane', async () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B')] }) ); const [item1] = pane.getItems(); item1.shouldPromptToSave = () => true; item1.getURI = () => '/test/path'; item1.save = jasmine.createSpy('save'); confirm.andCallFake((options, callback) => callback(0)); await pane.close(); expect(confirm).toHaveBeenCalled(); expect(item1.save).toHaveBeenCalled(); expect(pane.isDestroyed()).toBe(true); }); it('does not destroy the pane if the user clicks cancel', async () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B')] }) ); const [item1] = pane.getItems(); item1.shouldPromptToSave = () => true; item1.getURI = () => '/test/path'; item1.save = jasmine.createSpy('save'); confirm.andCallFake((options, callback) => callback(1)); await pane.close(); expect(confirm).toHaveBeenCalled(); expect(item1.save).not.toHaveBeenCalled(); expect(pane.isDestroyed()).toBe(false); }); it('does not destroy the pane if the user starts to save but then does not choose a path', async () => { const pane = new Pane( paneParams({ items: [new Item('A'), new Item('B')] }) ); const [item1] = pane.getItems(); item1.shouldPromptToSave = () => true; item1.saveAs = jasmine.createSpy('saveAs'); confirm.andCallFake((options, callback) => callback(0)); showSaveDialog.andCallFake((options, callback) => callback(undefined)); await pane.close(); expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); expect(confirm.callCount).toBe(1); expect(item1.saveAs).not.toHaveBeenCalled(); expect(pane.isDestroyed()).toBe(false); }); describe('when item fails to save', () => { let pane, item1; beforeEach(() => { pane = new Pane({ items: [new Item('A'), new Item('B')], applicationDelegate: atom.applicationDelegate, config: atom.config }); [item1] = pane.getItems(); item1.shouldPromptToSave = () => true; item1.getURI = () => '/test/path'; item1.save = jasmine.createSpy('save').andCallFake(() => { const error = new Error("EACCES, permission denied '/test/path'"); error.path = '/test/path'; error.code = 'EACCES'; throw error; }); }); it('does not destroy the pane if save fails and user clicks cancel', async () => { let confirmations = 0; confirm.andCallFake((options, callback) => { confirmations++; if (confirmations === 1) { callback(0); // click save } else { callback(1); } }); // click cancel await pane.close(); expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); expect(confirmations).toBe(2); expect(item1.save).toHaveBeenCalled(); expect(pane.isDestroyed()).toBe(false); }); it('does destroy the pane if the user saves the file under a new name', async () => { item1.saveAs = jasmine.createSpy('saveAs').andReturn(true); let confirmations = 0; confirm.andCallFake((options, callback) => { confirmations++; callback(0); }); // save and then save as showSaveDialog.andCallFake((options, callback) => callback('new/path')); await pane.close(); expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); expect(confirmations).toBe(2); expect( atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0] ).toEqual({}); expect(item1.save).toHaveBeenCalled(); expect(item1.saveAs).toHaveBeenCalled(); expect(pane.isDestroyed()).toBe(true); }); it('asks again if the saveAs also fails', async () => { item1.saveAs = jasmine.createSpy('saveAs').andCallFake(() => { const error = new Error("EACCES, permission denied '/test/path'"); error.path = '/test/path'; error.code = 'EACCES'; throw error; }); let confirmations = 0; confirm.andCallFake((options, callback) => { confirmations++; if (confirmations < 3) { callback(0); // save, save as, save as } else { callback(2); // don't save } }); showSaveDialog.andCallFake((options, callback) => callback('new/path')); await pane.close(); expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); expect(confirmations).toBe(3); expect( atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0] ).toEqual({}); expect(item1.save).toHaveBeenCalled(); expect(item1.saveAs).toHaveBeenCalled(); expect(pane.isDestroyed()).toBe(true); }); }); }); describe('::destroy()', () => { let container, pane1, pane2; beforeEach(() => { container = new PaneContainer({ config: atom.config, confirm }); pane1 = container.root; pane1.addItems([new Item('A'), new Item('B')]); pane2 = pane1.splitRight(); }); it('invokes ::onWillDestroy observers before destroying items', () => { let itemsDestroyed = null; pane1.onWillDestroy(() => { itemsDestroyed = pane1.getItems().map(item => item.isDestroyed()); }); pane1.destroy(); expect(itemsDestroyed).toEqual([false, false]); }); it("destroys the pane's destroyable items", () => { const [item1, item2] = pane1.getItems(); pane1.destroy(); expect(item1.isDestroyed()).toBe(true); expect(item2.isDestroyed()).toBe(true); }); describe('if the pane is active', () => { it('makes the next pane active', () => { expect(pane2.isActive()).toBe(true); pane2.destroy(); expect(pane1.isActive()).toBe(true); }); }); describe("if the pane's parent has more than two children", () => { it('removes the pane from its parent', () => { const pane3 = pane2.splitRight(); expect(container.root.children).toEqual([pane1, pane2, pane3]); pane2.destroy(); expect(container.root.children).toEqual([pane1, pane3]); }); }); describe("if the pane's parent has two children", () => { it('replaces the parent with its last remaining child', () => { const pane3 = pane2.splitDown(); expect(container.root.children[0]).toBe(pane1); expect(container.root.children[1].children).toEqual([pane2, pane3]); pane3.destroy(); expect(container.root.children).toEqual([pane1, pane2]); pane2.destroy(); expect(container.root).toBe(pane1); }); }); }); describe('pending state', () => { let editor1, pane, eventCount; beforeEach(async () => { editor1 = await atom.workspace.open('sample.txt', { pending: true }); pane = atom.workspace.getActivePane(); eventCount = 0; editor1.onDidTerminatePendingState(() => eventCount++); }); it('does not open file in pending state by default', async () => { await atom.workspace.open('sample.js'); expect(pane.getPendingItem()).toBeNull(); }); it("opens file in pending state if 'pending' option is true", () => { expect(pane.getPendingItem()).toEqual(editor1); }); it('terminates pending state if ::terminatePendingState is invoked', () => { editor1.terminatePendingState(); expect(pane.getPendingItem()).toBeNull(); expect(eventCount).toBe(1); }); it('terminates pending state when buffer is changed', () => { editor1.insertText("I'll be back!"); advanceClock(editor1.getBuffer().stoppedChangingDelay); expect(pane.getPendingItem()).toBeNull(); expect(eventCount).toBe(1); }); it('only calls terminate handler once when text is modified twice', async () => { const originalText = editor1.getText(); editor1.insertText('Some text'); advanceClock(editor1.getBuffer().stoppedChangingDelay); await editor1.save(); editor1.insertText('More text'); advanceClock(editor1.getBuffer().stoppedChangingDelay); expect(pane.getPendingItem()).toBeNull(); expect(eventCount).toBe(1); // Reset fixture back to original state editor1.setText(originalText); await editor1.save(); }); it('only calls clearPendingItem if there is a pending item to clear', () => { spyOn(pane, 'clearPendingItem').andCallThrough(); editor1.terminatePendingState(); editor1.terminatePendingState(); expect(pane.getPendingItem()).toBeNull(); expect(pane.clearPendingItem.callCount).toBe(1); }); }); describe('serialization', () => { let pane = null; beforeEach(() => { pane = new Pane( paneParams({ items: [new Item('A', 'a'), new Item('B', 'b'), new Item('C', 'c')], flexScale: 2 }) ); }); it('can serialize and deserialize the pane and all its items', () => { const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.getItems()).toEqual(pane.getItems()); }); it('restores the active item on deserialization', () => { pane.activateItemAtIndex(1); const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.getActiveItem()).toEqual(newPane.itemAtIndex(1)); }); it("restores the active item when it doesn't implement getURI()", () => { pane.items[1].getURI = null; pane.activateItemAtIndex(1); const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.getActiveItem()).toEqual(newPane.itemAtIndex(1)); }); it("restores the correct item when it doesn't implement getURI() and some items weren't deserialized", () => { const unserializable = {}; pane.addItem(unserializable, { index: 0 }); pane.items[2].getURI = null; pane.activateItemAtIndex(2); const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.getActiveItem()).toEqual(newPane.itemAtIndex(1)); }); it('does not include items that cannot be deserialized', () => { spyOn(console, 'warn'); const unserializable = {}; pane.activateItem(unserializable); const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.getActiveItem()).toEqual(pane.itemAtIndex(0)); expect(newPane.getItems().length).toBe(pane.getItems().length - 1); }); it("includes the pane's focus state in the serialized state", () => { pane.focus(); const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.focused).toBe(true); }); it('can serialize and deserialize the order of the items in the itemStack', () => { const [item1, item2, item3] = pane.getItems(); pane.itemStack = [item3, item1, item2]; const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.itemStack).toEqual(pane.itemStack); expect(newPane.itemStack[2]).toEqual(item2); }); it('builds the itemStack if the itemStack is not serialized', () => { const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.getItems()).toEqual(newPane.itemStack); }); it('rebuilds the itemStack if items.length does not match itemStack.length', () => { const [, item2, item3] = pane.getItems(); pane.itemStack = [item2, item3]; const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.getItems()).toEqual(newPane.itemStack); }); it('does not serialize the reference to the items in the itemStack for pane items that will not be serialized', () => { const [item1, item2, item3] = pane.getItems(); pane.itemStack = [item2, item1, item3]; const unserializable = {}; pane.activateItem(unserializable); const newPane = Pane.deserialize(pane.serialize(), atom); expect(newPane.itemStack).toEqual([item2, item1, item3]); }); }); }); ================================================ FILE: spec/panel-container-element-spec.js ================================================ 'use strict'; const Panel = require('../src/panel'); const PanelContainer = require('../src/panel-container'); describe('PanelContainerElement', () => { let jasmineContent, element, container; class TestPanelContainerItem {} class TestPanelContainerItemElement_ extends HTMLElement { connectedCallback() { this.classList.add('test-root'); } initialize(model) { this.model = model; return this; } focus() {} } window.customElements.define( 'atom-test-container-item-element', TestPanelContainerItemElement_ ); const TestPanelContainerItemElement = document.createElement( 'atom-test-container-item-element' ); beforeEach(() => { jasmineContent = document.body.querySelector('#jasmine-content'); atom.views.addViewProvider(TestPanelContainerItem, model => TestPanelContainerItemElement.initialize(model) ); container = new PanelContainer({ viewRegistry: atom.views, location: 'left' }); element = container.getElement(); jasmineContent.appendChild(element); }); it('has a location class with value from the model', () => { expect(element).toHaveClass('left'); }); it('removes the element when the container is destroyed', () => { expect(element.parentNode).toBe(jasmineContent); container.destroy(); expect(element.parentNode).not.toBe(jasmineContent); }); describe('adding and removing panels', () => { it('allows panels to be inserted at any position', () => { const panel1 = new Panel( { item: new TestPanelContainerItem(), priority: 10 }, atom.views ); const panel2 = new Panel( { item: new TestPanelContainerItem(), priority: 5 }, atom.views ); const panel3 = new Panel( { item: new TestPanelContainerItem(), priority: 8 }, atom.views ); container.addPanel(panel1); container.addPanel(panel2); container.addPanel(panel3); expect(element.childNodes[2]).toBe(panel1.getElement()); expect(element.childNodes[1]).toBe(panel3.getElement()); expect(element.childNodes[0]).toBe(panel2.getElement()); }); describe('when the container is at the left location', () => it('adds atom-panel elements when a new panel is added to the container; removes them when the panels are destroyed', () => { expect(element.childNodes.length).toBe(0); const panel1 = new Panel( { item: new TestPanelContainerItem() }, atom.views ); container.addPanel(panel1); expect(element.childNodes.length).toBe(1); expect(element.childNodes[0]).toHaveClass('left'); expect(element.childNodes[0]).toHaveClass('tool-panel'); // legacy selector support expect(element.childNodes[0]).toHaveClass('panel-left'); // legacy selector support expect(element.childNodes[0].tagName).toBe('ATOM-PANEL'); const panel2 = new Panel( { item: new TestPanelContainerItem() }, atom.views ); container.addPanel(panel2); expect(element.childNodes.length).toBe(2); expect(panel1.getElement().style.display).not.toBe('none'); expect(panel2.getElement().style.display).not.toBe('none'); panel1.destroy(); expect(element.childNodes.length).toBe(1); panel2.destroy(); expect(element.childNodes.length).toBe(0); })); describe('when the container is at the bottom location', () => { beforeEach(() => { container = new PanelContainer({ viewRegistry: atom.views, location: 'bottom' }); element = container.getElement(); jasmineContent.appendChild(element); }); it('adds atom-panel elements when a new panel is added to the container; removes them when the panels are destroyed', () => { expect(element.childNodes.length).toBe(0); const panel1 = new Panel( { item: new TestPanelContainerItem(), className: 'one' }, atom.views ); container.addPanel(panel1); expect(element.childNodes.length).toBe(1); expect(element.childNodes[0]).toHaveClass('bottom'); expect(element.childNodes[0]).toHaveClass('tool-panel'); // legacy selector support expect(element.childNodes[0]).toHaveClass('panel-bottom'); // legacy selector support expect(element.childNodes[0].tagName).toBe('ATOM-PANEL'); expect(panel1.getElement()).toHaveClass('one'); const panel2 = new Panel( { item: new TestPanelContainerItem(), className: 'two' }, atom.views ); container.addPanel(panel2); expect(element.childNodes.length).toBe(2); expect(panel2.getElement()).toHaveClass('two'); panel1.destroy(); expect(element.childNodes.length).toBe(1); panel2.destroy(); expect(element.childNodes.length).toBe(0); }); }); }); describe('when the container is modal', () => { beforeEach(() => { container = new PanelContainer({ viewRegistry: atom.views, location: 'modal' }); element = container.getElement(); jasmineContent.appendChild(element); }); it('allows only one panel to be visible at a time', () => { const panel1 = new Panel( { item: new TestPanelContainerItem() }, atom.views ); container.addPanel(panel1); expect(panel1.getElement().style.display).not.toBe('none'); const panel2 = new Panel( { item: new TestPanelContainerItem() }, atom.views ); container.addPanel(panel2); expect(panel1.getElement().style.display).toBe('none'); expect(panel2.getElement().style.display).not.toBe('none'); panel1.show(); expect(panel1.getElement().style.display).not.toBe('none'); expect(panel2.getElement().style.display).toBe('none'); }); it("adds the 'modal' class to panels", () => { const panel1 = new Panel( { item: new TestPanelContainerItem() }, atom.views ); container.addPanel(panel1); expect(panel1.getElement()).toHaveClass('modal'); // legacy selector support expect(panel1.getElement()).not.toHaveClass('tool-panel'); expect(panel1.getElement()).toHaveClass('overlay'); expect(panel1.getElement()).toHaveClass('from-top'); }); describe('autoFocus', () => { function createPanel(autoFocus = true) { const panel = new Panel( { item: new TestPanelContainerItem(), autoFocus: autoFocus, visible: false }, atom.views ); container.addPanel(panel); return panel; } it('focuses the first tabbable item if available', () => { const panel = createPanel(); const panelEl = panel.getElement(); const inputEl = document.createElement('input'); panelEl.appendChild(inputEl); expect(document.activeElement).not.toBe(inputEl); panel.show(); expect(document.activeElement).toBe(inputEl); }); it('focuses the autoFocus element if available', () => { const inputEl1 = document.createElement('input'); const inputEl2 = document.createElement('input'); const panel = createPanel(inputEl2); const panelEl = panel.getElement(); panelEl.appendChild(inputEl1); panelEl.appendChild(inputEl2); expect(document.activeElement).not.toBe(inputEl2); panel.show(); expect(document.activeElement).toBe(inputEl2); }); it('focuses the entire panel item when no tabbable item is available and the panel is focusable', () => { const panel = createPanel(); const panelEl = panel.getElement(); spyOn(panelEl, 'focus'); panel.show(); expect(panelEl.focus).toHaveBeenCalled(); }); it('returns focus to the original activeElement', () => { const panel = createPanel(); const previousActiveElement = document.activeElement; const panelEl = panel.getElement(); panelEl.appendChild(document.createElement('input')); panel.show(); panel.hide(); waitsFor(() => document.activeElement === previousActiveElement); runs(() => { expect(document.activeElement).toBe(previousActiveElement); }); }); }); }); }); ================================================ FILE: spec/panel-container-spec.js ================================================ 'use strict'; const Panel = require('../src/panel'); const PanelContainer = require('../src/panel-container'); describe('PanelContainer', () => { let container; class TestPanelItem {} beforeEach(() => { container = new PanelContainer({ viewRegistry: atom.views }); }); describe('::addPanel(panel)', () => { it('emits an onDidAddPanel event with the index the panel was inserted at', () => { const addPanelSpy = jasmine.createSpy(); container.onDidAddPanel(addPanelSpy); const panel1 = new Panel({ item: new TestPanelItem() }, atom.views); container.addPanel(panel1); expect(addPanelSpy).toHaveBeenCalledWith({ panel: panel1, index: 0 }); const panel2 = new Panel({ item: new TestPanelItem() }, atom.views); container.addPanel(panel2); expect(addPanelSpy).toHaveBeenCalledWith({ panel: panel2, index: 1 }); }); }); describe('when a panel is destroyed', () => { it('emits an onDidRemovePanel event with the index of the removed item', () => { const removePanelSpy = jasmine.createSpy(); container.onDidRemovePanel(removePanelSpy); const panel1 = new Panel({ item: new TestPanelItem() }, atom.views); container.addPanel(panel1); const panel2 = new Panel({ item: new TestPanelItem() }, atom.views); container.addPanel(panel2); expect(removePanelSpy).not.toHaveBeenCalled(); panel2.destroy(); expect(removePanelSpy).toHaveBeenCalledWith({ panel: panel2, index: 1 }); panel1.destroy(); expect(removePanelSpy).toHaveBeenCalledWith({ panel: panel1, index: 0 }); }); }); describe('::destroy()', () => { it('destroys the container and all of its panels', () => { const destroyedPanels = []; const panel1 = new Panel({ item: new TestPanelItem() }, atom.views); panel1.onDidDestroy(() => { destroyedPanels.push(panel1); }); container.addPanel(panel1); const panel2 = new Panel({ item: new TestPanelItem() }, atom.views); panel2.onDidDestroy(() => { destroyedPanels.push(panel2); }); container.addPanel(panel2); container.destroy(); expect(container.getPanels().length).toBe(0); expect(destroyedPanels).toEqual([panel1, panel2]); }); }); describe('panel priority', () => { describe('left / top panel container', () => { let initialPanel; beforeEach(() => { // 'left' logic is the same as 'top' container = new PanelContainer({ location: 'left' }); initialPanel = new Panel({ item: new TestPanelItem() }, atom.views); container.addPanel(initialPanel); }); describe('when a panel with low priority is added', () => { it('is inserted at the beginning of the list', () => { const addPanelSpy = jasmine.createSpy(); container.onDidAddPanel(addPanelSpy); const panel = new Panel( { item: new TestPanelItem(), priority: 0 }, atom.views ); container.addPanel(panel); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); expect(container.getPanels()[0]).toBe(panel); }); }); describe('when a panel with priority between two other panels is added', () => { it('is inserted at the between the two panels', () => { const addPanelSpy = jasmine.createSpy(); let panel = new Panel( { item: new TestPanelItem(), priority: 1000 }, atom.views ); container.addPanel(panel); container.onDidAddPanel(addPanelSpy); panel = new Panel( { item: new TestPanelItem(), priority: 101 }, atom.views ); container.addPanel(panel); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 1 }); expect(container.getPanels()[1]).toBe(panel); }); }); }); describe('right / bottom panel container', () => { let initialPanel; beforeEach(() => { // 'bottom' logic is the same as 'right' container = new PanelContainer({ location: 'right' }); initialPanel = new Panel({ item: new TestPanelItem() }, atom.views); container.addPanel(initialPanel); }); describe('when a panel with high priority is added', () => { it('is inserted at the beginning of the list', () => { const addPanelSpy = jasmine.createSpy(); container.onDidAddPanel(addPanelSpy); const panel = new Panel( { item: new TestPanelItem(), priority: 1000 }, atom.views ); container.addPanel(panel); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); expect(container.getPanels()[0]).toBe(panel); }); }); describe('when a panel with low priority is added', () => { it('is inserted at the end of the list', () => { const addPanelSpy = jasmine.createSpy(); container.onDidAddPanel(addPanelSpy); const panel = new Panel( { item: new TestPanelItem(), priority: 0 }, atom.views ); container.addPanel(panel); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 1 }); expect(container.getPanels()[1]).toBe(panel); }); }); }); }); }); ================================================ FILE: spec/panel-spec.js ================================================ const Panel = require('../src/panel'); describe('Panel', () => { class TestPanelItem { getElement() { if (!this.element) { this.element = document.createElement('div'); this.element.tabIndex = -1; this.element.className = 'test-root'; } return this.element; } } it("adds the item's element as a child of the panel", () => { const panel = new Panel({ item: new TestPanelItem() }, atom.views); const element = panel.getElement(); expect(element.tagName.toLowerCase()).toBe('atom-panel'); expect(element.firstChild).toBe(panel.getItem().getElement()); }); describe('destroying the panel', () => { it('removes the element when the panel is destroyed', () => { const panel = new Panel({ item: new TestPanelItem() }, atom.views); const element = panel.getElement(); const jasmineContent = document.getElementById('jasmine-content'); jasmineContent.appendChild(element); expect(element.parentNode).toBe(jasmineContent); panel.destroy(); expect(element.parentNode).not.toBe(jasmineContent); }); it('does not try to remove the element twice', () => { const item = new TestPanelItem(); const panel = new Panel({ item }, atom.views); const element = panel.getElement(); const jasmineContent = document.getElementById('jasmine-content'); jasmineContent.appendChild(element); item.getElement().focus(); expect(item.getElement()).toHaveFocus(); // Avoid this error: // NotFoundError: Failed to execute 'remove' on 'Element': // The node to be removed is no longer a child of this node. // Perhaps it was moved in a 'blur' event handler? item.getElement().addEventListener('blur', () => panel.destroy()); panel.destroy(); }); }); describe('changing panel visibility', () => { it('notifies observers added with onDidChangeVisible', () => { const panel = new Panel({ item: new TestPanelItem() }, atom.views); const spy = jasmine.createSpy(); panel.onDidChangeVisible(spy); panel.hide(); expect(panel.isVisible()).toBe(false); expect(spy).toHaveBeenCalledWith(false); spy.reset(); panel.show(); expect(panel.isVisible()).toBe(true); expect(spy).toHaveBeenCalledWith(true); panel.destroy(); expect(panel.isVisible()).toBe(false); expect(spy).toHaveBeenCalledWith(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'); }); it('hides and shows the panel element when Panel::hide() and Panel::show() are called', () => { const panel = new Panel({ item: new TestPanelItem() }, atom.views); const element = panel.getElement(); expect(element.style.display).not.toBe('none'); panel.hide(); expect(element.style.display).toBe('none'); panel.show(); expect(element.style.display).not.toBe('none'); }); }); describe('when a class name is specified', () => { it('initially renders panel created with visible: false', () => { const panel = new Panel( { className: 'some classes', item: new TestPanelItem() }, atom.views ); const element = panel.getElement(); expect(element).toHaveClass('some'); expect(element).toHaveClass('classes'); }); }); describe('creating an atom-panel via markup', () => { it('does not throw an error', () => { document.createElement('atom-panel'); }); }); }); ================================================ FILE: spec/path-watcher-spec.js ================================================ /** @babel */ import temp from 'temp'; import fs from 'fs-plus'; import path from 'path'; import { promisify } from 'util'; import { CompositeDisposable } from 'event-kit'; import { watchPath, stopAllWatchers } from '../src/path-watcher'; temp.track(); const writeFile = promisify(fs.writeFile); const mkdir = promisify(fs.mkdir); const appendFile = promisify(fs.appendFile); const realpath = promisify(fs.realpath); const tempMkdir = promisify(temp.mkdir); describe('watchPath', function() { let subs; beforeEach(function() { subs = new CompositeDisposable(); }); afterEach(async function() { subs.dispose(); await stopAllWatchers(); }); function waitForChanges(watcher, ...fileNames) { const waiting = new Set(fileNames); let fired = false; const relevantEvents = []; return new Promise(resolve => { const sub = watcher.onDidChange(events => { for (const event of events) { if (waiting.delete(event.path)) { relevantEvents.push(event); } } if (!fired && waiting.size === 0) { fired = true; resolve(relevantEvents); sub.dispose(); } }); }); } describe('watchPath()', function() { it('resolves the returned promise when the watcher begins listening', async function() { const rootDir = await tempMkdir('atom-fsmanager-test-'); const watcher = await watchPath(rootDir, {}, () => {}); expect(watcher.constructor.name).toBe('PathWatcher'); }); it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function() { const rootDir = await tempMkdir('atom-fsmanager-test-'); const watcher0 = await watchPath(rootDir, {}, () => {}); const watcher1 = await watchPath(rootDir, {}, () => {}); expect(watcher0.native).toBe(watcher1.native); }); it("reuses existing native watchers even while they're still starting", async function() { const rootDir = await tempMkdir('atom-fsmanager-test-'); const [watcher0, watcher1] = await Promise.all([ watchPath(rootDir, {}, () => {}), watchPath(rootDir, {}, () => {}) ]); expect(watcher0.native).toBe(watcher1.native); }); it("doesn't attach new watchers to a native watcher that's stopping", async function() { const rootDir = await tempMkdir('atom-fsmanager-test-'); const watcher0 = await watchPath(rootDir, {}, () => {}); const native0 = watcher0.native; watcher0.dispose(); const watcher1 = await watchPath(rootDir, {}, () => {}); expect(watcher1.native).not.toBe(native0); }); it('reuses an existing native watcher on a parent directory and filters events', async function() { const rootDir = await tempMkdir('atom-fsmanager-test-').then(realpath); const rootFile = path.join(rootDir, 'rootfile.txt'); const subDir = path.join(rootDir, 'subdir'); const subFile = path.join(subDir, 'subfile.txt'); await mkdir(subDir); // Keep the watchers alive with an undisposed subscription const rootWatcher = await watchPath(rootDir, {}, () => {}); const childWatcher = await watchPath(subDir, {}, () => {}); expect(rootWatcher.native).toBe(childWatcher.native); expect(rootWatcher.native.isRunning()).toBe(true); const firstChanges = Promise.all([ waitForChanges(rootWatcher, subFile), waitForChanges(childWatcher, subFile) ]); await writeFile(subFile, 'subfile\n', { encoding: 'utf8' }); await firstChanges; const nextRootEvent = waitForChanges(rootWatcher, rootFile); await writeFile(rootFile, 'rootfile\n', { encoding: 'utf8' }); await nextRootEvent; }); it('adopts existing child watchers and filters events appropriately to them', async function() { const parentDir = await tempMkdir('atom-fsmanager-test-').then(realpath); // Create the directory tree const rootFile = path.join(parentDir, 'rootfile.txt'); const subDir0 = path.join(parentDir, 'subdir0'); const subFile0 = path.join(subDir0, 'subfile0.txt'); const subDir1 = path.join(parentDir, 'subdir1'); const subFile1 = path.join(subDir1, 'subfile1.txt'); await mkdir(subDir0); await mkdir(subDir1); await Promise.all([ writeFile(rootFile, 'rootfile\n', { encoding: 'utf8' }), writeFile(subFile0, 'subfile 0\n', { encoding: 'utf8' }), writeFile(subFile1, 'subfile 1\n', { encoding: 'utf8' }) ]); // Begin the child watchers and keep them alive const subWatcher0 = await watchPath(subDir0, {}, () => {}); const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0); const subWatcher1 = await watchPath(subDir1, {}, () => {}); const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1); expect(subWatcher0.native).not.toBe(subWatcher1.native); // Create the parent watcher const parentWatcher = await watchPath(parentDir, {}, () => {}); const parentWatcherChanges = waitForChanges( parentWatcher, rootFile, subFile0, subFile1 ); expect(subWatcher0.native).toBe(parentWatcher.native); expect(subWatcher1.native).toBe(parentWatcher.native); // Ensure events are filtered correctly await Promise.all([ appendFile(rootFile, 'change\n', { encoding: 'utf8' }), appendFile(subFile0, 'change\n', { encoding: 'utf8' }), appendFile(subFile1, 'change\n', { encoding: 'utf8' }) ]); await Promise.all([ subWatcherChanges0, subWatcherChanges1, parentWatcherChanges ]); }); }); }); ================================================ FILE: spec/project-spec.js ================================================ 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, grammarRegistry: atom.grammars }); 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, grammarRegistry: atom.grammars }); 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, grammarRegistry: atom.grammars }); }); 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, grammarRegistry: atom.grammars }); }); 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, grammarRegistry: atom.grammars }); }); 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, grammarRegistry: atom.grammars }); }); 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, grammarRegistry: atom.grammars }); }); 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, grammarRegistry: atom.grammars }); }); 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, grammarRegistry: atom.grammars }); }); 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, grammarRegistry: atom.grammars }); }); 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('.replace', () => { let projectSpecification, projectPath1, projectPath2; beforeEach(() => { atom.project.replace(null); projectPath1 = temp.mkdirSync('project-path1'); projectPath2 = temp.mkdirSync('project-path2'); projectSpecification = { paths: [projectPath1, projectPath2], originPath: 'originPath', config: { baz: 'buzz' } }; }); it('sets a project specification', () => { expect(atom.config.get('baz')).toBeUndefined(); atom.project.replace(projectSpecification); expect(atom.project.getPaths()).toEqual([projectPath1, projectPath2]); expect(atom.config.get('baz')).toBe('buzz'); }); it('clears a project through replace with no params', () => { expect(atom.config.get('baz')).toBeUndefined(); atom.project.replace(projectSpecification); expect(atom.config.get('baz')).toBe('buzz'); expect(atom.project.getPaths()).toEqual([projectPath1, projectPath2]); atom.project.replace(); expect(atom.config.get('baz')).toBeUndefined(); expect(atom.project.getPaths()).toEqual([]); }); it('responds to change of project specification', () => { let wasCalled = false; const callback = () => { wasCalled = true; }; atom.project.onDidReplace(callback); atom.project.replace(projectSpecification); expect(wasCalled).toBe(true); wasCalled = false; atom.project.replace(); expect(wasCalled).toBe(true); }); }); 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); } onDidChangeFiles(callback) { onDidChangeFilesCallback = callback; return { dispose: () => {} }; } } let serviceDisposable = null; let onDidChangeFilesCallback = 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; } } } ); onDidChangeFilesCallback = 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); }); it('uses the custom onDidChangeFiles as the watcher if available', () => { // Ensure that all preexisting watchers are stopped waitsForPromise(() => stopAllWatchers()); const remotePath = 'ssh://another-directory:8080/does-exist'; runs(() => atom.project.setPaths([remotePath])); waitsForPromise(() => atom.project.getWatcherPromise(remotePath)); runs(() => { expect(onDidChangeFilesCallback).not.toBeNull(); const changeSpy = jasmine.createSpy('atom.project.onDidChangeFiles'); const disposable = atom.project.onDidChangeFiles(changeSpy); const events = [{ action: 'created', path: remotePath + '/test.txt' }]; onDidChangeFilesCallback(events); expect(changeSpy).toHaveBeenCalledWith(events); disposable.dispose(); }); }); }); describe('.open(path)', () => { 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; let events; let checkCallback = () => {}; beforeEach(() => { events = []; 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) => { let expireTimeoutId; checkCallback = () => { for (let event of events) { remaining.delete(event.path); } if (remaining.size === 0) { clearTimeout(expireTimeoutId); resolve(); } }; const expire = () => { checkCallback = () => {}; console.error('Paths not seen:', remaining); reject( new Error('Expired before all expected events were delivered.') ); }; expireTimeoutId = setTimeout(expire, 2000); checkCallback(); }); }; it('reports filesystem changes within project paths', async () => { jasmine.useRealClock(); 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 await stopAllWatchers(); atom.project.setPaths([dirOne]); await atom.project.getWatcherPromise(dirOne); expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined); fs.writeFileSync(fileThree, 'three\n'); fs.writeFileSync(fileTwo, 'two\n'); fs.writeFileSync(fileOne, 'one\n'); await waitForEvents([fileOne, fileTwo]); 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('.observeRepositories()', () => { it('invokes the observer with current and future repositories', () => { const observed = []; const directory1 = temp.mkdirSync('git-repo1'); const gitDirPath1 = fs.absolute( path.join(__dirname, 'fixtures', 'git', 'master.git') ); fs.copySync(gitDirPath1, path.join(directory1, '.git')); const directory2 = temp.mkdirSync('git-repo2'); const gitDirPath2 = fs.absolute( path.join( __dirname, 'fixtures', 'git', 'repo-with-submodules', 'git.git' ) ); fs.copySync(gitDirPath2, path.join(directory2, '.git')); atom.project.setPaths([directory1]); const disposable = atom.project.observeRepositories(repo => observed.push(repo) ); expect(observed.length).toBe(1); expect(observed[0].getReferenceTarget('refs/heads/master')).toBe( 'ef046e9eecaa5255ea5e9817132d4001724d6ae1' ); atom.project.addPath(directory2); expect(observed.length).toBe(2); expect(observed[1].getReferenceTarget('refs/heads/master')).toBe( 'd2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5' ); disposable.dispose(); }); }); describe('.onDidAddRepository()', () => { it('invokes callback when a path is added and the path is the root of a repository', () => { const observed = []; const disposable = atom.project.onDidAddRepository(repo => observed.push(repo) ); const projectRootPath = temp.mkdirSync(); const fixtureRepoPath = fs.absolute( path.join(__dirname, 'fixtures', 'git', 'master.git') ); fs.copySync(fixtureRepoPath, path.join(projectRootPath, '.git')); atom.project.addPath(projectRootPath); expect(observed.length).toBe(1); expect(observed[0].getOriginURL()).toEqual( 'https://github.com/example-user/example-repo.git' ); disposable.dispose(); }); it('invokes callback when a path is added and the path is subdirectory of a repository', () => { const observed = []; const disposable = atom.project.onDidAddRepository(repo => observed.push(repo) ); const projectRootPath = temp.mkdirSync(); const fixtureRepoPath = fs.absolute( path.join(__dirname, 'fixtures', 'git', 'master.git') ); fs.copySync(fixtureRepoPath, path.join(projectRootPath, '.git')); const projectSubDirPath = path.join(projectRootPath, 'sub-dir'); fs.mkdirSync(projectSubDirPath); atom.project.addPath(projectSubDirPath); expect(observed.length).toBe(1); expect(observed[0].getOriginURL()).toEqual( 'https://github.com/example-user/example-repo.git' ); disposable.dispose(); }); it('does not invoke callback when a path is added and the path is not part of a repository', () => { const observed = []; const disposable = atom.project.onDidAddRepository(repo => observed.push(repo) ); atom.project.addPath(temp.mkdirSync('not-a-repository')); expect(observed.length).toBe(0); disposable.dispose(); }); }); 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'); }); }); }); ================================================ FILE: spec/reopen-project-menu-manager-spec.js ================================================ /** @babel */ import { Disposable } from 'event-kit'; const ReopenProjectMenuManager = require('../src/reopen-project-menu-manager'); function numberRange(low, high) { const size = high - low; const result = new Array(size); for (var i = 0; i < size; i++) result[i] = low + i; return result; } describe('ReopenProjectMenuManager', () => { let menuManager, commandRegistry, config, historyManager, reopenProjects; let commandDisposable, configDisposable, historyDisposable; let openFunction; beforeEach(() => { menuManager = jasmine.createSpyObj('MenuManager', ['add']); menuManager.add.andReturn(new Disposable()); commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']); commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']); commandRegistry.add.andReturn(commandDisposable); config = jasmine.createSpyObj('Config', ['onDidChange', 'get']); config.get.andReturn(10); configDisposable = jasmine.createSpyObj('Disposable', ['dispose']); config.didChangeListener = {}; config.onDidChange.andCallFake((key, fn) => { config.didChangeListener[key] = fn; return configDisposable; }); historyManager = jasmine.createSpyObj('historyManager', [ 'getProjects', 'onDidChangeProjects' ]); historyManager.getProjects.andReturn([]); historyDisposable = jasmine.createSpyObj('Disposable', ['dispose']); historyManager.onDidChangeProjects.andCallFake(fn => { historyManager.changeProjectsListener = fn; return historyDisposable; }); openFunction = jasmine.createSpy(); reopenProjects = new ReopenProjectMenuManager({ menu: menuManager, commands: commandRegistry, history: historyManager, config, open: openFunction }); }); describe('constructor', () => { it("registers the 'reopen-project' command function", () => { expect(commandRegistry.add).toHaveBeenCalled(); const cmdCall = commandRegistry.add.calls[0]; expect(cmdCall.args.length).toBe(2); expect(cmdCall.args[0]).toBe('atom-workspace'); expect(typeof cmdCall.args[1]['application:reopen-project']).toBe( 'function' ); }); }); describe('dispose', () => { it('disposes of the history, command and config disposables', () => { reopenProjects.dispose(); expect(historyDisposable.dispose).toHaveBeenCalled(); expect(configDisposable.dispose).toHaveBeenCalled(); expect(commandDisposable.dispose).toHaveBeenCalled(); }); it('disposes of the menu disposable once used', () => { const menuDisposable = jasmine.createSpyObj('Disposable', ['dispose']); menuManager.add.andReturn(menuDisposable); reopenProjects.update(); expect(menuDisposable.dispose).not.toHaveBeenCalled(); reopenProjects.dispose(); expect(menuDisposable.dispose).toHaveBeenCalled(); }); }); describe('the command', () => { it('calls open with the paths of the project specified by the detail index', () => { historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] } ]); reopenProjects.update(); const reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project']; reopenProjectCommand({ detail: { index: 1 } }); expect(openFunction).toHaveBeenCalled(); expect(openFunction.calls[0].args[0]).toEqual(['/b', 'c:\\']); }); it('does not call open when no command detail is supplied', () => { const reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project']; reopenProjectCommand({}); expect(openFunction).not.toHaveBeenCalled(); }); it('does not call open when no command detail index is supplied', () => { const reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project']; reopenProjectCommand({ detail: { anything: 'here' } }); expect(openFunction).not.toHaveBeenCalled(); }); }); describe('update', () => { it('adds menu items to MenuManager based on projects from HistoryManager', () => { historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] } ]); reopenProjects.update(); expect(historyManager.getProjects).toHaveBeenCalled(); expect(menuManager.add).toHaveBeenCalled(); const menuArg = menuManager.add.calls[0].args[0]; expect(menuArg.length).toBe(1); expect(menuArg[0].label).toBe('File'); expect(menuArg[0].submenu.length).toBe(1); const projectsMenu = menuArg[0].submenu[0]; expect(projectsMenu.label).toBe('Reopen Project'); expect(projectsMenu.submenu.length).toBe(2); const first = projectsMenu.submenu[0]; expect(first.label).toBe('/a'); expect(first.command).toBe('application:reopen-project'); expect(first.commandDetail).toEqual({ index: 0, paths: ['/a'] }); const second = projectsMenu.submenu[1]; expect(second.label).toBe('b, c:\\'); expect(second.command).toBe('application:reopen-project'); expect(second.commandDetail).toEqual({ index: 1, paths: ['/b', 'c:\\'] }); }); it("adds only the number of menu items specified in the 'core.reopenProjectMenuCount' config", () => { historyManager.getProjects.andReturn( numberRange(1, 100).map(i => ({ paths: ['/test/' + i] })) ); reopenProjects.update(); expect(menuManager.add).toHaveBeenCalled(); const menu = menuManager.add.calls[0].args[0][0]; expect(menu.label).toBe('File'); expect(menu.submenu.length).toBe(1); expect(menu.submenu[0].label).toBe('Reopen Project'); expect(menu.submenu[0].submenu.length).toBe(10); }); it('disposes the previously menu built', () => { const menuDisposable = jasmine.createSpyObj('Disposable', ['dispose']); menuManager.add.andReturn(menuDisposable); reopenProjects.update(); expect(menuDisposable.dispose).not.toHaveBeenCalled(); reopenProjects.update(); expect(menuDisposable.dispose).toHaveBeenCalled(); }); it("is called when the Config changes for 'core.reopenProjectMenuCount'", () => { historyManager.getProjects.andReturn( numberRange(1, 100).map(i => ({ paths: ['/test/' + i] })) ); reopenProjects.update(); config.get.andReturn(25); config.didChangeListener['core.reopenProjectMenuCount']({ oldValue: 10, newValue: 25 }); const finalArgs = menuManager.add.calls[1].args[0]; const projectsMenu = finalArgs[0].submenu[0].submenu; expect(projectsMenu.length).toBe(25); }); it("is called when the HistoryManager's projects change", () => { reopenProjects.update(); historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] } ]); historyManager.changeProjectsListener(); expect(menuManager.add.calls.length).toBe(2); const finalArgs = menuManager.add.calls[1].args[0]; const projectsMenu = finalArgs[0].submenu[0]; const first = projectsMenu.submenu[0]; expect(first.label).toBe('/a'); expect(first.command).toBe('application:reopen-project'); expect(first.commandDetail).toEqual({ index: 0, paths: ['/a'] }); const second = projectsMenu.submenu[1]; expect(second.label).toBe('b, c:\\'); expect(second.command).toBe('application:reopen-project'); expect(second.commandDetail).toEqual({ index: 1, paths: ['/b', 'c:\\'] }); }); }); describe('updateProjects', () => { it('creates correct menu items commands for recent projects', () => { const projects = [ { paths: ['/users/neila'] }, { paths: ['/users/buzza', 'users/michaelc'] } ]; const menu = ReopenProjectMenuManager.createProjectsMenu(projects); expect(menu.label).toBe('File'); expect(menu.submenu.length).toBe(1); const recentMenu = menu.submenu[0]; expect(recentMenu.label).toBe('Reopen Project'); expect(recentMenu.submenu.length).toBe(2); const first = recentMenu.submenu[0]; expect(first.label).toBe('/users/neila'); expect(first.command).toBe('application:reopen-project'); expect(first.commandDetail).toEqual({ index: 0, paths: ['/users/neila'] }); const second = recentMenu.submenu[1]; expect(second.label).toBe('buzza, michaelc'); expect(second.command).toBe('application:reopen-project'); expect(second.commandDetail).toEqual({ index: 1, paths: ['/users/buzza', 'users/michaelc'] }); }); }); describe('createLabel', () => { it('returns the Unix path unchanged if there is only one', () => { const label = ReopenProjectMenuManager.createLabel({ paths: ['/a/b/c/d/e/f'] }); expect(label).toBe('/a/b/c/d/e/f'); }); it('returns the Windows path unchanged if there is only one', () => { const label = ReopenProjectMenuManager.createLabel({ paths: ['c:\\missions\\apollo11'] }); expect(label).toBe('c:\\missions\\apollo11'); }); it('returns the URL unchanged if there is only one', () => { const label = ReopenProjectMenuManager.createLabel({ paths: ['https://launch.pad/apollo/11'] }); expect(label).toBe('https://launch.pad/apollo/11'); }); 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'); }); describe('betterBaseName', () => { it('returns the standard base name for an absolute Unix path', () => { const name = ReopenProjectMenuManager.betterBaseName('/one/to/three'); expect(name).toBe('three'); }); it('returns the standard base name for a relative Windows path', () => { if (process.platform === 'win32') { const name = ReopenProjectMenuManager.betterBaseName('.\\one\\two'); expect(name).toBe('two'); } }); it('returns the standard base name for an absolute Windows path', () => { if (process.platform === 'win32') { const name = ReopenProjectMenuManager.betterBaseName( 'c:\\missions\\apollo\\11' ); expect(name).toBe('11'); } }); it('returns the drive root for a Windows drive name', () => { const name = ReopenProjectMenuManager.betterBaseName('d:'); expect(name).toBe('d:\\'); }); it('returns the drive root for a Windows drive root', () => { const name = ReopenProjectMenuManager.betterBaseName('e:\\'); expect(name).toBe('e:\\'); }); it('returns the final path for a URI', () => { const name = ReopenProjectMenuManager.betterBaseName( 'https://something/else' ); expect(name).toBe('else'); }); }); }); }); ================================================ FILE: spec/selection-spec.js ================================================ const TextEditor = require('../src/text-editor'); describe('Selection', () => { let buffer, editor, selection; beforeEach(() => { buffer = atom.project.bufferForPathSync('sample.js'); editor = new TextEditor({ buffer, tabLength: 2 }); selection = editor.getLastSelection(); }); afterEach(() => buffer.destroy()); describe('.deleteSelectedText()', () => { describe('when nothing is selected', () => { it('deletes nothing', () => { selection.setBufferRange([[0, 3], [0, 3]]); selection.deleteSelectedText(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); }); }); describe('when one line is selected', () => { it('deletes selected text and clears the selection', () => { selection.setBufferRange([[0, 4], [0, 14]]); selection.deleteSelectedText(); expect(buffer.lineForRow(0)).toBe('var = function () {'); const endOfLine = buffer.lineForRow(0).length; selection.setBufferRange([[0, 0], [0, endOfLine]]); selection.deleteSelectedText(); expect(buffer.lineForRow(0)).toBe(''); expect(selection.isEmpty()).toBeTruthy(); }); }); describe('when multiple lines are selected', () => { it('deletes selected text and clears the selection', () => { selection.setBufferRange([[0, 1], [2, 39]]); selection.deleteSelectedText(); expect(buffer.lineForRow(0)).toBe('v;'); expect(selection.isEmpty()).toBeTruthy(); }); }); describe('when the cursor precedes the tail', () => { it('deletes selected text and clears the selection', () => { selection.cursor.setScreenPosition([0, 13]); selection.selectToScreenPosition([0, 4]); selection.delete(); expect(buffer.lineForRow(0)).toBe('var = function () {'); expect(selection.isEmpty()).toBeTruthy(); }); }); }); describe('.isReversed()', () => { it('returns true if the cursor precedes the tail', () => { selection.cursor.setScreenPosition([0, 20]); selection.selectToScreenPosition([0, 10]); expect(selection.isReversed()).toBeTruthy(); selection.selectToScreenPosition([0, 25]); expect(selection.isReversed()).toBeFalsy(); }); }); describe('.selectLine(row)', () => { describe('when passed a row', () => { it('selects the specified row', () => { selection.setBufferRange([[2, 4], [3, 4]]); selection.selectLine(5); expect(selection.getBufferRange()).toEqual([[5, 0], [6, 0]]); }); }); describe('when not passed a row', () => { it('selects all rows spanned by the selection', () => { selection.setBufferRange([[2, 4], [3, 4]]); selection.selectLine(); expect(selection.getBufferRange()).toEqual([[2, 0], [4, 0]]); }); }); }); describe("when the selection's range is moved", () => { it('notifies ::onDidChangeRange observers', () => { selection.setBufferRange([[2, 0], [2, 10]]); const changeScreenRangeHandler = jasmine.createSpy( 'changeScreenRangeHandler' ); selection.onDidChangeRange(changeScreenRangeHandler); buffer.insert([2, 5], 'abc'); expect(changeScreenRangeHandler).toHaveBeenCalled(); expect( changeScreenRangeHandler.mostRecentCall.args[0] ).not.toBeUndefined(); }); }); describe("when only the selection's tail is moved (regression)", () => { it('notifies ::onDidChangeRange observers', () => { selection.setBufferRange([[2, 0], [2, 10]], { reversed: true }); const changeScreenRangeHandler = jasmine.createSpy( 'changeScreenRangeHandler' ); selection.onDidChangeRange(changeScreenRangeHandler); buffer.insert([2, 5], 'abc'); expect(changeScreenRangeHandler).toHaveBeenCalled(); expect( changeScreenRangeHandler.mostRecentCall.args[0] ).not.toBeUndefined(); }); }); describe('when the selection is destroyed', () => { it('destroys its marker', () => { selection.setBufferRange([[2, 0], [2, 10]]); const { marker } = selection; selection.destroy(); expect(marker.isDestroyed()).toBeTruthy(); }); }); describe('.insertText(text, options)', () => { it('allows pasting white space only lines when autoIndent is enabled', () => { selection.setBufferRange([[0, 0], [0, 0]]); selection.insertText(' \n \n\n', { autoIndent: true }); expect(buffer.lineForRow(0)).toBe(' '); expect(buffer.lineForRow(1)).toBe(' '); expect(buffer.lineForRow(2)).toBe(''); }); it('auto-indents if only a newline is inserted', () => { selection.setBufferRange([[2, 0], [3, 0]]); selection.insertText('\n', { autoIndent: true }); expect(buffer.lineForRow(2)).toBe(' '); }); it('auto-indents if only a carriage return + newline is inserted', () => { selection.setBufferRange([[2, 0], [3, 0]]); selection.insertText('\r\n', { autoIndent: true }); expect(buffer.lineForRow(2)).toBe(' '); }); it('does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true', () => { selection.setBufferRange([[5, 0], [5, 0]]); selection.insertText(' foo\n bar\n', { preserveTrailingLineIndentation: true, indentBasis: 1 }); expect(buffer.lineForRow(6)).toBe(' bar'); }); }); describe('.fold()', () => { it('folds the buffer range spanned by the selection', () => { selection.setBufferRange([[0, 3], [1, 6]]); selection.fold(); expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]]); expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]]); expect(editor.lineTextForScreenRow(0)).toBe( `var${editor.displayLayer.foldCharacter}sort = function(items) {` ); expect(editor.isFoldedAtBufferRow(0)).toBe(true); }); it("doesn't create a fold when the selection is empty", () => { selection.setBufferRange([[0, 3], [0, 3]]); selection.fold(); expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]]); expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]]); expect(editor.lineTextForScreenRow(0)).toBe( 'var quicksort = function () {' ); expect(editor.isFoldedAtBufferRow(0)).toBe(false); }); }); describe('within a read-only editor', () => { beforeEach(() => { editor.setReadOnly(true); selection.setBufferRange([[0, 0], [0, 13]]); }); const modifications = [ { name: 'insertText', op: opts => selection.insertText('yes', opts) }, { name: 'backspace', op: opts => selection.backspace(opts) }, { name: 'deleteToPreviousWordBoundary', op: opts => selection.deleteToPreviousWordBoundary(opts) }, { name: 'deleteToNextWordBoundary', op: opts => selection.deleteToNextWordBoundary(opts) }, { name: 'deleteToBeginningOfWord', op: opts => selection.deleteToBeginningOfWord(opts) }, { name: 'deleteToBeginningOfLine', op: opts => selection.deleteToBeginningOfLine(opts) }, { name: 'delete', op: opts => selection.delete(opts) }, { name: 'deleteToEndOfLine', op: opts => selection.deleteToEndOfLine(opts) }, { name: 'deleteToEndOfWord', op: opts => selection.deleteToEndOfWord(opts) }, { name: 'deleteToBeginningOfSubword', op: opts => selection.deleteToBeginningOfSubword(opts) }, { name: 'deleteToEndOfSubword', op: opts => selection.deleteToEndOfSubword(opts) }, { name: 'deleteSelectedText', op: opts => selection.deleteSelectedText(opts) }, { name: 'deleteLine', op: opts => selection.deleteLine(opts) }, { name: 'joinLines', op: opts => selection.joinLines(opts) }, { name: 'outdentSelectedRows', op: opts => selection.outdentSelectedRows(opts) }, { name: 'autoIndentSelectedRows', op: opts => selection.autoIndentSelectedRows(opts) }, { name: 'toggleLineComments', op: opts => selection.toggleLineComments(opts) }, { name: 'cutToEndOfLine', op: opts => selection.cutToEndOfLine(false, opts) }, { name: 'cutToEndOfBufferLine', op: opts => selection.cutToEndOfBufferLine(false, opts) }, { name: 'cut', op: opts => selection.cut(false, false, opts.bypassReadOnly) }, { name: 'indent', op: opts => selection.indent(opts) }, { name: 'indentSelectedRows', op: opts => selection.indentSelectedRows(opts) } ]; describe('without bypassReadOnly', () => { for (const { name, op } of modifications) { it(`throws an error on ${name}`, () => { expect(op).toThrow(); }); } }); describe('with bypassReadOnly', () => { for (const { name, op } of modifications) { it(`permits ${name}`, () => { op({ bypassReadOnly: true }); }); } }); }); }); ================================================ FILE: spec/spec-helper-platform.js ================================================ const path = require('path'); const fs = require('fs-plus'); // # Platform specific helpers module.exports = { // Public: Returns true if being run from within Windows isWindows() { return !!process.platform.match(/^win/); }, // Public: Some files can not exist on Windows filesystems, so we have to // selectively generate our fixtures. // // Returns nothing. generateEvilFiles() { let filenames; const evilFilesPath = path.join(__dirname, 'fixtures', 'evil-files'); if (fs.existsSync(evilFilesPath)) { fs.removeSync(evilFilesPath); } fs.mkdirSync(evilFilesPath); if (this.isWindows()) { filenames = [ 'a_file_with_utf8.txt', 'file with spaces.txt', 'utfa\u0306.md' ]; } else { filenames = [ 'a_file_with_utf8.txt', 'file with spaces.txt', 'goddam\nnewlines', 'quote".txt', 'utfa\u0306.md' ]; } filenames.map(filename => fs.writeFileSync(path.join(evilFilesPath, filename), 'evil file!', { flag: 'w' }) ); } }; ================================================ FILE: spec/spec-helper.coffee ================================================ require 'jasmine-json' require '../src/window' require '../vendor/jasmine-jquery' path = require 'path' _ = require 'underscore-plus' fs = require 'fs-plus' Grim = require 'grim' pathwatcher = require 'pathwatcher' FindParentDir = require 'find-parent-dir' {CompositeDisposable} = require 'event-kit' TextEditor = require '../src/text-editor' TextEditorElement = require '../src/text-editor-element' TextMateLanguageMode = require '../src/text-mate-language-mode' TreeSitterLanguageMode = require '../src/tree-sitter-language-mode' {clipboard} = require 'electron' jasmineStyle = document.createElement('style') jasmineStyle.textContent = atom.themes.loadStylesheet(atom.themes.resolveStylesheet('../static/jasmine')) document.head.appendChild(jasmineStyle) fixturePackagesPath = path.resolve(__dirname, './fixtures/packages') atom.packages.packageDirPaths.unshift(fixturePackagesPath) document.querySelector('html').style.overflow = 'auto' document.body.style.overflow = 'auto' Set.prototype.jasmineToString = -> result = "Set {" first = true @forEach (element) -> result += ", " unless first result += element.toString() first = false result + "}" Set.prototype.isEqual = (other) -> if other instanceof Set return false if @size isnt other.size values = @values() until (next = values.next()).done return false unless other.has(next.value) true else false jasmine.getEnv().addEqualityTester (a, b) -> # Match jasmine.any's equality matching logic return a.jasmineMatches(b) if a?.jasmineMatches? return b.jasmineMatches(a) if b?.jasmineMatches? # Use underscore's definition of equality for toEqual assertions _.isEqual(a, b) if process.env.CI jasmine.getEnv().defaultTimeoutInterval = 120000 else jasmine.getEnv().defaultTimeoutInterval = 5000 {testPaths} = atom.getLoadSettings() if specPackagePath = FindParentDir.sync(testPaths[0], 'package.json') packageMetadata = require(path.join(specPackagePath, 'package.json')) specPackageName = packageMetadata.name if specDirectory = FindParentDir.sync(testPaths[0], 'fixtures') specProjectPath = path.join(specDirectory, 'fixtures') else specProjectPath = require('os').tmpdir() beforeEach -> # Do not clobber recent project history spyOn(Object.getPrototypeOf(atom.history), 'saveState').andReturn(Promise.resolve()) atom.project.setPaths([specProjectPath]) window.resetTimeouts() spyOn(_._, "now").andCallFake -> window.now spyOn(Date, 'now').andCallFake(-> window.now) spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout spy = spyOn(atom.packages, 'resolvePackagePath').andCallFake (packageName) -> if specPackageName and packageName is specPackageName resolvePackagePath(specPackagePath) else resolvePackagePath(packageName) resolvePackagePath = _.bind(spy.originalValue, atom.packages) # prevent specs from modifying Atom's menus spyOn(atom.menu, 'sendToBrowserProcess') # reset config before each spec atom.config.set "core.destroyEmptyPanes", false atom.config.set "editor.fontFamily", "Courier" atom.config.set "editor.fontSize", 16 atom.config.set "editor.autoIndent", false atom.config.set "core.disabledPackages", ["package-that-throws-an-exception", "package-with-broken-package-json", "package-with-broken-keymap"] advanceClock(1000) window.setTimeout.reset() # make editor display updates synchronous TextEditorElement::setUpdatedSynchronously(true) spyOn(pathwatcher.File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection() spyOn(TextEditor.prototype, "shouldPromptToSave").andReturn false # make tokenization synchronous TextMateLanguageMode.prototype.chunkSize = Infinity TreeSitterLanguageMode.prototype.syncTimeoutMicros = Infinity spyOn(TextMateLanguageMode.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk() # Without this spy, TextEditor.onDidTokenize callbacks would not be called # after the buffer's language mode changed, because by the time the editor # called its new language mode's onDidTokenize method, the language mode # would already be fully tokenized. spyOn(TextEditor.prototype, "onDidTokenize").andCallFake (callback) -> new CompositeDisposable( @emitter.on("did-tokenize", callback), @onDidChangeGrammar => languageMode = @buffer.getLanguageMode() if languageMode.tokenizeInBackground?.originalValue callback() ) clipboardContent = 'initial clipboard content' spyOn(clipboard, 'writeText').andCallFake (text) -> clipboardContent = text spyOn(clipboard, 'readText').andCallFake -> clipboardContent addCustomMatchers(this) afterEach -> ensureNoDeprecatedFunctionCalls() ensureNoDeprecatedStylesheets() 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() if watchedPaths.length > 0 console.error("WARNING: Leaking subscriptions for paths: " + watchedPaths.join(", ")) pathwatcher.closeAllWatchers() ensureNoDeprecatedFunctionCalls = -> deprecations = _.clone(Grim.getDeprecations()) Grim.clearDeprecations() if deprecations.length > 0 originalPrepareStackTrace = Error.prepareStackTrace Error.prepareStackTrace = (error, stack) -> output = [] for deprecation in deprecations output.push "#{deprecation.originName} is deprecated. #{deprecation.message}" output.push _.multiplyString("-", output[output.length - 1].length) for stack in deprecation.getStacks() for {functionName, location} in stack output.push "#{functionName} -- #{location}" output.push "" output.join("\n") error = new Error("Deprecated function(s) #{deprecations.map(({originName}) -> originName).join ', '}) were called.") error.stack Error.prepareStackTrace = originalPrepareStackTrace throw error ensureNoDeprecatedStylesheets = -> deprecations = _.clone(atom.styles.getDeprecations()) atom.styles.clearDeprecations() for sourcePath, deprecation of deprecations title = if sourcePath isnt 'undefined' "Deprecated stylesheet at '#{sourcePath}':" else "Deprecated stylesheet:" throw new Error("#{title}\n#{deprecation.message}") emitObject = jasmine.StringPrettyPrinter.prototype.emitObject jasmine.StringPrettyPrinter.prototype.emitObject = (obj) -> if obj.inspect @append obj.inspect() else emitObject.call(this, obj) jasmine.unspy = (object, methodName) -> throw new Error("Not a spy") unless object[methodName].hasOwnProperty('originalValue') object[methodName] = object[methodName].originalValue jasmine.attachToDOM = (element) -> jasmineContent = document.querySelector('#jasmine-content') jasmineContent.appendChild(element) unless jasmineContent.contains(element) grimDeprecationsSnapshot = null stylesDeprecationsSnapshot = null jasmine.snapshotDeprecations = -> grimDeprecationsSnapshot = _.clone(Grim.deprecations) stylesDeprecationsSnapshot = _.clone(atom.styles.deprecationsBySourcePath) jasmine.restoreDeprecationsSnapshot = -> Grim.deprecations = grimDeprecationsSnapshot atom.styles.deprecationsBySourcePath = stylesDeprecationsSnapshot jasmine.useRealClock = -> jasmine.unspy(window, 'setTimeout') jasmine.unspy(window, 'clearTimeout') jasmine.unspy(_._, 'now') jasmine.unspy(Date, 'now') # The clock is halfway mocked now in a sad and terrible way... only setTimeout # and clearTimeout are included. This method will also include setInterval. We # would do this everywhere if didn't cause us to break a bunch of package tests. jasmine.useMockClock = -> spyOn(window, 'setInterval').andCallFake(fakeSetInterval) spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) addCustomMatchers = (spec) -> spec.addMatchers toBeInstanceOf: (expected) -> beOrNotBe = if @isNot then "not be" else "be" this.message = => "Expected #{jasmine.pp(@actual)} to #{beOrNotBe} instance of #{expected.name} class" @actual instanceof expected toHaveLength: (expected) -> if not @actual? this.message = => "Expected object #{@actual} has no length method" false else haveOrNotHave = if @isNot then "not have" else "have" this.message = => "Expected object with length #{@actual.length} to #{haveOrNotHave} length #{expected}" @actual.length is expected toExistOnDisk: (expected) -> toOrNotTo = this.isNot and "not to" or "to" @message = -> return "Expected path '#{@actual}' #{toOrNotTo} exist." fs.existsSync(@actual) toHaveFocus: -> toOrNotTo = this.isNot and "not to" or "to" if not document.hasFocus() console.error "Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner." @message = -> return "Expected element '#{@actual}' or its descendants #{toOrNotTo} have focus." element = @actual element = element.get(0) if element.jquery element is document.activeElement or element.contains(document.activeElement) toShow: -> toOrNotTo = this.isNot and "not to" or "to" element = @actual element = element.get(0) if element.jquery @message = -> return "Expected element '#{element}' or its descendants #{toOrNotTo} show." computedStyle = getComputedStyle(element) computedStyle.display isnt 'none' and computedStyle.visibility is 'visible' and not element.hidden toEqualPath: (expected) -> actualPath = path.normalize(@actual) expectedPath = path.normalize(expected) @message = -> return "Expected path '#{actualPath}' to be equal to '#{expectedPath}'." actualPath is expectedPath toBeNear: (expected, acceptedError = 1, actual) -> return (typeof expected is 'number') and (typeof acceptedError is 'number') and (typeof @actual is 'number') and (expected - acceptedError <= @actual) and (@actual <= expected + acceptedError) toHaveNearPixels: (expected, acceptedError = 1, actual) -> expectedNumber = parseFloat(expected) actualNumber = parseFloat(@actual) return (typeof expected is 'string') and (typeof acceptedError is 'number') and (typeof @actual is 'string') and (expected.indexOf('px') >= 1) and (@actual.indexOf('px') >= 1) and (expectedNumber - acceptedError <= actualNumber) and (actualNumber <= expectedNumber + acceptedError) window.waitsForPromise = (args...) -> label = null if args.length > 1 {shouldReject, timeout, label} = args[0] else shouldReject = false label ?= 'promise to be resolved or rejected' fn = _.last(args) window.waitsFor label, timeout, (moveOn) -> promise = fn() if shouldReject promise.catch.call(promise, moveOn) promise.then -> jasmine.getEnv().currentSpec.fail("Expected promise to be rejected, but it was resolved") moveOn() else promise.then(moveOn) promise.catch.call promise, (error) -> jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with: #{error?.message} #{jasmine.pp(error)}") moveOn() window.resetTimeouts = -> window.now = 0 window.timeoutCount = 0 window.intervalCount = 0 window.timeouts = [] window.intervalTimeouts = {} window.fakeSetTimeout = (callback, ms=0) -> id = ++window.timeoutCount window.timeouts.push([id, window.now + ms, callback]) id window.fakeClearTimeout = (idToClear) -> window.timeouts = window.timeouts.filter ([id]) -> id isnt idToClear window.fakeSetInterval = (callback, ms) -> id = ++window.intervalCount action = -> callback() window.intervalTimeouts[id] = window.fakeSetTimeout(action, ms) window.intervalTimeouts[id] = window.fakeSetTimeout(action, ms) id window.fakeClearInterval = (idToClear) -> window.fakeClearTimeout(@intervalTimeouts[idToClear]) window.advanceClock = (delta=1) -> window.now += delta callbacks = [] window.timeouts = window.timeouts.filter ([id, strikeTime, callback]) -> if strikeTime <= window.now callbacks.push(callback) false else true callback() for callback in callbacks exports.mockLocalStorage = -> items = {} spyOn(global.localStorage, 'setItem').andCallFake (key, item) -> items[key] = item.toString(); undefined spyOn(global.localStorage, 'getItem').andCallFake (key) -> items[key] ? null spyOn(global.localStorage, 'removeItem').andCallFake (key) -> delete items[key]; undefined ================================================ FILE: spec/squirrel-update-spec.js ================================================ const electron = require('electron'); const fs = require('fs-plus'); const path = require('path'); const temp = require('temp').track(); electron.app = { getName() { return 'Atom'; }, getVersion() { return '1.0.0'; }, getPath() { return '/tmp/atom.exe'; } }; const SquirrelUpdate = require('../src/main-process/squirrel-update'); const Spawner = require('../src/main-process/spawner'); const WinShell = require('../src/main-process/win-shell'); // Run passed callback as Spawner.spawn() would do const invokeCallback = function(callback) { const error = null; const stdout = ''; return typeof callback === 'function' ? callback(error, stdout) : undefined; }; describe('Windows Squirrel Update', function() { let tempHomeDirectory = null; beforeEach(function() { // Prevent the actual home directory from being manipulated tempHomeDirectory = temp.mkdirSync('atom-temp-home-'); spyOn(fs, 'getHomeDirectory').andReturn(tempHomeDirectory); // Prevent any spawned command from actually running and affecting the host spyOn(Spawner, 'spawn').andCallFake(( command, args, callback // do nothing on command, just run passed callback ) => invokeCallback(callback)); // Prevent any actual change to Windows Shell class FakeShellOption { isRegistered(callback) { return callback(true); } register(callback) { return callback(null); } deregister(callback) { return callback(null, true); } update(callback) { return callback(null); } } WinShell.fileHandler = new FakeShellOption(); WinShell.fileContextMenu = new FakeShellOption(); WinShell.folderContextMenu = new FakeShellOption(); WinShell.folderBackgroundContextMenu = new FakeShellOption(); electron.app.quit = jasmine.createSpy('quit'); }); afterEach(function() { electron.app.quit.reset(); try { temp.cleanupSync(); } catch (error) {} }); it('quits the app on all squirrel events', function() { expect(SquirrelUpdate.handleStartupEvent('--squirrel-install')).toBe(true); waitsFor(() => electron.app.quit.callCount === 1); runs(function() { electron.app.quit.reset(); expect(SquirrelUpdate.handleStartupEvent('--squirrel-updated')).toBe( true ); }); waitsFor(() => electron.app.quit.callCount === 1); runs(function() { electron.app.quit.reset(); expect(SquirrelUpdate.handleStartupEvent('--squirrel-uninstall')).toBe( true ); }); waitsFor(() => electron.app.quit.callCount === 1); runs(function() { electron.app.quit.reset(); expect(SquirrelUpdate.handleStartupEvent('--squirrel-obsolete')).toBe( true ); }); waitsFor(() => electron.app.quit.callCount === 1); runs(() => expect(SquirrelUpdate.handleStartupEvent('--not-squirrel')).toBe(false) ); }); describe('Desktop shortcut', function() { let desktopShortcutPath = '/non/existing/path'; beforeEach(function() { desktopShortcutPath = path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk'); jasmine.unspy(Spawner, 'spawn'); spyOn(Spawner, 'spawn').andCallFake(function(command, args, callback) { if ( path.basename(command) === 'Update.exe' && (args != null ? args[0] : undefined) === '--createShortcut' && (args != null ? args[3].match(/Desktop/i) : undefined) ) { fs.writeFileSync(desktopShortcutPath, ''); } else { } // simply ignore other commands invokeCallback(callback); }); }); it('does not exist before install', () => expect(fs.existsSync(desktopShortcutPath)).toBe(false)); describe('on install', function() { beforeEach(function() { SquirrelUpdate.handleStartupEvent('--squirrel-install'); waitsFor(() => electron.app.quit.callCount === 1); }); it('creates desktop shortcut', () => expect(fs.existsSync(desktopShortcutPath)).toBe(true)); describe('when shortcut is deleted and then app is updated', function() { beforeEach(function() { fs.removeSync(desktopShortcutPath); expect(fs.existsSync(desktopShortcutPath)).toBe(false); SquirrelUpdate.handleStartupEvent('--squirrel-updated'); waitsFor(() => electron.app.quit.callCount === 2); }); it('does not recreate shortcut', () => expect(fs.existsSync(desktopShortcutPath)).toBe(false)); }); describe('when shortcut is kept and app is updated', function() { beforeEach(function() { SquirrelUpdate.handleStartupEvent('--squirrel-updated'); waitsFor(() => electron.app.quit.callCount === 2); }); it('still has desktop shortcut', () => expect(fs.existsSync(desktopShortcutPath)).toBe(true)); }); }); }); }); ================================================ FILE: spec/state-store-spec.js ================================================ /** @babel */ const StateStore = require('../src/state-store.js'); describe('StateStore', () => { let databaseName = `test-database-${Date.now()}`; let version = 1; it('can save, load, and delete states', () => { const store = new StateStore(databaseName, version); return store .save('key', { foo: 'bar' }) .then(() => store.load('key')) .then(state => { expect(state).toEqual({ foo: 'bar' }); }) .then(() => store.delete('key')) .then(() => store.load('key')) .then(value => { expect(value).toBeNull(); }) .then(() => store.count()) .then(count => { expect(count).toBe(0); }); }); it('resolves with null when a non-existent key is loaded', () => { const store = new StateStore(databaseName, version); return store.load('no-such-key').then(value => { expect(value).toBeNull(); }); }); it('can clear the state object store', () => { const store = new StateStore(databaseName, version); return store .save('key', { foo: 'bar' }) .then(() => store.count()) .then(count => expect(count).toBe(1)) .then(() => store.clear()) .then(() => store.count()) .then(count => { expect(count).toBe(0); }); }); describe('when there is an error reading from the database', () => { it('rejects the promise returned by load', () => { const store = new StateStore(databaseName, version); const fakeErrorEvent = { target: { errorCode: 'Something bad happened' } }; spyOn(IDBObjectStore.prototype, 'get').andCallFake(key => { let request = {}; process.nextTick(() => request.onerror(fakeErrorEvent)); return request; }); return store .load('nonexistentKey') .then(() => { throw new Error('Promise should have been rejected'); }) .catch(event => { expect(event).toBe(fakeErrorEvent); }); }); }); }); ================================================ FILE: spec/style-manager-spec.js ================================================ const temp = require('temp').track(); const StyleManager = require('../src/style-manager'); describe('StyleManager', () => { let [styleManager, addEvents, removeEvents, updateEvents] = []; beforeEach(() => { styleManager = new StyleManager({ configDirPath: temp.mkdirSync('atom-config') }); addEvents = []; removeEvents = []; updateEvents = []; styleManager.onDidAddStyleElement(event => { addEvents.push(event); }); styleManager.onDidRemoveStyleElement(event => { removeEvents.push(event); }); styleManager.onDidUpdateStyleElement(event => { updateEvents.push(event); }); }); afterEach(() => { try { temp.cleanupSync(); } catch (e) { // Do nothing } }); describe('::addStyleSheet(source, params)', () => { it('adds a style sheet based on the given source and returns a disposable allowing it to be removed', () => { const disposable = styleManager.addStyleSheet('a {color: red}'); expect(addEvents.length).toBe(1); expect(addEvents[0].textContent).toBe('a {color: red}'); const styleElements = styleManager.getStyleElements(); expect(styleElements.length).toBe(1); expect(styleElements[0].textContent).toBe('a {color: red}'); disposable.dispose(); expect(removeEvents.length).toBe(1); expect(removeEvents[0].textContent).toBe('a {color: red}'); expect(styleManager.getStyleElements().length).toBe(0); }); describe('atom-text-editor shadow DOM selectors upgrades', () => { beforeEach(() => { // attach styles element to the DOM to parse CSS rules styleManager.onDidAddStyleElement(styleElement => { jasmine.attachToDOM(styleElement); }); }); it('removes the ::shadow pseudo-element from atom-text-editor selectors', () => { styleManager.addStyleSheet(` atom-text-editor::shadow .class-1, atom-text-editor::shadow .class-2 { color: red } atom-text-editor::shadow > .class-3 { color: yellow } atom-text-editor .class-4 { color: blue } atom-text-editor[data-grammar*="js"]::shadow .class-6 { color: green; } atom-text-editor[mini].is-focused::shadow .class-7 { color: green; } `); expect( Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map( r => r.selectorText ) ).toEqual([ 'atom-text-editor.editor .class-1, atom-text-editor.editor .class-2', 'atom-text-editor.editor > .class-3', 'atom-text-editor .class-4', 'atom-text-editor[data-grammar*="js"].editor .class-6', 'atom-text-editor[mini].is-focused.editor .class-7' ]); }); describe('when a selector targets the atom-text-editor shadow DOM', () => { it('prepends "--syntax" to class selectors matching a grammar scope name and not already starting with "syntax--"', () => { styleManager.addStyleSheet( ` .class-1 { color: red } .source > .js, .source.coffee { color: green } .syntax--source { color: gray } #id-1 { color: blue } `, { context: 'atom-text-editor' } ); expect( Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map( r => r.selectorText ) ).toEqual([ '.class-1', '.syntax--source > .syntax--js, .syntax--source.syntax--coffee', '.syntax--source', '#id-1' ]); styleManager.addStyleSheet(` .source > .js, .source.coffee { color: green } atom-text-editor::shadow .source > .js { color: yellow } atom-text-editor[mini].is-focused::shadow .source > .js { color: gray } atom-text-editor .source > .js { color: red } `); expect( Array.from(styleManager.getStyleElements()[1].sheet.cssRules).map( r => r.selectorText ) ).toEqual([ '.source > .js, .source.coffee', 'atom-text-editor.editor .syntax--source > .syntax--js', 'atom-text-editor[mini].is-focused.editor .syntax--source > .syntax--js', 'atom-text-editor .source > .js' ]); }); }); it('replaces ":host" with "atom-text-editor" only when the context of a style sheet is "atom-text-editor"', () => { styleManager.addStyleSheet( ':host .class-1, :host .class-2 { color: red; }' ); expect( Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map( r => r.selectorText ) ).toEqual([':host .class-1, :host .class-2']); styleManager.addStyleSheet( ':host .class-1, :host .class-2 { color: red; }', { context: 'atom-text-editor' } ); expect( Array.from(styleManager.getStyleElements()[1].sheet.cssRules).map( r => r.selectorText ) ).toEqual(['atom-text-editor .class-1, atom-text-editor .class-2']); }); it('does not throw exceptions on rules with no selectors', () => { styleManager.addStyleSheet('@media screen {font-size: 10px}', { context: 'atom-text-editor' }); }); }); describe('when a sourcePath parameter is specified', () => { it('ensures a maximum of one style element for the given source path, updating a previous if it exists', () => { styleManager.addStyleSheet('a {color: red}', { sourcePath: '/foo/bar' }); expect(addEvents.length).toBe(1); expect(addEvents[0].getAttribute('source-path')).toBe('/foo/bar'); const disposable2 = styleManager.addStyleSheet('a {color: blue}', { sourcePath: '/foo/bar' }); expect(addEvents.length).toBe(1); expect(updateEvents.length).toBe(1); expect(updateEvents[0].getAttribute('source-path')).toBe('/foo/bar'); expect(updateEvents[0].textContent).toBe('a {color: blue}'); disposable2.dispose(); addEvents = []; styleManager.addStyleSheet('a {color: yellow}', { sourcePath: '/foo/bar' }); expect(addEvents.length).toBe(1); expect(addEvents[0].getAttribute('source-path')).toBe('/foo/bar'); expect(addEvents[0].textContent).toBe('a {color: yellow}'); }); }); describe('when a priority parameter is specified', () => { it('inserts the style sheet based on the priority', () => { styleManager.addStyleSheet('a {color: red}', { priority: 1 }); styleManager.addStyleSheet('a {color: blue}', { priority: 0 }); styleManager.addStyleSheet('a {color: green}', { priority: 2 }); styleManager.addStyleSheet('a {color: yellow}', { priority: 1 }); expect( styleManager.getStyleElements().map(elt => elt.textContent) ).toEqual([ 'a {color: blue}', 'a {color: red}', 'a {color: yellow}', 'a {color: green}' ]); }); }); }); }); ================================================ FILE: spec/styles-element-spec.js ================================================ const { createStylesElement } = require('../src/styles-element'); describe('StylesElement', function() { let [ element, addedStyleElements, removedStyleElements, updatedStyleElements ] = []; beforeEach(function() { element = createStylesElement(); element.initialize(atom.styles); document.querySelector('#jasmine-content').appendChild(element); addedStyleElements = []; removedStyleElements = []; updatedStyleElements = []; element.onDidAddStyleElement(element => addedStyleElements.push(element)); element.onDidRemoveStyleElement(element => removedStyleElements.push(element) ); element.onDidUpdateStyleElement(element => updatedStyleElements.push(element) ); }); it('renders a style tag for all currently active stylesheets in the style manager', function() { const initialChildCount = element.children.length; const disposable1 = atom.styles.addStyleSheet('a {color: red;}'); expect(element.children.length).toBe(initialChildCount + 1); expect(element.children[initialChildCount].textContent).toBe( 'a {color: red;}' ); expect(addedStyleElements).toEqual([element.children[initialChildCount]]); atom.styles.addStyleSheet('a {color: blue;}'); expect(element.children.length).toBe(initialChildCount + 2); expect(element.children[initialChildCount + 1].textContent).toBe( 'a {color: blue;}' ); expect(addedStyleElements).toEqual([ element.children[initialChildCount], element.children[initialChildCount + 1] ]); disposable1.dispose(); expect(element.children.length).toBe(initialChildCount + 1); expect(element.children[initialChildCount].textContent).toBe( 'a {color: blue;}' ); expect(removedStyleElements).toEqual([addedStyleElements[0]]); }); it('orders style elements by priority', function() { const initialChildCount = element.children.length; atom.styles.addStyleSheet('a {color: red}', { priority: 1 }); atom.styles.addStyleSheet('a {color: blue}', { priority: 0 }); atom.styles.addStyleSheet('a {color: green}', { priority: 2 }); atom.styles.addStyleSheet('a {color: yellow}', { priority: 1 }); expect(element.children[initialChildCount].textContent).toBe( 'a {color: blue}' ); expect(element.children[initialChildCount + 1].textContent).toBe( 'a {color: red}' ); expect(element.children[initialChildCount + 2].textContent).toBe( 'a {color: yellow}' ); expect(element.children[initialChildCount + 3].textContent).toBe( 'a {color: green}' ); }); it('updates existing style nodes when style elements are updated', function() { const initialChildCount = element.children.length; atom.styles.addStyleSheet('a {color: red;}', { sourcePath: '/foo/bar' }); atom.styles.addStyleSheet('a {color: blue;}', { sourcePath: '/foo/bar' }); expect(element.children.length).toBe(initialChildCount + 1); expect(element.children[initialChildCount].textContent).toBe( 'a {color: blue;}' ); expect(updatedStyleElements).toEqual([element.children[initialChildCount]]); }); it("only includes style elements matching the 'context' attribute", function() { const initialChildCount = element.children.length; atom.styles.addStyleSheet('a {color: red;}', { context: 'test-context' }); atom.styles.addStyleSheet('a {color: green;}'); expect(element.children.length).toBe(initialChildCount + 2); expect(element.children[initialChildCount].textContent).toBe( 'a {color: red;}' ); expect(element.children[initialChildCount + 1].textContent).toBe( 'a {color: green;}' ); element.setAttribute('context', 'test-context'); expect(element.children.length).toBe(1); expect(element.children[0].textContent).toBe('a {color: red;}'); atom.styles.addStyleSheet('a {color: blue;}', { context: 'test-context' }); atom.styles.addStyleSheet('a {color: yellow;}'); expect(element.children.length).toBe(2); expect(element.children[0].textContent).toBe('a {color: red;}'); expect(element.children[1].textContent).toBe('a {color: blue;}'); }); }); ================================================ FILE: spec/syntax-scope-map-spec.js ================================================ const SyntaxScopeMap = require('../src/syntax-scope-map'); describe('SyntaxScopeMap', () => { it('can match immediate child selectors', () => { const map = new SyntaxScopeMap({ 'a > b > c': 'x', 'b > c': 'y', c: 'z' }); expect(map.get(['a', 'b', 'c'], [0, 0, 0])).toBe('x'); expect(map.get(['d', 'b', 'c'], [0, 0, 0])).toBe('y'); expect(map.get(['d', 'e', 'c'], [0, 0, 0])).toBe('z'); expect(map.get(['e', 'c'], [0, 0, 0])).toBe('z'); expect(map.get(['c'], [0, 0, 0])).toBe('z'); expect(map.get(['d'], [0, 0, 0])).toBe(undefined); }); it('can match :nth-child pseudo-selectors on leaves', () => { const map = new SyntaxScopeMap({ 'a > b': 'w', 'a > b:nth-child(1)': 'x', b: 'y', 'b:nth-child(2)': 'z' }); expect(map.get(['a', 'b'], [0, 0])).toBe('w'); expect(map.get(['a', 'b'], [0, 1])).toBe('x'); expect(map.get(['a', 'b'], [0, 2])).toBe('w'); expect(map.get(['b'], [0])).toBe('y'); expect(map.get(['b'], [1])).toBe('y'); expect(map.get(['b'], [2])).toBe('z'); }); it('can match :nth-child pseudo-selectors on interior nodes', () => { const map = new SyntaxScopeMap({ 'b:nth-child(1) > c': 'w', 'a > b > c': 'x', 'a > b:nth-child(2) > c': 'y' }); expect(map.get(['b', 'c'], [0, 0])).toBe(undefined); expect(map.get(['b', 'c'], [1, 0])).toBe('w'); expect(map.get(['a', 'b', 'c'], [1, 0, 0])).toBe('x'); expect(map.get(['a', 'b', 'c'], [1, 2, 0])).toBe('y'); }); it('allows anonymous tokens to be referred to by their string value', () => { const map = new SyntaxScopeMap({ '"b"': 'w', 'a > "b"': 'x', 'a > "b":nth-child(1)': 'y', '"\\""': 'z' }); expect(map.get(['b'], [0], true)).toBe(undefined); expect(map.get(['b'], [0], false)).toBe('w'); expect(map.get(['a', 'b'], [0, 0], false)).toBe('x'); expect(map.get(['a', 'b'], [0, 1], false)).toBe('y'); expect(map.get(['a', '"'], [0, 1], false)).toBe('z'); }); it('supports the wildcard selector', () => { const map = new SyntaxScopeMap({ '*': 'w', 'a > *': 'x', 'a > *:nth-child(1)': 'y', 'a > *:nth-child(1) > b': 'z' }); expect(map.get(['b'], [0])).toBe('w'); expect(map.get(['c'], [0])).toBe('w'); expect(map.get(['a', 'b'], [0, 0])).toBe('x'); expect(map.get(['a', 'b'], [0, 1])).toBe('y'); expect(map.get(['a', 'c'], [0, 1])).toBe('y'); expect(map.get(['a', 'c', 'b'], [0, 1, 1])).toBe('z'); expect(map.get(['a', 'c', 'b'], [0, 2, 1])).toBe('w'); }); it('distinguishes between an anonymous * token and the wildcard selector', () => { const map = new SyntaxScopeMap({ '"*"': 'x', 'a > "b"': 'y' }); expect(map.get(['b'], [0], false)).toBe(undefined); expect(map.get(['*'], [0], false)).toBe('x'); }); }); ================================================ FILE: spec/task-spec.js ================================================ const Task = require('../src/task'); const Grim = require('grim'); describe('Task', function() { describe('@once(taskPath, args..., callback)', () => it('terminates the process after it completes', function() { let handlerResult = null; const task = Task.once( require.resolve('./fixtures/task-spec-handler'), result => (handlerResult = result) ); let processErrored = false; const { childProcess } = task; spyOn(childProcess, 'kill').andCallThrough(); task.childProcess.on('error', () => (processErrored = true)); waitsFor(() => handlerResult != null); runs(function() { expect(handlerResult).toBe('hello'); expect(childProcess.kill).toHaveBeenCalled(); expect(processErrored).toBe(false); }); })); it('calls listeners registered with ::on when events are emitted in the task', function() { const task = new Task(require.resolve('./fixtures/task-spec-handler')); const eventSpy = jasmine.createSpy('eventSpy'); task.on('some-event', eventSpy); waitsFor(done => task.start(done)); runs(() => expect(eventSpy).toHaveBeenCalledWith(1, 2, 3)); }); it('unregisters listeners when the Disposable returned by ::on is disposed', function() { const task = new Task(require.resolve('./fixtures/task-spec-handler')); const eventSpy = jasmine.createSpy('eventSpy'); const disposable = task.on('some-event', eventSpy); disposable.dispose(); waitsFor(done => task.start(done)); runs(() => expect(eventSpy).not.toHaveBeenCalled()); }); it('reports deprecations in tasks', function() { jasmine.snapshotDeprecations(); const handlerPath = require.resolve( './fixtures/task-handler-with-deprecations' ); const task = new Task(handlerPath); waitsFor(done => task.start(done)); runs(function() { const deprecations = Grim.getDeprecations(); expect(deprecations.length).toBe(1); expect(deprecations[0].getStacks()[0][1].fileName).toBe(handlerPath); jasmine.restoreDeprecationsSnapshot(); }); }); it('adds data listeners to standard out and error to report output', function() { const task = new Task(require.resolve('./fixtures/task-spec-handler')); const { stdout, stderr } = task.childProcess; task.start(); task.start(); expect(stdout.listeners('data').length).toBe(1); expect(stderr.listeners('data').length).toBe(1); task.terminate(); expect(stdout.listeners('data').length).toBe(0); expect(stderr.listeners('data').length).toBe(0); }); it('does not throw an error for forked processes missing stdout/stderr', function() { spyOn(require('child_process'), 'fork').andCallFake(function() { const Events = require('events'); const fakeProcess = new Events(); fakeProcess.send = function() {}; fakeProcess.kill = function() {}; return fakeProcess; }); const task = new Task(require.resolve('./fixtures/task-spec-handler')); expect(() => task.start()).not.toThrow(); expect(() => task.terminate()).not.toThrow(); }); describe('::cancel()', function() { it("dispatches 'task:cancelled' when invoked on an active task", function() { const task = new Task(require.resolve('./fixtures/task-spec-handler')); const cancelledEventSpy = jasmine.createSpy('eventSpy'); task.on('task:cancelled', cancelledEventSpy); const completedEventSpy = jasmine.createSpy('eventSpy'); task.on('task:completed', completedEventSpy); expect(task.cancel()).toBe(true); expect(cancelledEventSpy).toHaveBeenCalled(); expect(completedEventSpy).not.toHaveBeenCalled(); }); it("does not dispatch 'task:cancelled' when invoked on an inactive task", function() { let handlerResult = null; const task = Task.once( require.resolve('./fixtures/task-spec-handler'), result => (handlerResult = result) ); waitsFor(() => handlerResult != null); runs(function() { const cancelledEventSpy = jasmine.createSpy('eventSpy'); task.on('task:cancelled', cancelledEventSpy); expect(task.cancel()).toBe(false); expect(cancelledEventSpy).not.toHaveBeenCalled(); }); }); }); }); ================================================ FILE: spec/text-editor-component-spec.js ================================================ const { conditionPromise } = 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'); const TextBuffer = require('text-buffer'); const { Point } = TextBuffer; const fs = require('fs'); const path = require('path'); const Grim = require('grim'); const electron = require('electron'); const clipboard = electron.clipboard; const SAMPLE_TEXT = fs.readFileSync( path.join(__dirname, 'fixtures', 'sample.js'), 'utf8' ); class DummyElement extends HTMLElement { connectedCallback() { this.didAttach(); } } window.customElements.define( 'text-editor-component-test-element', DummyElement ); document.createElement('text-editor-component-test-element'); const editors = []; let verticalScrollbarWidth, horizontalScrollbarHeight; describe('TextEditorComponent', () => { beforeEach(() => { jasmine.useRealClock(); // Force scrollbars to be visible regardless of local system configuration const scrollbarStyle = document.createElement('style'); scrollbarStyle.textContent = 'atom-text-editor ::-webkit-scrollbar { -webkit-appearance: none }'; jasmine.attachToDOM(scrollbarStyle); if (verticalScrollbarWidth == null) { const { component, element } = buildComponent({ text: 'abcdefgh\n'.repeat(10), width: 30, height: 30 }); verticalScrollbarWidth = getVerticalScrollbarWidth(component); horizontalScrollbarHeight = getHorizontalScrollbarHeight(component); element.remove(); } }); afterEach(() => { for (const editor of editors) { editor.destroy(); } editors.length = 0; }); describe('rendering', () => { it('renders lines and line numbers for the visible region', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 3, autoHeight: false }); expect(queryOnScreenLineNumberElements(element).length).toBe(13); expect(queryOnScreenLineElements(element).length).toBe(13); element.style.height = 4 * component.measurements.lineHeight + 'px'; await component.getNextUpdatePromise(); expect(queryOnScreenLineNumberElements(element).length).toBe(9); expect(queryOnScreenLineElements(element).length).toBe(9); await setScrollTop(component, 5 * component.getLineHeight()); // After scrolling down beyond > 3 rows, the order of line numbers and lines // in the DOM is a bit weird because the first tile is recycled to the bottom // when it is scrolled out of view expect( queryOnScreenLineNumberElements(element).map(element => element.textContent.trim() ) ).toEqual(['10', '11', '12', '4', '5', '6', '7', '8', '9']); expect( queryOnScreenLineElements(element).map( element => element.dataset.screenRow ) ).toEqual(['9', '10', '11', '3', '4', '5', '6', '7', '8']); expect( queryOnScreenLineElements(element).map(element => element.textContent) ).toEqual([ editor.lineTextForScreenRow(9), ' ', // this line is blank in the model, but we render a space to prevent the line from collapsing vertically editor.lineTextForScreenRow(11), editor.lineTextForScreenRow(3), editor.lineTextForScreenRow(4), editor.lineTextForScreenRow(5), editor.lineTextForScreenRow(6), editor.lineTextForScreenRow(7), editor.lineTextForScreenRow(8) ]); await setScrollTop(component, 2.5 * component.getLineHeight()); expect( queryOnScreenLineNumberElements(element).map(element => element.textContent.trim() ) ).toEqual(['1', '2', '3', '4', '5', '6', '7', '8', '9']); expect( queryOnScreenLineElements(element).map( element => element.dataset.screenRow ) ).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8']); expect( queryOnScreenLineElements(element).map(element => element.textContent) ).toEqual([ editor.lineTextForScreenRow(0), editor.lineTextForScreenRow(1), editor.lineTextForScreenRow(2), editor.lineTextForScreenRow(3), editor.lineTextForScreenRow(4), editor.lineTextForScreenRow(5), editor.lineTextForScreenRow(6), editor.lineTextForScreenRow(7), editor.lineTextForScreenRow(8) ]); }); it('bases the width of the lines div on the width of the longest initially-visible screen line', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 2, height: 20, width: 100 }); { expect(editor.getApproximateLongestScreenRow()).toBe(3); const expectedWidth = Math.ceil( component.pixelPositionForScreenPosition(Point(3, Infinity)).left + component.getBaseCharacterWidth() ); expect(element.querySelector('.lines').style.width).toBe( expectedWidth + 'px' ); } { // Get the next update promise synchronously here to ensure we don't // miss the update while polling the condition. const nextUpdatePromise = component.getNextUpdatePromise(); await conditionPromise( () => editor.getApproximateLongestScreenRow() === 6 ); await nextUpdatePromise; // Capture the width of the lines before requesting the width of // longest line, because making that request forces a DOM update const actualWidth = element.querySelector('.lines').style.width; const expectedWidth = Math.ceil( component.pixelPositionForScreenPosition(Point(6, Infinity)).left + component.getBaseCharacterWidth() ); expect(actualWidth).toBe(expectedWidth + 'px'); } // eslint-disable-next-line no-lone-blocks { // Make sure we do not throw an error if a synchronous update is // triggered before measuring the longest line from a // previously-scheduled update. editor.getBuffer().insert(Point(12, Infinity), 'x'.repeat(100)); expect(editor.getLongestScreenRow()).toBe(12); TextEditorComponent.getScheduler().readDocument(() => { // This will happen before the measurement phase of the update // triggered above. component.pixelPositionForScreenPosition(Point(11, Infinity)); }); await component.getNextUpdatePromise(); } }); it('re-renders lines when their height changes', async () => { const { component, element } = buildComponent({ rowsPerTile: 3, autoHeight: false }); element.style.height = 4 * component.measurements.lineHeight + 'px'; await component.getNextUpdatePromise(); expect(queryOnScreenLineNumberElements(element).length).toBe(9); expect(queryOnScreenLineElements(element).length).toBe(9); element.style.lineHeight = '2.0'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(queryOnScreenLineNumberElements(element).length).toBe(6); expect(queryOnScreenLineElements(element).length).toBe(6); element.style.lineHeight = '0.7'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(queryOnScreenLineNumberElements(element).length).toBe(12); expect(queryOnScreenLineElements(element).length).toBe(12); element.style.lineHeight = '0.05'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(queryOnScreenLineNumberElements(element).length).toBe(13); expect(queryOnScreenLineElements(element).length).toBe(13); element.style.lineHeight = '0'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(queryOnScreenLineNumberElements(element).length).toBe(13); expect(queryOnScreenLineElements(element).length).toBe(13); element.style.lineHeight = '1'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(queryOnScreenLineNumberElements(element).length).toBe(9); expect(queryOnScreenLineElements(element).length).toBe(9); }); it('makes the content at least as tall as the scroll container client height', async () => { const { component, editor } = buildComponent({ text: 'a'.repeat(100), width: 50, height: 100 }); expect(component.refs.content.offsetHeight).toBe( 100 - getHorizontalScrollbarHeight(component) ); editor.setText('a\n'.repeat(30)); await component.getNextUpdatePromise(); expect(component.refs.content.offsetHeight).toBeGreaterThan(100); expect(component.refs.content.offsetHeight).toBeNear( component.getContentHeight(), 2 ); }); it('honors the scrollPastEnd option by adding empty space equivalent to the clientHeight to the end of the content area', async () => { const { component, editor } = buildComponent({ autoHeight: false, autoWidth: false }); await editor.update({ scrollPastEnd: true }); await setEditorHeightInLines(component, 6); // scroll to end await setScrollTop(component, Infinity); expect(component.getFirstVisibleRow()).toBe( editor.getScreenLineCount() - 3 ); editor.update({ scrollPastEnd: false }); await component.getNextUpdatePromise(); // wait for scrollable content resize expect(component.getFirstVisibleRow()).toBe( editor.getScreenLineCount() - 6 ); // Always allows at least 3 lines worth of overscroll if the editor is short await setEditorHeightInLines(component, 2); await editor.update({ scrollPastEnd: true }); await setScrollTop(component, Infinity); expect(component.getFirstVisibleRow()).toBe( editor.getScreenLineCount() + 1 ); }); it('does not fire onDidChangeScrollTop listeners when assigning the same maximal value and the content height has fractional pixels (regression)', async () => { const { component, element, editor } = buildComponent({ autoHeight: false, autoWidth: false }); await setEditorHeightInLines(component, 3); // Force a fractional content height with a block decoration const item = document.createElement('div'); item.style.height = '10.6px'; editor.decorateMarker(editor.markBufferPosition([0, 0]), { type: 'block', item }); await component.getNextUpdatePromise(); component.setScrollTop(Infinity); element.onDidChangeScrollTop(newScrollTop => { throw new Error('Scroll top should not have changed'); }); component.setScrollTop(component.getScrollTop()); }); it('gives the line number tiles an explicit width and height so their layout can be strictly contained', async () => { const { component, editor } = buildComponent({ rowsPerTile: 3 }); const lineNumberGutterElement = component.refs.gutterContainer.refs.lineNumberGutter.element; expect(lineNumberGutterElement.offsetHeight).toBeNear( component.getScrollHeight() ); for (const child of lineNumberGutterElement.children) { expect(child.offsetWidth).toBe(lineNumberGutterElement.offsetWidth); if (!child.classList.contains('line-number')) { for (const lineNumberElement of child.children) { expect(lineNumberElement.offsetWidth).toBe( lineNumberGutterElement.offsetWidth ); } } } editor.setText('x\n'.repeat(99)); await component.getNextUpdatePromise(); expect(lineNumberGutterElement.offsetHeight).toBeNear( component.getScrollHeight() ); for (const child of lineNumberGutterElement.children) { expect(child.offsetWidth).toBe(lineNumberGutterElement.offsetWidth); if (!child.classList.contains('line-number')) { for (const lineNumberElement of child.children) { expect(lineNumberElement.offsetWidth).toBe( lineNumberGutterElement.offsetWidth ); } } } }); it('keeps the number of tiles stable when the visible line count changes during vertical scrolling', async () => { const { component } = buildComponent({ rowsPerTile: 3, autoHeight: false }); await setEditorHeightInLines(component, 5.5); expect(component.refs.lineTiles.children.length).toBe(3 + 2); // account for cursors and highlights containers await setScrollTop(component, 0.5 * component.getLineHeight()); expect(component.refs.lineTiles.children.length).toBe(3 + 2); // account for cursors and highlights containers await setScrollTop(component, 1 * component.getLineHeight()); expect(component.refs.lineTiles.children.length).toBe(3 + 2); // account for cursors and highlights containers }); it('recycles tiles on resize', async () => { const { component } = buildComponent({ rowsPerTile: 2, autoHeight: false }); await setEditorHeightInLines(component, 7); await setScrollTop(component, 3.5 * component.getLineHeight()); const lineNode = lineNodeForScreenRow(component, 7); await setEditorHeightInLines(component, 4); expect(lineNodeForScreenRow(component, 7)).toBe(lineNode); }); it("updates lines numbers when a row's foldability changes (regression)", async () => { const { component, editor } = buildComponent({ text: 'abc\n' }); editor.setCursorBufferPosition([1, 0]); await component.getNextUpdatePromise(); expect( lineNumberNodeForScreenRow(component, 0).querySelector('.foldable') ).toBeNull(); editor.insertText(' def'); await component.getNextUpdatePromise(); expect( lineNumberNodeForScreenRow(component, 0).querySelector('.foldable') ).toBeDefined(); editor.undo(); await component.getNextUpdatePromise(); expect( lineNumberNodeForScreenRow(component, 0).querySelector('.foldable') ).toBeNull(); }); it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => { const { component } = buildComponent({ text: 'abc\n de\nfghijklm\n no', softWrapped: true }); await setEditorWidthInCharacters(component, 5); expect( lineNumberNodeForScreenRow(component, 0).classList.contains('foldable') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('foldable') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 2).classList.contains('foldable') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 3).classList.contains('foldable') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 4).classList.contains('foldable') ).toBe(false); }); it('renders dummy vertical and horizontal scrollbars when content overflows', async () => { const { component, editor } = buildComponent({ height: 100, width: 100 }); const verticalScrollbar = component.refs.verticalScrollbar.element; const horizontalScrollbar = component.refs.horizontalScrollbar.element; expect(verticalScrollbar.scrollHeight).toBeNear( component.getContentHeight() ); expect(horizontalScrollbar.scrollWidth).toBeNear( component.getContentWidth() ); expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0); expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0); expect(verticalScrollbar.style.bottom).toBe( getVerticalScrollbarWidth(component) + 'px' ); expect(verticalScrollbar.style.visibility).toBe(''); expect(horizontalScrollbar.style.right).toBe( getHorizontalScrollbarHeight(component) + 'px' ); expect(horizontalScrollbar.style.visibility).toBe(''); expect(component.refs.scrollbarCorner).toBeDefined(); setScrollTop(component, 100); await setScrollLeft(component, 100); expect(verticalScrollbar.scrollTop).toBe(100); expect(horizontalScrollbar.scrollLeft).toBe(100); verticalScrollbar.scrollTop = 120; horizontalScrollbar.scrollLeft = 120; await component.getNextUpdatePromise(); expect(component.getScrollTop()).toBe(120); expect(component.getScrollLeft()).toBe(120); editor.setText('a\n'.repeat(15)); await component.getNextUpdatePromise(); expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0); expect(getHorizontalScrollbarHeight(component)).toBe(0); expect(verticalScrollbar.style.visibility).toBe(''); expect(horizontalScrollbar.style.visibility).toBe('hidden'); editor.setText('a'.repeat(100)); await component.getNextUpdatePromise(); expect(getVerticalScrollbarWidth(component)).toBe(0); expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0); expect(verticalScrollbar.style.visibility).toBe('hidden'); expect(horizontalScrollbar.style.visibility).toBe(''); editor.setText(''); await component.getNextUpdatePromise(); expect(getVerticalScrollbarWidth(component)).toBe(0); expect(getHorizontalScrollbarHeight(component)).toBe(0); expect(verticalScrollbar.style.visibility).toBe('hidden'); expect(horizontalScrollbar.style.visibility).toBe('hidden'); }); 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(); // Updating scrollbar styles. const style = document.createElement('style'); style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }'; jasmine.attachToDOM(style); TextEditor.didUpdateScrollbarStyles(); await component.getNextUpdatePromise(); expect(getHorizontalScrollbarHeight(component)).toBeNear(10); expect(getVerticalScrollbarWidth(component)).toBeNear(10); expect( component.refs.horizontalScrollbar.element.style.right ).toHaveNearPixels('10px'); expect( component.refs.verticalScrollbar.element.style.bottom ).toHaveNearPixels('10px'); expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeNear( 10 ); expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear(20); expect(component.getScrollContainerClientHeight()).toBeNear(100 - 10); expect(component.getScrollContainerClientWidth()).toBeNear( 100 - component.getGutterContainerWidth() - 10 ); // Detaching and re-attaching the editor element. element.remove(); jasmine.attachToDOM(element); expect(getHorizontalScrollbarHeight(component)).toBeNear(10); expect(getVerticalScrollbarWidth(component)).toBeNear(10); expect( component.refs.horizontalScrollbar.element.style.right ).toHaveNearPixels('10px'); expect( component.refs.verticalScrollbar.element.style.bottom ).toHaveNearPixels('10px'); expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeNear( 10 ); expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear(20); expect(component.getScrollContainerClientHeight()).toBeNear(100 - 10); expect(component.getScrollContainerClientWidth()).toBeNear( 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 () => { const { component, element, editor } = buildComponent({ height: 40, rowsPerTile: 2 }); await setScrollTop(component, 100); expect(component.getRenderedStartRow()).toBe(4); expect(component.getRenderedEndRow()).toBe(10); editor.setCursorScreenPosition([0, 0], { autoscroll: false }); // out of view editor.addCursorAtScreenPosition([2, 2], { autoscroll: false }); // out of view editor.addCursorAtScreenPosition([4, 0], { autoscroll: false }); // line start editor.addCursorAtScreenPosition([4, 4], { autoscroll: false }); // at token boundary editor.addCursorAtScreenPosition([4, 6], { autoscroll: false }); // within token editor.addCursorAtScreenPosition([5, Infinity], { autoscroll: false }); // line end editor.addCursorAtScreenPosition([10, 2], { autoscroll: false }); // out of view await component.getNextUpdatePromise(); let cursorNodes = Array.from(element.querySelectorAll('.cursor')); expect(cursorNodes.length).toBe(4); verifyCursorPosition(component, cursorNodes[0], 4, 0); verifyCursorPosition(component, cursorNodes[1], 4, 4); verifyCursorPosition(component, cursorNodes[2], 4, 6); verifyCursorPosition(component, cursorNodes[3], 5, 30); editor.setCursorScreenPosition([8, 11], { autoscroll: false }); await component.getNextUpdatePromise(); cursorNodes = Array.from(element.querySelectorAll('.cursor')); expect(cursorNodes.length).toBe(1); verifyCursorPosition(component, cursorNodes[0], 8, 11); editor.setCursorScreenPosition([0, 0], { autoscroll: false }); await component.getNextUpdatePromise(); cursorNodes = Array.from(element.querySelectorAll('.cursor')); expect(cursorNodes.length).toBe(0); editor.setSelectedScreenRange([[8, 0], [12, 0]], { autoscroll: false }); await component.getNextUpdatePromise(); cursorNodes = Array.from(element.querySelectorAll('.cursor')); expect(cursorNodes.length).toBe(0); }); it('hides cursors with non-empty selections when showCursorOnSelection is false', async () => { const { component, element, editor } = buildComponent(); editor.setSelectedScreenRanges([[[0, 0], [0, 3]], [[1, 0], [1, 0]]]); await component.getNextUpdatePromise(); { const cursorNodes = Array.from(element.querySelectorAll('.cursor')); expect(cursorNodes.length).toBe(2); verifyCursorPosition(component, cursorNodes[0], 0, 3); verifyCursorPosition(component, cursorNodes[1], 1, 0); } editor.update({ showCursorOnSelection: false }); await component.getNextUpdatePromise(); { const cursorNodes = Array.from(element.querySelectorAll('.cursor')); expect(cursorNodes.length).toBe(1); verifyCursorPosition(component, cursorNodes[0], 1, 0); } editor.setSelectedScreenRanges([[[0, 0], [0, 3]], [[1, 0], [1, 4]]]); await component.getNextUpdatePromise(); { const cursorNodes = Array.from(element.querySelectorAll('.cursor')); expect(cursorNodes.length).toBe(0); } }); it('blinks cursors when the editor is focused and the cursors are not moving', async () => { assertDocumentFocused(); const { component, element, editor } = buildComponent(); component.props.cursorBlinkPeriod = 30; component.props.cursorBlinkResumeDelay = 30; editor.addCursorAtScreenPosition([1, 0]); element.focus(); await component.getNextUpdatePromise(); const [cursor1, cursor2] = element.querySelectorAll('.cursor'); await conditionPromise( () => getComputedStyle(cursor1).opacity === '1' && getComputedStyle(cursor2).opacity === '1' ); await conditionPromise( () => getComputedStyle(cursor1).opacity === '0' && getComputedStyle(cursor2).opacity === '0' ); await conditionPromise( () => getComputedStyle(cursor1).opacity === '1' && getComputedStyle(cursor2).opacity === '1' ); editor.moveRight(); await component.getNextUpdatePromise(); expect(getComputedStyle(cursor1).opacity).toBe('1'); expect(getComputedStyle(cursor2).opacity).toBe('1'); }); it('gives cursors at the end of lines the width of an "x" character', async () => { const { component, element, editor } = buildComponent(); editor.setText('abcde'); await setEditorWidthInCharacters(component, 5.5); editor.setCursorScreenPosition([0, Infinity]); await component.getNextUpdatePromise(); expect(element.querySelector('.cursor').offsetWidth).toBe( Math.round(component.getBaseCharacterWidth()) ); // Clip cursor width when soft-wrap is on and the cursor is at the end of // the line. This prevents the parent tile from disabling sub-pixel // anti-aliasing. For some reason, adding overflow: hidden to the cursor // container doesn't solve this issue so we're adding this workaround instead. editor.setSoftWrapped(true); await component.getNextUpdatePromise(); expect(element.querySelector('.cursor').offsetWidth).toBeLessThan( Math.round(component.getBaseCharacterWidth()) ); }); it('positions and sizes cursors correctly when they are located next to a fold marker', async () => { const { component, element, editor } = buildComponent(); editor.foldBufferRange([[0, 3], [0, 6]]); editor.setCursorScreenPosition([0, 3]); await component.getNextUpdatePromise(); verifyCursorPosition(component, element.querySelector('.cursor'), 0, 3); editor.setCursorScreenPosition([0, 4]); await component.getNextUpdatePromise(); verifyCursorPosition(component, element.querySelector('.cursor'), 0, 4); }); it('positions cursors and placeholder text correctly when the lines container has a margin and/or is padded', async () => { const { component, element, editor } = buildComponent({ placeholderText: 'testing' }); component.refs.lineTiles.style.marginLeft = '10px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); editor.setCursorBufferPosition([0, 3]); await component.getNextUpdatePromise(); verifyCursorPosition(component, element.querySelector('.cursor'), 0, 3); editor.setCursorScreenPosition([1, 0]); await component.getNextUpdatePromise(); verifyCursorPosition(component, element.querySelector('.cursor'), 1, 0); component.refs.lineTiles.style.paddingTop = '5px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); verifyCursorPosition(component, element.querySelector('.cursor'), 1, 0); editor.setCursorScreenPosition([2, 2]); TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); verifyCursorPosition(component, element.querySelector('.cursor'), 2, 2); editor.setText(''); await component.getNextUpdatePromise(); const placeholderTextLeft = element .querySelector('.placeholder-text') .getBoundingClientRect().left; const linesLeft = component.refs.lineTiles.getBoundingClientRect().left; expect(placeholderTextLeft).toBe(linesLeft); }); it('places the hidden input element at the location of the last cursor if it is visible', async () => { const { component, editor } = buildComponent({ height: 60, width: 120, rowsPerTile: 2 }); const { hiddenInput } = component.refs.cursorsAndInput.refs; setScrollTop(component, 100); await setScrollLeft(component, 40); expect(component.getRenderedStartRow()).toBe(4); expect(component.getRenderedEndRow()).toBe(10); // When out of view, the hidden input is positioned at 0, 0 expect(editor.getCursorScreenPosition()).toEqual([0, 0]); expect(hiddenInput.offsetTop).toBe(0); expect(hiddenInput.offsetLeft).toBe(0); // Otherwise it is positioned at the last cursor position editor.addCursorAtScreenPosition([7, 4]); await component.getNextUpdatePromise(); expect(hiddenInput.getBoundingClientRect().top).toBe( clientTopForLine(component, 7) ); expect(Math.round(hiddenInput.getBoundingClientRect().left)).toBeNear( clientLeftForCharacter(component, 7, 4) ); }); it('soft wraps lines based on the content width when soft wrap is enabled', async () => { let baseCharacterWidth, gutterContainerWidth; { const { component, editor } = buildComponent(); baseCharacterWidth = component.getBaseCharacterWidth(); gutterContainerWidth = component.getGutterContainerWidth(); editor.destroy(); } const { component, element, editor } = buildComponent({ width: gutterContainerWidth + baseCharacterWidth * 55, attach: false }); editor.setSoftWrapped(true); jasmine.attachToDOM(element); expect(getEditorWidthInBaseCharacters(component)).toBe(55); expect(lineNodeForScreenRow(component, 3).textContent).toBe( ' var pivot = items.shift(), current, left = [], ' ); expect(lineNodeForScreenRow(component, 4).textContent).toBe( ' right = [];' ); const { scrollContainer } = component.refs; expect(scrollContainer.clientWidth).toBe(scrollContainer.scrollWidth); }); it('correctly forces the display layer to index visible rows when resizing (regression)', async () => { const text = 'a'.repeat(30) + '\n' + 'b'.repeat(1000); const { component, element, editor } = buildComponent({ height: 300, width: 800, attach: false, text }); editor.setSoftWrapped(true); jasmine.attachToDOM(element); element.style.width = 200 + 'px'; await component.getNextUpdatePromise(); expect(queryOnScreenLineElements(element).length).toBe(24); }); it('decorates the line numbers of folded lines', async () => { const { component, editor } = buildComponent(); editor.foldBufferRow(1); await component.getNextUpdatePromise(); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('folded') ).toBe(true); }); it('makes lines at least as wide as the scrollContainer', async () => { const { component, element, editor } = buildComponent(); const { scrollContainer } = component.refs; editor.setText('a'); await component.getNextUpdatePromise(); expect(element.querySelector('.line').offsetWidth).toBe( scrollContainer.offsetWidth - verticalScrollbarWidth ); }); it('resizes based on the content when the autoHeight and/or autoWidth options are true', async () => { const { component, element, editor } = buildComponent({ autoHeight: true, autoWidth: true }); const editorPadding = 3; element.style.padding = editorPadding + 'px'; const initialWidth = element.offsetWidth; const initialHeight = element.offsetHeight; expect(initialWidth).toBe( component.getGutterContainerWidth() + component.getContentWidth() + verticalScrollbarWidth + 2 * editorPadding ); expect(initialHeight).toBeNear( component.getContentHeight() + horizontalScrollbarHeight + 2 * editorPadding ); // When autoWidth is enabled, width adjusts to content editor.setCursorScreenPosition([6, Infinity]); editor.insertText('x'.repeat(50)); await component.getNextUpdatePromise(); expect(element.offsetWidth).toBe( component.getGutterContainerWidth() + component.getContentWidth() + verticalScrollbarWidth + 2 * editorPadding ); expect(element.offsetWidth).toBeGreaterThan(initialWidth); // When autoHeight is enabled, height adjusts to content editor.insertText('\n'.repeat(5)); await component.getNextUpdatePromise(); expect(element.offsetHeight).toBeNear( component.getContentHeight() + horizontalScrollbarHeight + 2 * editorPadding ); expect(element.offsetHeight).toBeGreaterThan(initialHeight); }); it('does not render the line number gutter at all if the isLineNumberGutterVisible parameter is false', () => { const { element } = buildComponent({ lineNumberGutterVisible: false }); expect(element.querySelector('.line-number')).toBe(null); }); it('does not render the line numbers but still renders the line number gutter if showLineNumbers is false', async () => { function checkScrollContainerLeft(component) { const { scrollContainer, gutterContainer } = component.refs; expect(scrollContainer.getBoundingClientRect().left).toBeNear( Math.round(gutterContainer.element.getBoundingClientRect().right) ); } const { component, element, editor } = buildComponent({ showLineNumbers: false }); expect( Array.from(element.querySelectorAll('.line-number')).every( e => e.textContent === '' ) ).toBe(true); checkScrollContainerLeft(component); await editor.update({ showLineNumbers: true }); expect( Array.from(element.querySelectorAll('.line-number')).map( e => e.textContent ) ).toEqual([ '00', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13' ]); checkScrollContainerLeft(component); await editor.update({ showLineNumbers: false }); expect( Array.from(element.querySelectorAll('.line-number')).every( e => e.textContent === '' ) ).toBe(true); checkScrollContainerLeft(component); }); it('supports the placeholderText parameter', () => { const placeholderText = 'Placeholder Test'; const { element } = buildComponent({ placeholderText, text: '' }); expect(element.textContent).toContain(placeholderText); }); it('adds the data-grammar attribute and updates it when the grammar changes', async () => { await atom.packages.activatePackage('language-javascript'); const { editor, element, component } = buildComponent(); expect(element.dataset.grammar).toBe('text plain null-grammar'); atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.js'); await component.getNextUpdatePromise(); expect(element.dataset.grammar).toBe('source js'); }); it('adds the data-encoding attribute and updates it when the encoding changes', async () => { const { editor, element, component } = buildComponent(); expect(element.dataset.encoding).toBe('utf8'); editor.setEncoding('ascii'); await component.getNextUpdatePromise(); expect(element.dataset.encoding).toBe('ascii'); }); it('adds the has-selection class when the editor has a non-empty selection', async () => { const { editor, element, component } = buildComponent(); expect(element.classList.contains('has-selection')).toBe(false); editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 10]]]); await component.getNextUpdatePromise(); expect(element.classList.contains('has-selection')).toBe(true); editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]); await component.getNextUpdatePromise(); expect(element.classList.contains('has-selection')).toBe(false); }); it('assigns buffer-row and screen-row to each line number as data fields', async () => { const { editor, element, component } = buildComponent(); editor.setSoftWrapped(true); await component.getNextUpdatePromise(); await setEditorWidthInCharacters(component, 40); { const bufferRows = queryOnScreenLineNumberElements(element).map( e => e.dataset.bufferRow ); const screenRows = queryOnScreenLineNumberElements(element).map( e => e.dataset.screenRow ); expect(bufferRows).toEqual([ '0', '1', '2', '2', '3', '3', '4', '5', '6', '6', '6', '7', '8', '8', '8', '9', '10', '11', '11', '12' ]); expect(screenRows).toEqual([ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19' ]); } editor.getBuffer().insert([2, 0], '\n'); await component.getNextUpdatePromise(); { const bufferRows = queryOnScreenLineNumberElements(element).map( e => e.dataset.bufferRow ); const screenRows = queryOnScreenLineNumberElements(element).map( e => e.dataset.screenRow ); expect(bufferRows).toEqual([ '0', '1', '2', '3', '3', '4', '4', '5', '6', '7', '7', '7', '8', '9', '9', '9', '10', '11', '12', '12', '13' ]); expect(screenRows).toEqual([ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20' ]); } }); it('does not blow away class names added to the element by packages when changing the class name', async () => { assertDocumentFocused(); const { component, element } = buildComponent(); element.classList.add('a', 'b'); expect(element.className).toBe('editor a b'); element.focus(); await component.getNextUpdatePromise(); expect(element.className).toBe('editor a b is-focused'); document.body.focus(); await component.getNextUpdatePromise(); expect(element.className).toBe('editor a b'); }); it('does not blow away class names managed by the component when packages change the element class name', async () => { assertDocumentFocused(); const { component, element } = buildComponent({ mini: true }); element.classList.add('a', 'b'); element.focus(); await component.getNextUpdatePromise(); expect(element.className).toBe('editor mini a b is-focused'); element.className = 'a c d'; await component.getNextUpdatePromise(); expect(element.className).toBe('a c d editor is-focused mini'); }); it('ignores resize events when the editor is hidden', async () => { const { component, element } = buildComponent({ autoHeight: false }); element.style.height = 5 * component.getLineHeight() + 'px'; await component.getNextUpdatePromise(); const originalClientContainerHeight = component.getClientContainerHeight(); const originalGutterContainerWidth = component.getGutterContainerWidth(); const originalLineNumberGutterWidth = component.getLineNumberGutterWidth(); expect(originalClientContainerHeight).toBeGreaterThan(0); expect(originalGutterContainerWidth).toBeGreaterThan(0); expect(originalLineNumberGutterWidth).toBeGreaterThan(0); element.style.display = 'none'; // In production, resize events are triggered before the intersection // observer detects the editor's visibility has changed. In tests, we are // unable to reproduce this scenario and so we simulate them. expect(component.visible).toBe(true); component.didResize(); component.didResizeGutterContainer(); expect(component.getClientContainerHeight()).toBe( originalClientContainerHeight ); expect(component.getGutterContainerWidth()).toBe( originalGutterContainerWidth ); expect(component.getLineNumberGutterWidth()).toBe( originalLineNumberGutterWidth ); // Ensure measurements stay the same after receiving the intersection // observer events. await conditionPromise(() => !component.visible); expect(component.getClientContainerHeight()).toBe( originalClientContainerHeight ); 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 = 1520247533732 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.style.fontSize = random(20) + 'px'; element.style.lineHeight = random.floatBetween(0.1, 2.0); TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); 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', () => { it('adds the mini attribute and class even when the element is not attached', () => { { const { element } = buildComponent({ mini: true }); expect(element.hasAttribute('mini')).toBe(true); expect(element.classList.contains('mini')).toBe(true); } { const { element } = buildComponent({ mini: true, attach: false }); expect(element.hasAttribute('mini')).toBe(true); expect(element.classList.contains('mini')).toBe(true); } }); it('does not render the gutter container', () => { const { component, element } = buildComponent({ mini: true }); expect(component.refs.gutterContainer).toBeUndefined(); expect(element.querySelector('gutter-container')).toBeNull(); }); it('does not render line decorations for the cursor line', async () => { const { component, element, editor } = buildComponent({ mini: true }); expect( element.querySelector('.line').classList.contains('cursor-line') ).toBe(false); editor.update({ mini: false }); await component.getNextUpdatePromise(); expect( element.querySelector('.line').classList.contains('cursor-line') ).toBe(true); editor.update({ mini: true }); await component.getNextUpdatePromise(); expect( element.querySelector('.line').classList.contains('cursor-line') ).toBe(false); }); it('does not render scrollbars', async () => { const { component, editor } = buildComponent({ mini: true, autoHeight: false }); await setEditorWidthInCharacters(component, 10); editor.setText('x'.repeat(20) + 'y'.repeat(20)); await component.getNextUpdatePromise(); expect(component.canScrollVertically()).toBe(false); expect(component.canScrollHorizontally()).toBe(false); expect(component.refs.horizontalScrollbar).toBeUndefined(); expect(component.refs.verticalScrollbar).toBeUndefined(); }); }); describe('focus', () => { beforeEach(() => { assertDocumentFocused(); }); it('focuses the hidden input element and adds the is-focused class when focused', async () => { const { component, element } = buildComponent(); const { hiddenInput } = component.refs.cursorsAndInput.refs; expect(document.activeElement).not.toBe(hiddenInput); element.focus(); expect(document.activeElement).toBe(hiddenInput); await component.getNextUpdatePromise(); expect(element.classList.contains('is-focused')).toBe(true); element.focus(); // focusing back to the element does not blur expect(document.activeElement).toBe(hiddenInput); expect(element.classList.contains('is-focused')).toBe(true); document.body.focus(); expect(document.activeElement).not.toBe(hiddenInput); await component.getNextUpdatePromise(); expect(element.classList.contains('is-focused')).toBe(false); }); it('updates the component when the hidden input is focused directly', async () => { const { component, element } = buildComponent(); const { hiddenInput } = component.refs.cursorsAndInput.refs; expect(element.classList.contains('is-focused')).toBe(false); expect(document.activeElement).not.toBe(hiddenInput); hiddenInput.focus(); await component.getNextUpdatePromise(); expect(element.classList.contains('is-focused')).toBe(true); }); it('gracefully handles a focus event that occurs prior to the attachedCallback of the element', () => { const { component, element } = buildComponent({ attach: false }); const parent = document.createElement( 'text-editor-component-test-element' ); parent.appendChild(element); parent.didAttach = () => element.focus(); jasmine.attachToDOM(parent); expect(document.activeElement).toBe( component.refs.cursorsAndInput.refs.hiddenInput ); }); it('gracefully handles a focus event that occurs prior to detecting the element has become visible', async () => { const { component, element } = buildComponent({ attach: false }); element.style.display = 'none'; jasmine.attachToDOM(element); element.style.display = 'block'; element.focus(); await component.getNextUpdatePromise(); expect(document.activeElement).toBe( component.refs.cursorsAndInput.refs.hiddenInput ); }); it('emits blur events only when focus shifts to something other than the editor itself or its hidden input', () => { const { element } = buildComponent(); let blurEventCount = 0; element.addEventListener('blur', () => blurEventCount++); element.focus(); expect(blurEventCount).toBe(0); element.focus(); expect(blurEventCount).toBe(0); document.body.focus(); expect(blurEventCount).toBe(1); }); }); describe('autoscroll', () => { it('automatically scrolls vertically when the requested range is within the vertical scroll margin of the top or bottom', async () => { const { component, editor } = buildComponent({ height: 120 + horizontalScrollbarHeight }); expect(component.getLastVisibleRow()).toBe(7); editor.scrollToScreenRange([[4, 0], [6, 0]]); await component.getNextUpdatePromise(); expect(component.getScrollBottom()).toBeNear( (6 + 1 + editor.verticalScrollMargin) * component.getLineHeight() ); editor.scrollToScreenPosition([8, 0]); await component.getNextUpdatePromise(); expect(component.getScrollBottom()).toBeNear( (8 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight ); editor.scrollToScreenPosition([3, 0]); await component.getNextUpdatePromise(); expect(component.getScrollTop()).toBeNear( (3 - editor.verticalScrollMargin) * component.measurements.lineHeight ); editor.scrollToScreenPosition([2, 0]); await component.getNextUpdatePromise(); expect(component.getScrollTop()).toBe(0); }); it('does not vertically autoscroll by more than half of the visible lines if the editor is shorter than twice the scroll margin', async () => { const { component, element, editor } = buildComponent({ autoHeight: false }); element.style.height = 5.5 * component.measurements.lineHeight + horizontalScrollbarHeight + 'px'; await component.getNextUpdatePromise(); expect(component.getLastVisibleRow()).toBe(5); const scrollMarginInLines = 2; editor.scrollToScreenPosition([6, 0]); await component.getNextUpdatePromise(); expect(component.getScrollBottom()).toBeNear( (6 + 1 + scrollMarginInLines) * component.measurements.lineHeight ); editor.scrollToScreenPosition([6, 4]); await component.getNextUpdatePromise(); expect(component.getScrollBottom()).toBeNear( (6 + 1 + scrollMarginInLines) * component.measurements.lineHeight ); editor.scrollToScreenRange([[4, 4], [6, 4]]); await component.getNextUpdatePromise(); expect(component.getScrollTop()).toBeNear( (4 - scrollMarginInLines) * component.measurements.lineHeight ); editor.scrollToScreenRange([[4, 4], [6, 4]], { reversed: false }); await component.getNextUpdatePromise(); expect(component.getScrollBottom()).toBeNear( (6 + 1 + scrollMarginInLines) * component.measurements.lineHeight ); }); it('autoscrolls the given range to the center of the screen if the `center` option is true', async () => { const { component, editor } = buildComponent({ height: 50 }); expect(component.getLastVisibleRow()).toBe(2); editor.scrollToScreenRange([[4, 0], [6, 0]], { center: true }); await component.getNextUpdatePromise(); const actualScrollCenter = (component.getScrollTop() + component.getScrollBottom()) / 2; const expectedScrollCenter = ((4 + 7) / 2) * component.getLineHeight(); expect(actualScrollCenter).toBeCloseTo(expectedScrollCenter, 0); }); it('automatically scrolls horizontally when the requested range is within the horizontal scroll margin of the right edge of the gutter or right edge of the scroll container', async () => { const { component, element, editor } = buildComponent(); element.style.width = component.getGutterContainerWidth() + 3 * editor.horizontalScrollMargin * component.measurements.baseCharacterWidth + 'px'; await component.getNextUpdatePromise(); editor.scrollToScreenRange([[1, 12], [2, 28]]); await component.getNextUpdatePromise(); let expectedScrollLeft = clientLeftForCharacter(component, 1, 12) - lineNodeForScreenRow(component, 1).getBoundingClientRect().left - editor.horizontalScrollMargin * component.measurements.baseCharacterWidth; expect(component.getScrollLeft()).toBeNear(expectedScrollLeft); editor.scrollToScreenRange([[1, 12], [2, 28]], { reversed: false }); await component.getNextUpdatePromise(); expectedScrollLeft = component.getGutterContainerWidth() + clientLeftForCharacter(component, 2, 28) - lineNodeForScreenRow(component, 2).getBoundingClientRect().left + editor.horizontalScrollMargin * component.measurements.baseCharacterWidth - component.getScrollContainerClientWidth(); expect(component.getScrollLeft()).toBeNear(expectedScrollLeft); }); it('does not horizontally autoscroll by more than half of the visible "base-width" characters if the editor is narrower than twice the scroll margin', async () => { const { component, editor } = buildComponent({ autoHeight: false }); await setEditorWidthInCharacters( component, 1.5 * editor.horizontalScrollMargin ); const editorWidthInChars = component.getScrollContainerClientWidth() / component.getBaseCharacterWidth(); expect(Math.round(editorWidthInChars)).toBe(9); editor.scrollToScreenRange([[6, 10], [6, 15]]); await component.getNextUpdatePromise(); let expectedScrollLeft = Math.floor( clientLeftForCharacter(component, 6, 10) - lineNodeForScreenRow(component, 1).getBoundingClientRect().left - Math.floor((editorWidthInChars - 1) / 2) * component.getBaseCharacterWidth() ); expect(component.getScrollLeft()).toBeNear(expectedScrollLeft); }); it('correctly autoscrolls after inserting a line that exceeds the current content width', async () => { const { component, element, editor } = buildComponent(); element.style.width = component.getGutterContainerWidth() + component.getContentWidth() + 'px'; await component.getNextUpdatePromise(); editor.setCursorScreenPosition([0, Infinity]); editor.insertText('x'.repeat(100)); await component.getNextUpdatePromise(); expect(component.getScrollLeft()).toBeNear( component.getScrollWidth() - component.getScrollContainerClientWidth() ); }); it('does not try to measure lines that do not exist when the animation frame is delivered', async () => { const { component, editor } = buildComponent({ autoHeight: false, height: 30, rowsPerTile: 2 }); editor.scrollToBufferPosition([11, 5]); editor.getBuffer().deleteRows(11, 12); await component.getNextUpdatePromise(); expect(component.getScrollBottom()).toBeNear( (10 + 1) * component.measurements.lineHeight ); }); it('accounts for the presence of horizontal scrollbars that appear during the same frame as the autoscroll', async () => { const { component, element, editor } = buildComponent({ autoHeight: false }); element.style.height = component.getContentHeight() / 2 + 'px'; element.style.width = component.getScrollWidth() + 'px'; await component.getNextUpdatePromise(); editor.setCursorScreenPosition([10, Infinity]); editor.insertText('\n\n' + 'x'.repeat(100)); await component.getNextUpdatePromise(); expect(component.getScrollTop()).toBeNear( component.getScrollHeight() - component.getScrollContainerClientHeight() ); expect(component.getScrollLeft()).toBeNear( component.getScrollWidth() - component.getScrollContainerClientWidth() ); // Scrolling to the top should not throw an error. This failed // previously due to horizontalPositionsToMeasure not being empty after // autoscrolling vertically to account for the horizontal scrollbar. spyOn(window, 'onerror'); await setScrollTop(component, 0); expect(window.onerror).not.toHaveBeenCalled(); }); }); describe('logical scroll positions', () => { it('allows the scrollTop to be changed and queried in terms of rows via setScrollTopRow and getScrollTopRow', () => { const { component, element } = buildComponent({ attach: false, height: 80 }); // Caches the scrollTopRow if we don't have measurements component.setScrollTopRow(6); expect(component.getScrollTopRow()).toBe(6); // Assigns the scrollTop based on the logical position when attached jasmine.attachToDOM(element); const expectedScrollTop = Math.round(6 * component.getLineHeight()); expect(component.getScrollTopRow()).toBeNear(6); expect(component.getScrollTop()).toBeNear(expectedScrollTop); expect(component.refs.content.style.transform).toBe( `translate(0px, -${expectedScrollTop}px)` ); // Allows the scrollTopRow to be updated while attached component.setScrollTopRow(4); expect(component.getScrollTopRow()).toBeNear(4); expect(component.getScrollTop()).toBeNear( Math.round(4 * component.getLineHeight()) ); // Preserves the scrollTopRow when detached element.remove(); expect(component.getScrollTopRow()).toBeNear(4); expect(component.getScrollTop()).toBeNear( Math.round(4 * component.getLineHeight()) ); component.setScrollTopRow(6); expect(component.getScrollTopRow()).toBeNear(6); expect(component.getScrollTop()).toBeNear( Math.round(6 * component.getLineHeight()) ); jasmine.attachToDOM(element); element.style.height = '60px'; expect(component.getScrollTopRow()).toBeNear(6); expect(component.getScrollTop()).toBeNear( Math.round(6 * component.getLineHeight()) ); }); it('allows the scrollLeft to be changed and queried in terms of base character columns via setScrollLeftColumn and getScrollLeftColumn', () => { const { component, element } = buildComponent({ attach: false, width: 80 }); // Caches the scrollTopRow if we don't have measurements component.setScrollLeftColumn(2); expect(component.getScrollLeftColumn()).toBe(2); // Assigns the scrollTop based on the logical position when attached jasmine.attachToDOM(element); expect(component.getScrollLeft()).toBeCloseTo( 2 * component.getBaseCharacterWidth(), 0 ); // Allows the scrollTopRow to be updated while attached component.setScrollLeftColumn(4); expect(component.getScrollLeft()).toBeCloseTo( 4 * component.getBaseCharacterWidth(), 0 ); // Preserves the scrollTopRow when detached element.remove(); expect(component.getScrollLeft()).toBeCloseTo( 4 * component.getBaseCharacterWidth(), 0 ); component.setScrollLeftColumn(6); expect(component.getScrollLeft()).toBeCloseTo( 6 * component.getBaseCharacterWidth(), 0 ); jasmine.attachToDOM(element); element.style.width = '60px'; expect(component.getScrollLeft()).toBeCloseTo( 6 * component.getBaseCharacterWidth(), 0 ); }); }); describe('scrolling via the mouse wheel', () => { it('scrolls vertically or horizontally depending on whether deltaX or deltaY is larger', () => { const scrollSensitivity = 30; const { component } = buildComponent({ height: 50, width: 50, scrollSensitivity }); // stub in place for Event.preventDefault() const eventPreventDefaultStub = function() {}; { const expectedScrollTop = 20 * (scrollSensitivity / 100); const expectedScrollLeft = component.getScrollLeft(); component.didMouseWheel({ wheelDeltaX: -5, wheelDeltaY: -20, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBeNear(expectedScrollTop); expect(component.getScrollLeft()).toBeNear(expectedScrollLeft); expect(component.refs.content.style.transform).toBe( `translate(${-expectedScrollLeft}px, ${-expectedScrollTop}px)` ); } { const expectedScrollTop = component.getScrollTop() - 10 * (scrollSensitivity / 100); const expectedScrollLeft = component.getScrollLeft(); component.didMouseWheel({ wheelDeltaX: -5, wheelDeltaY: 10, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBeNear(expectedScrollTop); expect(component.getScrollLeft()).toBeNear(expectedScrollLeft); expect(component.refs.content.style.transform).toBe( `translate(${-expectedScrollLeft}px, ${-expectedScrollTop}px)` ); } { const expectedScrollTop = component.getScrollTop(); const expectedScrollLeft = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: -20, wheelDeltaY: 10, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBeNear(expectedScrollTop); expect(component.getScrollLeft()).toBeNear(expectedScrollLeft); expect(component.refs.content.style.transform).toBe( `translate(${-expectedScrollLeft}px, ${-expectedScrollTop}px)` ); } { const expectedScrollTop = component.getScrollTop(); const expectedScrollLeft = component.getScrollLeft() - 10 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: 10, wheelDeltaY: -8, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBeNear(expectedScrollTop); expect(component.getScrollLeft()).toBeNear(expectedScrollLeft); expect(component.refs.content.style.transform).toBe( `translate(${-expectedScrollLeft}px, ${-expectedScrollTop}px)` ); } }); it('inverts deltaX and deltaY when holding shift on Windows and Linux', async () => { const scrollSensitivity = 50; const { component } = buildComponent({ height: 50, width: 50, scrollSensitivity }); // stub in place for Event.preventDefault() const eventPreventDefaultStub = function() {}; component.props.platform = 'linux'; { const expectedScrollTop = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: 0, wheelDeltaY: -20, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBeNear(expectedScrollTop); expect(component.refs.content.style.transform).toBe( `translate(0px, -${expectedScrollTop}px)` ); await setScrollTop(component, 0); } { const expectedScrollLeft = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: 0, wheelDeltaY: -20, shiftKey: true, preventDefault: eventPreventDefaultStub }); expect(component.getScrollLeft()).toBeNear(expectedScrollLeft); expect(component.refs.content.style.transform).toBe( `translate(-${expectedScrollLeft}px, 0px)` ); await setScrollLeft(component, 0); } { const expectedScrollTop = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: -20, wheelDeltaY: 0, shiftKey: true, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBe(expectedScrollTop); expect(component.refs.content.style.transform).toBe( `translate(0px, -${expectedScrollTop}px)` ); await setScrollTop(component, 0); } component.props.platform = 'win32'; { const expectedScrollTop = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: 0, wheelDeltaY: -20, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBe(expectedScrollTop); expect(component.refs.content.style.transform).toBe( `translate(0px, -${expectedScrollTop}px)` ); await setScrollTop(component, 0); } { const expectedScrollLeft = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: 0, wheelDeltaY: -20, shiftKey: true, preventDefault: eventPreventDefaultStub }); expect(component.getScrollLeft()).toBe(expectedScrollLeft); expect(component.refs.content.style.transform).toBe( `translate(-${expectedScrollLeft}px, 0px)` ); await setScrollLeft(component, 0); } { const expectedScrollTop = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: -20, wheelDeltaY: 0, shiftKey: true, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBe(expectedScrollTop); expect(component.refs.content.style.transform).toBe( `translate(0px, -${expectedScrollTop}px)` ); await setScrollTop(component, 0); } component.props.platform = 'darwin'; { const expectedScrollTop = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: 0, wheelDeltaY: -20, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBe(expectedScrollTop); expect(component.refs.content.style.transform).toBe( `translate(0px, -${expectedScrollTop}px)` ); await setScrollTop(component, 0); } { const expectedScrollTop = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: 0, wheelDeltaY: -20, shiftKey: true, preventDefault: eventPreventDefaultStub }); expect(component.getScrollTop()).toBe(expectedScrollTop); expect(component.refs.content.style.transform).toBe( `translate(0px, -${expectedScrollTop}px)` ); await setScrollTop(component, 0); } { const expectedScrollLeft = 20 * (scrollSensitivity / 100); component.didMouseWheel({ wheelDeltaX: -20, wheelDeltaY: 0, shiftKey: true, preventDefault: eventPreventDefaultStub }); expect(component.getScrollLeft()).toBe(expectedScrollLeft); expect(component.refs.content.style.transform).toBe( `translate(-${expectedScrollLeft}px, 0px)` ); await setScrollLeft(component, 0); } }); }); describe('scrolling via the API', () => { it('ignores scroll requests to NaN, null or undefined positions', async () => { const { component } = buildComponent({ rowsPerTile: 2, autoHeight: false }); await setEditorHeightInLines(component, 3); await setEditorWidthInCharacters(component, 10); const initialScrollTop = Math.round(2 * component.getLineHeight()); const initialScrollLeft = Math.round( 5 * component.getBaseCharacterWidth() ); setScrollTop(component, initialScrollTop); setScrollLeft(component, initialScrollLeft); await component.getNextUpdatePromise(); setScrollTop(component, NaN); setScrollLeft(component, NaN); await component.getNextUpdatePromise(); expect(component.getScrollTop()).toBeNear(initialScrollTop); expect(component.getScrollLeft()).toBeNear(initialScrollLeft); setScrollTop(component, null); setScrollLeft(component, null); await component.getNextUpdatePromise(); expect(component.getScrollTop()).toBeNear(initialScrollTop); expect(component.getScrollLeft()).toBeNear(initialScrollLeft); setScrollTop(component, undefined); setScrollLeft(component, undefined); await component.getNextUpdatePromise(); expect(component.getScrollTop()).toBeNear(initialScrollTop); expect(component.getScrollLeft()).toBeNear(initialScrollLeft); }); }); describe('line and line number decorations', () => { it('adds decoration classes on screen lines spanned by decorated markers', async () => { const { component, editor } = buildComponent({ softWrapped: true }); await setEditorWidthInCharacters(component, 55); expect(lineNodeForScreenRow(component, 3).textContent).toBe( ' var pivot = items.shift(), current, left = [], ' ); expect(lineNodeForScreenRow(component, 4).textContent).toBe( ' right = [];' ); const marker1 = editor.markScreenRange([[1, 10], [3, 10]]); const layer = editor.addMarkerLayer(); layer.markScreenPosition([5, 0]); layer.markScreenPosition([8, 0]); const marker4 = layer.markScreenPosition([10, 0]); editor.decorateMarker(marker1, { type: ['line', 'line-number'], class: 'a' }); const layerDecoration = editor.decorateMarkerLayer(layer, { type: ['line', 'line-number'], class: 'b' }); layerDecoration.setPropertiesForMarker(marker4, { type: 'line', class: 'c' }); await component.getNextUpdatePromise(); expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 4).classList.contains('a')).toBe( false ); expect(lineNodeForScreenRow(component, 5).classList.contains('b')).toBe( true ); expect(lineNodeForScreenRow(component, 8).classList.contains('b')).toBe( true ); expect(lineNodeForScreenRow(component, 10).classList.contains('b')).toBe( false ); expect(lineNodeForScreenRow(component, 10).classList.contains('c')).toBe( true ); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('a') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 2).classList.contains('a') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 3).classList.contains('a') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 4).classList.contains('a') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 5).classList.contains('b') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 8).classList.contains('b') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 10).classList.contains('b') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 10).classList.contains('c') ).toBe(false); marker1.setScreenRange([[5, 0], [8, 0]]); await component.getNextUpdatePromise(); expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe( false ); expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe( false ); expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe( false ); expect(lineNodeForScreenRow(component, 4).classList.contains('a')).toBe( false ); expect(lineNodeForScreenRow(component, 5).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 5).classList.contains('b')).toBe( true ); expect(lineNodeForScreenRow(component, 6).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 7).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 8).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 8).classList.contains('b')).toBe( true ); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('a') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 2).classList.contains('a') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 3).classList.contains('a') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 4).classList.contains('a') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 5).classList.contains('a') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 5).classList.contains('b') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 6).classList.contains('a') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 7).classList.contains('a') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 8).classList.contains('a') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 8).classList.contains('b') ).toBe(true); }); it('honors the onlyEmpty and onlyNonEmpty decoration options', async () => { const { component, editor } = buildComponent(); const marker = editor.markScreenPosition([1, 0]); editor.decorateMarker(marker, { type: ['line', 'line-number'], class: 'a', onlyEmpty: true }); editor.decorateMarker(marker, { type: ['line', 'line-number'], class: 'b', onlyNonEmpty: true }); editor.decorateMarker(marker, { type: ['line', 'line-number'], class: 'c' }); await component.getNextUpdatePromise(); expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe( false ); expect(lineNodeForScreenRow(component, 1).classList.contains('c')).toBe( true ); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('a') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('b') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('c') ).toBe(true); marker.setScreenRange([[1, 0], [2, 4]]); await component.getNextUpdatePromise(); expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe( false ); expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe( true ); expect(lineNodeForScreenRow(component, 1).classList.contains('c')).toBe( true ); expect(lineNodeForScreenRow(component, 2).classList.contains('b')).toBe( true ); expect(lineNodeForScreenRow(component, 2).classList.contains('c')).toBe( true ); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('a') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('b') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('c') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 2).classList.contains('b') ).toBe(true); expect( lineNumberNodeForScreenRow(component, 2).classList.contains('c') ).toBe(true); }); it('honors the onlyHead option', async () => { const { component, editor } = buildComponent(); const marker = editor.markScreenRange([[1, 4], [3, 4]]); editor.decorateMarker(marker, { type: ['line', 'line-number'], class: 'a', onlyHead: true }); await component.getNextUpdatePromise(); expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe( false ); expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe( true ); expect( lineNumberNodeForScreenRow(component, 1).classList.contains('a') ).toBe(false); expect( lineNumberNodeForScreenRow(component, 3).classList.contains('a') ).toBe(true); }); it('only decorates the last row of non-empty ranges that end at column 0 if omitEmptyLastRow is false', async () => { const { component, editor } = buildComponent(); const marker = editor.markScreenRange([[1, 0], [3, 0]]); editor.decorateMarker(marker, { type: ['line', 'line-number'], class: 'a' }); editor.decorateMarker(marker, { type: ['line', 'line-number'], class: 'b', omitEmptyLastRow: false }); await component.getNextUpdatePromise(); expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe( true ); expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe( false ); expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe( true ); expect(lineNodeForScreenRow(component, 2).classList.contains('b')).toBe( true ); expect(lineNodeForScreenRow(component, 3).classList.contains('b')).toBe( true ); }); it('does not decorate invalidated markers', async () => { const { component, editor } = buildComponent(); const marker = editor.markScreenRange([[1, 0], [3, 0]], { invalidate: 'touch' }); editor.decorateMarker(marker, { type: ['line', 'line-number'], class: 'a' }); await component.getNextUpdatePromise(); expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe( true ); editor.getBuffer().insert([2, 0], 'x'); expect(marker.isValid()).toBe(false); await component.getNextUpdatePromise(); expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe( false ); }); }); describe('highlight decorations', () => { it('renders single-line highlights', async () => { const { component, element, editor } = buildComponent(); const marker = editor.markScreenRange([[1, 2], [1, 10]]); editor.decorateMarker(marker, { type: 'highlight', class: 'a' }); await component.getNextUpdatePromise(); { const regions = element.querySelectorAll('.highlight.a .region.a'); expect(regions.length).toBe(1); const regionRect = regions[0].getBoundingClientRect(); expect(regionRect.top).toBe( lineNodeForScreenRow(component, 1).getBoundingClientRect().top ); expect(Math.round(regionRect.left)).toBeNear( clientLeftForCharacter(component, 1, 2) ); expect(Math.round(regionRect.right)).toBeNear( clientLeftForCharacter(component, 1, 10) ); } marker.setScreenRange([[1, 4], [1, 8]]); await component.getNextUpdatePromise(); { const regions = element.querySelectorAll('.highlight.a .region.a'); expect(regions.length).toBe(1); const regionRect = regions[0].getBoundingClientRect(); expect(regionRect.top).toBe( lineNodeForScreenRow(component, 1).getBoundingClientRect().top ); expect(regionRect.bottom).toBe( lineNodeForScreenRow(component, 1).getBoundingClientRect().bottom ); expect(Math.round(regionRect.left)).toBeNear( clientLeftForCharacter(component, 1, 4) ); expect(Math.round(regionRect.right)).toBeNear( clientLeftForCharacter(component, 1, 8) ); } }); it('renders multi-line highlights', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 3 }); const marker = editor.markScreenRange([[2, 4], [3, 4]]); editor.decorateMarker(marker, { type: 'highlight', class: 'a' }); await component.getNextUpdatePromise(); { expect(element.querySelectorAll('.highlight.a').length).toBe(1); const regions = element.querySelectorAll('.highlight.a .region.a'); expect(regions.length).toBe(2); const region0Rect = regions[0].getBoundingClientRect(); expect(region0Rect.top).toBe( lineNodeForScreenRow(component, 2).getBoundingClientRect().top ); expect(region0Rect.bottom).toBe( lineNodeForScreenRow(component, 2).getBoundingClientRect().bottom ); expect(Math.round(region0Rect.left)).toBeNear( clientLeftForCharacter(component, 2, 4) ); expect(Math.round(region0Rect.right)).toBeNear( component.refs.content.getBoundingClientRect().right ); const region1Rect = regions[1].getBoundingClientRect(); expect(region1Rect.top).toBeNear( lineNodeForScreenRow(component, 3).getBoundingClientRect().top ); expect(region1Rect.bottom).toBeNear( lineNodeForScreenRow(component, 3).getBoundingClientRect().bottom ); expect(Math.round(region1Rect.left)).toBeNear( clientLeftForCharacter(component, 3, 0) ); expect(Math.round(region1Rect.right)).toBeNear( clientLeftForCharacter(component, 3, 4) ); } marker.setScreenRange([[2, 4], [5, 4]]); await component.getNextUpdatePromise(); { expect(element.querySelectorAll('.highlight.a').length).toBe(1); const regions = element.querySelectorAll('.highlight.a .region.a'); expect(regions.length).toBe(3); const region0Rect = regions[0].getBoundingClientRect(); expect(region0Rect.top).toBeNear( lineNodeForScreenRow(component, 2).getBoundingClientRect().top ); expect(region0Rect.bottom).toBeNear( lineNodeForScreenRow(component, 2).getBoundingClientRect().bottom ); expect(Math.round(region0Rect.left)).toBeNear( clientLeftForCharacter(component, 2, 4) ); expect(Math.round(region0Rect.right)).toBeNear( component.refs.content.getBoundingClientRect().right ); const region1Rect = regions[1].getBoundingClientRect(); expect(region1Rect.top).toBeNear( lineNodeForScreenRow(component, 3).getBoundingClientRect().top ); expect(region1Rect.bottom).toBeNear( lineNodeForScreenRow(component, 5).getBoundingClientRect().top ); expect(Math.round(region1Rect.left)).toBeNear( component.refs.content.getBoundingClientRect().left ); expect(Math.round(region1Rect.right)).toBeNear( component.refs.content.getBoundingClientRect().right ); const region2Rect = regions[2].getBoundingClientRect(); expect(region2Rect.top).toBeNear( lineNodeForScreenRow(component, 5).getBoundingClientRect().top ); expect(region2Rect.bottom).toBeNear( lineNodeForScreenRow(component, 6).getBoundingClientRect().top ); expect(Math.round(region2Rect.left)).toBeNear( component.refs.content.getBoundingClientRect().left ); expect(Math.round(region2Rect.right)).toBeNear( clientLeftForCharacter(component, 5, 4) ); } }); it('can flash highlight decorations', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 3, height: 200 }); const marker = editor.markScreenRange([[2, 4], [3, 4]]); const decoration = editor.decorateMarker(marker, { type: 'highlight', class: 'a' }); decoration.flash('b', 10); // Flash on initial appearance of highlight await component.getNextUpdatePromise(); const highlights = element.querySelectorAll('.highlight.a'); expect(highlights.length).toBe(1); expect(highlights[0].classList.contains('b')).toBe(true); await conditionPromise(() => !highlights[0].classList.contains('b')); // Don't flash on next update if another flash wasn't requested await setScrollTop(component, 100); expect(highlights[0].classList.contains('b')).toBe(false); // Flashing the same class again before the first flash completes // removes the flash class and adds it back on the next frame to ensure // CSS transitions apply to the second flash. decoration.flash('e', 100); await component.getNextUpdatePromise(); expect(highlights[0].classList.contains('e')).toBe(true); decoration.flash('e', 100); await component.getNextUpdatePromise(); expect(highlights[0].classList.contains('e')).toBe(false); await conditionPromise(() => highlights[0].classList.contains('e')); await conditionPromise(() => !highlights[0].classList.contains('e')); }); it("flashing a highlight decoration doesn't unflash other highlight decorations", async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 3, height: 200 }); const marker = editor.markScreenRange([[2, 4], [3, 4]]); const decoration = editor.decorateMarker(marker, { type: 'highlight', class: 'a' }); // Flash one class decoration.flash('c', 1000); await component.getNextUpdatePromise(); const highlights = element.querySelectorAll('.highlight.a'); expect(highlights.length).toBe(1); expect(highlights[0].classList.contains('c')).toBe(true); // Flash another class while the previously-flashed class is still highlighted decoration.flash('d', 100); await component.getNextUpdatePromise(); expect(highlights[0].classList.contains('c')).toBe(true); expect(highlights[0].classList.contains('d')).toBe(true); }); it('supports layer decorations', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 12 }); const markerLayer = editor.addMarkerLayer(); const marker1 = markerLayer.markScreenRange([[2, 4], [3, 4]]); const marker2 = markerLayer.markScreenRange([[5, 6], [7, 8]]); const decoration = editor.decorateMarkerLayer(markerLayer, { type: 'highlight', class: 'a' }); await component.getNextUpdatePromise(); const highlights = element.querySelectorAll('.highlight'); expect(highlights[0].classList.contains('a')).toBe(true); expect(highlights[1].classList.contains('a')).toBe(true); decoration.setPropertiesForMarker(marker1, { type: 'highlight', class: 'b' }); await component.getNextUpdatePromise(); expect(highlights[0].classList.contains('b')).toBe(true); expect(highlights[1].classList.contains('a')).toBe(true); decoration.setPropertiesForMarker(marker1, null); decoration.setPropertiesForMarker(marker2, { type: 'highlight', class: 'c' }); await component.getNextUpdatePromise(); expect(highlights[0].classList.contains('a')).toBe(true); expect(highlights[1].classList.contains('c')).toBe(true); }); it('clears highlights when recycling a tile that previously contained highlights and now does not', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 2, autoHeight: false }); await setEditorHeightInLines(component, 2); const marker = editor.markScreenRange([[1, 2], [1, 10]]); editor.decorateMarker(marker, { type: 'highlight', class: 'a' }); await component.getNextUpdatePromise(); expect(element.querySelectorAll('.highlight.a').length).toBe(1); await setScrollTop(component, component.getLineHeight() * 3); expect(element.querySelectorAll('.highlight.a').length).toBe(0); }); it('does not move existing highlights when adding or removing other highlight decorations (regression)', async () => { const { component, element, editor } = buildComponent(); const marker1 = editor.markScreenRange([[1, 6], [1, 10]]); editor.decorateMarker(marker1, { type: 'highlight', class: 'a' }); await component.getNextUpdatePromise(); const marker1Region = element.querySelector('.highlight.a'); expect( Array.from(marker1Region.parentElement.children).indexOf(marker1Region) ).toBe(0); const marker2 = editor.markScreenRange([[1, 2], [1, 4]]); editor.decorateMarker(marker2, { type: 'highlight', class: 'b' }); await component.getNextUpdatePromise(); const marker2Region = element.querySelector('.highlight.b'); expect( Array.from(marker1Region.parentElement.children).indexOf(marker1Region) ).toBe(0); expect( Array.from(marker2Region.parentElement.children).indexOf(marker2Region) ).toBe(1); marker2.destroy(); await component.getNextUpdatePromise(); expect( Array.from(marker1Region.parentElement.children).indexOf(marker1Region) ).toBe(0); }); it('correctly positions highlights that end on rows preceding or following block decorations', async () => { const { editor, element, component } = buildComponent(); const item1 = document.createElement('div'); item1.style.height = '30px'; item1.style.backgroundColor = 'blue'; editor.decorateMarker(editor.markBufferPosition([4, 0]), { type: 'block', position: 'after', item: item1 }); const item2 = document.createElement('div'); item2.style.height = '30px'; item2.style.backgroundColor = 'yellow'; editor.decorateMarker(editor.markBufferPosition([4, 0]), { type: 'block', position: 'before', item: item2 }); editor.decorateMarker(editor.markBufferRange([[3, 0], [4, Infinity]]), { type: 'highlight', class: 'highlight' }); await component.getNextUpdatePromise(); const regions = element.querySelectorAll('.highlight .region'); expect(regions[0].offsetTop).toBeNear(3 * component.getLineHeight()); expect(regions[0].offsetHeight).toBeNear(component.getLineHeight()); expect(regions[1].offsetTop).toBeNear(4 * component.getLineHeight() + 30); }); }); describe('overlay decorations', () => { function attachFakeWindow(component) { const fakeWindow = document.createElement('div'); fakeWindow.style.position = 'absolute'; fakeWindow.style.padding = 20 + 'px'; fakeWindow.style.backgroundColor = 'blue'; fakeWindow.appendChild(component.element); jasmine.attachToDOM(fakeWindow); spyOn(component, 'getWindowInnerWidth').andCallFake( () => fakeWindow.getBoundingClientRect().width ); spyOn(component, 'getWindowInnerHeight').andCallFake( () => fakeWindow.getBoundingClientRect().height ); return fakeWindow; } it('renders overlay elements at the specified screen position unless it would overflow the window', async () => { const { component, editor } = buildComponent({ width: 200, height: 100, attach: false }); const fakeWindow = attachFakeWindow(component); await setScrollTop(component, 50); await setScrollLeft(component, 100); const marker = editor.markScreenPosition([4, 25]); const overlayElement = document.createElement('div'); overlayElement.style.width = '50px'; overlayElement.style.height = '50px'; overlayElement.style.margin = '3px'; overlayElement.style.backgroundColor = 'red'; 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).toBeNear( clientTopForLine(component, 5) ); expect(overlayWrapper.getBoundingClientRect().left).toBeNear( clientLeftForCharacter(component, 4, 25) ); // Updates the horizontal position on scroll await setScrollLeft(component, 150); expect(overlayWrapper.getBoundingClientRect().left).toBeNear( clientLeftForCharacter(component, 4, 25) ); // Shifts the overlay horizontally to ensure the overlay element does not // overflow the window await setScrollLeft(component, 30); expect(overlayElement.getBoundingClientRect().right).toBeNear( fakeWindow.getBoundingClientRect().right ); await setScrollLeft(component, 280); expect(overlayElement.getBoundingClientRect().left).toBeNear( fakeWindow.getBoundingClientRect().left ); // Updates the vertical position on scroll await setScrollTop(component, 60); expect(overlayWrapper.getBoundingClientRect().top).toBeNear( clientTopForLine(component, 5) ); // Flips the overlay vertically to ensure the overlay element does not // overflow the bottom of the window setScrollLeft(component, 100); await setScrollTop(component, 0); expect(overlayWrapper.getBoundingClientRect().bottom).toBeNear( clientTopForLine(component, 4) ); // Flips the overlay vertically on overlay resize if necessary await setScrollTop(component, 20); expect(overlayWrapper.getBoundingClientRect().top).toBeNear( clientTopForLine(component, 5) ); overlayElement.style.height = 60 + 'px'; await overlayComponent.getNextUpdatePromise(); expect(overlayWrapper.getBoundingClientRect().bottom).toBeNear( clientTopForLine(component, 4) ); // Does not flip the overlay vertically if it would overflow the top of the window overlayElement.style.height = 80 + 'px'; await overlayComponent.getNextUpdatePromise(); expect(overlayWrapper.getBoundingClientRect().top).toBeNear( clientTopForLine(component, 5) ); // Can update overlay wrapper class decoration.setProperties({ type: 'overlay', item: overlayElement, class: 'b' }); await component.getNextUpdatePromise(); expect(overlayWrapper.classList.contains('a')).toBe(false); expect(overlayWrapper.classList.contains('b')).toBe(true); decoration.setProperties({ type: 'overlay', item: overlayElement }); await component.getNextUpdatePromise(); expect(overlayWrapper.classList.contains('b')).toBe(false); }); it('does not attempt to avoid overflowing the window if `avoidOverflow` is false on the decoration', async () => { const { component, editor } = buildComponent({ width: 200, height: 100, attach: false }); const fakeWindow = attachFakeWindow(component); const overlayElement = document.createElement('div'); overlayElement.style.width = '50px'; overlayElement.style.height = '50px'; overlayElement.style.margin = '3px'; overlayElement.style.backgroundColor = 'red'; const marker = editor.markScreenPosition([4, 25]); editor.decorateMarker(marker, { type: 'overlay', item: overlayElement, avoidOverflow: false }); await component.getNextUpdatePromise(); await setScrollLeft(component, 30); expect(overlayElement.getBoundingClientRect().right).toBeGreaterThan( fakeWindow.getBoundingClientRect().right ); await setScrollLeft(component, 280); expect(overlayElement.getBoundingClientRect().left).toBeLessThan( fakeWindow.getBoundingClientRect().left ); }); }); describe('custom gutter decorations', () => { it('arranges custom gutters based on their priority', async () => { const { component, editor } = buildComponent(); editor.addGutter({ name: 'e', priority: 2 }); editor.addGutter({ name: 'a', priority: -2 }); editor.addGutter({ name: 'd', priority: 1 }); editor.addGutter({ name: 'b', priority: -1 }); editor.addGutter({ name: 'c', priority: 0 }); await component.getNextUpdatePromise(); const gutters = component.refs.gutterContainer.element.querySelectorAll( '.gutter' ); expect( Array.from(gutters).map(g => g.getAttribute('gutter-name')) ).toEqual(['a', 'b', 'c', 'line-number', 'd', 'e']); }); it('adjusts the left edge of the scroll container based on changes to the gutter container width', async () => { const { component, editor } = buildComponent(); const { scrollContainer, gutterContainer } = component.refs; function checkScrollContainerLeft() { expect(scrollContainer.getBoundingClientRect().left).toBeNear( Math.round(gutterContainer.element.getBoundingClientRect().right) ); } checkScrollContainerLeft(); const gutterA = editor.addGutter({ name: 'a' }); await component.getNextUpdatePromise(); checkScrollContainerLeft(); const gutterB = editor.addGutter({ name: 'b' }); await component.getNextUpdatePromise(); checkScrollContainerLeft(); gutterA.getElement().style.width = 100 + 'px'; await component.getNextUpdatePromise(); checkScrollContainerLeft(); gutterA.hide(); await component.getNextUpdatePromise(); checkScrollContainerLeft(); gutterA.show(); await component.getNextUpdatePromise(); checkScrollContainerLeft(); gutterA.destroy(); await component.getNextUpdatePromise(); checkScrollContainerLeft(); gutterB.destroy(); await component.getNextUpdatePromise(); checkScrollContainerLeft(); }); it('allows the element of custom gutters to be retrieved before being rendered in the editor component', async () => { const { component, element, editor } = buildComponent(); const [lineNumberGutter] = editor.getGutters(); const gutterA = editor.addGutter({ name: 'a', priority: -1 }); const gutterB = editor.addGutter({ name: 'b', priority: 1 }); const lineNumberGutterElement = lineNumberGutter.getElement(); const gutterAElement = gutterA.getElement(); const gutterBElement = gutterB.getElement(); await component.getNextUpdatePromise(); expect(element.contains(lineNumberGutterElement)).toBe(true); expect(element.contains(gutterAElement)).toBe(true); expect(element.contains(gutterBElement)).toBe(true); }); it('can show and hide custom gutters', async () => { const { component, editor } = buildComponent(); const gutterA = editor.addGutter({ name: 'a', priority: -1 }); const gutterB = editor.addGutter({ name: 'b', priority: 1 }); const gutterAElement = gutterA.getElement(); const gutterBElement = gutterB.getElement(); await component.getNextUpdatePromise(); expect(gutterAElement.style.display).toBe(''); expect(gutterBElement.style.display).toBe(''); gutterA.hide(); await component.getNextUpdatePromise(); expect(gutterAElement.style.display).toBe('none'); expect(gutterBElement.style.display).toBe(''); gutterB.hide(); await component.getNextUpdatePromise(); expect(gutterAElement.style.display).toBe('none'); expect(gutterBElement.style.display).toBe('none'); gutterA.show(); await component.getNextUpdatePromise(); expect(gutterAElement.style.display).toBe(''); expect(gutterBElement.style.display).toBe('none'); }); it('renders decorations in custom gutters', async () => { const { component, element, editor } = buildComponent(); const gutterA = editor.addGutter({ name: 'a', priority: -1 }); const gutterB = editor.addGutter({ name: 'b', priority: 1 }); const marker1 = editor.markScreenRange([[2, 0], [4, 0]]); const marker2 = editor.markScreenRange([[6, 0], [7, 0]]); const marker3 = editor.markScreenRange([[9, 0], [12, 0]]); const decorationElement1 = document.createElement('div'); const decorationElement2 = document.createElement('div'); // Packages may adopt this class name for decorations to be styled the same as line numbers decorationElement2.className = 'line-number'; const decoration1 = gutterA.decorateMarker(marker1, { class: 'a' }); const decoration2 = gutterA.decorateMarker(marker2, { class: 'b', item: decorationElement1 }); const decoration3 = gutterB.decorateMarker(marker3, { item: decorationElement2 }); await component.getNextUpdatePromise(); let [ decorationNode1, decorationNode2 ] = gutterA.getElement().firstChild.children; const [decorationNode3] = gutterB.getElement().firstChild.children; expect(decorationNode1.className).toBe('decoration a'); expect(decorationNode1.getBoundingClientRect().top).toBeNear( clientTopForLine(component, 2) ); expect(decorationNode1.getBoundingClientRect().bottom).toBeNear( clientTopForLine(component, 5) ); expect(decorationNode1.firstChild).toBeNull(); expect(decorationNode2.className).toBe('decoration b'); expect(decorationNode2.getBoundingClientRect().top).toBeNear( clientTopForLine(component, 6) ); expect(decorationNode2.getBoundingClientRect().bottom).toBeNear( clientTopForLine(component, 8) ); expect(decorationNode2.firstChild).toBe(decorationElement1); expect(decorationElement1.offsetHeight).toBe( decorationNode2.offsetHeight ); expect(decorationElement1.offsetWidth).toBe(decorationNode2.offsetWidth); expect(decorationNode3.className).toBe('decoration'); expect(decorationNode3.getBoundingClientRect().top).toBeNear( clientTopForLine(component, 9) ); expect(decorationNode3.getBoundingClientRect().bottom).toBeNear( clientTopForLine(component, 12) + component.getLineHeight() ); expect(decorationNode3.firstChild).toBe(decorationElement2); expect(decorationElement2.offsetHeight).toBe( decorationNode3.offsetHeight ); expect(decorationElement2.offsetWidth).toBe(decorationNode3.offsetWidth); // Inline styled height is updated when line height changes element.style.fontSize = parseInt(getComputedStyle(element).fontSize) + 10 + 'px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(decorationElement1.offsetHeight).toBe( decorationNode2.offsetHeight ); expect(decorationElement2.offsetHeight).toBe( decorationNode3.offsetHeight ); decoration1.setProperties({ type: 'gutter', gutterName: 'a', class: 'c', item: decorationElement1 }); decoration2.setProperties({ type: 'gutter', gutterName: 'a' }); decoration3.destroy(); await component.getNextUpdatePromise(); expect(decorationNode1.className).toBe('decoration c'); expect(decorationNode1.firstChild).toBe(decorationElement1); expect(decorationElement1.offsetHeight).toBe( decorationNode1.offsetHeight ); expect(decorationNode2.className).toBe('decoration'); expect(decorationNode2.firstChild).toBeNull(); expect(gutterB.getElement().firstChild.children.length).toBe(0); }); it('renders custom line number gutters', async () => { const { component, editor } = buildComponent(); const gutterA = editor.addGutter({ name: 'a', priority: 1, type: 'line-number', class: 'a-number', labelFn: ({ bufferRow }) => `a - ${bufferRow}` }); const gutterB = editor.addGutter({ name: 'b', priority: 1, type: 'line-number', class: 'b-number', labelFn: ({ bufferRow }) => `b - ${bufferRow}` }); editor.setText('0000\n0001\n0002\n0003\n0004\n'); await component.getNextUpdatePromise(); const gutterAElement = gutterA.getElement(); const aNumbers = gutterAElement.querySelectorAll( 'div.line-number[data-buffer-row]' ); const aLabels = Array.from(aNumbers, e => e.textContent); expect(aLabels).toEqual([ 'a - 0', 'a - 1', 'a - 2', 'a - 3', 'a - 4', 'a - 5' ]); const gutterBElement = gutterB.getElement(); const bNumbers = gutterBElement.querySelectorAll( 'div.line-number[data-buffer-row]' ); const bLabels = Array.from(bNumbers, e => e.textContent); expect(bLabels).toEqual([ 'b - 0', 'b - 1', 'b - 2', 'b - 3', 'b - 4', 'b - 5' ]); }); it("updates the editor's soft wrap width when a custom gutter's measurement is available", () => { const { component, element, editor } = buildComponent({ lineNumberGutterVisible: false, width: 400, softWrapped: true, attach: false }); const gutter = editor.addGutter({ name: 'a', priority: 10 }); gutter.getElement().style.width = '100px'; jasmine.attachToDOM(element); expect(component.getGutterContainerWidth()).toBe(100); // Component client width - gutter container width - vertical scrollbar width const softWrapColumn = Math.floor( (400 - 100 - component.getVerticalScrollbarWidth()) / component.getBaseCharacterWidth() ); expect(editor.getSoftWrapColumn()).toBe(softWrapColumn); }); }); describe('block decorations', () => { it('renders visible block decorations between the appropriate lines, refreshing and measuring them as needed', async () => { const editor = buildEditor({ autoHeight: false }); const { item: item1, decoration: decoration1 } = createBlockDecorationAtScreenRow(editor, 0, { height: 11, position: 'before' }); const { item: item2, decoration: decoration2 } = createBlockDecorationAtScreenRow(editor, 2, { height: 22, margin: 10, position: 'before' }); // render an editor that already contains some block decorations const { component, element } = buildComponent({ editor, rowsPerTile: 3 }); element.style.height = 4 * component.getLineHeight() + horizontalScrollbarHeight + 'px'; await component.getNextUpdatePromise(); expect(component.getRenderedStartRow()).toBe(0); expect(component.getRenderedEndRow()).toBe(9); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item1) + getElementHeight(item2) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item1) + getElementHeight(item2) }, { tileStartRow: 3, height: 3 * component.getLineHeight() } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(item1.previousSibling).toBeNull(); expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1)); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2)); // add block decorations const { item: item3, decoration: decoration3 } = createBlockDecorationAtScreenRow(editor, 4, { height: 33, position: 'before' }); const { item: item4 } = createBlockDecorationAtScreenRow(editor, 7, { height: 44, position: 'before' }); const { item: item5 } = createBlockDecorationAtScreenRow(editor, 7, { height: 50, marginBottom: 5, position: 'after' }); const { item: item6 } = createBlockDecorationAtScreenRow(editor, 12, { height: 60, marginTop: 6, position: 'after' }); await component.getNextUpdatePromise(); expect(component.getRenderedStartRow()).toBe(0); expect(component.getRenderedEndRow()).toBe(9); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item1) + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item1) + getElementHeight(item2) }, { tileStartRow: 3, height: 3 * component.getLineHeight() + getElementHeight(item3) } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(item1.previousSibling).toBeNull(); expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1)); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2)); expect(item3.previousSibling).toBe(lineNodeForScreenRow(component, 3)); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 4)); expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)); expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)); expect(element.contains(item6)).toBe(false); // destroy decoration1 decoration1.destroy(); await component.getNextUpdatePromise(); expect(component.getRenderedStartRow()).toBe(0); expect(component.getRenderedEndRow()).toBe(9); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) }, { tileStartRow: 3, height: 3 * component.getLineHeight() + getElementHeight(item3) } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(element.contains(item1)).toBe(false); expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1)); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2)); expect(item3.previousSibling).toBe(lineNodeForScreenRow(component, 3)); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 4)); expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)); expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)); expect(element.contains(item6)).toBe(false); // move decoration2 and decoration3 decoration2.getMarker().setHeadScreenPosition([1, 0]); decoration3.getMarker().setHeadScreenPosition([0, 0]); await component.getNextUpdatePromise(); expect(component.getRenderedStartRow()).toBe(0); expect(component.getRenderedEndRow()).toBe(9); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) }, { tileStartRow: 3, height: 3 * component.getLineHeight() } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(element.contains(item1)).toBe(false); expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)); expect(item3.previousSibling).toBeNull(); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)); expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)); expect(element.contains(item6)).toBe(false); // change the text editor.getBuffer().setTextInRange([[0, 5], [0, 5]], '\n\n'); await component.getNextUpdatePromise(); expect(component.getRenderedStartRow()).toBe(0); expect(component.getRenderedEndRow()).toBe(9); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item3) }, { tileStartRow: 3, height: 3 * component.getLineHeight() + getElementHeight(item2) } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(element.contains(item1)).toBe(false); expect(item2.previousSibling).toBeNull(); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3)); expect(item3.previousSibling).toBeNull(); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(element.contains(item4)).toBe(false); expect(element.contains(item5)).toBe(false); expect(element.contains(item6)).toBe(false); // scroll past the first tile await setScrollTop( component, 3 * component.getLineHeight() + getElementHeight(item3) ); expect(component.getRenderedStartRow()).toBe(3); expect(component.getRenderedEndRow()).toBe(12); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 3, height: 3 * component.getLineHeight() + getElementHeight(item2) }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(element.contains(item1)).toBe(false); expect(item2.previousSibling).toBeNull(); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3)); expect(element.contains(item3)).toBe(false); expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 9)); expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 9)); expect(element.contains(item6)).toBe(false); await setScrollTop(component, 0); // undo the previous change editor.undo(); await component.getNextUpdatePromise(); expect(component.getRenderedStartRow()).toBe(0); expect(component.getRenderedEndRow()).toBe(9); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) }, { tileStartRow: 3, height: 3 * component.getLineHeight() } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(element.contains(item1)).toBe(false); expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)); expect(item3.previousSibling).toBeNull(); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)); expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)); expect(element.contains(item6)).toBe(false); // invalidate decorations. this also tests a case where two decorations in // the same tile change their height without affecting the tile height nor // the content height. item3.style.height = '22px'; item3.style.margin = '10px'; item2.style.height = '33px'; item2.style.margin = '0px'; await component.getNextUpdatePromise(); expect(component.getRenderedStartRow()).toBe(0); expect(component.getRenderedEndRow()).toBe(9); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) }, { tileStartRow: 3, height: 3 * component.getLineHeight() } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(element.contains(item1)).toBe(false); expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)); expect(item3.previousSibling).toBeNull(); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)); expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)); expect(element.contains(item6)).toBe(false); // make decoration before row 0 as wide as the editor, and insert some text into it so that it wraps. item3.style.height = ''; item3.style.margin = ''; item3.style.width = ''; item3.style.wordWrap = 'break-word'; const contentWidthInCharacters = Math.floor( component.getScrollContainerClientWidth() / component.getBaseCharacterWidth() ); item3.textContent = 'x'.repeat(contentWidthInCharacters * 2); await component.getNextUpdatePromise(); // make the editor wider, so that the decoration doesn't wrap anymore. component.element.style.width = component.getGutterContainerWidth() + component.getScrollContainerClientWidth() * 2 + verticalScrollbarWidth + 'px'; await component.getNextUpdatePromise(); expect(component.getRenderedStartRow()).toBe(0); expect(component.getRenderedEndRow()).toBe(9); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) }, { tileStartRow: 3, height: 3 * component.getLineHeight() } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(9); expect(element.contains(item1)).toBe(false); expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)); expect(item3.previousSibling).toBeNull(); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)); expect(element.contains(item6)).toBe(false); // make the editor taller and wider and the same time, ensuring the number // of rendered lines is correct. setEditorHeightInLines(component, 13); setEditorWidthInCharacters(component, 50); await conditionPromise( () => component.getRenderedStartRow() === 0 && component.getRenderedEndRow() === 13 ); expect(component.getScrollHeight()).toBeNear( editor.getScreenLineCount() * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) + getElementHeight(item4) + getElementHeight(item5) + getElementHeight(item6) ); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + getElementHeight(item2) + getElementHeight(item3) }, { tileStartRow: 3, height: 3 * component.getLineHeight() }, { tileStartRow: 6, height: 3 * component.getLineHeight() + getElementHeight(item4) + getElementHeight(item5) } ]); assertLinesAreAlignedWithLineNumbers(component); expect(queryOnScreenLineElements(element).length).toBe(13); expect(element.contains(item1)).toBe(false); expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1)); expect(item3.previousSibling).toBeNull(); expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0)); expect(item4.previousSibling).toBe(lineNodeForScreenRow(component, 6)); expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7)); expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7)); expect(item5.nextSibling).toBe(lineNodeForScreenRow(component, 8)); expect(item6.previousSibling).toBe(lineNodeForScreenRow(component, 12)); }); it('correctly positions line numbers when block decorations are located at tile boundaries', async () => { const { editor, component } = buildComponent({ rowsPerTile: 3 }); createBlockDecorationAtScreenRow(editor, 0, { height: 5, position: 'before' }); createBlockDecorationAtScreenRow(editor, 2, { height: 7, position: 'after' }); createBlockDecorationAtScreenRow(editor, 3, { height: 9, position: 'before' }); createBlockDecorationAtScreenRow(editor, 3, { height: 11, position: 'after' }); createBlockDecorationAtScreenRow(editor, 5, { height: 13, position: 'after' }); await component.getNextUpdatePromise(); assertLinesAreAlignedWithLineNumbers(component); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + 5 + 7 }, { tileStartRow: 3, height: 3 * component.getLineHeight() + 9 + 11 + 13 }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); }); it('removes block decorations whose markers have been destroyed', async () => { const { editor, component } = 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' } ); const { component } = buildComponent({ editor, rowsPerTile: 3 }); // Invalidating the marker removes the block decoration. editor.getBuffer().deleteRows(2, 3); await component.getNextUpdatePromise(); expect(item.parentElement).toBeNull(); assertLinesAreAlignedWithLineNumbers(component); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() }, { tileStartRow: 3, height: 3 * component.getLineHeight() }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); // Moving invalid markers is ignored. marker.setScreenRange([[2, 0], [2, 0]]); await component.getNextUpdatePromise(); expect(item.parentElement).toBeNull(); assertLinesAreAlignedWithLineNumbers(component); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() }, { tileStartRow: 3, height: 3 * component.getLineHeight() }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); // Making the marker valid again adds back the block decoration. marker.bufferMarker.valid = true; marker.setScreenRange([[3, 0], [3, 0]]); await component.getNextUpdatePromise(); expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 3)); assertLinesAreAlignedWithLineNumbers(component); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() }, { tileStartRow: 3, height: 3 * component.getLineHeight() + 44 }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); // Destroying the decoration and invalidating the marker at the same time // removes the block decoration correctly. editor.getBuffer().deleteRows(2, 3); decoration.destroy(); await component.getNextUpdatePromise(); expect(item.parentElement).toBeNull(); assertLinesAreAlignedWithLineNumbers(component); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() }, { tileStartRow: 3, height: 3 * component.getLineHeight() }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); }); it('does not render block decorations when decorating invalid markers', async () => { const editor = buildEditor({ rowsPerTile: 3, autoHeight: false }); const { component } = buildComponent({ editor, rowsPerTile: 3 }); const marker = editor.markScreenPosition([3, 0], { invalidate: 'touch' }); const item = document.createElement('div'); item.style.height = 30 + 'px'; item.style.width = 30 + 'px'; editor.getBuffer().deleteRows(1, 4); editor.decorateMarker(marker, { type: 'block', item, position: 'before' }); await component.getNextUpdatePromise(); expect(item.parentElement).toBeNull(); assertLinesAreAlignedWithLineNumbers(component); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() }, { tileStartRow: 3, height: 3 * component.getLineHeight() }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); // Making the marker valid again causes the corresponding block decoration // to be added to the editor. marker.bufferMarker.valid = true; marker.setScreenRange([[2, 0], [2, 0]]); await component.getNextUpdatePromise(); expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 2)); assertLinesAreAlignedWithLineNumbers(component); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() + 30 }, { tileStartRow: 3, height: 3 * component.getLineHeight() }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); }); it('does not try to remeasure block decorations whose markers are invalid (regression)', async () => { const editor = buildEditor({ rowsPerTile: 3, autoHeight: false }); const { component } = buildComponent({ editor, rowsPerTile: 3 }); createBlockDecorationAtScreenRow(editor, 2, { height: '12px', invalidate: 'touch' }); editor.getBuffer().deleteRows(0, 3); await component.getNextUpdatePromise(); // Trigger a re-measurement of all block decorations. await setEditorWidthInCharacters(component, 20); assertLinesAreAlignedWithLineNumbers(component); assertTilesAreSizedAndPositionedCorrectly(component, [ { tileStartRow: 0, height: 3 * component.getLineHeight() }, { tileStartRow: 3, height: 3 * component.getLineHeight() }, { tileStartRow: 6, height: 3 * component.getLineHeight() } ]); }); 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 }); const marker = editor.markScreenPosition([0, 0]); const item = document.createElement('div'); item.textContent = 'block decoration'; editor.decorateMarker(marker, { type: 'block', item }); jasmine.attachToDOM(element); assertLinesAreAlignedWithLineNumbers(component); } { const { editor, component, element } = buildComponent({ autoHeight: false, width: 800 }); const marker = editor.markScreenPosition([0, 0]); const item = document.createElement('div'); item.textContent = 'block decoration that could wrap many times'; editor.decorateMarker(marker, { type: 'block', item }); element.style.width = '50px'; await component.getNextUpdatePromise(); assertLinesAreAlignedWithLineNumbers(component); } }); it('bases the width of the block decoration measurement area on the editor scroll width', async () => { const { component, element } = buildComponent({ autoHeight: false, width: 150 }); expect(component.refs.blockDecorationMeasurementArea.offsetWidth).toBe( component.getScrollWidth() ); element.style.width = '800px'; await component.getNextUpdatePromise(); expect(component.refs.blockDecorationMeasurementArea.offsetWidth).toBe( component.getScrollWidth() ); }); it('does not change the cursor position when clicking on a block decoration', async () => { const { editor, component } = buildComponent(); const decorationElement = document.createElement('div'); decorationElement.textContent = 'Parent'; const childElement = document.createElement('div'); childElement.textContent = 'Child'; decorationElement.appendChild(childElement); const marker = editor.markScreenPosition([4, 0]); editor.decorateMarker(marker, { type: 'block', item: decorationElement }); await component.getNextUpdatePromise(); const decorationElementClientRect = decorationElement.getBoundingClientRect(); component.didMouseDownOnContent({ target: decorationElement, detail: 1, button: 0, clientX: decorationElementClientRect.left, clientY: decorationElementClientRect.top }); expect(editor.getCursorScreenPosition()).toEqual([0, 0]); const childElementClientRect = childElement.getBoundingClientRect(); component.didMouseDownOnContent({ target: childElement, detail: 1, button: 0, clientX: childElementClientRect.left, clientY: childElementClientRect.top }); expect(editor.getCursorScreenPosition()).toEqual([0, 0]); }); it('uses the order property to control the order of block decorations at the same screen row', async () => { const editor = buildEditor({ autoHeight: false }); const { component, element } = buildComponent({ editor }); element.style.height = 10 * component.getLineHeight() + horizontalScrollbarHeight + 'px'; await component.getNextUpdatePromise(); // Order parameters that differ from creation order; that collide; and that are not provided. const [beforeItems, beforeDecorations] = [ 30, 20, undefined, 20, 10, undefined ] .map(order => { return createBlockDecorationAtScreenRow(editor, 2, { height: 10, position: 'before', order }); }) .reduce( (lists, result) => { lists[0].push(result.item); lists[1].push(result.decoration); return lists; }, [[], []] ); const [afterItems] = [undefined, 1, 6, undefined, 6, 2] .map(order => { return createBlockDecorationAtScreenRow(editor, 2, { height: 10, position: 'after', order }); }) .reduce( (lists, result) => { lists[0].push(result.item); lists[1].push(result.decoration); return lists; }, [[], []] ); await component.getNextUpdatePromise(); expect(beforeItems[4].previousSibling).toBe( lineNodeForScreenRow(component, 1) ); expect(beforeItems[4].nextSibling).toBe(beforeItems[1]); expect(beforeItems[1].nextSibling).toBe(beforeItems[3]); expect(beforeItems[3].nextSibling).toBe(beforeItems[0]); expect(beforeItems[0].nextSibling).toBe(beforeItems[2]); expect(beforeItems[2].nextSibling).toBe(beforeItems[5]); expect(beforeItems[5].nextSibling).toBe( lineNodeForScreenRow(component, 2) ); expect(afterItems[1].previousSibling).toBe( lineNodeForScreenRow(component, 2) ); expect(afterItems[1].nextSibling).toBe(afterItems[5]); expect(afterItems[5].nextSibling).toBe(afterItems[2]); expect(afterItems[2].nextSibling).toBe(afterItems[4]); expect(afterItems[4].nextSibling).toBe(afterItems[0]); expect(afterItems[0].nextSibling).toBe(afterItems[3]); // Create a decoration somewhere else and move it to the same screen row as the existing decorations const { item: later, decoration } = createBlockDecorationAtScreenRow( editor, 4, { height: 20, position: 'after', order: 3 } ); await component.getNextUpdatePromise(); expect(later.previousSibling).toBe(lineNodeForScreenRow(component, 4)); expect(later.nextSibling).toBe(lineNodeForScreenRow(component, 5)); decoration.getMarker().setHeadScreenPosition([2, 0]); await component.getNextUpdatePromise(); expect(later.previousSibling).toBe(afterItems[5]); expect(later.nextSibling).toBe(afterItems[2]); // Move a decoration away from its screen row and ensure the rest maintain their order beforeDecorations[3].getMarker().setHeadScreenPosition([5, 0]); await component.getNextUpdatePromise(); expect(beforeItems[3].previousSibling).toBe( lineNodeForScreenRow(component, 4) ); expect(beforeItems[3].nextSibling).toBe( lineNodeForScreenRow(component, 5) ); expect(beforeItems[4].previousSibling).toBe( lineNodeForScreenRow(component, 1) ); expect(beforeItems[4].nextSibling).toBe(beforeItems[1]); expect(beforeItems[1].nextSibling).toBe(beforeItems[0]); expect(beforeItems[0].nextSibling).toBe(beforeItems[2]); expect(beforeItems[2].nextSibling).toBe(beforeItems[5]); expect(beforeItems[5].nextSibling).toBe( lineNodeForScreenRow(component, 2) ); }); function createBlockDecorationAtScreenRow( editor, screenRow, { height, margin, marginTop, marginBottom, position, order, invalidate } ) { const marker = editor.markScreenPosition([screenRow, 0], { invalidate: invalidate || 'never' }); const item = document.createElement('div'); item.style.height = height + 'px'; if (margin != null) item.style.margin = margin + 'px'; if (marginTop != null) item.style.marginTop = marginTop + 'px'; if (marginBottom != null) item.style.marginBottom = marginBottom + 'px'; item.style.width = 30 + 'px'; const decoration = editor.decorateMarker(marker, { type: 'block', item, position, order }); return { item, decoration, marker }; } function assertTilesAreSizedAndPositionedCorrectly(component, tiles) { let top = 0; for (let tile of tiles) { const linesTileElement = lineNodeForScreenRow( component, tile.tileStartRow ).parentElement; const linesTileBoundingRect = linesTileElement.getBoundingClientRect(); expect(linesTileBoundingRect.height).toBeNear(tile.height); expect(linesTileBoundingRect.top).toBeNear(top); const lineNumbersTileElement = lineNumberNodeForScreenRow( component, tile.tileStartRow ).parentElement; const lineNumbersTileBoundingRect = lineNumbersTileElement.getBoundingClientRect(); expect(lineNumbersTileBoundingRect.height).toBeNear(tile.height); expect(lineNumbersTileBoundingRect.top).toBeNear(top); top += tile.height; } } function assertLinesAreAlignedWithLineNumbers(component) { const startRow = component.getRenderedStartRow(); const endRow = component.getRenderedEndRow(); for (let row = startRow; row < endRow; row++) { const lineNode = lineNodeForScreenRow(component, row); const lineNumberNode = lineNumberNodeForScreenRow(component, row); expect(lineNumberNode.getBoundingClientRect().top).toBeNear( lineNode.getBoundingClientRect().top ); } } }); describe('cursor decorations', () => { it('allows default cursors to be customized', async () => { const { component, element, editor } = buildComponent(); editor.addCursorAtScreenPosition([1, 0]); const [cursorMarker1, cursorMarker2] = editor .getCursors() .map(c => c.getMarker()); editor.decorateMarker(cursorMarker1, { type: 'cursor', class: 'a' }); editor.decorateMarker(cursorMarker2, { type: 'cursor', class: 'b', style: { visibility: 'hidden' } }); editor.decorateMarker(cursorMarker2, { type: 'cursor', style: { backgroundColor: 'red' } }); await component.getNextUpdatePromise(); const cursorNodes = element.querySelectorAll('.cursor'); expect(cursorNodes.length).toBe(2); expect(cursorNodes[0].className).toBe('cursor a'); expect(cursorNodes[1].className).toBe('cursor b'); expect(cursorNodes[1].style.visibility).toBe('hidden'); expect(cursorNodes[1].style.backgroundColor).toBe('red'); }); it('allows markers that are not actually associated with cursors to be decorated as if they were cursors', async () => { const { component, element, editor } = buildComponent(); const marker = editor.markScreenPosition([1, 0]); editor.decorateMarker(marker, { type: 'cursor', class: 'a' }); await component.getNextUpdatePromise(); const cursorNodes = element.querySelectorAll('.cursor'); expect(cursorNodes.length).toBe(2); expect(cursorNodes[0].className).toBe('cursor'); expect(cursorNodes[1].className).toBe('cursor a'); }); }); describe('text decorations', () => { it('injects spans with custom class names and inline styles based on text decorations', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 2 }); const markerLayer = editor.addMarkerLayer(); const marker1 = markerLayer.markBufferRange([[0, 2], [2, 7]]); const marker2 = markerLayer.markBufferRange([[0, 2], [3, 8]]); const marker3 = markerLayer.markBufferRange([[1, 13], [2, 7]]); editor.decorateMarker(marker1, { type: 'text', class: 'a', style: { color: 'red' } }); editor.decorateMarker(marker2, { type: 'text', class: 'b', style: { color: 'blue' } }); editor.decorateMarker(marker3, { type: 'text', class: 'c', style: { color: 'green' } }); await component.getNextUpdatePromise(); expect(textContentOnRowMatchingSelector(component, 0, '.a')).toBe( editor.lineTextForScreenRow(0).slice(2) ); expect(textContentOnRowMatchingSelector(component, 1, '.a')).toBe( editor.lineTextForScreenRow(1) ); expect(textContentOnRowMatchingSelector(component, 2, '.a')).toBe( editor.lineTextForScreenRow(2).slice(0, 7) ); expect(textContentOnRowMatchingSelector(component, 3, '.a')).toBe(''); expect(textContentOnRowMatchingSelector(component, 0, '.b')).toBe( editor.lineTextForScreenRow(0).slice(2) ); expect(textContentOnRowMatchingSelector(component, 1, '.b')).toBe( editor.lineTextForScreenRow(1) ); expect(textContentOnRowMatchingSelector(component, 2, '.b')).toBe( editor.lineTextForScreenRow(2) ); expect(textContentOnRowMatchingSelector(component, 3, '.b')).toBe( editor.lineTextForScreenRow(3).slice(0, 8) ); expect(textContentOnRowMatchingSelector(component, 0, '.c')).toBe(''); expect(textContentOnRowMatchingSelector(component, 1, '.c')).toBe( editor.lineTextForScreenRow(1).slice(13) ); expect(textContentOnRowMatchingSelector(component, 2, '.c')).toBe( editor.lineTextForScreenRow(2).slice(0, 7) ); expect(textContentOnRowMatchingSelector(component, 3, '.c')).toBe(''); for (const span of element.querySelectorAll('.a:not(.c)')) { expect(span.style.color).toBe('red'); } for (const span of element.querySelectorAll('.b:not(.c):not(.a)')) { expect(span.style.color).toBe('blue'); } for (const span of element.querySelectorAll('.c')) { expect(span.style.color).toBe('green'); } marker2.setHeadScreenPosition([3, 10]); await component.getNextUpdatePromise(); expect(textContentOnRowMatchingSelector(component, 3, '.b')).toBe( editor.lineTextForScreenRow(3).slice(0, 10) ); }); it('correctly handles text decorations starting before the first rendered row and/or ending after the last rendered row', async () => { const { component, element, editor } = buildComponent({ autoHeight: false, rowsPerTile: 1 }); element.style.height = 4 * component.getLineHeight() + 'px'; await component.getNextUpdatePromise(); await setScrollTop(component, 4 * component.getLineHeight()); expect(component.getRenderedStartRow()).toBeNear(4); expect(component.getRenderedEndRow()).toBeNear(9); const markerLayer = editor.addMarkerLayer(); const marker1 = markerLayer.markBufferRange([[0, 0], [4, 5]]); const marker2 = markerLayer.markBufferRange([[7, 2], [10, 8]]); editor.decorateMarker(marker1, { type: 'text', class: 'a' }); editor.decorateMarker(marker2, { type: 'text', class: 'b' }); await component.getNextUpdatePromise(); expect(textContentOnRowMatchingSelector(component, 4, '.a')).toBe( editor.lineTextForScreenRow(4).slice(0, 5) ); expect(textContentOnRowMatchingSelector(component, 5, '.a')).toBe(''); expect(textContentOnRowMatchingSelector(component, 6, '.a')).toBe(''); expect(textContentOnRowMatchingSelector(component, 7, '.a')).toBe(''); expect(textContentOnRowMatchingSelector(component, 8, '.a')).toBe(''); expect(textContentOnRowMatchingSelector(component, 4, '.b')).toBe(''); expect(textContentOnRowMatchingSelector(component, 5, '.b')).toBe(''); expect(textContentOnRowMatchingSelector(component, 6, '.b')).toBe(''); expect(textContentOnRowMatchingSelector(component, 7, '.b')).toBe( editor.lineTextForScreenRow(7).slice(2) ); expect(textContentOnRowMatchingSelector(component, 8, '.b')).toBe( editor.lineTextForScreenRow(8) ); }); it('does not create empty spans when a text decoration contains a row but another text decoration starts or ends at the beginning of it', async () => { const { component, element, editor } = buildComponent(); const markerLayer = editor.addMarkerLayer(); const marker1 = markerLayer.markBufferRange([[0, 2], [4, 0]]); const marker2 = markerLayer.markBufferRange([[2, 0], [5, 8]]); editor.decorateMarker(marker1, { type: 'text', class: 'a' }); editor.decorateMarker(marker2, { type: 'text', class: 'b' }); await component.getNextUpdatePromise(); for (const decorationSpan of element.querySelectorAll('.a, .b')) { expect(decorationSpan.textContent).not.toBe(''); } }); it('does not create empty text nodes when a text decoration ends right after a text tag', async () => { const { component, editor } = buildComponent(); const marker = editor.markBufferRange([[0, 8], [0, 29]]); editor.decorateMarker(marker, { type: 'text', class: 'a' }); await component.getNextUpdatePromise(); for (const textNode of textNodesForScreenRow(component, 0)) { expect(textNode.textContent).not.toBe(''); } }); function textContentOnRowMatchingSelector(component, row, selector) { return Array.from( lineNodeForScreenRow(component, row).querySelectorAll(selector) ) .map(span => span.textContent) .join(''); } }); describe('mouse input', () => { describe('on the lines', () => { describe('when there is only one cursor', () => { it('positions the cursor on single-click or when middle-clicking', async () => { atom.config.set('editor.selectionClipboard', true); for (const button of [0, 1]) { const { component, editor } = buildComponent(); const { lineHeight } = component.measurements; editor.setCursorScreenPosition([Infinity, Infinity], { autoscroll: false }); component.didMouseDownOnContent({ detail: 1, button, clientX: clientLeftForCharacter(component, 0, 0) - 1, clientY: clientTopForLine(component, 0) - 1 }); expect(editor.getCursorScreenPosition()).toEqual([0, 0]); const maxRow = editor.getLastScreenRow(); editor.setCursorScreenPosition([Infinity, Infinity], { autoscroll: false }); component.didMouseDownOnContent({ detail: 1, button, clientX: clientLeftForCharacter( component, maxRow, editor.lineLengthForScreenRow(maxRow) ) + 1, clientY: clientTopForLine(component, maxRow) + 1 }); expect(editor.getCursorScreenPosition()).toEqual([ maxRow, editor.lineLengthForScreenRow(maxRow) ]); component.didMouseDownOnContent({ detail: 1, button, clientX: clientLeftForCharacter( component, 0, editor.lineLengthForScreenRow(0) ) + 1, clientY: clientTopForLine(component, 0) + lineHeight / 2 }); expect(editor.getCursorScreenPosition()).toEqual([ 0, editor.lineLengthForScreenRow(0) ]); component.didMouseDownOnContent({ detail: 1, button, clientX: (clientLeftForCharacter(component, 3, 0) + clientLeftForCharacter(component, 3, 1)) / 2, clientY: clientTopForLine(component, 1) + lineHeight / 2 }); expect(editor.getCursorScreenPosition()).toEqual([1, 0]); component.didMouseDownOnContent({ detail: 1, button, clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2, clientY: clientTopForLine(component, 3) + lineHeight / 2 }); expect(editor.getCursorScreenPosition()).toEqual([3, 14]); component.didMouseDownOnContent({ detail: 1, button, clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2 + 1, clientY: clientTopForLine(component, 3) + lineHeight / 2 }); expect(editor.getCursorScreenPosition()).toEqual([3, 15]); editor.getBuffer().setTextInRange([[3, 14], [3, 15]], '🐣'); await component.getNextUpdatePromise(); component.didMouseDownOnContent({ detail: 1, button, clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2, clientY: clientTopForLine(component, 3) + lineHeight / 2 }); expect(editor.getCursorScreenPosition()).toEqual([3, 14]); component.didMouseDownOnContent({ detail: 1, button, clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2 + 1, clientY: clientTopForLine(component, 3) + lineHeight / 2 }); expect(editor.getCursorScreenPosition()).toEqual([3, 16]); expect(editor.testAutoscrollRequests).toEqual([]); } }); }); describe('when the input is for the primary mouse button', () => { it('selects words on double-click', () => { const { component, editor } = buildComponent(); const { clientX, clientY } = clientPositionForCharacter( component, 1, 16 ); 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', () => { const { component, editor } = buildComponent(); const { clientX, clientY } = clientPositionForCharacter( component, 1, 16 ); component.didMouseDownOnContent({ detail: 1, button: 0, clientX, clientY }); 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', () => { atom.config.set('editor.multiCursorOnClick', true); const { component, editor } = buildComponent({ platform: 'darwin' }); expect(editor.getCursorScreenPositions()).toEqual([[0, 0]]); // add cursor at 1, 16 component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, button: 0, metaKey: true }) ); expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]); // remove cursor at 0, 0 component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 0, 0), { detail: 1, button: 0, metaKey: true }) ); expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]); // cmd-click cursor at 1, 16 but don't remove it because it's the last one component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, button: 0, metaKey: true }) ); expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]); // cmd-clicking within a selection destroys it editor.addSelectionForScreenRange([[2, 10], [2, 15]], { autoscroll: false }); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 16], [1, 16]], [[2, 10], [2, 15]] ]); component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 2, 13), { detail: 1, button: 0, metaKey: true }) ); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 16], [1, 16]] ]); // ctrl-click does not add cursors on macOS, nor does it move the cursor component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 4), { detail: 1, button: 0, ctrlKey: true }) ); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 16], [1, 16]] ]); // ctrl-click adds cursors on platforms *other* than macOS component.props.platform = 'win32'; editor.setCursorScreenPosition([1, 4], { autoscroll: false }); component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, button: 0, ctrlKey: true }) ); expect(editor.getCursorScreenPositions()).toEqual([[1, 4], [1, 16]]); expect(editor.testAutoscrollRequests).toEqual([]); }); it('adds word selections when holding cmd or ctrl when double-clicking', () => { atom.config.set('editor.multiCursorOnClick', true); const { component, editor } = buildComponent(); editor.addCursorAtScreenPosition([1, 16], { autoscroll: false }); expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]); component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, button: 0, metaKey: true }) ); component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 2, button: 0, metaKey: true }) ); expect(editor.getSelectedScreenRanges()).toEqual([ [[0, 0], [0, 0]], [[1, 13], [1, 21]] ]); expect(editor.testAutoscrollRequests).toEqual([]); }); it('adds line selections when holding cmd or ctrl when triple-clicking', () => { atom.config.set('editor.multiCursorOnClick', true); const { component, editor } = buildComponent(); editor.addCursorAtScreenPosition([1, 16], { autoscroll: false }); expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]); const { clientX, clientY } = clientPositionForCharacter( component, 1, 16 ); component.didMouseDownOnContent({ detail: 1, button: 0, metaKey: true, clientX, clientY }); component.didMouseDownOnContent({ detail: 2, button: 0, metaKey: true, clientX, clientY }); component.didMouseDownOnContent({ detail: 3, button: 0, metaKey: true, clientX, clientY }); expect(editor.getSelectedScreenRanges()).toEqual([ [[0, 0], [0, 0]], [[1, 0], [2, 0]] ]); expect(editor.testAutoscrollRequests).toEqual([]); }); it('does not add cursors when holding cmd or ctrl when single-clicking', () => { atom.config.set('editor.multiCursorOnClick', false); const { component, editor } = buildComponent({ platform: 'darwin' }); expect(editor.getCursorScreenPositions()).toEqual([[0, 0]]); // moves cursor to 1, 16 component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, button: 0, metaKey: true }) ); expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]); // ctrl-click does not add cursors on macOS, nor does it move the cursor component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 4), { detail: 1, button: 0, ctrlKey: true }) ); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 16], [1, 16]] ]); // ctrl-click does not add cursors on platforms *other* than macOS component.props.platform = 'win32'; editor.setCursorScreenPosition([1, 4], { autoscroll: false }); component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, button: 0, ctrlKey: true }) ); expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]); expect(editor.testAutoscrollRequests).toEqual([]); }); it('does not add word selections when holding cmd or ctrl when double-clicking', () => { atom.config.set('editor.multiCursorOnClick', false); const { component, editor } = buildComponent(); component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 1, button: 0, metaKey: true }) ); component.didMouseDownOnContent( Object.assign(clientPositionForCharacter(component, 1, 16), { detail: 2, button: 0, metaKey: true }) ); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 13], [1, 21]] ]); expect(editor.testAutoscrollRequests).toEqual([]); }); it('does not add line selections when holding cmd or ctrl when triple-clicking', () => { atom.config.set('editor.multiCursorOnClick', false); const { component, editor } = buildComponent(); const { clientX, clientY } = clientPositionForCharacter( component, 1, 16 ); component.didMouseDownOnContent({ detail: 1, button: 0, metaKey: true, clientX, clientY }); component.didMouseDownOnContent({ detail: 2, button: 0, metaKey: true, clientX, clientY }); component.didMouseDownOnContent({ detail: 3, button: 0, metaKey: true, clientX, clientY }); expect(editor.getSelectedScreenRanges()).toEqual([[[1, 0], [2, 0]]]); expect(editor.testAutoscrollRequests).toEqual([]); }); it('expands the last selection on shift-click', () => { const { component, editor } = buildComponent(); editor.setCursorScreenPosition([2, 18], { autoscroll: false }); component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0, shiftKey: true }, clientPositionForCharacter(component, 1, 4) ) ); expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [2, 18]]); component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0, shiftKey: true }, clientPositionForCharacter(component, 4, 4) ) ); expect(editor.getSelectedScreenRange()).toEqual([[2, 18], [4, 4]]); // reorients word-wise selections to keep the word selected regardless of // where the subsequent shift-click occurs editor.setCursorScreenPosition([2, 18], { autoscroll: false }); editor.getLastSelection().selectWord({ autoscroll: false }); component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0, shiftKey: true }, clientPositionForCharacter(component, 1, 4) ) ); expect(editor.getSelectedScreenRange()).toEqual([[1, 2], [2, 20]]); component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0, shiftKey: true }, clientPositionForCharacter(component, 3, 11) ) ); expect(editor.getSelectedScreenRange()).toEqual([[2, 14], [3, 13]]); // reorients line-wise selections to keep the line selected regardless of // where the subsequent shift-click occurs editor.setCursorScreenPosition([2, 18], { autoscroll: false }); editor.getLastSelection().selectLine(null, { autoscroll: false }); component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0, shiftKey: true }, clientPositionForCharacter(component, 1, 4) ) ); expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 0]]); component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0, 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', () => { atom.config.set('editor.multiCursorOnClick', true); const { component, editor } = buildComponent(); spyOn(component, 'handleMouseDragUntilMouseUp'); component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0 }, clientPositionForCharacter(component, 1, 4) ) ); { const { didDrag, didStopDragging } = component.handleMouseDragUntilMouseUp.argsForCall[0][0]; didDrag(clientPositionForCharacter(component, 8, 8)); expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [8, 8]]); didDrag(clientPositionForCharacter(component, 4, 8)); expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [4, 8]]); didStopDragging(); expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [4, 8]]); } // Click-drag a second selection... selections are not merged until the // drag stops. component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0, metaKey: 1 }, clientPositionForCharacter(component, 8, 8) ) ); { const { didDrag, didStopDragging } = component.handleMouseDragUntilMouseUp.argsForCall[1][0]; didDrag(clientPositionForCharacter(component, 2, 8)); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 4], [4, 8]], [[2, 8], [8, 8]] ]); didDrag(clientPositionForCharacter(component, 6, 8)); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 4], [4, 8]], [[6, 8], [8, 8]] ]); didDrag(clientPositionForCharacter(component, 2, 8)); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 4], [4, 8]], [[2, 8], [8, 8]] ]); didStopDragging(); expect(editor.getSelectedScreenRanges()).toEqual([ [[1, 4], [8, 8]] ]); } }); it('expands the selection word-wise on double-click-drag', () => { const { component, editor } = buildComponent(); spyOn(component, 'handleMouseDragUntilMouseUp'); component.didMouseDownOnContent( Object.assign( { detail: 1, button: 0 }, clientPositionForCharacter(component, 1, 4) ) ); component.didMouseDownOnContent( Object.assign( { detail: 2, button: 0 }, clientPositionForCharacter(component, 1, 4) ) ); const { didDrag } = component.handleMouseDragUntilMouseUp.argsForCall[1][0]; didDrag(clientPositionForCharacter(component, 0, 8)); expect(editor.getSelectedScreenRange()).toEqual([[0, 4], [1, 5]]); didDrag(clientPositionForCharacter(component, 2, 10)); expect(editor.getSelectedScreenRange()).toEqual([[1, 2], [2, 13]]); }); it('expands the selection line-wise on triple-click-drag', () => { const { component, editor } = buildComponent(); spyOn(component, 'handleMouseDragUntilMouseUp'); const tripleClickPosition = clientPositionForCharacter( component, 2, 8 ); component.didMouseDownOnContent( Object.assign({ detail: 1, button: 0 }, tripleClickPosition) ); component.didMouseDownOnContent( Object.assign({ detail: 2, button: 0 }, tripleClickPosition) ); component.didMouseDownOnContent( Object.assign({ detail: 3, button: 0 }, tripleClickPosition) ); const { didDrag } = component.handleMouseDragUntilMouseUp.argsForCall[2][0]; didDrag(clientPositionForCharacter(component, 1, 8)); expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 0]]); didDrag(clientPositionForCharacter(component, 4, 10)); expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [5, 0]]); }); it('destroys folds when clicking on their fold markers', async () => { const { component, element, editor } = buildComponent(); editor.foldBufferRow(1); await component.getNextUpdatePromise(); const target = element.querySelector('.fold-marker'); const { clientX, clientY } = clientPositionForCharacter( component, 1, editor.lineLengthForScreenRow(1) ); component.didMouseDownOnContent({ detail: 1, button: 0, target, clientX, clientY }); expect(editor.isFoldedAtBufferRow(1)).toBe(false); expect(editor.getCursorScreenPosition()).toEqual([0, 0]); }); it('autoscrolls the content when dragging near the edge of the scroll container', async () => { const { component } = buildComponent({ width: 200, height: 200 }); spyOn(component, 'handleMouseDragUntilMouseUp'); let previousScrollTop = 0; let previousScrollLeft = 0; function assertScrolledDownAndRight() { expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop); previousScrollTop = component.getScrollTop(); expect(component.getScrollLeft()).toBeGreaterThan( previousScrollLeft ); previousScrollLeft = component.getScrollLeft(); } function assertScrolledUpAndLeft() { expect(component.getScrollTop()).toBeLessThan(previousScrollTop); previousScrollTop = component.getScrollTop(); expect(component.getScrollLeft()).toBeLessThan(previousScrollLeft); previousScrollLeft = component.getScrollLeft(); } component.didMouseDownOnContent({ detail: 1, button: 0, clientX: 100, clientY: 100 }); const { didDrag } = component.handleMouseDragUntilMouseUp.argsForCall[0][0]; didDrag({ clientX: 199, clientY: 199 }); assertScrolledDownAndRight(); didDrag({ clientX: 199, clientY: 199 }); assertScrolledDownAndRight(); didDrag({ clientX: 199, clientY: 199 }); assertScrolledDownAndRight(); didDrag({ clientX: component.getGutterContainerWidth() + 1, clientY: 1 }); assertScrolledUpAndLeft(); didDrag({ clientX: component.getGutterContainerWidth() + 1, clientY: 1 }); assertScrolledUpAndLeft(); didDrag({ clientX: component.getGutterContainerWidth() + 1, clientY: 1 }); assertScrolledUpAndLeft(); // Don't artificially update scroll position beyond possible values expect(component.getScrollTop()).toBe(0); expect(component.getScrollLeft()).toBe(0); didDrag({ clientX: component.getGutterContainerWidth() + 1, clientY: 1 }); expect(component.getScrollTop()).toBe(0); expect(component.getScrollLeft()).toBe(0); const maxScrollTop = component.getMaxScrollTop(); const maxScrollLeft = component.getMaxScrollLeft(); setScrollTop(component, maxScrollTop); await setScrollLeft(component, maxScrollLeft); didDrag({ clientX: 199, clientY: 199 }); didDrag({ clientX: 199, clientY: 199 }); didDrag({ clientX: 199, clientY: 199 }); expect(component.getScrollTop()).toBeNear(maxScrollTop); expect(component.getScrollLeft()).toBeNear(maxScrollLeft); }); }); it('pastes the previously selected text when clicking the middle mouse button on Linux', async () => { spyOn(electron.ipcRenderer, 'send').andCallFake(function( eventName, selectedText ) { if (eventName === 'write-text-to-selection-clipboard') { clipboard.writeText(selectedText, 'selection'); } }); const { component, editor } = buildComponent({ platform: 'linux' }); // Middle mouse pasting. atom.config.set('editor.selectionClipboard', true); editor.setSelectedBufferRange([[1, 6], [1, 10]]); await conditionPromise(() => TextEditor.clipboard.read() === 'sort'); component.didMouseDownOnContent({ button: 1, clientX: clientLeftForCharacter(component, 10, 0), clientY: clientTopForLine(component, 10) }); expect(TextEditor.clipboard.read()).toBe('sort'); expect(editor.lineTextForBufferRow(10)).toBe('sort'); editor.undo(); // Doesn't paste when middle mouse button is clicked atom.config.set('editor.selectionClipboard', false); editor.setSelectedBufferRange([[1, 6], [1, 10]]); component.didMouseDownOnContent({ button: 1, clientX: clientLeftForCharacter(component, 10, 0), clientY: clientTopForLine(component, 10) }); expect(TextEditor.clipboard.read()).toBe('sort'); expect(editor.lineTextForBufferRow(10)).toBe(''); // Ensure left clicks don't interfere. atom.config.set('editor.selectionClipboard', true); editor.setSelectedBufferRange([[1, 2], [1, 5]]); await conditionPromise(() => TextEditor.clipboard.read() === 'var'); component.didMouseDownOnContent({ button: 0, detail: 1, clientX: clientLeftForCharacter(component, 10, 0), clientY: clientTopForLine(component, 10) }); component.didMouseDownOnContent({ button: 1, clientX: clientLeftForCharacter(component, 10, 0), clientY: clientTopForLine(component, 10) }); expect(editor.lineTextForBufferRow(10)).toBe('var'); }); it('does not paste into a read only editor when clicking the middle mouse button on Linux', async () => { spyOn(electron.ipcRenderer, 'send').andCallFake(function( eventName, selectedText ) { if (eventName === 'write-text-to-selection-clipboard') { clipboard.writeText(selectedText, 'selection'); } }); const { component, editor } = buildComponent({ platform: 'linux', readOnly: true }); // Select the word 'sort' on line 2 and copy to clipboard editor.setSelectedBufferRange([[1, 6], [1, 10]]); await conditionPromise(() => TextEditor.clipboard.read() === 'sort'); // Middle-click in the buffer at line 11, column 1 component.didMouseDownOnContent({ button: 1, clientX: clientLeftForCharacter(component, 10, 0), clientY: clientTopForLine(component, 10) }); // Ensure that the correct text was copied but not pasted expect(TextEditor.clipboard.read()).toBe('sort'); expect(editor.lineTextForBufferRow(10)).toBe(''); }); }); describe('on the line number gutter', () => { it('selects all buffer rows intersecting the clicked screen row when a line number is clicked', async () => { const { component, editor } = buildComponent(); spyOn(component, 'handleMouseDragUntilMouseUp'); editor.setSoftWrapped(true); await component.getNextUpdatePromise(); await setEditorWidthInCharacters(component, 50); editor.foldBufferRange([[4, Infinity], [7, Infinity]]); await component.getNextUpdatePromise(); // Selects entire buffer line when clicked screen line is soft-wrapped component.didMouseDownOnLineNumberGutter({ button: 0, clientY: clientTopForLine(component, 3) }); expect(editor.getSelectedScreenRange()).toEqual([[3, 0], [5, 0]]); expect(editor.getSelectedBufferRange()).toEqual([[3, 0], [4, 0]]); // Selects entire screen line, even if folds cause that selection to // span multiple buffer lines component.didMouseDownOnLineNumberGutter({ button: 0, clientY: clientTopForLine(component, 5) }); expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]); expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [8, 0]]); }); it('adds new selections when a line number is meta-clicked', async () => { const { component, editor } = buildComponent(); editor.setSoftWrapped(true); await component.getNextUpdatePromise(); await setEditorWidthInCharacters(component, 50); editor.foldBufferRange([[4, Infinity], [7, Infinity]]); await component.getNextUpdatePromise(); // Selects entire buffer line when clicked screen line is soft-wrapped component.didMouseDownOnLineNumberGutter({ button: 0, metaKey: true, clientY: clientTopForLine(component, 3) }); expect(editor.getSelectedScreenRanges()).toEqual([ [[0, 0], [0, 0]], [[3, 0], [5, 0]] ]); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 0], [0, 0]], [[3, 0], [4, 0]] ]); // Selects entire screen line, even if folds cause that selection to // span multiple buffer lines component.didMouseDownOnLineNumberGutter({ button: 0, metaKey: true, clientY: clientTopForLine(component, 5) }); expect(editor.getSelectedScreenRanges()).toEqual([ [[0, 0], [0, 0]], [[3, 0], [5, 0]], [[5, 0], [6, 0]] ]); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 0], [0, 0]], [[3, 0], [4, 0]], [[4, 0], [8, 0]] ]); }); it('expands the last selection when a line number is shift-clicked', async () => { const { component, editor } = buildComponent(); spyOn(component, 'handleMouseDragUntilMouseUp'); editor.setSoftWrapped(true); await component.getNextUpdatePromise(); await setEditorWidthInCharacters(component, 50); editor.foldBufferRange([[4, Infinity], [7, Infinity]]); await component.getNextUpdatePromise(); editor.setSelectedScreenRange([[3, 4], [3, 8]]); editor.addCursorAtScreenPosition([2, 10]); component.didMouseDownOnLineNumberGutter({ button: 0, shiftKey: true, clientY: clientTopForLine(component, 5) }); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 4], [3, 8]], [[2, 10], [8, 0]] ]); // Original selection is preserved when shift-click-dragging const { didDrag, didStopDragging } = component.handleMouseDragUntilMouseUp.argsForCall[0][0]; didDrag({ clientY: clientTopForLine(component, 1) }); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 4], [3, 8]], [[1, 0], [2, 10]] ]); didDrag({ clientY: clientTopForLine(component, 5) }); didStopDragging(); expect(editor.getSelectedBufferRanges()).toEqual([[[2, 10], [8, 0]]]); }); it('expands the selection when dragging', async () => { const { component, editor } = buildComponent(); spyOn(component, 'handleMouseDragUntilMouseUp'); editor.setSoftWrapped(true); await component.getNextUpdatePromise(); await setEditorWidthInCharacters(component, 50); editor.foldBufferRange([[4, Infinity], [7, Infinity]]); await component.getNextUpdatePromise(); editor.setSelectedScreenRange([[3, 4], [3, 6]]); component.didMouseDownOnLineNumberGutter({ button: 0, metaKey: true, clientY: clientTopForLine(component, 2) }); const { didDrag, didStopDragging } = component.handleMouseDragUntilMouseUp.argsForCall[0][0]; didDrag({ clientY: clientTopForLine(component, 1) }); expect(editor.getSelectedScreenRanges()).toEqual([ [[3, 4], [3, 6]], [[1, 0], [3, 0]] ]); didDrag({ clientY: clientTopForLine(component, 5) }); expect(editor.getSelectedScreenRanges()).toEqual([ [[3, 4], [3, 6]], [[2, 0], [6, 0]] ]); expect(editor.isFoldedAtBufferRow(4)).toBe(true); didDrag({ clientY: clientTopForLine(component, 3) }); expect(editor.getSelectedScreenRanges()).toEqual([ [[3, 4], [3, 6]], [[2, 0], [4, 4]] ]); didStopDragging(); expect(editor.getSelectedScreenRanges()).toEqual([[[2, 0], [4, 4]]]); }); it('toggles folding when clicking on the right icon of a foldable line number', async () => { const { component, element, editor } = buildComponent(); let target = element .querySelectorAll('.line-number')[1] .querySelector('.icon-right'); expect(editor.isFoldedAtScreenRow(1)).toBe(false); component.didMouseDownOnLineNumberGutter({ target, button: 0, clientY: clientTopForLine(component, 1) }); expect(editor.isFoldedAtScreenRow(1)).toBe(true); await component.getNextUpdatePromise(); component.didMouseDownOnLineNumberGutter({ target, button: 0, clientY: clientTopForLine(component, 1) }); await component.getNextUpdatePromise(); expect(editor.isFoldedAtScreenRow(1)).toBe(false); editor.foldBufferRange([[5, 12], [5, 17]]); await component.getNextUpdatePromise(); expect(editor.isFoldedAtScreenRow(5)).toBe(true); 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 () => { const { component } = buildComponent({ width: 200, height: 200 }); spyOn(component, 'handleMouseDragUntilMouseUp'); let previousScrollTop = 0; let previousScrollLeft = 0; function assertScrolledDown() { expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop); previousScrollTop = component.getScrollTop(); expect(component.getScrollLeft()).toBe(previousScrollLeft); previousScrollLeft = component.getScrollLeft(); } function assertScrolledUp() { expect(component.getScrollTop()).toBeLessThan(previousScrollTop); previousScrollTop = component.getScrollTop(); expect(component.getScrollLeft()).toBe(previousScrollLeft); previousScrollLeft = component.getScrollLeft(); } component.didMouseDownOnLineNumberGutter({ detail: 1, button: 0, clientX: 0, clientY: 100 }); const { didDrag } = component.handleMouseDragUntilMouseUp.argsForCall[0][0]; didDrag({ clientX: 199, clientY: 199 }); assertScrolledDown(); didDrag({ clientX: 199, clientY: 199 }); assertScrolledDown(); didDrag({ clientX: 199, clientY: 199 }); assertScrolledDown(); didDrag({ clientX: component.getGutterContainerWidth() + 1, clientY: 1 }); assertScrolledUp(); didDrag({ clientX: component.getGutterContainerWidth() + 1, clientY: 1 }); assertScrolledUp(); didDrag({ clientX: component.getGutterContainerWidth() + 1, clientY: 1 }); assertScrolledUp(); // Don't artificially update scroll measurements beyond the minimum or // maximum possible scroll positions expect(component.getScrollTop()).toBe(0); expect(component.getScrollLeft()).toBe(0); didDrag({ clientX: component.getGutterContainerWidth() + 1, clientY: 1 }); expect(component.getScrollTop()).toBe(0); expect(component.getScrollLeft()).toBe(0); const maxScrollTop = component.getMaxScrollTop(); const maxScrollLeft = component.getMaxScrollLeft(); setScrollTop(component, maxScrollTop); await setScrollLeft(component, maxScrollLeft); didDrag({ clientX: 199, clientY: 199 }); didDrag({ clientX: 199, clientY: 199 }); didDrag({ clientX: 199, clientY: 199 }); expect(component.getScrollTop()).toBeNear(maxScrollTop); expect(component.getScrollLeft()).toBeNear(maxScrollLeft); }); }); describe('on the scrollbars', () => { it('delegates the mousedown events to the parent component unless the mousedown was on the actual scrollbar', async () => { const { component, editor } = buildComponent({ height: 100 }); await setEditorWidthInCharacters(component, 6); const verticalScrollbar = component.refs.verticalScrollbar; const horizontalScrollbar = component.refs.horizontalScrollbar; const leftEdgeOfVerticalScrollbar = verticalScrollbar.element.getBoundingClientRect().right - verticalScrollbarWidth; const topEdgeOfHorizontalScrollbar = horizontalScrollbar.element.getBoundingClientRect().bottom - horizontalScrollbarHeight; verticalScrollbar.didMouseDown({ button: 0, detail: 1, clientY: clientTopForLine(component, 4), clientX: leftEdgeOfVerticalScrollbar }); expect(editor.getCursorScreenPosition()).toEqual([0, 0]); verticalScrollbar.didMouseDown({ button: 0, detail: 1, clientY: clientTopForLine(component, 4), clientX: leftEdgeOfVerticalScrollbar - 1 }); expect(editor.getCursorScreenPosition()).toEqual([4, 6]); horizontalScrollbar.didMouseDown({ button: 0, detail: 1, clientY: topEdgeOfHorizontalScrollbar, clientX: component.refs.content.getBoundingClientRect().left }); expect(editor.getCursorScreenPosition()).toEqual([4, 6]); horizontalScrollbar.didMouseDown({ button: 0, detail: 1, clientY: topEdgeOfHorizontalScrollbar - 1, clientX: component.refs.content.getBoundingClientRect().left }); expect(editor.getCursorScreenPosition()).toEqual([4, 0]); }); }); }); describe('paste event', () => { it("prevents the browser's default processing for the event on Linux", () => { const { component } = buildComponent({ platform: 'linux' }); const event = { preventDefault: () => {} }; spyOn(event, 'preventDefault'); component.didPaste(event); expect(event.preventDefault).toHaveBeenCalled(); }); }); describe('keyboard input', () => { it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => { const { editor, component } = buildComponent({ text: '', chromeVersion: 57 }); editor.insertText('x'); editor.setCursorBufferPosition([0, 1]); // Simulate holding the A key to open the press-and-hold menu, // then closing it via ESC. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeydown({ code: 'KeyA' }); component.didKeydown({ code: 'KeyA' }); component.didKeyup({ code: 'KeyA' }); component.didKeydown({ code: 'Escape' }); component.didKeyup({ code: 'Escape' }); expect(editor.getText()).toBe('xa'); // Ensure another "a" can be typed correctly. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeyup({ code: 'KeyA' }); expect(editor.getText()).toBe('xaa'); editor.undo(); expect(editor.getText()).toBe('x'); // Simulate holding the A key to open the press-and-hold menu, // then selecting an alternative by typing a number. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeydown({ code: 'KeyA' }); component.didKeydown({ code: 'KeyA' }); component.didKeyup({ code: 'KeyA' }); component.didKeydown({ code: 'Digit2' }); component.didKeyup({ code: 'Digit2' }); component.didTextInput({ data: 'á', stopPropagation: () => {}, preventDefault: () => {} }); expect(editor.getText()).toBe('xá'); // Ensure another "a" can be typed correctly. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeyup({ code: 'KeyA' }); expect(editor.getText()).toBe('xáa'); editor.undo(); expect(editor.getText()).toBe('x'); // Simulate holding the A key to open the press-and-hold menu, // then selecting an alternative by clicking on it. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeydown({ code: 'KeyA' }); component.didKeydown({ code: 'KeyA' }); component.didKeyup({ code: 'KeyA' }); component.didTextInput({ data: 'á', stopPropagation: () => {}, preventDefault: () => {} }); expect(editor.getText()).toBe('xá'); // Ensure another "a" can be typed correctly. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeyup({ code: 'KeyA' }); expect(editor.getText()).toBe('xáa'); editor.undo(); expect(editor.getText()).toBe('x'); // Simulate holding the A key to open the press-and-hold menu, // cycling through the alternatives with the arrows, then selecting one of them with Enter. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeydown({ code: 'KeyA' }); component.didKeydown({ code: 'KeyA' }); component.didKeyup({ code: 'KeyA' }); component.didKeydown({ code: 'ArrowRight' }); component.didCompositionStart({ data: '' }); component.didCompositionUpdate({ data: 'à' }); component.didKeyup({ code: 'ArrowRight' }); expect(editor.getText()).toBe('xà'); component.didKeydown({ code: 'ArrowRight' }); component.didCompositionUpdate({ data: 'á' }); component.didKeyup({ code: 'ArrowRight' }); expect(editor.getText()).toBe('xá'); component.didKeydown({ code: 'Enter' }); component.didCompositionUpdate({ data: 'á' }); component.didTextInput({ data: 'á', stopPropagation: () => {}, preventDefault: () => {} }); component.didCompositionEnd({ data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput }); component.didKeyup({ code: 'Enter' }); expect(editor.getText()).toBe('xá'); // Ensure another "a" can be typed correctly. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeyup({ code: 'KeyA' }); expect(editor.getText()).toBe('xáa'); editor.undo(); expect(editor.getText()).toBe('x'); // Simulate holding the A key to open the press-and-hold menu, // cycling through the alternatives with the arrows, then closing it via ESC. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeydown({ code: 'KeyA' }); component.didKeydown({ code: 'KeyA' }); component.didKeyup({ code: 'KeyA' }); component.didKeydown({ code: 'ArrowRight' }); component.didCompositionStart({ data: '' }); component.didCompositionUpdate({ data: 'à' }); component.didKeyup({ code: 'ArrowRight' }); expect(editor.getText()).toBe('xà'); component.didKeydown({ code: 'ArrowRight' }); component.didCompositionUpdate({ data: 'á' }); component.didKeyup({ code: 'ArrowRight' }); expect(editor.getText()).toBe('xá'); component.didKeydown({ code: 'Escape' }); component.didCompositionUpdate({ data: 'a' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didCompositionEnd({ data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput }); component.didKeyup({ code: 'Escape' }); expect(editor.getText()).toBe('xa'); // Ensure another "a" can be typed correctly. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeyup({ code: 'KeyA' }); expect(editor.getText()).toBe('xaa'); editor.undo(); expect(editor.getText()).toBe('x'); // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key, // cycling through the alternatives with the arrows, then closing it via ESC. component.didKeydown({ code: 'KeyO' }); component.didKeypress({ code: 'KeyO' }); component.didTextInput({ data: 'o', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeyup({ code: 'KeyO' }); component.didKeydown({ code: 'KeyA' }); component.didKeydown({ code: 'KeyA' }); component.didKeydown({ code: 'ArrowRight' }); component.didCompositionStart({ data: '' }); component.didCompositionUpdate({ data: 'à' }); component.didKeyup({ code: 'ArrowRight' }); expect(editor.getText()).toBe('xoà'); component.didKeydown({ code: 'ArrowRight' }); component.didCompositionUpdate({ data: 'á' }); component.didKeyup({ code: 'ArrowRight' }); expect(editor.getText()).toBe('xoá'); component.didKeydown({ code: 'Escape' }); component.didCompositionUpdate({ data: 'a' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didCompositionEnd({ data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput }); component.didKeyup({ code: 'Escape' }); expect(editor.getText()).toBe('xoa'); // Ensure another "a" can be typed correctly. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeyup({ code: 'KeyA' }); editor.undo(); expect(editor.getText()).toBe('x'); // Simulate holding the A key to open the press-and-hold menu, // cycling through the alternatives with the arrows, then closing it by changing focus. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeydown({ code: 'KeyA' }); component.didKeydown({ code: 'KeyA' }); component.didKeyup({ code: 'KeyA' }); component.didKeydown({ code: 'ArrowRight' }); component.didCompositionStart({ data: '' }); component.didCompositionUpdate({ data: 'à' }); component.didKeyup({ code: 'ArrowRight' }); expect(editor.getText()).toBe('xà'); component.didKeydown({ code: 'ArrowRight' }); component.didCompositionUpdate({ data: 'á' }); component.didKeyup({ code: 'ArrowRight' }); expect(editor.getText()).toBe('xá'); component.didCompositionUpdate({ data: 'á' }); component.didTextInput({ data: 'á', stopPropagation: () => {}, preventDefault: () => {} }); component.didCompositionEnd({ data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput }); expect(editor.getText()).toBe('xá'); // Ensure another "a" can be typed correctly. component.didKeydown({ code: 'KeyA' }); component.didKeypress({ code: 'KeyA' }); component.didTextInput({ data: 'a', stopPropagation: () => {}, preventDefault: () => {} }); component.didKeyup({ code: 'KeyA' }); expect(editor.getText()).toBe('xáa'); editor.undo(); expect(editor.getText()).toBe('x'); }); }); describe('styling changes', () => { it('updates the rendered content based on new measurements when the font dimensions change', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 1, autoHeight: false }); await setEditorHeightInLines(component, 3); editor.setCursorScreenPosition([1, 29], { autoscroll: false }); await component.getNextUpdatePromise(); let cursorNode = element.querySelector('.cursor'); const initialBaseCharacterWidth = editor.getDefaultCharWidth(); const initialDoubleCharacterWidth = editor.getDoubleWidthCharWidth(); const initialHalfCharacterWidth = editor.getHalfWidthCharWidth(); const initialKoreanCharacterWidth = editor.getKoreanCharWidth(); const initialRenderedLineCount = queryOnScreenLineElements(element) .length; const initialFontSize = parseInt(getComputedStyle(element).fontSize); expect(initialKoreanCharacterWidth).toBeDefined(); expect(initialDoubleCharacterWidth).toBeDefined(); expect(initialHalfCharacterWidth).toBeDefined(); expect(initialBaseCharacterWidth).toBeDefined(); expect(initialDoubleCharacterWidth).not.toBe(initialBaseCharacterWidth); expect(initialHalfCharacterWidth).not.toBe(initialBaseCharacterWidth); expect(initialKoreanCharacterWidth).not.toBe(initialBaseCharacterWidth); verifyCursorPosition(component, cursorNode, 1, 29); element.style.fontSize = initialFontSize - 5 + 'px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(editor.getDefaultCharWidth()).toBeLessThan( initialBaseCharacterWidth ); expect(editor.getDoubleWidthCharWidth()).toBeLessThan( initialDoubleCharacterWidth ); expect(editor.getHalfWidthCharWidth()).toBeLessThan( initialHalfCharacterWidth ); expect(editor.getKoreanCharWidth()).toBeLessThan( initialKoreanCharacterWidth ); expect(queryOnScreenLineElements(element).length).toBeGreaterThan( initialRenderedLineCount ); verifyCursorPosition(component, cursorNode, 1, 29); element.style.fontSize = initialFontSize + 10 + 'px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(editor.getDefaultCharWidth()).toBeGreaterThan( initialBaseCharacterWidth ); expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan( initialDoubleCharacterWidth ); expect(editor.getHalfWidthCharWidth()).toBeGreaterThan( initialHalfCharacterWidth ); expect(editor.getKoreanCharWidth()).toBeGreaterThan( initialKoreanCharacterWidth ); expect(queryOnScreenLineElements(element).length).toBeLessThan( initialRenderedLineCount ); verifyCursorPosition(component, cursorNode, 1, 29); }); it('maintains the scrollTopRow and scrollLeftColumn when the font size changes', async () => { const { component, element } = buildComponent({ rowsPerTile: 1, autoHeight: false }); await setEditorHeightInLines(component, 3); await setEditorWidthInCharacters(component, 20); component.setScrollTopRow(4); component.setScrollLeftColumn(10); await component.getNextUpdatePromise(); const initialFontSize = parseInt(getComputedStyle(element).fontSize); element.style.fontSize = initialFontSize - 5 + 'px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(component.getScrollTopRow()).toBe(4); element.style.fontSize = initialFontSize + 5 + 'px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); expect(component.getScrollTopRow()).toBe(4); }); it('gracefully handles the editor being hidden after a styling change', async () => { const { component, element } = buildComponent({ autoHeight: false }); element.style.fontSize = parseInt(getComputedStyle(element).fontSize) + 5 + 'px'; TextEditor.didUpdateStyles(); element.style.display = 'none'; await component.getNextUpdatePromise(); }); it('does not throw an exception when the editor is soft-wrapped and changing the font size changes also the longest screen line', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 3, autoHeight: false }); editor.setText( 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do\n' + 'eiusmod tempor incididunt ut labore et dolore magna' + 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation' ); editor.setSoftWrapped(true); await setEditorHeightInLines(component, 2); await setEditorWidthInCharacters(component, 56); await setScrollTop(component, 3 * component.getLineHeight()); element.style.fontSize = '20px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); }); it('updates the width of the lines div based on the longest screen line', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 1, autoHeight: false }); editor.setText( 'Lorem ipsum dolor sit\n' + 'amet, consectetur adipisicing\n' + 'elit, sed do\n' + 'eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation' ); await setEditorHeightInLines(component, 2); element.style.fontSize = '20px'; TextEditor.didUpdateStyles(); await component.getNextUpdatePromise(); // Capture the width of the lines before requesting the width of // longest line, because making that request forces a DOM update const actualWidth = element.querySelector('.lines').style.width; const expectedWidth = Math.ceil( component.pixelPositionForScreenPosition(Point(3, Infinity)).left + component.getBaseCharacterWidth() ); expect(actualWidth).toBe(expectedWidth + 'px'); }); }); describe('synchronous updates', () => { let editorElementWasUpdatedSynchronously; beforeEach(() => { editorElementWasUpdatedSynchronously = TextEditorElement.prototype.updatedSynchronously; }); afterEach(() => { TextEditorElement.prototype.setUpdatedSynchronously( editorElementWasUpdatedSynchronously ); }); it('updates synchronously when updatedSynchronously is true', () => { const editor = buildEditor(); const { element } = new TextEditorComponent({ model: editor, updatedSynchronously: true }); jasmine.attachToDOM(element); editor.setText('Lorem ipsum dolor'); expect( queryOnScreenLineElements(element).map(l => l.textContent) ).toEqual([editor.lineTextForScreenRow(0)]); }); it('does not throw an exception on attachment when setting the soft-wrap column', () => { const { element, editor } = buildComponent({ width: 435, attach: false, updatedSynchronously: true }); editor.setSoftWrapped(true); spyOn(window, 'onerror').andCallThrough(); jasmine.attachToDOM(element); // should not throw an exception expect(window.onerror).not.toHaveBeenCalled(); }); it('updates synchronously when creating a component via TextEditor and TextEditorElement.prototype.updatedSynchronously is true', () => { TextEditorElement.prototype.setUpdatedSynchronously(true); const editor = buildEditor(); const element = editor.element; jasmine.attachToDOM(element); editor.setText('Lorem ipsum dolor'); expect( queryOnScreenLineElements(element).map(l => l.textContent) ).toEqual([editor.lineTextForScreenRow(0)]); }); it('measures dimensions synchronously when measureDimensions is called on the component', () => { TextEditorElement.prototype.setUpdatedSynchronously(true); const editor = buildEditor({ autoHeight: false }); const element = editor.element; jasmine.attachToDOM(element); element.style.height = '100px'; expect(element.component.getClientContainerHeight()).not.toBe(100); element.component.measureDimensions(); expect(element.component.getClientContainerHeight()).toBe(100); }); }); describe('pixelPositionForScreenPosition(point)', () => { it('returns the pixel position for the given point, regardless of whether or not it is currently on screen', async () => { const { component, editor } = buildComponent({ rowsPerTile: 2, autoHeight: false }); await setEditorHeightInLines(component, 3); await setScrollTop(component, 3 * component.getLineHeight()); const { component: referenceComponent } = buildComponent(); const referenceContentRect = referenceComponent.refs.content.getBoundingClientRect(); { const { top, left } = component.pixelPositionForScreenPosition({ row: 0, column: 0 }); expect(top).toBe( clientTopForLine(referenceComponent, 0) - referenceContentRect.top ); expect(left).toBe( clientLeftForCharacter(referenceComponent, 0, 0) - referenceContentRect.left ); } { const { top, left } = component.pixelPositionForScreenPosition({ row: 0, column: 5 }); expect(top).toBe( clientTopForLine(referenceComponent, 0) - referenceContentRect.top ); expect(left).toBeNear( clientLeftForCharacter(referenceComponent, 0, 5) - referenceContentRect.left ); } { const { top, left } = component.pixelPositionForScreenPosition({ row: 12, column: 1 }); expect(top).toBeNear( clientTopForLine(referenceComponent, 12) - referenceContentRect.top ); expect(left).toBeNear( clientLeftForCharacter(referenceComponent, 12, 1) - referenceContentRect.left ); } // Measuring a currently rendered line while an autoscroll that causes // that line to go off-screen is in progress. { editor.setCursorScreenPosition([10, 0]); const { top, left } = component.pixelPositionForScreenPosition({ row: 3, column: 5 }); expect(top).toBeNear( clientTopForLine(referenceComponent, 3) - referenceContentRect.top ); expect(left).toBeNear( clientLeftForCharacter(referenceComponent, 3, 5) - referenceContentRect.left ); } }); it('does not get the component into an inconsistent state when the model has unflushed changes (regression)', async () => { const { component, editor } = buildComponent({ rowsPerTile: 2, autoHeight: false, text: '' }); await setEditorHeightInLines(component, 10); const updatePromise = editor.getBuffer().append('hi\n'); component.screenPositionForPixelPosition({ top: 800, left: 1 }); await updatePromise; }); it('does not shift cursors downward or render off-screen content when measuring off-screen lines (regression)', async () => { const { component, element } = buildComponent({ rowsPerTile: 2, autoHeight: false }); await setEditorHeightInLines(component, 3); component.pixelPositionForScreenPosition({ row: 12, column: 1 }); expect(element.querySelector('.cursor').getBoundingClientRect().top).toBe( component.refs.lineTiles.getBoundingClientRect().top ); expect( element.querySelector('.line[data-screen-row="12"]').style.visibility ).toBe('hidden'); // Ensure previously measured off screen lines don't have any weird // styling when they come on screen in the next frame await setEditorHeightInLines(component, 13); const previouslyMeasuredLineElement = element.querySelector( '.line[data-screen-row="12"]' ); expect(previouslyMeasuredLineElement.style.display).toBe(''); expect(previouslyMeasuredLineElement.style.visibility).toBe(''); }); }); describe('screenPositionForPixelPosition', () => { it('returns the screen position for the given pixel position, regardless of whether or not it is currently on screen', async () => { const { component, editor } = buildComponent({ rowsPerTile: 2, autoHeight: false }); await setEditorHeightInLines(component, 3); await setScrollTop(component, 3 * component.getLineHeight()); const { component: referenceComponent } = buildComponent(); { const pixelPosition = referenceComponent.pixelPositionForScreenPosition( { row: 0, column: 0 } ); pixelPosition.top += component.getLineHeight() / 3; pixelPosition.left += component.getBaseCharacterWidth() / 3; expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual( [0, 0] ); } { const pixelPosition = referenceComponent.pixelPositionForScreenPosition( { row: 0, column: 5 } ); pixelPosition.top += component.getLineHeight() / 3; pixelPosition.left += component.getBaseCharacterWidth() / 3; expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual( [0, 5] ); } { const pixelPosition = referenceComponent.pixelPositionForScreenPosition( { row: 5, column: 7 } ); pixelPosition.top += component.getLineHeight() / 3; pixelPosition.left += component.getBaseCharacterWidth() / 3; expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual( [5, 7] ); } { const pixelPosition = referenceComponent.pixelPositionForScreenPosition( { row: 12, column: 1 } ); pixelPosition.top += component.getLineHeight() / 3; pixelPosition.left += component.getBaseCharacterWidth() / 3; expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual( [12, 1] ); } // Measuring a currently rendered line while an autoscroll that causes // that line to go off-screen is in progress. { const pixelPosition = referenceComponent.pixelPositionForScreenPosition( { row: 3, column: 4 } ); pixelPosition.top += component.getLineHeight() / 3; pixelPosition.left += component.getBaseCharacterWidth() / 3; editor.setCursorBufferPosition([10, 0]); expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual( [3, 4] ); } }); }); describe('model methods that delegate to the component / element', () => { it('delegates setHeight and getHeight to the component', async () => { const { component, editor } = buildComponent({ autoHeight: false }); spyOn(Grim, 'deprecate'); expect(editor.getHeight()).toBe(component.getScrollContainerHeight()); expect(Grim.deprecate.callCount).toBe(1); editor.setHeight(100); await component.getNextUpdatePromise(); expect(component.getScrollContainerHeight()).toBe(100); expect(Grim.deprecate.callCount).toBe(2); }); it('delegates setWidth and getWidth to the component', async () => { const { component, editor } = buildComponent(); spyOn(Grim, 'deprecate'); expect(editor.getWidth()).toBe(component.getScrollContainerWidth()); expect(Grim.deprecate.callCount).toBe(1); editor.setWidth(100); await component.getNextUpdatePromise(); expect(component.getScrollContainerWidth()).toBe(100); expect(Grim.deprecate.callCount).toBe(2); }); it('delegates getFirstVisibleScreenRow, getLastVisibleScreenRow, and getVisibleRowRange to the component', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 3, autoHeight: false }); element.style.height = 4 * component.measurements.lineHeight + 'px'; await component.getNextUpdatePromise(); await setScrollTop(component, 5 * component.getLineHeight()); expect(editor.getFirstVisibleScreenRow()).toBe( component.getFirstVisibleRow() ); expect(editor.getLastVisibleScreenRow()).toBe( component.getLastVisibleRow() ); expect(editor.getVisibleRowRange()).toEqual([ component.getFirstVisibleRow(), component.getLastVisibleRow() ]); }); it('assigns scrollTop on the component when calling setFirstVisibleScreenRow', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 3, autoHeight: false }); element.style.height = 4 * component.measurements.lineHeight + horizontalScrollbarHeight + 'px'; await component.getNextUpdatePromise(); expect(component.getMaxScrollTop() / component.getLineHeight()).toBeNear( 9 ); expect(component.refs.verticalScrollbar.element.scrollTop).toBe( 0 * component.getLineHeight() ); editor.setFirstVisibleScreenRow(1); expect(component.getFirstVisibleRow()).toBe(1); await component.getNextUpdatePromise(); expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear( 1 * component.getLineHeight() ); editor.setFirstVisibleScreenRow(5); expect(component.getFirstVisibleRow()).toBe(5); await component.getNextUpdatePromise(); expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear( 5 * component.getLineHeight() ); editor.setFirstVisibleScreenRow(11); expect(component.getFirstVisibleRow()).toBe(9); await component.getNextUpdatePromise(); expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear( 9 * component.getLineHeight() ); }); it('delegates setFirstVisibleScreenColumn and getFirstVisibleScreenColumn to the component', async () => { const { component, element, editor } = buildComponent({ rowsPerTile: 3, autoHeight: false }); element.style.width = 30 * component.getBaseCharacterWidth() + 'px'; await component.getNextUpdatePromise(); expect(editor.getFirstVisibleScreenColumn()).toBe(0); expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(0); setScrollLeft(component, 5.5 * component.getBaseCharacterWidth()); expect(editor.getFirstVisibleScreenColumn()).toBe(5); await component.getNextUpdatePromise(); expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeCloseTo( 5.5 * component.getBaseCharacterWidth(), -1 ); editor.setFirstVisibleScreenColumn(12); expect(component.getScrollLeft()).toBeCloseTo( 12 * component.getBaseCharacterWidth(), -1 ); await component.getNextUpdatePromise(); expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeCloseTo( 12 * component.getBaseCharacterWidth(), -1 ); }); }); describe('handleMouseDragUntilMouseUp', () => { it('repeatedly schedules `didDrag` calls on new animation frames after moving the mouse, and calls `didStopDragging` on mouseup', async () => { const { component } = buildComponent(); let dragEvents; let dragging = false; component.handleMouseDragUntilMouseUp({ didDrag: event => { dragging = true; dragEvents.push(event); }, didStopDragging: () => { dragging = false; } }); expect(dragging).toBe(false); dragEvents = []; const moveEvent1 = new MouseEvent('mousemove'); window.dispatchEvent(moveEvent1); expect(dragging).toBe(false); await getNextAnimationFramePromise(); expect(dragging).toBe(true); expect(dragEvents).toEqual([moveEvent1]); await getNextAnimationFramePromise(); expect(dragging).toBe(true); expect(dragEvents).toEqual([moveEvent1, moveEvent1]); dragEvents = []; const moveEvent2 = new MouseEvent('mousemove'); window.dispatchEvent(moveEvent2); expect(dragging).toBe(true); expect(dragEvents).toEqual([]); await getNextAnimationFramePromise(); expect(dragging).toBe(true); expect(dragEvents).toEqual([moveEvent2]); await getNextAnimationFramePromise(); expect(dragging).toBe(true); expect(dragEvents).toEqual([moveEvent2, moveEvent2]); dragEvents = []; window.dispatchEvent(new MouseEvent('mouseup')); expect(dragging).toBe(false); expect(dragEvents).toEqual([]); window.dispatchEvent(new MouseEvent('mousemove')); await getNextAnimationFramePromise(); expect(dragging).toBe(false); expect(dragEvents).toEqual([]); }); it('calls `didStopDragging` if the user interacts with the keyboard while dragging', async () => { const { component, editor } = buildComponent(); let dragging = false; function startDragging() { component.handleMouseDragUntilMouseUp({ didDrag: event => { dragging = true; }, didStopDragging: () => { dragging = false; } }); } startDragging(); window.dispatchEvent(new MouseEvent('mousemove')); await getNextAnimationFramePromise(); expect(dragging).toBe(true); // 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); // Pressing a modifier key does not terminate dragging, (to ensure we can add new selections with the mouse) startDragging(); window.dispatchEvent(new MouseEvent('mousemove')); await getNextAnimationFramePromise(); expect(dragging).toBe(true); component.didKeydown({ key: 'Control' }); component.didKeydown({ key: 'Alt' }); component.didKeydown({ key: 'Shift' }); component.didKeydown({ key: 'Meta' }); expect(dragging).toBe(true); }); function getNextAnimationFramePromise() { return new Promise(resolve => requestAnimationFrame(resolve)); } }); }); function buildEditor(params = {}) { const text = params.text != null ? params.text : SAMPLE_TEXT; const buffer = new TextBuffer({ text }); const editorParams = { buffer, readOnly: params.readOnly }; if (params.height != null) params.autoHeight = false; for (const paramName of [ 'mini', 'autoHeight', 'autoWidth', 'lineNumberGutterVisible', 'showLineNumbers', 'placeholderText', 'softWrapped', 'scrollSensitivity' ]) { if (params[paramName] != null) editorParams[paramName] = params[paramName]; } atom.grammars.autoAssignLanguageMode(buffer); const editor = new TextEditor(editorParams); editor.testAutoscrollRequests = []; editor.onDidRequestAutoscroll(request => { editor.testAutoscrollRequests.push(request); }); editors.push(editor); return editor; } function buildComponent(params = {}) { const editor = params.editor || buildEditor(params); const component = new TextEditorComponent({ model: editor, rowsPerTile: params.rowsPerTile, updatedSynchronously: params.updatedSynchronously || false, platform: params.platform, chromeVersion: params.chromeVersion }); const { element } = component; if (!editor.getAutoHeight()) { element.style.height = params.height ? params.height + 'px' : '600px'; } if (!editor.getAutoWidth()) { element.style.width = params.width ? params.width + 'px' : '800px'; } if (params.attach !== false) jasmine.attachToDOM(element); return { component, element, editor }; } function getEditorWidthInBaseCharacters(component) { return Math.round( component.getScrollContainerWidth() / component.getBaseCharacterWidth() ); } async function setEditorHeightInLines(component, heightInLines) { component.element.style.height = component.getLineHeight() * heightInLines + 'px'; await component.getNextUpdatePromise(); } async function setEditorWidthInCharacters(component, widthInCharacters) { component.element.style.width = component.getGutterContainerWidth() + widthInCharacters * component.measurements.baseCharacterWidth + verticalScrollbarWidth + 'px'; await component.getNextUpdatePromise(); } function verifyCursorPosition(component, cursorNode, row, column) { const rect = cursorNode.getBoundingClientRect(); expect(Math.round(rect.top)).toBeNear(clientTopForLine(component, row)); expect(Math.round(rect.left)).toBe( Math.round(clientLeftForCharacter(component, row, column)) ); } function clientTopForLine(component, row) { return lineNodeForScreenRow(component, row).getBoundingClientRect().top; } function clientLeftForCharacter(component, row, column) { const textNodes = textNodesForScreenRow(component, row); let textNodeStartColumn = 0; for (const textNode of textNodes) { const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length; if (column < textNodeEndColumn) { const range = document.createRange(); range.setStart(textNode, column - textNodeStartColumn); range.setEnd(textNode, column - textNodeStartColumn); return range.getBoundingClientRect().left; } textNodeStartColumn = textNodeEndColumn; } const lastTextNode = textNodes[textNodes.length - 1]; const range = document.createRange(); range.setStart(lastTextNode, 0); range.setEnd(lastTextNode, lastTextNode.textContent.length); return range.getBoundingClientRect().right; } function clientPositionForCharacter(component, row, column) { return { clientX: clientLeftForCharacter(component, row, column), clientY: clientTopForLine(component, row) }; } function lineNumberNodeForScreenRow(component, row) { const gutterElement = component.refs.gutterContainer.refs.lineNumberGutter.element; const tileStartRow = component.tileStartRowForRow(row); const tileIndex = component.renderedTileStartRows.indexOf(tileStartRow); return gutterElement.children[tileIndex + 1].children[row - tileStartRow]; } function lineNodeForScreenRow(component, row) { const renderedScreenLine = component.renderedScreenLineForRow(row); return component.lineComponentsByScreenLineId.get(renderedScreenLine.id) .element; } function textNodesForScreenRow(component, row) { const screenLine = component.renderedScreenLineForRow(row); return component.lineComponentsByScreenLineId.get(screenLine.id).textNodes; } function setScrollTop(component, scrollTop) { component.setScrollTop(scrollTop); component.scheduleUpdate(); return component.getNextUpdatePromise(); } function setScrollLeft(component, scrollLeft) { component.setScrollLeft(scrollLeft); component.scheduleUpdate(); return component.getNextUpdatePromise(); } function getHorizontalScrollbarHeight(component) { const element = component.refs.horizontalScrollbar.element; return element.offsetHeight - element.clientHeight; } function getVerticalScrollbarWidth(component) { const element = component.refs.verticalScrollbar.element; return element.offsetWidth - element.clientWidth; } function assertDocumentFocused() { if (!document.hasFocus()) { throw new Error('The document needs to be focused to run this test'); } } function getElementHeight(element) { const topRuler = document.createElement('div'); const bottomRuler = document.createElement('div'); let height; if (document.body.contains(element)) { element.parentElement.insertBefore(topRuler, element); element.parentElement.insertBefore(bottomRuler, element.nextSibling); height = bottomRuler.offsetTop - topRuler.offsetTop; } else { jasmine.attachToDOM(topRuler); jasmine.attachToDOM(element); jasmine.attachToDOM(bottomRuler); height = bottomRuler.offsetTop - topRuler.offsetTop; element.remove(); } topRuler.remove(); bottomRuler.remove(); return height; } function queryOnScreenLineNumberElements(element) { return Array.from(element.querySelectorAll('.line-number:not(.dummy)')); } function queryOnScreenLineElements(element) { return Array.from( element.querySelectorAll('.line:not(.dummy):not([data-off-screen])') ); } ================================================ FILE: spec/text-editor-element-spec.js ================================================ const TextEditor = require('../src/text-editor'); const TextEditorElement = require('../src/text-editor-element'); describe('TextEditorElement', () => { let jasmineContent; beforeEach(() => { jasmineContent = document.body.querySelector('#jasmine-content'); // Force scrollbars to be visible regardless of local system configuration const scrollbarStyle = document.createElement('style'); scrollbarStyle.textContent = 'atom-text-editor ::-webkit-scrollbar { -webkit-appearance: none }'; jasmine.attachToDOM(scrollbarStyle); }); function buildTextEditorElement(options = {}) { const element = TextEditorElement.createTextEditorElement(); element.setUpdatedSynchronously(false); if (options.attach !== false) jasmine.attachToDOM(element); return element; } it("honors the 'mini' attribute", () => { jasmineContent.innerHTML = ''; const element = jasmineContent.firstChild; expect(element.getModel().isMini()).toBe(true); element.removeAttribute('mini'); expect(element.getModel().isMini()).toBe(false); expect(element.getComponent().getGutterContainerWidth()).toBe(0); element.setAttribute('mini', ''); expect(element.getModel().isMini()).toBe(true); }); it('sets the editor to mini if the model is accessed prior to attaching the element', () => { const parent = document.createElement('div'); parent.innerHTML = ''; const element = parent.firstChild; expect(element.getModel().isMini()).toBe(true); }); it("honors the 'placeholder-text' attribute", () => { jasmineContent.innerHTML = ""; const element = jasmineContent.firstChild; expect(element.getModel().getPlaceholderText()).toBe('testing'); element.setAttribute('placeholder-text', 'placeholder'); expect(element.getModel().getPlaceholderText()).toBe('placeholder'); element.removeAttribute('placeholder-text'); expect(element.getModel().getPlaceholderText()).toBeNull(); }); it("only assigns 'placeholder-text' on the model if the attribute is present", () => { const editor = new TextEditor({ placeholderText: 'placeholder' }); editor.getElement(); expect(editor.getPlaceholderText()).toBe('placeholder'); }); it("honors the 'gutter-hidden' attribute", () => { jasmineContent.innerHTML = ''; const element = jasmineContent.firstChild; expect(element.getModel().isLineNumberGutterVisible()).toBe(false); element.removeAttribute('gutter-hidden'); expect(element.getModel().isLineNumberGutterVisible()).toBe(true); element.setAttribute('gutter-hidden', ''); expect(element.getModel().isLineNumberGutterVisible()).toBe(false); }); it("honors the 'readonly' attribute", async function() { jasmineContent.innerHTML = ''; const element = jasmineContent.firstChild; expect(element.getComponent().isInputEnabled()).toBe(false); element.removeAttribute('readonly'); expect(element.getComponent().isInputEnabled()).toBe(true); element.setAttribute('readonly', true); expect(element.getComponent().isInputEnabled()).toBe(false); }); it('honors the text content', () => { jasmineContent.innerHTML = 'testing'; const element = jasmineContent.firstChild; expect(element.getModel().getText()).toBe('testing'); }); describe('tabIndex', () => { it('uses a default value of -1', () => { jasmineContent.innerHTML = ''; const element = jasmineContent.firstChild; expect(element.tabIndex).toBe(-1); expect(element.querySelector('input').tabIndex).toBe(-1); }); it('uses the custom value when given', () => { jasmineContent.innerHTML = ''; const element = jasmineContent.firstChild; expect(element.tabIndex).toBe(-1); expect(element.querySelector('input').tabIndex).toBe(42); }); }); describe('when the model is assigned', () => it("adds the 'mini' attribute if .isMini() returns true on the model", async () => { const element = buildTextEditorElement(); element.getModel().update({ mini: true }); await atom.views.getNextUpdatePromise(); expect(element.hasAttribute('mini')).toBe(true); })); describe('when the editor is attached to the DOM', () => it('mounts the component and unmounts when removed from the dom', () => { const element = buildTextEditorElement(); const { component } = element; expect(component.attached).toBe(true); element.remove(); expect(component.attached).toBe(false); jasmine.attachToDOM(element); expect(element.component.attached).toBe(true); })); describe('when the editor is detached from the DOM and then reattached', () => { it('does not render duplicate line numbers', () => { const editor = new TextEditor(); editor.setText('1\n2\n3'); const element = editor.getElement(); jasmine.attachToDOM(element); const initialCount = element.querySelectorAll('.line-number').length; element.remove(); jasmine.attachToDOM(element); expect(element.querySelectorAll('.line-number').length).toBe( initialCount ); }); it('does not render duplicate decorations in custom gutters', () => { const editor = new TextEditor(); editor.setText('1\n2\n3'); editor.addGutter({ name: 'test-gutter' }); const marker = editor.markBufferRange([[0, 0], [2, 0]]); editor.decorateMarker(marker, { type: 'gutter', gutterName: 'test-gutter' }); const element = editor.getElement(); jasmine.attachToDOM(element); const initialDecorationCount = element.querySelectorAll('.decoration') .length; element.remove(); jasmine.attachToDOM(element); expect(element.querySelectorAll('.decoration').length).toBe( initialDecorationCount ); }); it('can be re-focused using the previous `document.activeElement`', () => { const editorElement = buildTextEditorElement(); editorElement.focus(); const { activeElement } = document; editorElement.remove(); jasmine.attachToDOM(editorElement); activeElement.focus(); expect(editorElement.hasFocus()).toBe(true); }); }); describe('focus and blur handling', () => { it('proxies focus/blur events to/from the hidden input', () => { const element = buildTextEditorElement(); jasmineContent.appendChild(element); let blurCalled = false; element.addEventListener('blur', () => { blurCalled = true; }); element.focus(); expect(blurCalled).toBe(false); expect(element.hasFocus()).toBe(true); expect(document.activeElement).toBe(element.querySelector('input')); document.body.focus(); expect(blurCalled).toBe(true); }); it("doesn't trigger a blur event on the editor element when focusing an already focused editor element", () => { let blurCalled = false; const element = buildTextEditorElement(); element.addEventListener('blur', () => { blurCalled = true; }); jasmineContent.appendChild(element); expect(document.activeElement).toBe(document.body); expect(blurCalled).toBe(false); element.focus(); expect(document.activeElement).toBe(element.querySelector('input')); expect(blurCalled).toBe(false); element.focus(); expect(document.activeElement).toBe(element.querySelector('input')); expect(blurCalled).toBe(false); }); describe('when focused while a parent node is being attached to the DOM', () => { class ElementThatFocusesChild extends HTMLElement { connectedCallback() { this.firstChild.focus(); } } window.customElements.define( 'element-that-focuses-child', ElementThatFocusesChild ); it('proxies the focus event to the hidden input', () => { const element = buildTextEditorElement(); const parentElement = document.createElement( 'element-that-focuses-child' ); parentElement.appendChild(element); jasmineContent.appendChild(parentElement); expect(document.activeElement).toBe(element.querySelector('input')); }); }); describe('if focused when invisible due to a zero height and width', () => { it('focuses the hidden input and does not throw an exception', () => { const parentElement = document.createElement('div'); parentElement.style.position = 'absolute'; parentElement.style.width = '0px'; parentElement.style.height = '0px'; const element = buildTextEditorElement({ attach: false }); parentElement.appendChild(element); jasmineContent.appendChild(parentElement); element.focus(); expect(document.activeElement).toBe(element.component.getHiddenInput()); }); }); }); describe('::setModel', () => { describe('when the element does not have an editor yet', () => { it('uses the supplied one', () => { const element = buildTextEditorElement({ attach: false }); const editor = new TextEditor(); element.setModel(editor); jasmine.attachToDOM(element); expect(editor.element).toBe(element); expect(element.getModel()).toBe(editor); }); }); describe('when the element already has an editor', () => { it('unbinds it and then swaps it with the supplied one', async () => { const element = buildTextEditorElement({ attach: true }); const previousEditor = element.getModel(); expect(previousEditor.element).toBe(element); const newEditor = new TextEditor(); element.setModel(newEditor); expect(previousEditor.element).not.toBe(element); expect(newEditor.element).toBe(element); expect(element.getModel()).toBe(newEditor); }); }); }); describe('::onDidAttach and ::onDidDetach', () => it('invokes callbacks when the element is attached and detached', () => { const element = buildTextEditorElement({ attach: false }); const attachedCallback = jasmine.createSpy('attachedCallback'); const detachedCallback = jasmine.createSpy('detachedCallback'); element.onDidAttach(attachedCallback); element.onDidDetach(detachedCallback); jasmine.attachToDOM(element); expect(attachedCallback).toHaveBeenCalled(); expect(detachedCallback).not.toHaveBeenCalled(); attachedCallback.reset(); element.remove(); expect(attachedCallback).not.toHaveBeenCalled(); expect(detachedCallback).toHaveBeenCalled(); })); describe('::setUpdatedSynchronously', () => { it('controls whether the text editor is updated synchronously', () => { spyOn(window, 'requestAnimationFrame').andCallFake(fn => fn()); const element = buildTextEditorElement(); expect(element.isUpdatedSynchronously()).toBe(false); element.getModel().setText('hello'); expect(window.requestAnimationFrame).toHaveBeenCalled(); expect(element.textContent).toContain('hello'); window.requestAnimationFrame.reset(); element.setUpdatedSynchronously(true); element.getModel().setText('goodbye'); expect(window.requestAnimationFrame).not.toHaveBeenCalled(); expect(element.textContent).toContain('goodbye'); }); }); describe('::getDefaultCharacterWidth', () => { it('returns 0 before the element is attached', () => { const element = buildTextEditorElement({ attach: false }); expect(element.getDefaultCharacterWidth()).toBe(0); }); it('returns the width of a character in the root scope', () => { const element = buildTextEditorElement(); jasmine.attachToDOM(element); expect(element.getDefaultCharacterWidth()).toBeGreaterThan(0); }); }); describe('::getMaxScrollTop', () => it('returns the maximum scroll top that can be applied to the element', async () => { const editor = new TextEditor(); editor.setText('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16'); const element = editor.getElement(); element.style.lineHeight = '10px'; element.style.width = '200px'; jasmine.attachToDOM(element); const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight(); expect(element.getMaxScrollTop()).toBe(0); await editor.update({ autoHeight: false }); element.style.height = 100 + horizontalScrollbarHeight + 'px'; await element.getNextUpdatePromise(); expect(element.getMaxScrollTop()).toBe(60); element.style.height = 120 + horizontalScrollbarHeight + 'px'; await element.getNextUpdatePromise(); expect(element.getMaxScrollTop()).toBe(40); element.style.height = 200 + horizontalScrollbarHeight + 'px'; await element.getNextUpdatePromise(); expect(element.getMaxScrollTop()).toBe(0); })); describe('::setScrollTop and ::setScrollLeft', () => { it('changes the scroll position', async () => { const element = buildTextEditorElement(); element.getModel().update({ autoHeight: false }); element.getModel().setText('lorem\nipsum\ndolor\nsit\namet'); element.setHeight(20); await element.getNextUpdatePromise(); element.setWidth(20); await element.getNextUpdatePromise(); element.setScrollTop(22); await element.getNextUpdatePromise(); expect(element.getScrollTop()).toBe(22); element.setScrollLeft(32); await element.getNextUpdatePromise(); expect(element.getScrollLeft()).toBe(32); }); }); describe('on TextEditor::setMini', () => it("changes the element's 'mini' attribute", async () => { const element = buildTextEditorElement(); expect(element.hasAttribute('mini')).toBe(false); element.getModel().setMini(true); await element.getNextUpdatePromise(); expect(element.hasAttribute('mini')).toBe(true); element.getModel().setMini(false); await element.getNextUpdatePromise(); expect(element.hasAttribute('mini')).toBe(false); })); describe('::intersectsVisibleRowRange(start, end)', () => { it('returns true if the given row range intersects the visible row range', async () => { const element = buildTextEditorElement(); const editor = element.getModel(); const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight(); editor.update({ autoHeight: false }); element.getModel().setText('x\n'.repeat(20)); element.style.height = 120 + horizontalScrollbarHeight + 'px'; await element.getNextUpdatePromise(); element.setScrollTop(80); await element.getNextUpdatePromise(); expect(element.getVisibleRowRange()).toEqual([4, 11]); expect(element.intersectsVisibleRowRange(0, 4)).toBe(false); expect(element.intersectsVisibleRowRange(0, 5)).toBe(true); expect(element.intersectsVisibleRowRange(5, 8)).toBe(true); expect(element.intersectsVisibleRowRange(11, 12)).toBe(false); expect(element.intersectsVisibleRowRange(12, 13)).toBe(false); }); }); describe('::pixelRectForScreenRange(range)', () => { it('returns a {top/left/width/height} object describing the rectangle between two screen positions, even if they are not on screen', async () => { const element = buildTextEditorElement(); const editor = element.getModel(); const horizontalScrollbarHeight = element.component.getHorizontalScrollbarHeight(); editor.update({ autoHeight: false }); element.getModel().setText('xxxxxxxxxxxxxxxxxxxxxx\n'.repeat(20)); element.style.height = 120 + horizontalScrollbarHeight + 'px'; await element.getNextUpdatePromise(); element.setScrollTop(80); await element.getNextUpdatePromise(); expect(element.getVisibleRowRange()).toEqual([4, 11]); const top = 2 * editor.getLineHeightInPixels(); const bottom = 13 * editor.getLineHeightInPixels(); const left = Math.round(3 * editor.getDefaultCharWidth()); const right = Math.round(11 * editor.getDefaultCharWidth()); const pixelRect = element.pixelRectForScreenRange([[2, 3], [13, 11]]); expect(pixelRect.top).toEqual(top); expect(pixelRect.left).toEqual(left); expect(pixelRect.height).toEqual( bottom + editor.getLineHeightInPixels() - top ); expect(pixelRect.width).toBeNear(right - left); }); }); describe('events', () => { let element = null; beforeEach(async () => { element = buildTextEditorElement(); element.getModel().update({ autoHeight: false }); element.getModel().setText('lorem\nipsum\ndolor\nsit\namet'); element.setHeight(20); await element.getNextUpdatePromise(); element.setWidth(20); await element.getNextUpdatePromise(); }); describe('::onDidChangeScrollTop(callback)', () => it('triggers even when subscribing before attaching the element', () => { const positions = []; const subscription1 = element.onDidChangeScrollTop(p => positions.push(p) ); element.onDidChangeScrollTop(p => positions.push(p)); positions.length = 0; element.setScrollTop(10); expect(positions).toEqual([10, 10]); element.remove(); jasmine.attachToDOM(element); positions.length = 0; element.setScrollTop(20); expect(positions).toEqual([20, 20]); subscription1.dispose(); positions.length = 0; element.setScrollTop(30); expect(positions).toEqual([30]); })); describe('::onDidChangeScrollLeft(callback)', () => it('triggers even when subscribing before attaching the element', () => { const positions = []; const subscription1 = element.onDidChangeScrollLeft(p => positions.push(p) ); element.onDidChangeScrollLeft(p => positions.push(p)); positions.length = 0; element.setScrollLeft(10); expect(positions).toEqual([10, 10]); element.remove(); jasmine.attachToDOM(element); positions.length = 0; element.setScrollLeft(20); expect(positions).toEqual([20, 20]); subscription1.dispose(); positions.length = 0; element.setScrollLeft(30); expect(positions).toEqual([30]); })); }); }); ================================================ FILE: spec/text-editor-registry-spec.js ================================================ const TextEditorRegistry = require('../src/text-editor-registry'); const TextEditor = require('../src/text-editor'); const TextBuffer = require('text-buffer'); const { Point, Range } = TextBuffer; const dedent = require('dedent'); const NullGrammar = require('../src/null-grammar'); describe('TextEditorRegistry', function() { let registry, editor, initialPackageActivation; beforeEach(function() { initialPackageActivation = Promise.resolve(); registry = new TextEditorRegistry({ assert: atom.assert, config: atom.config, grammarRegistry: atom.grammars, packageManager: { getActivatePromise() { return initialPackageActivation; } } }); editor = new TextEditor({ autoHeight: false }); expect( atom.grammars.assignLanguageMode(editor, 'text.plain.null-grammar') ).toBe(true); }); afterEach(function() { registry.destroy(); }); describe('.add', function() { it('adds an editor to the list of registered editors', function() { registry.add(editor); expect(editor.registered).toBe(true); expect(registry.editors.size).toBe(1); expect(registry.editors.has(editor)).toBe(true); }); it('returns a Disposable that can unregister the editor', function() { const disposable = registry.add(editor); expect(registry.editors.size).toBe(1); disposable.dispose(); expect(registry.editors.size).toBe(0); expect(editor.registered).toBe(false); expect(retainedEditorCount(registry)).toBe(0); }); }); describe('.observe', function() { it('calls the callback for current and future editors until unsubscribed', function() { const spy = jasmine.createSpy(); const [editor1, editor2, editor3] = [{}, {}, {}]; registry.add(editor1); const subscription = registry.observe(spy); expect(spy.calls.length).toBe(1); registry.add(editor2); expect(spy.calls.length).toBe(2); expect(spy.argsForCall[0][0]).toBe(editor1); expect(spy.argsForCall[1][0]).toBe(editor2); subscription.dispose(); registry.add(editor3); expect(spy.calls.length).toBe(2); }); }); describe('.build', function() { it('constructs a TextEditor with the right parameters based on its path and text', function() { atom.config.set('editor.tabLength', 8, { scope: '.source.js' }); const languageMode = { grammar: NullGrammar, onDidChangeHighlighting: jasmine.createSpy() }; const buffer = new TextBuffer({ filePath: 'test.js' }); buffer.setLanguageMode(languageMode); const editor = registry.build({ buffer }); expect(editor.getTabLength()).toBe(8); expect(editor.getGrammar()).toEqual(NullGrammar); expect(languageMode.onDidChangeHighlighting.calls.length).toBe(1); }); }); describe('.getActiveTextEditor', function() { it('gets the currently focused text editor', function() { const disposable = registry.add(editor); var editorElement = editor.getElement(); jasmine.attachToDOM(editorElement); editorElement.focus(); expect(registry.getActiveTextEditor()).toBe(editor); disposable.dispose(); }); }); describe('.maintainConfig(editor)', function() { it('does not update the editor when config settings change for unrelated scope selectors', async function() { await atom.packages.activatePackage('language-javascript'); const editor2 = new TextEditor(); atom.grammars.assignLanguageMode(editor2, 'source.js'); registry.maintainConfig(editor); registry.maintainConfig(editor2); await initialPackageActivation; expect(editor.getRootScopeDescriptor().getScopesArray()).toEqual([ 'text.plain.null-grammar' ]); expect(editor2.getRootScopeDescriptor().getScopesArray()).toEqual([ 'source.js' ]); expect(editor.getEncoding()).toBe('utf8'); expect(editor2.getEncoding()).toBe('utf8'); atom.config.set('core.fileEncoding', 'utf16le', { scopeSelector: '.text.plain.null-grammar' }); atom.config.set('core.fileEncoding', 'utf16be', { scopeSelector: '.source.js' }); expect(editor.getEncoding()).toBe('utf16le'); expect(editor2.getEncoding()).toBe('utf16be'); }); it('does not update the editor before the initial packages have loaded', async function() { let resolveActivatePromise; initialPackageActivation = new Promise(resolve => { resolveActivatePromise = resolve; }); atom.config.set('core.fileEncoding', 'utf16le'); registry.maintainConfig(editor); await Promise.resolve(); expect(editor.getEncoding()).toBe('utf8'); atom.config.set('core.fileEncoding', 'utf16be'); await Promise.resolve(); expect(editor.getEncoding()).toBe('utf8'); resolveActivatePromise(); await initialPackageActivation; expect(editor.getEncoding()).toBe('utf16be'); }); it("updates the editor's settings when its grammar changes", async function() { await atom.packages.activatePackage('language-javascript'); registry.maintainConfig(editor); await initialPackageActivation; atom.config.set('core.fileEncoding', 'utf16be', { scopeSelector: '.source.js' }); expect(editor.getEncoding()).toBe('utf8'); atom.config.set('core.fileEncoding', 'utf16le', { scopeSelector: '.source.js' }); expect(editor.getEncoding()).toBe('utf8'); atom.grammars.assignLanguageMode(editor, 'source.js'); await initialPackageActivation; expect(editor.getEncoding()).toBe('utf16le'); atom.config.set('core.fileEncoding', 'utf16be', { scopeSelector: '.source.js' }); expect(editor.getEncoding()).toBe('utf16be'); atom.grammars.assignLanguageMode(editor, 'text.plain.null-grammar'); await initialPackageActivation; expect(editor.getEncoding()).toBe('utf8'); }); it("preserves editor settings that haven't changed between previous and current language modes", async function() { await atom.packages.activatePackage('language-javascript'); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getEncoding()).toBe('utf8'); editor.setEncoding('utf16le'); expect(editor.getEncoding()).toBe('utf16le'); expect(editor.isSoftWrapped()).toBe(false); editor.setSoftWrapped(true); expect(editor.isSoftWrapped()).toBe(true); atom.grammars.assignLanguageMode(editor, 'source.js'); await initialPackageActivation; expect(editor.getEncoding()).toBe('utf16le'); expect(editor.isSoftWrapped()).toBe(true); }); it('updates editor settings that have changed between previous and current language modes', async function() { await atom.packages.activatePackage('language-javascript'); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getEncoding()).toBe('utf8'); atom.config.set('core.fileEncoding', 'utf16be', { scopeSelector: '.text.plain.null-grammar' }); atom.config.set('core.fileEncoding', 'utf16le', { scopeSelector: '.source.js' }); expect(editor.getEncoding()).toBe('utf16be'); editor.setEncoding('utf8'); expect(editor.getEncoding()).toBe('utf8'); atom.grammars.assignLanguageMode(editor, 'source.js'); await initialPackageActivation; expect(editor.getEncoding()).toBe('utf16le'); }); it("returns a disposable that can be used to stop the registry from updating the editor's config", async function() { await atom.packages.activatePackage('language-javascript'); const previousSubscriptionCount = getSubscriptionCount(editor); const disposable = registry.maintainConfig(editor); await initialPackageActivation; expect(getSubscriptionCount(editor)).toBeGreaterThan( previousSubscriptionCount ); expect(registry.editorsWithMaintainedConfig.size).toBe(1); atom.config.set('core.fileEncoding', 'utf16be'); expect(editor.getEncoding()).toBe('utf16be'); atom.config.set('core.fileEncoding', 'utf8'); expect(editor.getEncoding()).toBe('utf8'); disposable.dispose(); atom.config.set('core.fileEncoding', 'utf16be'); expect(editor.getEncoding()).toBe('utf8'); expect(getSubscriptionCount(editor)).toBe(previousSubscriptionCount); expect(retainedEditorCount(registry)).toBe(0); }); it('sets the encoding based on the config', async function() { editor.update({ encoding: 'utf8' }); expect(editor.getEncoding()).toBe('utf8'); atom.config.set('core.fileEncoding', 'utf16le'); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getEncoding()).toBe('utf16le'); atom.config.set('core.fileEncoding', 'utf8'); expect(editor.getEncoding()).toBe('utf8'); }); it('sets the tab length based on the config', async function() { editor.update({ tabLength: 4 }); expect(editor.getTabLength()).toBe(4); atom.config.set('editor.tabLength', 8); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getTabLength()).toBe(8); atom.config.set('editor.tabLength', 4); expect(editor.getTabLength()).toBe(4); }); it('enables soft tabs when the tabType config setting is "soft"', async function() { atom.config.set('editor.tabType', 'soft'); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getSoftTabs()).toBe(true); }); it('disables soft tabs when the tabType config setting is "hard"', async function() { atom.config.set('editor.tabType', 'hard'); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getSoftTabs()).toBe(false); }); describe('when the "tabType" config setting is "auto"', function() { it("enables or disables soft tabs based on the editor's content", async function() { await initialPackageActivation; await atom.packages.activatePackage('language-javascript'); atom.grammars.assignLanguageMode(editor, 'source.js'); atom.config.set('editor.tabType', 'auto'); await initialPackageActivation; editor.setText(dedent` { hello; } `); let disposable = registry.maintainConfig(editor); expect(editor.getSoftTabs()).toBe(true); /* eslint-disable no-tabs */ editor.setText(dedent` { hello; } `); /* eslint-enable no-tabs */ disposable.dispose(); disposable = registry.maintainConfig(editor); expect(editor.getSoftTabs()).toBe(false); editor.setTextInBufferRange( new Range(Point.ZERO, Point.ZERO), dedent` /* * Comment with a leading space. */ ` + '\n' ); disposable.dispose(); disposable = registry.maintainConfig(editor); expect(editor.getSoftTabs()).toBe(false); /* eslint-disable no-tabs */ editor.setText(dedent` /* * Comment with a leading space. */ { hello; } `); /* eslint-enable no-tabs */ disposable.dispose(); disposable = registry.maintainConfig(editor); expect(editor.getSoftTabs()).toBe(false); editor.setText(dedent` /* * Comment with a leading space. */ { hello; } `); disposable.dispose(); disposable = registry.maintainConfig(editor); expect(editor.getSoftTabs()).toBe(true); }); }); describe('when the "tabType" config setting is "auto"', function() { it('enables or disables soft tabs based on the "softTabs" config setting', async function() { registry.maintainConfig(editor); await initialPackageActivation; editor.setText('abc\ndef'); atom.config.set('editor.softTabs', true); atom.config.set('editor.tabType', 'auto'); expect(editor.getSoftTabs()).toBe(true); atom.config.set('editor.softTabs', false); expect(editor.getSoftTabs()).toBe(false); }); }); it('enables or disables soft tabs based on the config', async function() { editor.update({ softTabs: true }); expect(editor.getSoftTabs()).toBe(true); atom.config.set('editor.tabType', 'hard'); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getSoftTabs()).toBe(false); atom.config.set('editor.tabType', 'soft'); expect(editor.getSoftTabs()).toBe(true); atom.config.set('editor.tabType', 'auto'); atom.config.set('editor.softTabs', true); expect(editor.getSoftTabs()).toBe(true); }); it('enables or disables atomic soft tabs based on the config', async function() { editor.update({ atomicSoftTabs: true }); expect(editor.hasAtomicSoftTabs()).toBe(true); atom.config.set('editor.atomicSoftTabs', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.hasAtomicSoftTabs()).toBe(false); atom.config.set('editor.atomicSoftTabs', true); expect(editor.hasAtomicSoftTabs()).toBe(true); }); it('enables or disables cursor on selection visibility based on the config', async function() { editor.update({ showCursorOnSelection: true }); expect(editor.getShowCursorOnSelection()).toBe(true); atom.config.set('editor.showCursorOnSelection', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getShowCursorOnSelection()).toBe(false); atom.config.set('editor.showCursorOnSelection', true); expect(editor.getShowCursorOnSelection()).toBe(true); }); it('enables or disables line numbers based on the config', async function() { editor.update({ showLineNumbers: true }); expect(editor.showLineNumbers).toBe(true); atom.config.set('editor.showLineNumbers', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.showLineNumbers).toBe(false); atom.config.set('editor.showLineNumbers', true); expect(editor.showLineNumbers).toBe(true); }); it('sets the invisibles based on the config', async function() { const invisibles1 = { tab: 'a', cr: false, eol: false, space: false }; const invisibles2 = { tab: 'b', cr: false, eol: false, space: false }; editor.update({ showInvisibles: true, invisibles: invisibles1 }); expect(editor.getInvisibles()).toEqual(invisibles1); atom.config.set('editor.showInvisibles', true); atom.config.set('editor.invisibles', invisibles2); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getInvisibles()).toEqual(invisibles2); atom.config.set('editor.invisibles', invisibles1); expect(editor.getInvisibles()).toEqual(invisibles1); atom.config.set('editor.showInvisibles', false); expect(editor.getInvisibles()).toEqual({}); }); it('enables or disables the indent guide based on the config', async function() { editor.update({ showIndentGuide: true }); expect(editor.doesShowIndentGuide()).toBe(true); atom.config.set('editor.showIndentGuide', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.doesShowIndentGuide()).toBe(false); atom.config.set('editor.showIndentGuide', true); expect(editor.doesShowIndentGuide()).toBe(true); }); it('enables or disables soft wrap based on the config', async function() { editor.update({ softWrapped: true }); expect(editor.isSoftWrapped()).toBe(true); atom.config.set('editor.softWrap', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.isSoftWrapped()).toBe(false); atom.config.set('editor.softWrap', true); expect(editor.isSoftWrapped()).toBe(true); }); it('sets the soft wrap indent length based on the config', async function() { editor.update({ softWrapHangingIndentLength: 4 }); expect(editor.getSoftWrapHangingIndentLength()).toBe(4); atom.config.set('editor.softWrapHangingIndent', 2); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getSoftWrapHangingIndentLength()).toBe(2); atom.config.set('editor.softWrapHangingIndent', 4); expect(editor.getSoftWrapHangingIndentLength()).toBe(4); }); it('enables or disables preferred line length-based soft wrap based on the config', async function() { editor.update({ softWrapped: true, preferredLineLength: 80, editorWidthInChars: 120, softWrapAtPreferredLineLength: true }); expect(editor.getSoftWrapColumn()).toBe(80); atom.config.set('editor.softWrap', true); atom.config.set('editor.softWrapAtPreferredLineLength', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getSoftWrapColumn()).toBe(120); atom.config.set('editor.softWrapAtPreferredLineLength', true); 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); atom.config.set('editor.preferredLineLength', 110); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getPreferredLineLength()).toBe(110); atom.config.set('editor.preferredLineLength', 80); expect(editor.getPreferredLineLength()).toBe(80); }); it('enables or disables auto-indent based on the config', async function() { editor.update({ autoIndent: true }); expect(editor.shouldAutoIndent()).toBe(true); atom.config.set('editor.autoIndent', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.shouldAutoIndent()).toBe(false); atom.config.set('editor.autoIndent', true); expect(editor.shouldAutoIndent()).toBe(true); }); it('enables or disables auto-indent-on-paste based on the config', async function() { editor.update({ autoIndentOnPaste: true }); expect(editor.shouldAutoIndentOnPaste()).toBe(true); atom.config.set('editor.autoIndentOnPaste', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.shouldAutoIndentOnPaste()).toBe(false); atom.config.set('editor.autoIndentOnPaste', true); expect(editor.shouldAutoIndentOnPaste()).toBe(true); }); it('enables or disables scrolling past the end of the buffer based on the config', async function() { editor.update({ scrollPastEnd: true }); expect(editor.getScrollPastEnd()).toBe(true); atom.config.set('editor.scrollPastEnd', false); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getScrollPastEnd()).toBe(false); atom.config.set('editor.scrollPastEnd', true); expect(editor.getScrollPastEnd()).toBe(true); }); it('sets the undo grouping interval based on the config', async function() { editor.update({ undoGroupingInterval: 300 }); expect(editor.getUndoGroupingInterval()).toBe(300); atom.config.set('editor.undoGroupingInterval', 600); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getUndoGroupingInterval()).toBe(600); atom.config.set('editor.undoGroupingInterval', 300); expect(editor.getUndoGroupingInterval()).toBe(300); }); it('sets the scroll sensitivity based on the config', async function() { editor.update({ scrollSensitivity: 50 }); expect(editor.getScrollSensitivity()).toBe(50); atom.config.set('editor.scrollSensitivity', 60); registry.maintainConfig(editor); await initialPackageActivation; expect(editor.getScrollSensitivity()).toBe(60); atom.config.set('editor.scrollSensitivity', 70); expect(editor.getScrollSensitivity()).toBe(70); }); describe('when called twice with a given editor', function() { it('does nothing the second time', async function() { editor.update({ scrollSensitivity: 50 }); const disposable1 = registry.maintainConfig(editor); const disposable2 = registry.maintainConfig(editor); await initialPackageActivation; atom.config.set('editor.scrollSensitivity', 60); expect(editor.getScrollSensitivity()).toBe(60); disposable2.dispose(); atom.config.set('editor.scrollSensitivity', 70); expect(editor.getScrollSensitivity()).toBe(70); disposable1.dispose(); atom.config.set('editor.scrollSensitivity', 80); expect(editor.getScrollSensitivity()).toBe(70); }); }); }); }); function getSubscriptionCount(editor) { return ( editor.emitter.getTotalListenerCount() + editor.tokenizedBuffer.emitter.getTotalListenerCount() + editor.buffer.emitter.getTotalListenerCount() + editor.displayLayer.emitter.getTotalListenerCount() ); } function retainedEditorCount(registry) { const editors = new Set(); registry.editors.forEach(e => editors.add(e)); registry.editorsWithMaintainedConfig.forEach(e => editors.add(e)); registry.editorsWithMaintainedGrammar.forEach(e => editors.add(e)); return editors.size; } ================================================ FILE: spec/text-editor-spec.js ================================================ const fs = require('fs'); const path = require('path'); const temp = require('temp').track(); const dedent = require('dedent'); const { clipboard } = require('electron'); const os = require('os'); const TextEditor = require('../src/text-editor'); const TextBuffer = require('text-buffer'); const TextMateLanguageMode = require('../src/text-mate-language-mode'); const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode'); describe('TextEditor', () => { let buffer, editor, lineLengths; beforeEach(async () => { editor = await atom.workspace.open('sample.js'); buffer = editor.buffer; editor.update({ autoIndent: false }); lineLengths = buffer.getLines().map(line => line.length); await atom.packages.activatePackage('language-javascript'); }); it('generates unique ids for each editor', async () => { // Deserialized editors are initialized with the serialized id. We can // initialize an editor with what we expect to be the next id: const deserialized = new TextEditor({ id: editor.id + 1 }); expect(deserialized.id).toEqual(editor.id + 1); // The id generator should skip the id used up by the deserialized one: const fresh = new TextEditor(); expect(fresh.id).toNotEqual(deserialized.id); }); describe('when the editor is deserialized', () => { it('restores selections and folds based on markers in the buffer', async () => { editor.setSelectedBufferRange([[1, 2], [3, 4]]); editor.addSelectionForBufferRange([[5, 6], [7, 5]], { reversed: true }); editor.foldBufferRow(4); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); const buffer2 = await TextBuffer.deserialize(editor.buffer.serialize()); const editor2 = TextEditor.deserialize(editor.serialize(), { assert: atom.assert, textEditors: atom.textEditors, project: { bufferForIdSync() { return buffer2; } } }); expect(editor2.id).toBe(editor.id); expect(editor2.getBuffer().getPath()).toBe(editor.getBuffer().getPath()); expect(editor2.getSelectedBufferRanges()).toEqual([ [[1, 2], [3, 4]], [[5, 6], [7, 5]] ]); expect(editor2.getSelections()[1].isReversed()).toBeTruthy(); expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy(); editor2.destroy(); }); it("restores the editor's layout configuration", async () => { editor.update({ softTabs: true, atomicSoftTabs: false, tabLength: 12, softWrapped: true, softWrapAtPreferredLineLength: true, softWrapHangingIndentLength: 8, invisibles: { space: 'S' }, showInvisibles: true, editorWidthInChars: 120 }); // Force buffer and display layer to be deserialized as well, rather than // reusing the same buffer instance const buffer2 = await TextBuffer.deserialize(editor.buffer.serialize()); const editor2 = TextEditor.deserialize(editor.serialize(), { assert: atom.assert, textEditors: atom.textEditors, project: { bufferForIdSync() { return buffer2; } } }); expect(editor2.getSoftTabs()).toBe(editor.getSoftTabs()); expect(editor2.hasAtomicSoftTabs()).toBe(editor.hasAtomicSoftTabs()); expect(editor2.getTabLength()).toBe(editor.getTabLength()); expect(editor2.getSoftWrapColumn()).toBe(editor.getSoftWrapColumn()); expect(editor2.getSoftWrapHangingIndentLength()).toBe( editor.getSoftWrapHangingIndentLength() ); expect(editor2.getInvisibles()).toEqual(editor.getInvisibles()); expect(editor2.getEditorWidthInChars()).toBe( editor.getEditorWidthInChars() ); expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength()); expect(editor2.displayLayer.softWrapColumn).toBe( editor2.getSoftWrapColumn() ); }); it('ignores buffers with retired IDs', () => { const editor2 = TextEditor.deserialize(editor.serialize(), { assert: atom.assert, textEditors: atom.textEditors, project: { bufferForIdSync() { return null; } } }); expect(editor2).toBeNull(); }); }); describe('.copy()', () => { it('returns a different editor with the same initial state', () => { expect(editor.getAutoHeight()).toBeFalsy(); expect(editor.getAutoWidth()).toBeFalsy(); expect(editor.getShowCursorOnSelection()).toBeTruthy(); const element = editor.getElement(); element.setHeight(100); element.setWidth(100); jasmine.attachToDOM(element); editor.update({ showCursorOnSelection: false }); editor.setSelectedBufferRange([[1, 2], [3, 4]]); editor.addSelectionForBufferRange([[5, 6], [7, 8]], { reversed: true }); editor.setScrollTopRow(3); expect(editor.getScrollTopRow()).toBe(3); editor.setScrollLeftColumn(4); expect(editor.getScrollLeftColumn()).toBe(4); editor.foldBufferRow(4); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); const editor2 = editor.copy(); const element2 = editor2.getElement(); element2.setHeight(100); element2.setWidth(100); jasmine.attachToDOM(element2); expect(editor2.id).not.toBe(editor.id); expect(editor2.getSelectedBufferRanges()).toEqual( editor.getSelectedBufferRanges() ); expect(editor2.getSelections()[1].isReversed()).toBeTruthy(); expect(editor2.getScrollTopRow()).toBe(3); expect(editor2.getScrollLeftColumn()).toBe(4); expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor2.getAutoWidth()).toBe(false); expect(editor2.getAutoHeight()).toBe(false); expect(editor2.getShowCursorOnSelection()).toBeFalsy(); // editor2 can now diverge from its origin edit session editor2.getLastSelection().setBufferRange([[2, 1], [4, 3]]); expect(editor2.getSelectedBufferRanges()).not.toEqual( editor.getSelectedBufferRanges() ); editor2.unfoldBufferRow(4); expect(editor2.isFoldedAtBufferRow(4)).not.toBe( editor.isFoldedAtBufferRow(4) ); }); }); describe('.update()', () => { it('updates the editor with the supplied config parameters', () => { let changeSpy; const { element } = editor; // force element initialization element.setUpdatedSynchronously(false); editor.update({ showInvisibles: true }); editor.onDidChange((changeSpy = jasmine.createSpy('onDidChange'))); const returnedPromise = editor.update({ tabLength: 6, softTabs: false, softWrapped: true, editorWidthInChars: 40, showInvisibles: false, mini: false, lineNumberGutterVisible: false, scrollPastEnd: true, autoHeight: false, maxScreenLineLength: 1000 }); expect(returnedPromise).toBe(element.component.getNextUpdatePromise()); expect(changeSpy.callCount).toBe(1); expect(editor.getTabLength()).toBe(6); expect(editor.getSoftTabs()).toBe(false); expect(editor.isSoftWrapped()).toBe(true); expect(editor.getEditorWidthInChars()).toBe(40); expect(editor.getInvisibles()).toEqual({}); expect(editor.isMini()).toBe(false); expect(editor.isLineNumberGutterVisible()).toBe(false); expect(editor.getScrollPastEnd()).toBe(true); expect(editor.getAutoHeight()).toBe(false); }); }); describe('title', () => { describe('.getTitle()', () => { it("uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", () => { expect(editor.getTitle()).toBe('sample.js'); buffer.setPath(undefined); expect(editor.getTitle()).toBe('untitled'); }); }); describe('.getLongTitle()', () => { it('returns file name when there is no opened file with identical name', () => { expect(editor.getLongTitle()).toBe('sample.js'); buffer.setPath(undefined); expect(editor.getLongTitle()).toBe('untitled'); }); it("returns '' when opened files have identical file names", async () => { const editor1 = await atom.workspace.open( path.join('sample-theme-1', 'readme') ); const editor2 = await atom.workspace.open( path.join('sample-theme-2', 'readme') ); expect(editor1.getLongTitle()).toBe('readme \u2014 sample-theme-1'); expect(editor2.getLongTitle()).toBe('readme \u2014 sample-theme-2'); }); it("returns '' when opened files have identical file names in subdirectories", async () => { const path1 = path.join('sample-theme-1', 'src', 'js'); const path2 = path.join('sample-theme-2', 'src', 'js'); const editor1 = await atom.workspace.open(path.join(path1, 'main.js')); const editor2 = await atom.workspace.open(path.join(path2, 'main.js')); expect(editor1.getLongTitle()).toBe(`main.js \u2014 ${path1}`); expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path2}`); }); it("returns '' when opened files have identical file and same parent dir name", async () => { const editor1 = await atom.workspace.open( path.join('sample-theme-2', 'src', 'js', 'main.js') ); const editor2 = await atom.workspace.open( path.join('sample-theme-2', 'src', 'js', 'plugin', 'main.js') ); expect(editor1.getLongTitle()).toBe('main.js \u2014 js'); expect(editor2.getLongTitle()).toBe( `main.js \u2014 ${path.join('js', 'plugin')}` ); }); it('returns the filename when the editor is not in the workspace', async () => { editor.onDidDestroy(() => { expect(editor.getLongTitle()).toBe('sample.js'); }); await atom.workspace.getActivePane().close(); expect(editor.isDestroyed()).toBe(true); }); }); it('notifies ::onDidChangeTitle observers when the underlying buffer path changes', () => { const observed = []; editor.onDidChangeTitle(title => observed.push(title)); buffer.setPath('/foo/bar/baz.txt'); buffer.setPath(undefined); expect(observed).toEqual(['baz.txt', 'untitled']); }); }); describe('path', () => { it('notifies ::onDidChangePath observers when the underlying buffer path changes', () => { const observed = []; editor.onDidChangePath(filePath => observed.push(filePath)); buffer.setPath(__filename); buffer.setPath(undefined); expect(observed).toEqual([__filename, undefined]); }); }); describe('encoding', () => { it('notifies ::onDidChangeEncoding observers when the editor encoding changes', () => { const observed = []; editor.onDidChangeEncoding(encoding => observed.push(encoding)); editor.setEncoding('utf16le'); editor.setEncoding('utf16le'); editor.setEncoding('utf16be'); editor.setEncoding(); editor.setEncoding(); expect(observed).toEqual(['utf16le', 'utf16be', 'utf8']); }); }); describe('cursor', () => { describe('.getLastCursor()', () => { it('returns the most recently created cursor', () => { editor.addCursorAtScreenPosition([1, 0]); const lastCursor = editor.addCursorAtScreenPosition([2, 0]); expect(editor.getLastCursor()).toBe(lastCursor); }); it('creates a new cursor at (0, 0) if the last cursor has been destroyed', () => { editor.getLastCursor().destroy(); expect(editor.getLastCursor().getBufferPosition()).toEqual([0, 0]); }); }); describe('.getCursors()', () => { it('creates a new cursor at (0, 0) if the last cursor has been destroyed', () => { editor.getLastCursor().destroy(); expect(editor.getCursors()[0].getBufferPosition()).toEqual([0, 0]); }); }); describe('when the cursor moves', () => { it('clears a goal column established by vertical movement', () => { editor.setText('b'); editor.setCursorBufferPosition([0, 0]); editor.insertNewline(); editor.moveUp(); editor.insertText('a'); editor.moveDown(); expect(editor.getCursorBufferPosition()).toEqual([1, 1]); }); it('emits an event with the old position, new position, and the cursor that moved', () => { const cursorCallback = jasmine.createSpy('cursor-changed-position'); const editorCallback = jasmine.createSpy( 'editor-changed-cursor-position' ); editor.getLastCursor().onDidChangePosition(cursorCallback); editor.onDidChangeCursorPosition(editorCallback); editor.setCursorBufferPosition([2, 4]); expect(editorCallback).toHaveBeenCalled(); expect(cursorCallback).toHaveBeenCalled(); const eventObject = editorCallback.mostRecentCall.args[0]; expect(cursorCallback.mostRecentCall.args[0]).toEqual(eventObject); expect(eventObject.oldBufferPosition).toEqual([0, 0]); expect(eventObject.oldScreenPosition).toEqual([0, 0]); expect(eventObject.newBufferPosition).toEqual([2, 4]); expect(eventObject.newScreenPosition).toEqual([2, 4]); expect(eventObject.cursor).toBe(editor.getLastCursor()); }); }); describe('.setCursorScreenPosition(screenPosition)', () => { it('clears a goal column established by vertical movement', () => { // set a goal column by moving down editor.setCursorScreenPosition({ row: 3, column: lineLengths[3] }); editor.moveDown(); expect(editor.getCursorScreenPosition().column).not.toBe(6); // clear the goal column by explicitly setting the cursor position editor.setCursorScreenPosition([4, 6]); expect(editor.getCursorScreenPosition().column).toBe(6); editor.moveDown(); expect(editor.getCursorScreenPosition().column).toBe(6); }); it('merges multiple cursors', () => { editor.setCursorScreenPosition([0, 0]); editor.addCursorAtScreenPosition([0, 1]); const [cursor1] = editor.getCursors(); editor.setCursorScreenPosition([4, 7]); expect(editor.getCursors().length).toBe(1); expect(editor.getCursors()).toEqual([cursor1]); expect(editor.getCursorScreenPosition()).toEqual([4, 7]); }); describe('when soft-wrap is enabled and code is folded', () => { beforeEach(() => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(50); editor.foldBufferRowRange(2, 3); }); it('positions the cursor at the buffer position that corresponds to the given screen position', () => { editor.setCursorScreenPosition([9, 0]); expect(editor.getCursorBufferPosition()).toEqual([8, 11]); }); }); }); describe('.moveUp()', () => { it('moves the cursor up', () => { editor.setCursorScreenPosition([2, 2]); editor.moveUp(); expect(editor.getCursorScreenPosition()).toEqual([1, 2]); }); it('retains the goal column across lines of differing length', () => { expect(lineLengths[6]).toBeGreaterThan(32); editor.setCursorScreenPosition({ row: 6, column: 32 }); editor.moveUp(); expect(editor.getCursorScreenPosition().column).toBe(lineLengths[5]); editor.moveUp(); expect(editor.getCursorScreenPosition().column).toBe(lineLengths[4]); editor.moveUp(); expect(editor.getCursorScreenPosition().column).toBe(32); }); describe('when the cursor is on the first line', () => { it('moves the cursor to the beginning of the line, but retains the goal column', () => { editor.setCursorScreenPosition([0, 4]); editor.moveUp(); expect(editor.getCursorScreenPosition()).toEqual([0, 0]); editor.moveDown(); expect(editor.getCursorScreenPosition()).toEqual([1, 4]); }); }); describe('when there is a selection', () => { beforeEach(() => editor.setSelectedBufferRange([[4, 9], [5, 10]])); it('moves above the selection', () => { const cursor = editor.getLastCursor(); editor.moveUp(); expect(cursor.getBufferPosition()).toEqual([3, 9]); }); }); it('merges cursors when they overlap', () => { editor.addCursorAtScreenPosition([1, 0]); const [cursor1] = editor.getCursors(); editor.moveUp(); expect(editor.getCursors()).toEqual([cursor1]); expect(cursor1.getBufferPosition()).toEqual([0, 0]); }); describe('when the cursor was moved down from the beginning of an indented soft-wrapped line', () => { it('moves to the beginning of the previous line', () => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(50); editor.setCursorScreenPosition([3, 0]); editor.moveDown(); editor.moveDown(); editor.moveUp(); expect(editor.getCursorScreenPosition()).toEqual([4, 4]); }); }); }); describe('.moveDown()', () => { it('moves the cursor down', () => { editor.setCursorScreenPosition([2, 2]); editor.moveDown(); expect(editor.getCursorScreenPosition()).toEqual([3, 2]); }); it('retains the goal column across lines of differing length', () => { editor.setCursorScreenPosition({ row: 3, column: lineLengths[3] }); editor.moveDown(); expect(editor.getCursorScreenPosition().column).toBe(lineLengths[4]); editor.moveDown(); expect(editor.getCursorScreenPosition().column).toBe(lineLengths[5]); editor.moveDown(); expect(editor.getCursorScreenPosition().column).toBe(lineLengths[3]); }); describe('when the cursor is on the last line', () => { it('moves the cursor to the end of line, but retains the goal column when moving back up', () => { const lastLineIndex = buffer.getLines().length - 1; const lastLine = buffer.lineForRow(lastLineIndex); expect(lastLine.length).toBeGreaterThan(0); editor.setCursorScreenPosition({ row: lastLineIndex, column: editor.getTabLength() }); editor.moveDown(); expect(editor.getCursorScreenPosition()).toEqual({ row: lastLineIndex, column: lastLine.length }); editor.moveUp(); expect(editor.getCursorScreenPosition().column).toBe( editor.getTabLength() ); }); it('retains a goal column of 0 when moving back up', () => { const lastLineIndex = buffer.getLines().length - 1; const lastLine = buffer.lineForRow(lastLineIndex); expect(lastLine.length).toBeGreaterThan(0); editor.setCursorScreenPosition({ row: lastLineIndex, column: 0 }); editor.moveDown(); editor.moveUp(); expect(editor.getCursorScreenPosition().column).toBe(0); }); }); describe('when the cursor is at the beginning of an indented soft-wrapped line', () => { it("moves to the beginning of the line's continuation on the next screen row", () => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(50); editor.setCursorScreenPosition([3, 0]); editor.moveDown(); expect(editor.getCursorScreenPosition()).toEqual([4, 4]); }); }); describe('when there is a selection', () => { beforeEach(() => editor.setSelectedBufferRange([[4, 9], [5, 10]])); it('moves below the selection', () => { const cursor = editor.getLastCursor(); editor.moveDown(); expect(cursor.getBufferPosition()).toEqual([6, 10]); }); }); it('merges cursors when they overlap', () => { editor.setCursorScreenPosition([12, 2]); editor.addCursorAtScreenPosition([11, 2]); const [cursor1] = editor.getCursors(); editor.moveDown(); expect(editor.getCursors()).toEqual([cursor1]); expect(cursor1.getBufferPosition()).toEqual([12, 2]); }); }); describe('.moveLeft()', () => { it('moves the cursor by one column to the left', () => { editor.setCursorScreenPosition([1, 8]); editor.moveLeft(); expect(editor.getCursorScreenPosition()).toEqual([1, 7]); }); it('moves the cursor by n columns to the left', () => { editor.setCursorScreenPosition([1, 8]); editor.moveLeft(4); expect(editor.getCursorScreenPosition()).toEqual([1, 4]); }); it('moves the cursor by two rows up when the columnCount is longer than an entire line', () => { editor.setCursorScreenPosition([2, 2]); editor.moveLeft(34); expect(editor.getCursorScreenPosition()).toEqual([0, 29]); }); it('moves the cursor to the beginning columnCount is longer than the position in the buffer', () => { editor.setCursorScreenPosition([1, 0]); editor.moveLeft(100); expect(editor.getCursorScreenPosition()).toEqual([0, 0]); }); describe('when the cursor is in the first column', () => { describe('when there is a previous line', () => { it('wraps to the end of the previous line', () => { editor.setCursorScreenPosition({ row: 1, column: 0 }); editor.moveLeft(); expect(editor.getCursorScreenPosition()).toEqual({ row: 0, column: buffer.lineForRow(0).length }); }); it('moves the cursor by one row up and n columns to the left', () => { editor.setCursorScreenPosition([1, 0]); editor.moveLeft(4); expect(editor.getCursorScreenPosition()).toEqual([0, 26]); }); }); describe('when the next line is empty', () => { it('wraps to the beginning of the previous line', () => { editor.setCursorScreenPosition([11, 0]); editor.moveLeft(); expect(editor.getCursorScreenPosition()).toEqual([10, 0]); }); }); describe('when line is wrapped and follow previous line indentation', () => { beforeEach(() => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(50); }); it('wraps to the end of the previous line', () => { editor.setCursorScreenPosition([4, 4]); editor.moveLeft(); expect(editor.getCursorScreenPosition()).toEqual([3, 46]); }); }); describe('when the cursor is on the first line', () => { it('remains in the same position (0,0)', () => { editor.setCursorScreenPosition({ row: 0, column: 0 }); editor.moveLeft(); expect(editor.getCursorScreenPosition()).toEqual({ row: 0, column: 0 }); }); it('remains in the same position (0,0) when columnCount is specified', () => { editor.setCursorScreenPosition([0, 0]); editor.moveLeft(4); expect(editor.getCursorScreenPosition()).toEqual([0, 0]); }); }); }); describe('when softTabs is enabled and the cursor is preceded by leading whitespace', () => { it('skips tabLength worth of whitespace at a time', () => { editor.setCursorBufferPosition([5, 6]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([5, 4]); }); }); describe('when there is a selection', () => { beforeEach(() => editor.setSelectedBufferRange([[5, 22], [5, 27]])); it('moves to the left of the selection', () => { const cursor = editor.getLastCursor(); editor.moveLeft(); expect(cursor.getBufferPosition()).toEqual([5, 22]); editor.moveLeft(); expect(cursor.getBufferPosition()).toEqual([5, 21]); }); }); it('merges cursors when they overlap', () => { editor.setCursorScreenPosition([0, 0]); editor.addCursorAtScreenPosition([0, 1]); const [cursor1] = editor.getCursors(); editor.moveLeft(); expect(editor.getCursors()).toEqual([cursor1]); expect(cursor1.getBufferPosition()).toEqual([0, 0]); }); }); describe('.moveRight()', () => { it('moves the cursor by one column to the right', () => { editor.setCursorScreenPosition([3, 3]); editor.moveRight(); expect(editor.getCursorScreenPosition()).toEqual([3, 4]); }); it('moves the cursor by n columns to the right', () => { editor.setCursorScreenPosition([3, 7]); editor.moveRight(4); expect(editor.getCursorScreenPosition()).toEqual([3, 11]); }); it('moves the cursor by two rows down when the columnCount is longer than an entire line', () => { editor.setCursorScreenPosition([0, 29]); editor.moveRight(34); expect(editor.getCursorScreenPosition()).toEqual([2, 2]); }); it('moves the cursor to the end of the buffer when columnCount is longer than the number of characters following the cursor position', () => { editor.setCursorScreenPosition([11, 5]); editor.moveRight(100); expect(editor.getCursorScreenPosition()).toEqual([12, 2]); }); describe('when the cursor is on the last column of a line', () => { describe('when there is a subsequent line', () => { it('wraps to the beginning of the next line', () => { editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]); editor.moveRight(); expect(editor.getCursorScreenPosition()).toEqual([1, 0]); }); it('moves the cursor by one row down and n columns to the right', () => { editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]); editor.moveRight(4); expect(editor.getCursorScreenPosition()).toEqual([1, 3]); }); }); describe('when the next line is empty', () => { it('wraps to the beginning of the next line', () => { editor.setCursorScreenPosition([9, 4]); editor.moveRight(); expect(editor.getCursorScreenPosition()).toEqual([10, 0]); }); }); describe('when the cursor is on the last line', () => { it('remains in the same position', () => { const lastLineIndex = buffer.getLines().length - 1; const lastLine = buffer.lineForRow(lastLineIndex); expect(lastLine.length).toBeGreaterThan(0); const lastPosition = { row: lastLineIndex, column: lastLine.length }; editor.setCursorScreenPosition(lastPosition); editor.moveRight(); expect(editor.getCursorScreenPosition()).toEqual(lastPosition); }); }); }); describe('when there is a selection', () => { beforeEach(() => editor.setSelectedBufferRange([[5, 22], [5, 27]])); it('moves to the left of the selection', () => { const cursor = editor.getLastCursor(); editor.moveRight(); expect(cursor.getBufferPosition()).toEqual([5, 27]); editor.moveRight(); expect(cursor.getBufferPosition()).toEqual([5, 28]); }); }); it('merges cursors when they overlap', () => { editor.setCursorScreenPosition([12, 2]); editor.addCursorAtScreenPosition([12, 1]); const [cursor1] = editor.getCursors(); editor.moveRight(); expect(editor.getCursors()).toEqual([cursor1]); expect(cursor1.getBufferPosition()).toEqual([12, 2]); }); }); describe('.moveToTop()', () => { it('moves the cursor to the top of the buffer', () => { editor.setCursorScreenPosition([11, 1]); editor.addCursorAtScreenPosition([12, 0]); editor.moveToTop(); expect(editor.getCursors().length).toBe(1); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); }); describe('.moveToBottom()', () => { it('moves the cursor to the bottom of the buffer', () => { editor.setCursorScreenPosition([0, 0]); editor.addCursorAtScreenPosition([1, 0]); editor.moveToBottom(); expect(editor.getCursors().length).toBe(1); expect(editor.getCursorBufferPosition()).toEqual([12, 2]); }); }); describe('.moveToBeginningOfScreenLine()', () => { describe('when soft wrap is on', () => { it('moves cursor to the beginning of the screen line', () => { editor.setSoftWrapped(true); editor.setEditorWidthInChars(10); editor.setCursorScreenPosition([1, 2]); editor.moveToBeginningOfScreenLine(); const cursor = editor.getLastCursor(); expect(cursor.getScreenPosition()).toEqual([1, 0]); }); }); describe('when soft wrap is off', () => { it('moves cursor to the beginning of the line', () => { editor.setCursorScreenPosition([0, 5]); editor.addCursorAtScreenPosition([1, 7]); editor.moveToBeginningOfScreenLine(); expect(editor.getCursors().length).toBe(2); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor2.getBufferPosition()).toEqual([1, 0]); }); }); }); describe('.moveToEndOfScreenLine()', () => { describe('when soft wrap is on', () => { it('moves cursor to the beginning of the screen line', () => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(10); editor.setCursorScreenPosition([1, 2]); editor.moveToEndOfScreenLine(); const cursor = editor.getLastCursor(); expect(cursor.getScreenPosition()).toEqual([1, 9]); }); }); describe('when soft wrap is off', () => { it('moves cursor to the end of line', () => { editor.setCursorScreenPosition([0, 0]); editor.addCursorAtScreenPosition([1, 0]); editor.moveToEndOfScreenLine(); expect(editor.getCursors().length).toBe(2); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([0, 29]); expect(cursor2.getBufferPosition()).toEqual([1, 30]); }); }); }); describe('.moveToBeginningOfLine()', () => { it('moves cursor to the beginning of the buffer line', () => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(10); editor.setCursorScreenPosition([1, 2]); editor.moveToBeginningOfLine(); const cursor = editor.getLastCursor(); expect(cursor.getScreenPosition()).toEqual([0, 0]); }); }); describe('.moveToEndOfLine()', () => { it('moves cursor to the end of the buffer line', () => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(10); editor.setCursorScreenPosition([0, 2]); editor.moveToEndOfLine(); const cursor = editor.getLastCursor(); expect(cursor.getScreenPosition()).toEqual([4, 4]); }); }); describe('.moveToFirstCharacterOfLine()', () => { describe('when soft wrap is on', () => { it("moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", () => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(10); editor.setCursorScreenPosition([2, 5]); editor.addCursorAtScreenPosition([8, 7]); editor.moveToFirstCharacterOfLine(); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getScreenPosition()).toEqual([2, 0]); expect(cursor2.getScreenPosition()).toEqual([8, 2]); editor.moveToFirstCharacterOfLine(); expect(cursor1.getScreenPosition()).toEqual([2, 0]); expect(cursor2.getScreenPosition()).toEqual([8, 2]); }); }); describe('when soft wrap is off', () => { it("moves to the first character of the current line or the beginning of the line if it's already on the first character", () => { editor.setCursorScreenPosition([0, 5]); editor.addCursorAtScreenPosition([1, 7]); editor.moveToFirstCharacterOfLine(); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor2.getBufferPosition()).toEqual([1, 2]); editor.moveToFirstCharacterOfLine(); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor2.getBufferPosition()).toEqual([1, 0]); }); it('moves to the beginning of the line if it only contains whitespace ', () => { editor.setText('first\n \nthird'); editor.setCursorScreenPosition([1, 2]); editor.moveToFirstCharacterOfLine(); const cursor = editor.getLastCursor(); expect(cursor.getBufferPosition()).toEqual([1, 0]); }); describe('when invisible characters are enabled with soft tabs', () => { it('moves to the first character of the current line without being confused by the invisible characters', () => { editor.update({ showInvisibles: true }); editor.setCursorScreenPosition([1, 7]); editor.moveToFirstCharacterOfLine(); expect(editor.getCursorBufferPosition()).toEqual([1, 2]); editor.moveToFirstCharacterOfLine(); expect(editor.getCursorBufferPosition()).toEqual([1, 0]); }); }); describe('when invisible characters are enabled with hard tabs', () => { it('moves to the first character of the current line without being confused by the invisible characters', () => { editor.update({ showInvisibles: true }); buffer.setTextInRange([[1, 0], [1, Infinity]], '\t\t\ta', { normalizeLineEndings: false }); editor.setCursorScreenPosition([1, 7]); editor.moveToFirstCharacterOfLine(); expect(editor.getCursorBufferPosition()).toEqual([1, 3]); editor.moveToFirstCharacterOfLine(); expect(editor.getCursorBufferPosition()).toEqual([1, 0]); }); }); }); it('clears the goal column', () => { editor.setText('first\n\nthird'); editor.setCursorScreenPosition([0, 3]); editor.moveDown(); editor.moveToFirstCharacterOfLine(); editor.moveDown(); expect(editor.getCursorBufferPosition()).toEqual([2, 0]); }); }); describe('.moveToBeginningOfWord()', () => { it('moves the cursor to the beginning of the word', () => { editor.setCursorBufferPosition([0, 8]); editor.addCursorAtBufferPosition([1, 12]); editor.addCursorAtBufferPosition([3, 0]); const [cursor1, cursor2, cursor3] = editor.getCursors(); editor.moveToBeginningOfWord(); expect(cursor1.getBufferPosition()).toEqual([0, 4]); expect(cursor2.getBufferPosition()).toEqual([1, 11]); expect(cursor3.getBufferPosition()).toEqual([2, 39]); }); it('does not fail at position [0, 0]', () => { editor.setCursorBufferPosition([0, 0]); editor.moveToBeginningOfWord(); }); it('treats lines with only whitespace as a word', () => { editor.setCursorBufferPosition([11, 0]); editor.moveToBeginningOfWord(); expect(editor.getCursorBufferPosition()).toEqual([10, 0]); }); it('treats lines with only whitespace as a word (CRLF line ending)', () => { editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')); editor.setCursorBufferPosition([11, 0]); editor.moveToBeginningOfWord(); expect(editor.getCursorBufferPosition()).toEqual([10, 0]); }); it('works when the current line is blank', () => { editor.setCursorBufferPosition([10, 0]); editor.moveToBeginningOfWord(); expect(editor.getCursorBufferPosition()).toEqual([9, 2]); }); it('works when the current line is blank (CRLF line ending)', () => { editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')); editor.setCursorBufferPosition([10, 0]); editor.moveToBeginningOfWord(); expect(editor.getCursorBufferPosition()).toEqual([9, 2]); editor.buffer.setText(buffer.getText().replace(/\r\n/g, '\n')); }); }); describe('.moveToPreviousWordBoundary()', () => { it('moves the cursor to the previous word boundary', () => { editor.setCursorBufferPosition([0, 8]); editor.addCursorAtBufferPosition([2, 0]); editor.addCursorAtBufferPosition([2, 4]); editor.addCursorAtBufferPosition([3, 14]); const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors(); editor.moveToPreviousWordBoundary(); expect(cursor1.getBufferPosition()).toEqual([0, 4]); expect(cursor2.getBufferPosition()).toEqual([1, 30]); expect(cursor3.getBufferPosition()).toEqual([2, 0]); expect(cursor4.getBufferPosition()).toEqual([3, 13]); }); }); describe('.moveToNextWordBoundary()', () => { it('moves the cursor to the previous word boundary', () => { editor.setCursorBufferPosition([0, 8]); editor.addCursorAtBufferPosition([2, 40]); editor.addCursorAtBufferPosition([3, 0]); editor.addCursorAtBufferPosition([3, 30]); const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors(); editor.moveToNextWordBoundary(); expect(cursor1.getBufferPosition()).toEqual([0, 13]); expect(cursor2.getBufferPosition()).toEqual([3, 0]); expect(cursor3.getBufferPosition()).toEqual([3, 4]); expect(cursor4.getBufferPosition()).toEqual([3, 31]); }); }); describe('.moveToEndOfWord()', () => { it('moves the cursor to the end of the word', () => { editor.setCursorBufferPosition([0, 6]); editor.addCursorAtBufferPosition([1, 10]); editor.addCursorAtBufferPosition([2, 40]); const [cursor1, cursor2, cursor3] = editor.getCursors(); editor.moveToEndOfWord(); expect(cursor1.getBufferPosition()).toEqual([0, 13]); expect(cursor2.getBufferPosition()).toEqual([1, 12]); expect(cursor3.getBufferPosition()).toEqual([3, 7]); }); it('does not blow up when there is no next word', () => { editor.setCursorBufferPosition([Infinity, Infinity]); const endPosition = editor.getCursorBufferPosition(); editor.moveToEndOfWord(); expect(editor.getCursorBufferPosition()).toEqual(endPosition); }); it('treats lines with only whitespace as a word', () => { editor.setCursorBufferPosition([9, 4]); editor.moveToEndOfWord(); expect(editor.getCursorBufferPosition()).toEqual([10, 0]); }); it('treats lines with only whitespace as a word (CRLF line ending)', () => { editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')); editor.setCursorBufferPosition([9, 4]); editor.moveToEndOfWord(); expect(editor.getCursorBufferPosition()).toEqual([10, 0]); }); it('works when the current line is blank', () => { editor.setCursorBufferPosition([10, 0]); editor.moveToEndOfWord(); expect(editor.getCursorBufferPosition()).toEqual([11, 8]); }); it('works when the current line is blank (CRLF line ending)', () => { editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')); editor.setCursorBufferPosition([10, 0]); editor.moveToEndOfWord(); expect(editor.getCursorBufferPosition()).toEqual([11, 8]); }); }); describe('.moveToBeginningOfNextWord()', () => { it('moves the cursor before the first character of the next word', () => { editor.setCursorBufferPosition([0, 6]); editor.addCursorAtBufferPosition([1, 11]); editor.addCursorAtBufferPosition([2, 0]); const [cursor1, cursor2, cursor3] = editor.getCursors(); editor.moveToBeginningOfNextWord(); expect(cursor1.getBufferPosition()).toEqual([0, 14]); expect(cursor2.getBufferPosition()).toEqual([1, 13]); expect(cursor3.getBufferPosition()).toEqual([2, 4]); // When the cursor is on whitespace editor.setText('ab cde- '); editor.setCursorBufferPosition([0, 2]); const cursor = editor.getLastCursor(); editor.moveToBeginningOfNextWord(); expect(cursor.getBufferPosition()).toEqual([0, 3]); }); it('does not blow up when there is no next word', () => { editor.setCursorBufferPosition([Infinity, Infinity]); const endPosition = editor.getCursorBufferPosition(); editor.moveToBeginningOfNextWord(); expect(editor.getCursorBufferPosition()).toEqual(endPosition); }); it('treats lines with only whitespace as a word', () => { editor.setCursorBufferPosition([9, 4]); editor.moveToBeginningOfNextWord(); expect(editor.getCursorBufferPosition()).toEqual([10, 0]); }); it('works when the current line is blank', () => { editor.setCursorBufferPosition([10, 0]); editor.moveToBeginningOfNextWord(); expect(editor.getCursorBufferPosition()).toEqual([11, 9]); }); }); describe('.moveToPreviousSubwordBoundary', () => { it('does not move the cursor when there is no previous subword boundary', () => { editor.setText(''); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); it('stops at word and underscore boundaries', () => { editor.setText('sub_word \n'); editor.setCursorBufferPosition([0, 9]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 8]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); editor.setText(' word\n'); editor.setCursorBufferPosition([0, 3]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); }); it('stops at camelCase boundaries', () => { editor.setText(' getPreviousWord\n'); editor.setCursorBufferPosition([0, 16]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 12]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); }); it('stops at camelCase boundaries with non-ascii characters', () => { editor.setText(' gétÁrevìôüsWord\n'); editor.setCursorBufferPosition([0, 16]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 12]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); }); it('skips consecutive non-word characters', () => { editor.setText('e, => \n'); editor.setCursorBufferPosition([0, 6]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 3]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); }); it('skips consecutive uppercase characters', () => { editor.setText(' AAADF \n'); editor.setCursorBufferPosition([0, 7]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 6]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); editor.setText('ALPhA\n'); editor.setCursorBufferPosition([0, 4]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); }); it('skips consecutive uppercase non-ascii letters', () => { editor.setText(' ÀÁÅDF \n'); editor.setCursorBufferPosition([0, 7]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 6]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); editor.setText('ALPhA\n'); editor.setCursorBufferPosition([0, 4]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); }); it('skips consecutive numbers', () => { editor.setText(' 88 \n'); editor.setCursorBufferPosition([0, 4]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 3]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); }); it('works with multiple cursors', () => { editor.setText('curOp\ncursorOptions\n'); editor.setCursorBufferPosition([0, 8]); editor.addCursorAtBufferPosition([1, 13]); const [cursor1, cursor2] = editor.getCursors(); editor.moveToPreviousSubwordBoundary(); expect(cursor1.getBufferPosition()).toEqual([0, 3]); expect(cursor2.getBufferPosition()).toEqual([1, 6]); }); it('works with non-English characters', () => { editor.setText('supåTøåst \n'); editor.setCursorBufferPosition([0, 9]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.setText('supaÖast \n'); editor.setCursorBufferPosition([0, 8]); editor.moveToPreviousSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); }); }); describe('.moveToNextSubwordBoundary', () => { it('does not move the cursor when there is no next subword boundary', () => { editor.setText(''); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); it('stops at word and underscore boundaries', () => { editor.setText(' sub_word \n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 9]); editor.setText('word \n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); }); it('stops at camelCase boundaries', () => { editor.setText('getPreviousWord \n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 3]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 11]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 15]); }); it('skips consecutive non-word characters', () => { editor.setText(', => \n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); }); it('skips consecutive uppercase characters', () => { editor.setText(' AAADF \n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 6]); editor.setText('ALPhA\n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); }); it('skips consecutive numbers', () => { editor.setText(' 88 \n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 3]); }); it('works with multiple cursors', () => { editor.setText('curOp\ncursorOptions\n'); editor.setCursorBufferPosition([0, 0]); editor.addCursorAtBufferPosition([1, 0]); const [cursor1, cursor2] = editor.getCursors(); editor.moveToNextSubwordBoundary(); expect(cursor1.getBufferPosition()).toEqual([0, 3]); expect(cursor2.getBufferPosition()).toEqual([1, 6]); }); it('works with non-English characters', () => { editor.setText('supåTøåst \n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.setText('supaÖast \n'); editor.setCursorBufferPosition([0, 0]); editor.moveToNextSubwordBoundary(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); }); }); describe('.moveToBeginningOfNextParagraph()', () => { it('moves the cursor before the first line of the next paragraph', () => { editor.setCursorBufferPosition([0, 6]); editor.foldBufferRow(4); editor.moveToBeginningOfNextParagraph(); expect(editor.getCursorBufferPosition()).toEqual([10, 0]); editor.setText(''); editor.setCursorBufferPosition([0, 0]); editor.moveToBeginningOfNextParagraph(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); it('moves the cursor before the first line of the next paragraph (CRLF line endings)', () => { editor.setText(editor.getText().replace(/\n/g, '\r\n')); editor.setCursorBufferPosition([0, 6]); editor.foldBufferRow(4); editor.moveToBeginningOfNextParagraph(); expect(editor.getCursorBufferPosition()).toEqual([10, 0]); editor.setText(''); editor.setCursorBufferPosition([0, 0]); editor.moveToBeginningOfNextParagraph(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); }); describe('.moveToBeginningOfPreviousParagraph()', () => { it('moves the cursor before the first line of the previous paragraph', () => { editor.setCursorBufferPosition([10, 0]); editor.foldBufferRow(4); editor.moveToBeginningOfPreviousParagraph(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); editor.setText(''); editor.setCursorBufferPosition([0, 0]); editor.moveToBeginningOfPreviousParagraph(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); it('moves the cursor before the first line of the previous paragraph (CRLF line endings)', () => { editor.setText(editor.getText().replace(/\n/g, '\r\n')); editor.setCursorBufferPosition([10, 0]); editor.foldBufferRow(4); editor.moveToBeginningOfPreviousParagraph(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); editor.setText(''); editor.setCursorBufferPosition([0, 0]); editor.moveToBeginningOfPreviousParagraph(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); }); describe('.getCurrentParagraphBufferRange()', () => { it('returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file', () => { buffer.setText( ' ' + dedent` I am the first paragraph, bordered by the beginning of the file ${' '} I am the second paragraph with blank lines above and below me. I am the last paragraph, bordered by the end of the file.\ ` ); // in a paragraph editor.setCursorBufferPosition([1, 7]); expect(editor.getCurrentParagraphBufferRange()).toEqual([ [0, 0], [2, 8] ]); editor.setCursorBufferPosition([7, 1]); expect(editor.getCurrentParagraphBufferRange()).toEqual([ [5, 0], [7, 3] ]); editor.setCursorBufferPosition([9, 10]); expect(editor.getCurrentParagraphBufferRange()).toEqual([ [9, 0], [10, 32] ]); // between paragraphs editor.setCursorBufferPosition([3, 1]); expect(editor.getCurrentParagraphBufferRange()).toBeUndefined(); }); it('will limit paragraph range to comments', () => { atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.js'); editor.setText(dedent` var quicksort = function () { /* Single line comment block */ var sort = function(items) {}; /* A multiline comment is here */ var sort = function(items) {}; // A comment // // Multiple comment // lines var sort = function(items) {}; // comment line after fn var nosort = function(items) { item; } };\ `); function paragraphBufferRangeForRow(row) { editor.setCursorBufferPosition([row, 0]); return editor.getLastCursor().getCurrentParagraphBufferRange(); } expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]]); expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]]); expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]]); expect(paragraphBufferRangeForRow(3)).toBeFalsy(); expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]]); expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]]); expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]]); expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]]); expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]]); expect(paragraphBufferRangeForRow(9)).toBeFalsy(); expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]]); expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]]); expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]]); expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]]); expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]]); expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]]); }); }); describe('getCursorAtScreenPosition(screenPosition)', () => { it('returns the cursor at the given screenPosition', () => { const cursor1 = editor.addCursorAtScreenPosition([0, 2]); const cursor2 = editor.getCursorAtScreenPosition( cursor1.getScreenPosition() ); expect(cursor2).toBe(cursor1); }); }); describe('::getCursorScreenPositions()', () => { it('returns the cursor positions in the order they were added', () => { editor.foldBufferRow(4); editor.addCursorAtBufferPosition([8, 5]); editor.addCursorAtBufferPosition([3, 5]); expect(editor.getCursorScreenPositions()).toEqual([ [0, 0], [5, 5], [3, 5] ]); }); }); describe('::getCursorsOrderedByBufferPosition()', () => { it('returns all cursors ordered by buffer positions', () => { const originalCursor = editor.getLastCursor(); const cursor1 = editor.addCursorAtBufferPosition([8, 5]); const cursor2 = editor.addCursorAtBufferPosition([4, 5]); expect(editor.getCursorsOrderedByBufferPosition()).toEqual([ originalCursor, cursor2, cursor1 ]); }); }); describe('addCursorAtScreenPosition(screenPosition)', () => { describe('when a cursor already exists at the position', () => { it('returns the existing cursor', () => { const cursor1 = editor.addCursorAtScreenPosition([0, 2]); const cursor2 = editor.addCursorAtScreenPosition([0, 2]); expect(cursor2).toBe(cursor1); }); }); }); describe('addCursorAtBufferPosition(bufferPosition)', () => { describe('when a cursor already exists at the position', () => { it('returns the existing cursor', () => { const cursor1 = editor.addCursorAtBufferPosition([1, 4]); const cursor2 = editor.addCursorAtBufferPosition([1, 4]); expect(cursor2.marker).toBe(cursor1.marker); }); }); }); describe('.getCursorScope()', () => { it('returns the current scope', () => { const descriptor = editor.getCursorScope(); expect(descriptor.scopes).toContain('source.js'); }); }); }); describe('selection', () => { let selection; beforeEach(() => { selection = editor.getLastSelection(); }); describe('.getLastSelection()', () => { it('creates a new selection at (0, 0) if the last selection has been destroyed', () => { editor.getLastSelection().destroy(); expect(editor.getLastSelection().getBufferRange()).toEqual([ [0, 0], [0, 0] ]); }); it("doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", () => { let callCount = 0; editor.getLastSelection().destroy(); editor.onDidAddCursor(function(cursor) { callCount++; editor.getLastSelection(); }); expect(editor.getLastSelection().getBufferRange()).toEqual([ [0, 0], [0, 0] ]); expect(callCount).toBe(1); }); }); describe('.getSelections()', () => { it('creates a new selection at (0, 0) if the last selection has been destroyed', () => { editor.getLastSelection().destroy(); expect(editor.getSelections()[0].getBufferRange()).toEqual([ [0, 0], [0, 0] ]); }); }); describe('when the selection range changes', () => { it('emits an event with the old range, new range, and the selection that moved', () => { let rangeChangedHandler; editor.setSelectedBufferRange([[3, 0], [4, 5]]); editor.onDidChangeSelectionRange( (rangeChangedHandler = jasmine.createSpy()) ); editor.selectToBufferPosition([6, 2]); expect(rangeChangedHandler).toHaveBeenCalled(); const eventObject = rangeChangedHandler.mostRecentCall.args[0]; expect(eventObject.oldBufferRange).toEqual([[3, 0], [4, 5]]); expect(eventObject.oldScreenRange).toEqual([[3, 0], [4, 5]]); expect(eventObject.newBufferRange).toEqual([[3, 0], [6, 2]]); expect(eventObject.newScreenRange).toEqual([[3, 0], [6, 2]]); expect(eventObject.selection).toBe(selection); }); }); describe('.selectUp/Down/Left/Right()', () => { it("expands each selection to its cursor's new location", () => { editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]); const [selection1, selection2] = editor.getSelections(); editor.selectRight(); expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 14]]); expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 22]]); editor.selectLeft(); editor.selectLeft(); expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]); expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]); editor.selectDown(); expect(selection1.getBufferRange()).toEqual([[0, 9], [1, 12]]); expect(selection2.getBufferRange()).toEqual([[3, 16], [4, 20]]); editor.selectUp(); expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]); expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]); }); it('merges selections when they intersect when moving down', () => { editor.setSelectedBufferRanges([ [[0, 9], [0, 13]], [[1, 10], [1, 20]], [[2, 15], [3, 25]] ]); const [selection1] = editor.getSelections(); editor.selectDown(); expect(editor.getSelections()).toEqual([selection1]); expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]); expect(selection1.isReversed()).toBeFalsy(); }); it('merges selections when they intersect when moving up', () => { editor.setSelectedBufferRanges( [[[0, 9], [0, 13]], [[1, 10], [1, 20]]], { reversed: true } ); const [selection1] = editor.getSelections(); editor.selectUp(); expect(editor.getSelections().length).toBe(1); expect(editor.getSelections()).toEqual([selection1]); expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]); expect(selection1.isReversed()).toBeTruthy(); }); it('merges selections when they intersect when moving left', () => { editor.setSelectedBufferRanges( [[[0, 9], [0, 13]], [[0, 13], [1, 20]]], { reversed: true } ); const [selection1] = editor.getSelections(); editor.selectLeft(); expect(editor.getSelections()).toEqual([selection1]); expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]); expect(selection1.isReversed()).toBeTruthy(); }); it('merges selections when they intersect when moving right', () => { editor.setSelectedBufferRanges([[[0, 9], [0, 14]], [[0, 14], [1, 20]]]); const [selection1] = editor.getSelections(); editor.selectRight(); expect(editor.getSelections()).toEqual([selection1]); expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]); expect(selection1.isReversed()).toBeFalsy(); }); describe('when counts are passed into the selection functions', () => { it("expands each selection to its cursor's new location", () => { editor.setSelectedBufferRanges([ [[0, 9], [0, 13]], [[3, 16], [3, 21]] ]); const [selection1, selection2] = editor.getSelections(); editor.selectRight(2); expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 15]]); expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 23]]); editor.selectLeft(3); expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]); expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]); editor.selectDown(3); expect(selection1.getBufferRange()).toEqual([[0, 9], [3, 12]]); expect(selection2.getBufferRange()).toEqual([[3, 16], [6, 20]]); editor.selectUp(2); expect(selection1.getBufferRange()).toEqual([[0, 9], [1, 12]]); expect(selection2.getBufferRange()).toEqual([[3, 16], [4, 20]]); }); }); }); describe('.selectToBufferPosition(bufferPosition)', () => { it('expands the last selection to the given position', () => { editor.setSelectedBufferRange([[3, 0], [4, 5]]); editor.addCursorAtBufferPosition([5, 6]); editor.selectToBufferPosition([6, 2]); const selections = editor.getSelections(); expect(selections.length).toBe(2); const [selection1, selection2] = selections; expect(selection1.getBufferRange()).toEqual([[3, 0], [4, 5]]); expect(selection2.getBufferRange()).toEqual([[5, 6], [6, 2]]); }); }); describe('.selectToScreenPosition(screenPosition)', () => { it('expands the last selection to the given position', () => { editor.setSelectedBufferRange([[3, 0], [4, 5]]); editor.addCursorAtScreenPosition([5, 6]); editor.selectToScreenPosition([6, 2]); const selections = editor.getSelections(); expect(selections.length).toBe(2); const [selection1, selection2] = selections; expect(selection1.getScreenRange()).toEqual([[3, 0], [4, 5]]); expect(selection2.getScreenRange()).toEqual([[5, 6], [6, 2]]); }); describe('when selecting with an initial screen range', () => { it('switches the direction of the selection when selecting to positions before/after the start of the initial range', () => { editor.setCursorScreenPosition([5, 10]); editor.selectWordsContainingCursors(); editor.selectToScreenPosition([3, 0]); expect(editor.getLastSelection().isReversed()).toBe(true); editor.selectToScreenPosition([9, 0]); expect(editor.getLastSelection().isReversed()).toBe(false); }); }); }); describe('.selectToBeginningOfNextParagraph()', () => { it('selects from the cursor to first line of the next paragraph', () => { editor.setSelectedBufferRange([[3, 0], [4, 5]]); editor.addCursorAtScreenPosition([5, 6]); editor.selectToScreenPosition([6, 2]); editor.selectToBeginningOfNextParagraph(); const selections = editor.getSelections(); expect(selections.length).toBe(1); expect(selections[0].getScreenRange()).toEqual([[3, 0], [10, 0]]); }); }); describe('.selectToBeginningOfPreviousParagraph()', () => { it('selects from the cursor to the first line of the previous paragraph', () => { editor.setSelectedBufferRange([[3, 0], [4, 5]]); editor.addCursorAtScreenPosition([5, 6]); editor.selectToScreenPosition([6, 2]); editor.selectToBeginningOfPreviousParagraph(); const selections = editor.getSelections(); expect(selections.length).toBe(1); expect(selections[0].getScreenRange()).toEqual([[0, 0], [5, 6]]); }); it('merges selections if they intersect, maintaining the directionality of the last selection', () => { editor.setCursorScreenPosition([4, 10]); editor.selectToScreenPosition([5, 27]); editor.addCursorAtScreenPosition([3, 10]); editor.selectToScreenPosition([6, 27]); let selections = editor.getSelections(); expect(selections.length).toBe(1); let [selection1] = selections; expect(selection1.getScreenRange()).toEqual([[3, 10], [6, 27]]); expect(selection1.isReversed()).toBeFalsy(); editor.addCursorAtScreenPosition([7, 4]); editor.selectToScreenPosition([4, 11]); selections = editor.getSelections(); expect(selections.length).toBe(1); [selection1] = selections; expect(selection1.getScreenRange()).toEqual([[3, 10], [7, 4]]); expect(selection1.isReversed()).toBeTruthy(); }); }); describe('.selectToTop()', () => { it('selects text from cursor position to the top of the buffer', () => { editor.setCursorScreenPosition([11, 2]); editor.addCursorAtScreenPosition([10, 0]); editor.selectToTop(); expect(editor.getCursors().length).toBe(1); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); expect(editor.getLastSelection().getBufferRange()).toEqual([ [0, 0], [11, 2] ]); expect(editor.getLastSelection().isReversed()).toBeTruthy(); }); }); describe('.selectToBottom()', () => { it('selects text from cursor position to the bottom of the buffer', () => { editor.setCursorScreenPosition([10, 0]); editor.addCursorAtScreenPosition([9, 3]); editor.selectToBottom(); expect(editor.getCursors().length).toBe(1); expect(editor.getCursorBufferPosition()).toEqual([12, 2]); expect(editor.getLastSelection().getBufferRange()).toEqual([ [9, 3], [12, 2] ]); expect(editor.getLastSelection().isReversed()).toBeFalsy(); }); }); describe('.selectAll()', () => { it('selects the entire buffer', () => { editor.selectAll(); expect(editor.getLastSelection().getBufferRange()).toEqual( buffer.getRange() ); }); }); describe('.selectToBeginningOfLine()', () => { it('selects text from cursor position to beginning of line', () => { editor.setCursorScreenPosition([12, 2]); editor.addCursorAtScreenPosition([11, 3]); editor.selectToBeginningOfLine(); expect(editor.getCursors().length).toBe(2); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([12, 0]); expect(cursor2.getBufferPosition()).toEqual([11, 0]); expect(editor.getSelections().length).toBe(2); const [selection1, selection2] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[12, 0], [12, 2]]); expect(selection1.isReversed()).toBeTruthy(); expect(selection2.getBufferRange()).toEqual([[11, 0], [11, 3]]); expect(selection2.isReversed()).toBeTruthy(); }); }); describe('.selectToEndOfLine()', () => { it('selects text from cursor position to end of line', () => { editor.setCursorScreenPosition([12, 0]); editor.addCursorAtScreenPosition([11, 3]); editor.selectToEndOfLine(); expect(editor.getCursors().length).toBe(2); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([12, 2]); expect(cursor2.getBufferPosition()).toEqual([11, 44]); expect(editor.getSelections().length).toBe(2); const [selection1, selection2] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[12, 0], [12, 2]]); expect(selection1.isReversed()).toBeFalsy(); expect(selection2.getBufferRange()).toEqual([[11, 3], [11, 44]]); expect(selection2.isReversed()).toBeFalsy(); }); }); describe('.selectLinesContainingCursors()', () => { it('selects to the entire line (including newlines) at given row', () => { editor.setCursorScreenPosition([1, 2]); editor.selectLinesContainingCursors(); expect(editor.getSelectedBufferRange()).toEqual([[1, 0], [2, 0]]); expect(editor.getSelectedText()).toBe( ' var sort = function(items) {\n' ); editor.setCursorScreenPosition([12, 2]); editor.selectLinesContainingCursors(); expect(editor.getSelectedBufferRange()).toEqual([[12, 0], [12, 2]]); editor.setCursorBufferPosition([0, 2]); editor.selectLinesContainingCursors(); editor.selectLinesContainingCursors(); expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [2, 0]]); }); describe('when the selection spans multiple row', () => { it('selects from the beginning of the first line to the last line', () => { selection = editor.getLastSelection(); selection.setBufferRange([[1, 10], [3, 20]]); editor.selectLinesContainingCursors(); expect(editor.getSelectedBufferRange()).toEqual([[1, 0], [4, 0]]); }); }); }); describe('.selectToBeginningOfWord()', () => { it('selects text from cursor position to beginning of word', () => { editor.setCursorScreenPosition([0, 13]); editor.addCursorAtScreenPosition([3, 49]); editor.selectToBeginningOfWord(); expect(editor.getCursors().length).toBe(2); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([0, 4]); expect(cursor2.getBufferPosition()).toEqual([3, 47]); expect(editor.getSelections().length).toBe(2); const [selection1, selection2] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 13]]); expect(selection1.isReversed()).toBeTruthy(); expect(selection2.getBufferRange()).toEqual([[3, 47], [3, 49]]); expect(selection2.isReversed()).toBeTruthy(); }); }); describe('.selectToEndOfWord()', () => { it('selects text from cursor position to end of word', () => { editor.setCursorScreenPosition([0, 4]); editor.addCursorAtScreenPosition([3, 48]); editor.selectToEndOfWord(); expect(editor.getCursors().length).toBe(2); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([0, 13]); expect(cursor2.getBufferPosition()).toEqual([3, 50]); expect(editor.getSelections().length).toBe(2); const [selection1, selection2] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 13]]); expect(selection1.isReversed()).toBeFalsy(); expect(selection2.getBufferRange()).toEqual([[3, 48], [3, 50]]); expect(selection2.isReversed()).toBeFalsy(); }); }); describe('.selectToBeginningOfNextWord()', () => { it('selects text from cursor position to beginning of next word', () => { editor.setCursorScreenPosition([0, 4]); editor.addCursorAtScreenPosition([3, 48]); editor.selectToBeginningOfNextWord(); expect(editor.getCursors().length).toBe(2); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([0, 14]); expect(cursor2.getBufferPosition()).toEqual([3, 51]); expect(editor.getSelections().length).toBe(2); const [selection1, selection2] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 14]]); expect(selection1.isReversed()).toBeFalsy(); expect(selection2.getBufferRange()).toEqual([[3, 48], [3, 51]]); expect(selection2.isReversed()).toBeFalsy(); }); }); describe('.selectToPreviousWordBoundary()', () => { it('select to the previous word boundary', () => { editor.setCursorBufferPosition([0, 8]); editor.addCursorAtBufferPosition([2, 0]); editor.addCursorAtBufferPosition([3, 4]); editor.addCursorAtBufferPosition([3, 14]); editor.selectToPreviousWordBoundary(); expect(editor.getSelections().length).toBe(4); const [ selection1, selection2, selection3, selection4 ] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[0, 8], [0, 4]]); expect(selection1.isReversed()).toBeTruthy(); expect(selection2.getBufferRange()).toEqual([[2, 0], [1, 30]]); expect(selection2.isReversed()).toBeTruthy(); expect(selection3.getBufferRange()).toEqual([[3, 4], [3, 0]]); expect(selection3.isReversed()).toBeTruthy(); expect(selection4.getBufferRange()).toEqual([[3, 14], [3, 13]]); expect(selection4.isReversed()).toBeTruthy(); }); }); describe('.selectToNextWordBoundary()', () => { it('select to the next word boundary', () => { editor.setCursorBufferPosition([0, 8]); editor.addCursorAtBufferPosition([2, 40]); editor.addCursorAtBufferPosition([4, 0]); editor.addCursorAtBufferPosition([3, 30]); editor.selectToNextWordBoundary(); expect(editor.getSelections().length).toBe(4); const [ selection1, selection2, selection3, selection4 ] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[0, 8], [0, 13]]); expect(selection1.isReversed()).toBeFalsy(); expect(selection2.getBufferRange()).toEqual([[2, 40], [3, 0]]); expect(selection2.isReversed()).toBeFalsy(); expect(selection3.getBufferRange()).toEqual([[4, 0], [4, 4]]); expect(selection3.isReversed()).toBeFalsy(); expect(selection4.getBufferRange()).toEqual([[3, 30], [3, 31]]); expect(selection4.isReversed()).toBeFalsy(); }); }); describe('.selectToPreviousSubwordBoundary', () => { it('selects subwords', () => { editor.setText(''); editor.insertText('_word\n'); editor.insertText(' getPreviousWord\n'); editor.insertText('e, => \n'); editor.insertText(' 88 \n'); editor.setCursorBufferPosition([0, 5]); editor.addCursorAtBufferPosition([1, 7]); editor.addCursorAtBufferPosition([2, 5]); editor.addCursorAtBufferPosition([3, 3]); const [ selection1, selection2, selection3, selection4 ] = editor.getSelections(); editor.selectToPreviousSubwordBoundary(); expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]); expect(selection1.isReversed()).toBeTruthy(); expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]); expect(selection2.isReversed()).toBeTruthy(); expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]); expect(selection3.isReversed()).toBeTruthy(); expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]); expect(selection4.isReversed()).toBeTruthy(); }); }); describe('.selectToNextSubwordBoundary', () => { it('selects subwords', () => { editor.setText(''); editor.insertText('word_\n'); editor.insertText('getPreviousWord\n'); editor.insertText('e, => \n'); editor.insertText(' 88 \n'); editor.setCursorBufferPosition([0, 1]); editor.addCursorAtBufferPosition([1, 7]); editor.addCursorAtBufferPosition([2, 2]); editor.addCursorAtBufferPosition([3, 1]); const [ selection1, selection2, selection3, selection4 ] = editor.getSelections(); editor.selectToNextSubwordBoundary(); expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]); expect(selection1.isReversed()).toBeFalsy(); expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]); expect(selection2.isReversed()).toBeFalsy(); expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]); expect(selection3.isReversed()).toBeFalsy(); expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]); expect(selection4.isReversed()).toBeFalsy(); }); }); describe('.deleteToBeginningOfSubword', () => { it('deletes subwords', () => { editor.setText(''); editor.insertText('_word\n'); editor.insertText(' getPreviousWord\n'); editor.insertText('e, => \n'); editor.insertText(' 88 \n'); editor.setCursorBufferPosition([0, 5]); editor.addCursorAtBufferPosition([1, 7]); editor.addCursorAtBufferPosition([2, 5]); editor.addCursorAtBufferPosition([3, 3]); const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors(); editor.deleteToBeginningOfSubword(); expect(buffer.lineForRow(0)).toBe('_'); expect(buffer.lineForRow(1)).toBe(' getviousWord'); expect(buffer.lineForRow(2)).toBe('e, '); expect(buffer.lineForRow(3)).toBe(' '); expect(cursor1.getBufferPosition()).toEqual([0, 1]); expect(cursor2.getBufferPosition()).toEqual([1, 4]); expect(cursor3.getBufferPosition()).toEqual([2, 3]); expect(cursor4.getBufferPosition()).toEqual([3, 1]); editor.deleteToBeginningOfSubword(); expect(buffer.lineForRow(0)).toBe(''); expect(buffer.lineForRow(1)).toBe(' viousWord'); expect(buffer.lineForRow(2)).toBe('e '); expect(buffer.lineForRow(3)).toBe(' '); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor2.getBufferPosition()).toEqual([1, 1]); expect(cursor3.getBufferPosition()).toEqual([2, 1]); expect(cursor4.getBufferPosition()).toEqual([3, 0]); editor.deleteToBeginningOfSubword(); expect(buffer.lineForRow(0)).toBe(''); expect(buffer.lineForRow(1)).toBe('viousWord'); expect(buffer.lineForRow(2)).toBe(' '); expect(buffer.lineForRow(3)).toBe(''); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor2.getBufferPosition()).toEqual([1, 0]); expect(cursor3.getBufferPosition()).toEqual([2, 0]); expect(cursor4.getBufferPosition()).toEqual([2, 1]); }); }); describe('.deleteToEndOfSubword', () => { it('deletes subwords', () => { editor.setText(''); editor.insertText('word_\n'); editor.insertText('getPreviousWord \n'); editor.insertText('e, => \n'); editor.insertText(' 88 \n'); editor.setCursorBufferPosition([0, 0]); editor.addCursorAtBufferPosition([1, 0]); editor.addCursorAtBufferPosition([2, 2]); editor.addCursorAtBufferPosition([3, 0]); const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors(); editor.deleteToEndOfSubword(); expect(buffer.lineForRow(0)).toBe('_'); expect(buffer.lineForRow(1)).toBe('PreviousWord '); expect(buffer.lineForRow(2)).toBe('e, '); expect(buffer.lineForRow(3)).toBe('88 '); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor2.getBufferPosition()).toEqual([1, 0]); expect(cursor3.getBufferPosition()).toEqual([2, 2]); expect(cursor4.getBufferPosition()).toEqual([3, 0]); editor.deleteToEndOfSubword(); expect(buffer.lineForRow(0)).toBe(''); expect(buffer.lineForRow(1)).toBe('Word '); expect(buffer.lineForRow(2)).toBe('e,'); expect(buffer.lineForRow(3)).toBe(' '); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor2.getBufferPosition()).toEqual([1, 0]); expect(cursor3.getBufferPosition()).toEqual([2, 2]); expect(cursor4.getBufferPosition()).toEqual([3, 0]); }); }); describe('.selectWordsContainingCursors()', () => { describe('when the cursor is inside a word', () => { it('selects the entire word', () => { editor.setCursorScreenPosition([0, 8]); editor.selectWordsContainingCursors(); expect(editor.getSelectedText()).toBe('quicksort'); }); }); describe('when the cursor is between two words', () => { it('selects the word the cursor is on', () => { editor.setCursorBufferPosition([0, 4]); editor.selectWordsContainingCursors(); expect(editor.getSelectedText()).toBe('quicksort'); editor.setCursorBufferPosition([0, 3]); editor.selectWordsContainingCursors(); expect(editor.getSelectedText()).toBe('var'); editor.setCursorBufferPosition([1, 22]); editor.selectWordsContainingCursors(); expect(editor.getSelectedText()).toBe('items'); }); }); describe('when the cursor is inside a region of whitespace', () => { it('selects the whitespace region', () => { editor.setCursorScreenPosition([5, 2]); editor.selectWordsContainingCursors(); expect(editor.getSelectedBufferRange()).toEqual([[5, 0], [5, 6]]); editor.setCursorScreenPosition([5, 0]); editor.selectWordsContainingCursors(); expect(editor.getSelectedBufferRange()).toEqual([[5, 0], [5, 6]]); }); }); describe('when the cursor is at the end of the text', () => { it('select the previous word', () => { editor.buffer.append('word'); editor.moveToBottom(); editor.selectWordsContainingCursors(); expect(editor.getSelectedBufferRange()).toEqual([[12, 2], [12, 6]]); }); }); it("selects words based on the non-word characters configured at the cursor's current scope", () => { editor.setText("one-one; 'two-two'; three-three"); editor.setCursorBufferPosition([0, 1]); editor.addCursorAtBufferPosition([0, 12]); const scopeDescriptors = editor .getCursors() .map(c => c.getScopeDescriptor()); expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js']); expect(scopeDescriptors[1].getScopesArray()).toEqual([ 'source.js', 'string.quoted' ]); spyOn( editor.getBuffer().getLanguageMode(), 'getNonWordCharacters' ).andCallFake(function(position) { const result = '/()"\':,.;<>~!@#$%^&*|+=[]{}`?'; const scopes = this.scopeDescriptorForPosition( position ).getScopesArray(); if (scopes.some(scope => scope.startsWith('string'))) { return result; } else { return result + '-'; } }); editor.selectWordsContainingCursors(); expect(editor.getSelections()[0].getText()).toBe('one'); expect(editor.getSelections()[1].getText()).toBe('two-two'); }); }); describe('.selectToFirstCharacterOfLine()', () => { it("moves to the first character of the current line or the beginning of the line if it's already on the first character", () => { editor.setCursorScreenPosition([0, 5]); editor.addCursorAtScreenPosition([1, 7]); editor.selectToFirstCharacterOfLine(); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor2.getBufferPosition()).toEqual([1, 2]); expect(editor.getSelections().length).toBe(2); let [selection1, selection2] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[0, 0], [0, 5]]); expect(selection1.isReversed()).toBeTruthy(); expect(selection2.getBufferRange()).toEqual([[1, 2], [1, 7]]); expect(selection2.isReversed()).toBeTruthy(); editor.selectToFirstCharacterOfLine(); [selection1, selection2] = editor.getSelections(); expect(selection1.getBufferRange()).toEqual([[0, 0], [0, 5]]); expect(selection1.isReversed()).toBeTruthy(); expect(selection2.getBufferRange()).toEqual([[1, 0], [1, 7]]); expect(selection2.isReversed()).toBeTruthy(); }); }); describe('.setSelectedBufferRanges(ranges)', () => { it('clears existing selections and creates selections for each of the given ranges', () => { editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 2], [3, 3]], [[4, 4], [5, 5]] ]); editor.setSelectedBufferRanges([[[5, 5], [6, 6]]]); expect(editor.getSelectedBufferRanges()).toEqual([[[5, 5], [6, 6]]]); }); it('merges intersecting selections', () => { editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]); expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [5, 5]]]); }); it('does not merge non-empty adjacent selections', () => { editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 2], [3, 3]], [[3, 3], [5, 5]] ]); }); it('recycles existing selection instances', () => { selection = editor.getLastSelection(); editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]); const [selection1] = editor.getSelections(); expect(selection1).toBe(selection); expect(selection1.getBufferRange()).toEqual([[2, 2], [3, 3]]); }); describe("when the 'preserveFolds' option is false (the default)", () => { it("removes folds that contain one or both of the selection's end points", () => { editor.setSelectedBufferRange([[0, 0], [0, 0]]); editor.foldBufferRowRange(1, 4); editor.foldBufferRowRange(2, 3); editor.foldBufferRowRange(6, 8); editor.foldBufferRowRange(10, 11); editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]); expect(editor.isFoldedAtScreenRow(1)).toBeFalsy(); expect(editor.isFoldedAtScreenRow(2)).toBeFalsy(); expect(editor.isFoldedAtScreenRow(6)).toBeFalsy(); expect(editor.isFoldedAtScreenRow(10)).toBeTruthy(); editor.setSelectedBufferRange([[10, 0], [12, 0]]); expect(editor.isFoldedAtScreenRow(10)).toBeTruthy(); }); }); describe("when the 'preserveFolds' option is true", () => { it('does not remove folds that contain the selections', () => { editor.setSelectedBufferRange([[0, 0], [0, 0]]); editor.foldBufferRowRange(1, 4); editor.foldBufferRowRange(6, 8); editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], { preserveFolds: true }); expect(editor.isFoldedAtBufferRow(1)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); }); }); }); describe('.setSelectedScreenRanges(ranges)', () => { beforeEach(() => editor.foldBufferRow(4)); it('clears existing selections and creates selections for each of the given ranges', () => { editor.setSelectedScreenRanges([[[3, 4], [3, 7]], [[5, 4], [5, 7]]]); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 4], [3, 7]], [[8, 4], [8, 7]] ]); editor.setSelectedScreenRanges([[[6, 2], [6, 4]]]); expect(editor.getSelectedScreenRanges()).toEqual([[[6, 2], [6, 4]]]); }); it('merges intersecting selections and unfolds the fold which contain them', () => { editor.foldBufferRow(0); // Use buffer ranges because only the first line is on screen editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]); expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [5, 5]]]); }); it('recycles existing selection instances', () => { selection = editor.getLastSelection(); editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]]); const [selection1] = editor.getSelections(); expect(selection1).toBe(selection); expect(selection1.getScreenRange()).toEqual([[2, 2], [3, 4]]); }); }); describe('.selectMarker(marker)', () => { describe('if the marker is valid', () => { it("selects the marker's range and returns the selected range", () => { const marker = editor.markBufferRange([[0, 1], [3, 3]]); expect(editor.selectMarker(marker)).toEqual([[0, 1], [3, 3]]); expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 3]]); }); }); describe('if the marker is invalid', () => { it('does not change the selection and returns a falsy value', () => { const marker = editor.markBufferRange([[0, 1], [3, 3]]); marker.destroy(); expect(editor.selectMarker(marker)).toBeFalsy(); expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 0]]); }); }); }); describe('.addSelectionForBufferRange(bufferRange)', () => { it('adds a selection for the specified buffer range', () => { editor.addSelectionForBufferRange([[3, 4], [5, 6]]); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 0], [0, 0]], [[3, 4], [5, 6]] ]); }); }); describe('.addSelectionBelow()', () => { describe('when the selection is non-empty', () => { it('selects the same region of the line below current selections if possible', () => { editor.setSelectedBufferRange([[3, 16], [3, 21]]); editor.addSelectionForBufferRange([[3, 25], [3, 34]]); editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 16], [3, 21]], [[3, 25], [3, 34]], [[4, 16], [4, 21]], [[4, 25], [4, 29]] ]); }); it('skips lines that are too short to create a non-empty selection', () => { editor.setSelectedBufferRange([[3, 31], [3, 38]]); editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 31], [3, 38]], [[6, 31], [6, 38]] ]); }); it("honors the original selection's range (goal range) when adding across shorter lines", () => { editor.setSelectedBufferRange([[3, 22], [3, 38]]); editor.addSelectionBelow(); editor.addSelectionBelow(); editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 22], [3, 38]], [[4, 22], [4, 29]], [[5, 22], [5, 30]], [[6, 22], [6, 38]] ]); }); it('clears selection goal ranges when the selection changes', () => { editor.setSelectedBufferRange([[3, 22], [3, 38]]); editor.addSelectionBelow(); editor.selectLeft(); editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 22], [3, 37]], [[4, 22], [4, 29]], [[5, 22], [5, 28]] ]); // goal range from previous add selection is honored next time editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 22], [3, 37]], [[4, 22], [4, 29]], [[5, 22], [5, 30]], // select to end of line 5 because line 4's goal range was reset by line 3 previously [[6, 22], [6, 28]] ]); }); it('can add selections to soft-wrapped line segments', () => { editor.setSoftWrapped(true); editor.setEditorWidthInChars(40); editor.setDefaultCharWidth(1); editor.setSelectedScreenRange([[3, 10], [3, 15]]); editor.addSelectionBelow(); expect(editor.getSelectedScreenRanges()).toEqual([ [[3, 10], [3, 15]], [[4, 10], [4, 15]] ]); }); it('takes atomic tokens into account', async () => { editor = await atom.workspace.open( 'sample-with-tabs-and-leading-comment.coffee', { autoIndent: false } ); editor.setSelectedBufferRange([[2, 1], [2, 3]]); editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 1], [2, 3]], [[3, 1], [3, 2]] ]); }); }); describe('when the selection is empty', () => { describe('when lines are soft-wrapped', () => { beforeEach(() => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(40); }); it('skips soft-wrap indentation tokens', () => { editor.setCursorScreenPosition([3, 0]); editor.addSelectionBelow(); expect(editor.getSelectedScreenRanges()).toEqual([ [[3, 0], [3, 0]], [[4, 4], [4, 4]] ]); }); it("does not skip them if they're shorter than the current column", () => { editor.setCursorScreenPosition([3, 37]); editor.addSelectionBelow(); expect(editor.getSelectedScreenRanges()).toEqual([ [[3, 37], [3, 37]], [[4, 26], [4, 26]] ]); }); }); it('does not skip lines that are shorter than the current column', () => { editor.setCursorBufferPosition([3, 36]); editor.addSelectionBelow(); editor.addSelectionBelow(); editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 36], [3, 36]], [[4, 29], [4, 29]], [[5, 30], [5, 30]], [[6, 36], [6, 36]] ]); }); it('skips empty lines when the column is non-zero', () => { editor.setCursorBufferPosition([9, 4]); editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[9, 4], [9, 4]], [[11, 4], [11, 4]] ]); }); it('does not skip empty lines when the column is zero', () => { editor.setCursorBufferPosition([9, 0]); editor.addSelectionBelow(); expect(editor.getSelectedBufferRanges()).toEqual([ [[9, 0], [9, 0]], [[10, 0], [10, 0]] ]); }); }); it('does not create a new selection if it would be fully contained within another selection', () => { editor.setText('abc\ndef\nghi\njkl\nmno'); editor.setCursorBufferPosition([0, 1]); let addedSelectionCount = 0; editor.onDidAddSelection(() => { addedSelectionCount++; }); editor.addSelectionBelow(); editor.addSelectionBelow(); editor.addSelectionBelow(); expect(addedSelectionCount).toBe(3); }); }); describe('.addSelectionAbove()', () => { describe('when the selection is non-empty', () => { it('selects the same region of the line above current selections if possible', () => { editor.setSelectedBufferRange([[3, 16], [3, 21]]); editor.addSelectionForBufferRange([[3, 37], [3, 44]]); editor.addSelectionAbove(); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 16], [3, 21]], [[3, 37], [3, 44]], [[2, 16], [2, 21]], [[2, 37], [2, 40]] ]); }); it('skips lines that are too short to create a non-empty selection', () => { editor.setSelectedBufferRange([[6, 31], [6, 38]]); editor.addSelectionAbove(); expect(editor.getSelectedBufferRanges()).toEqual([ [[6, 31], [6, 38]], [[3, 31], [3, 38]] ]); }); it("honors the original selection's range (goal range) when adding across shorter lines", () => { editor.setSelectedBufferRange([[6, 22], [6, 38]]); editor.addSelectionAbove(); editor.addSelectionAbove(); editor.addSelectionAbove(); expect(editor.getSelectedBufferRanges()).toEqual([ [[6, 22], [6, 38]], [[5, 22], [5, 30]], [[4, 22], [4, 29]], [[3, 22], [3, 38]] ]); }); it('can add selections to soft-wrapped line segments', () => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(40); editor.setSelectedScreenRange([[4, 10], [4, 15]]); editor.addSelectionAbove(); expect(editor.getSelectedScreenRanges()).toEqual([ [[4, 10], [4, 15]], [[3, 10], [3, 15]] ]); }); it('takes atomic tokens into account', async () => { editor = await atom.workspace.open( 'sample-with-tabs-and-leading-comment.coffee', { autoIndent: false } ); editor.setSelectedBufferRange([[3, 1], [3, 2]]); editor.addSelectionAbove(); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 1], [3, 2]], [[2, 1], [2, 3]] ]); }); }); describe('when the selection is empty', () => { describe('when lines are soft-wrapped', () => { beforeEach(() => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(40); }); it('skips soft-wrap indentation tokens', () => { editor.setCursorScreenPosition([5, 0]); editor.addSelectionAbove(); expect(editor.getSelectedScreenRanges()).toEqual([ [[5, 0], [5, 0]], [[4, 4], [4, 4]] ]); }); it("does not skip them if they're shorter than the current column", () => { editor.setCursorScreenPosition([5, 29]); editor.addSelectionAbove(); expect(editor.getSelectedScreenRanges()).toEqual([ [[5, 29], [5, 29]], [[4, 26], [4, 26]] ]); }); }); it('does not skip lines that are shorter than the current column', () => { editor.setCursorBufferPosition([6, 36]); editor.addSelectionAbove(); editor.addSelectionAbove(); editor.addSelectionAbove(); expect(editor.getSelectedBufferRanges()).toEqual([ [[6, 36], [6, 36]], [[5, 30], [5, 30]], [[4, 29], [4, 29]], [[3, 36], [3, 36]] ]); }); it('skips empty lines when the column is non-zero', () => { editor.setCursorBufferPosition([11, 4]); editor.addSelectionAbove(); expect(editor.getSelectedBufferRanges()).toEqual([ [[11, 4], [11, 4]], [[9, 4], [9, 4]] ]); }); it('does not skip empty lines when the column is zero', () => { editor.setCursorBufferPosition([10, 0]); editor.addSelectionAbove(); expect(editor.getSelectedBufferRanges()).toEqual([ [[10, 0], [10, 0]], [[9, 0], [9, 0]] ]); }); }); it('does not create a new selection if it would be fully contained within another selection', () => { editor.setText('abc\ndef\nghi\njkl\nmno'); editor.setCursorBufferPosition([4, 1]); let addedSelectionCount = 0; editor.onDidAddSelection(() => { addedSelectionCount++; }); editor.addSelectionAbove(); editor.addSelectionAbove(); editor.addSelectionAbove(); expect(addedSelectionCount).toBe(3); }); }); describe('.splitSelectionsIntoLines()', () => { it('splits all multi-line selections into one selection per line', () => { editor.setSelectedBufferRange([[0, 3], [2, 4]]); editor.splitSelectionsIntoLines(); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 3], [0, 29]], [[1, 0], [1, 30]], [[2, 0], [2, 4]] ]); editor.setSelectedBufferRange([[0, 3], [1, 10]]); editor.splitSelectionsIntoLines(); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 3], [0, 29]], [[1, 0], [1, 10]] ]); editor.setSelectedBufferRange([[0, 0], [0, 3]]); editor.splitSelectionsIntoLines(); expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]]]); }); }); describe('::consolidateSelections()', () => { const makeMultipleSelections = () => { selection.setBufferRange([[3, 16], [3, 21]]); const selection2 = editor.addSelectionForBufferRange([ [3, 25], [3, 34] ]); const selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]]); const selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]]); expect(editor.getSelections()).toEqual([ selection, selection2, selection3, selection4 ]); return [selection, selection2, selection3, selection4]; }; it('destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed', () => { const [selection1] = makeMultipleSelections(); const autoscrollEvents = []; editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)); expect(editor.consolidateSelections()).toBeTruthy(); expect(editor.getSelections()).toEqual([selection1]); expect(selection1.isEmpty()).toBeFalsy(); expect(editor.consolidateSelections()).toBeFalsy(); expect(editor.getSelections()).toEqual([selection1]); expect(autoscrollEvents).toEqual([ { screenRange: selection1.getScreenRange(), options: { center: true, reversed: false } } ]); }); }); describe('when the cursor is moved while there is a selection', () => { const makeSelection = () => selection.setBufferRange([[1, 2], [1, 5]]); it('clears the selection', () => { makeSelection(); editor.moveDown(); expect(selection.isEmpty()).toBeTruthy(); makeSelection(); editor.moveUp(); expect(selection.isEmpty()).toBeTruthy(); makeSelection(); editor.moveLeft(); expect(selection.isEmpty()).toBeTruthy(); makeSelection(); editor.moveRight(); expect(selection.isEmpty()).toBeTruthy(); makeSelection(); editor.setCursorScreenPosition([3, 3]); expect(selection.isEmpty()).toBeTruthy(); }); }); it('does not share selections between different edit sessions for the same buffer', async () => { atom.workspace.getActivePane().splitRight(); const editor2 = await atom.workspace.open(editor.getPath()); expect(editor2.getText()).toBe(editor.getText()); editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]); editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]); expect(editor2.getSelectedBufferRanges()).not.toEqual( editor.getSelectedBufferRanges() ); }); }); describe('buffer manipulation', () => { describe('.moveLineUp', () => { it('moves the line under the cursor up', () => { editor.setCursorBufferPosition([1, 0]); editor.moveLineUp(); expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe( ' var sort = function(items) {' ); expect(editor.indentationForBufferRow(0)).toBe(1); expect(editor.indentationForBufferRow(1)).toBe(0); }); it("updates the line's indentation when the the autoIndent setting is true", () => { editor.update({ autoIndent: true }); editor.setCursorBufferPosition([1, 0]); editor.moveLineUp(); expect(editor.indentationForBufferRow(0)).toBe(0); expect(editor.indentationForBufferRow(1)).toBe(0); }); describe('when there is a single selection', () => { describe('when the selection spans a single line', () => { describe('when there is no fold in the preceding row', () => it('moves the line to the preceding row', () => { expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); editor.setSelectedBufferRange([[3, 2], [3, 9]]); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [2, 9]]); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); })); describe('when the cursor is at the beginning of a fold', () => it('moves the line to the previous row without breaking the fold', () => { expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); editor.setSelectedBufferRange([[4, 2], [4, 9]], { preserveFolds: true }); expect(editor.getSelectedBufferRange()).toEqual([[4, 2], [4, 9]]); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [3, 9]]); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(7)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeFalsy(); })); describe('when the preceding row consists of folded code', () => it('moves the line above the folded row and perseveres the correct folds', () => { expect(editor.lineTextForBufferRow(8)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(9)).toBe(' };'); editor.foldBufferRowRange(4, 7); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.setSelectedBufferRange([[8, 0], [8, 4]]); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [4, 4]]); expect(editor.lineTextForBufferRow(4)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(5)).toBe( ' while(items.length > 0) {' ); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); })); }); describe('when the selection spans multiple lines', () => { it('moves the lines spanned by the selection to the preceding row', () => { expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.setSelectedBufferRange([[3, 2], [4, 9]]); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 9]]); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(4)).toBe( ' if (items.length <= 1) return items;' ); }); describe("when the selection's end intersects a fold", () => it('moves the lines to the previous row without breaking the fold', () => { expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); editor.setSelectedBufferRange([[3, 2], [4, 9]], { preserveFolds: true }); expect(editor.isFoldedAtBufferRow(3)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 9]]); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(7)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.isFoldedAtBufferRow(2)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeFalsy(); })); describe("when the selection's start intersects a fold", () => it('moves the lines to the previous row without breaking the fold', () => { expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); editor.setSelectedBufferRange([[4, 2], [8, 9]], { preserveFolds: true }); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [7, 9]]); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(7)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(8)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); })); }); describe('when the selection spans multiple lines, but ends at column 0', () => { it('does not move the last line of the selection', () => { expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.setSelectedBufferRange([[3, 2], [4, 0]]); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 0]]); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); }); }); describe('when the preceding row is a folded row', () => { it('moves the lines spanned by the selection to the preceding row, but preserves the folded code', () => { expect(editor.lineTextForBufferRow(8)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(9)).toBe(' };'); editor.foldBufferRowRange(4, 7); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.setSelectedBufferRange([[8, 0], [9, 2]]); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [5, 2]]); expect(editor.lineTextForBufferRow(4)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(5)).toBe(' };'); expect(editor.lineTextForBufferRow(6)).toBe( ' while(items.length > 0) {' ); expect(editor.isFoldedAtBufferRow(5)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(10)).toBeFalsy(); }); }); }); describe('when there are multiple selections', () => { describe('when all the selections span different lines', () => { describe('when there is no folds', () => it('moves all lines that are spanned by a selection to the preceding row', () => { editor.setSelectedBufferRanges([ [[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]] ]); editor.moveLineUp(); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]] ]); expect(editor.lineTextForBufferRow(0)).toBe( ' var sort = function(items) {' ); expect(editor.lineTextForBufferRow(1)).toBe( 'var quicksort = function () {' ); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(4)).toBe( ' current = items.shift();' ); expect(editor.lineTextForBufferRow(5)).toBe( ' while(items.length > 0) {' ); })); describe('when one selection intersects a fold', () => it('moves the lines to the previous row without breaking the fold', () => { expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); editor.setSelectedBufferRanges( [[[2, 2], [2, 9]], [[4, 2], [4, 9]]], { preserveFolds: true } ); expect(editor.isFoldedAtBufferRow(2)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(3)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); editor.moveLineUp(); expect(editor.getSelectedBufferRanges()).toEqual([ [[1, 2], [1, 9]], [[3, 2], [3, 9]] ]); expect(editor.lineTextForBufferRow(1)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(2)).toBe( ' var sort = function(items) {' ); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(7)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.isFoldedAtBufferRow(1)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(2)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); })); describe('when there is a fold', () => it('moves all lines that spanned by a selection to preceding row, preserving all folds', () => { editor.foldBufferRowRange(4, 7); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.setSelectedBufferRanges([ [[8, 0], [8, 3]], [[11, 0], [11, 5]] ]); editor.moveLineUp(); expect(editor.getSelectedBufferRanges()).toEqual([ [[4, 0], [4, 3]], [[10, 0], [10, 5]] ]); expect(editor.lineTextForBufferRow(4)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(10)).toBe( ' return sort(Array.apply(this, arguments));' ); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); })); }); describe('when there are many folds', () => { beforeEach(async () => { editor = await atom.workspace.open('sample-with-many-folds.js', { autoIndent: false }); }); describe('and many selections intersects folded rows', () => it('moves and preserves all the folds', () => { editor.foldBufferRowRange(2, 4); editor.foldBufferRowRange(7, 9); editor.setSelectedBufferRanges( [[[1, 0], [5, 4]], [[7, 0], [7, 4]]], { preserveFolds: true } ); editor.moveLineUp(); expect(editor.lineTextForBufferRow(1)).toEqual('function f3() {'); expect(editor.lineTextForBufferRow(4)).toEqual('6;'); expect(editor.lineTextForBufferRow(5)).toEqual('1;'); expect(editor.lineTextForBufferRow(6)).toEqual('function f8() {'); expect(editor.lineTextForBufferRow(9)).toEqual('7;'); expect(editor.isFoldedAtBufferRow(1)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(2)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); })); }); describe('when some of the selections span the same lines', () => { it('moves lines that contain multiple selections correctly', () => { editor.setSelectedBufferRanges([ [[3, 2], [3, 9]], [[3, 12], [3, 13]] ]); editor.moveLineUp(); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 2], [2, 9]], [[2, 12], [2, 13]] ]); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); }); }); describe('when one of the selections spans line 0', () => { it("doesn't move any lines, since line 0 can't move", () => { editor.setSelectedBufferRanges([ [[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]] ]); editor.moveLineUp(); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]] ]); expect(buffer.isModified()).toBe(false); }); }); describe('when one of the selections spans the last line, and it is empty', () => { it("doesn't move any lines, since the last line can't move", () => { buffer.append('\n'); editor.setSelectedBufferRanges([ [[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]] ]); editor.moveLineUp(); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]] ]); }); }); }); }); describe('.moveLineDown', () => { it('moves the line under the cursor down', () => { editor.setCursorBufferPosition([0, 0]); editor.moveLineDown(); expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe( 'var quicksort = function () {' ); expect(editor.indentationForBufferRow(0)).toBe(1); expect(editor.indentationForBufferRow(1)).toBe(0); }); it("updates the line's indentation when the editor.autoIndent setting is true", () => { editor.update({ autoIndent: true }); editor.setCursorBufferPosition([0, 0]); editor.moveLineDown(); expect(editor.indentationForBufferRow(0)).toBe(1); expect(editor.indentationForBufferRow(1)).toBe(2); }); describe('when there is a single selection', () => { describe('when the selection spans a single line', () => { describe('when there is no fold in the following row', () => it('moves the line to the following row', () => { expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); editor.setSelectedBufferRange([[2, 2], [2, 9]]); editor.moveLineDown(); expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [3, 9]]); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); })); describe('when the cursor is at the beginning of a fold', () => it('moves the line to the following row without breaking the fold', () => { expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); editor.setSelectedBufferRange([[4, 2], [4, 9]], { preserveFolds: true }); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.moveLineDown(); expect(editor.getSelectedBufferRange()).toEqual([[5, 2], [5, 9]]); expect(editor.lineTextForBufferRow(4)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(5)).toBe( ' while(items.length > 0) {' ); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); })); describe('when the following row is a folded row', () => it('moves the line below the folded row and preserves the fold', () => { expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.setSelectedBufferRange([[3, 0], [3, 4]]); editor.moveLineDown(); expect(editor.getSelectedBufferRange()).toEqual([[7, 0], [7, 4]]); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeFalsy(); expect(editor.lineTextForBufferRow(7)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); })); }); describe('when the selection spans multiple lines', () => { it('moves the lines spanned by the selection to the following row', () => { expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.setSelectedBufferRange([[2, 2], [3, 9]]); editor.moveLineDown(); expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [4, 9]]); expect(editor.lineTextForBufferRow(2)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(4)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); }); }); describe('when the selection spans multiple lines, but ends at column 0', () => { it('does not move the last line of the selection', () => { expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.setSelectedBufferRange([[2, 2], [3, 0]]); editor.moveLineDown(); expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [4, 0]]); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); }); }); describe("when the selection's end intersects a fold", () => { it('moves the lines to the following row without breaking the fold', () => { expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); editor.setSelectedBufferRange([[3, 2], [4, 9]], { preserveFolds: true }); expect(editor.isFoldedAtBufferRow(3)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.moveLineDown(); expect(editor.getSelectedBufferRange()).toEqual([[4, 2], [5, 9]]); expect(editor.lineTextForBufferRow(3)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(4)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(5)).toBe( ' while(items.length > 0) {' ); expect(editor.isFoldedAtBufferRow(4)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); }); }); describe("when the selection's start intersects a fold", () => { it('moves the lines to the following row without breaking the fold', () => { expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); editor.setSelectedBufferRange([[4, 2], [8, 9]], { preserveFolds: true }); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); editor.moveLineDown(); expect(editor.getSelectedBufferRange()).toEqual([[5, 2], [9, 9]]); expect(editor.lineTextForBufferRow(4)).toBe(' };'); expect(editor.lineTextForBufferRow(5)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(9)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.isFoldedAtBufferRow(4)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(10)).toBeFalsy(); }); }); describe('when the following row is a folded row', () => { it('moves the lines spanned by the selection to the following row, but preserves the folded code', () => { expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); editor.foldBufferRowRange(4, 7); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.setSelectedBufferRange([[2, 0], [3, 2]]); editor.moveLineDown(); expect(editor.getSelectedBufferRange()).toEqual([[6, 0], [7, 2]]); expect(editor.lineTextForBufferRow(2)).toBe( ' while(items.length > 0) {' ); expect(editor.isFoldedAtBufferRow(1)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(2)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeFalsy(); expect(editor.lineTextForBufferRow(6)).toBe( ' if (items.length <= 1) return items;' ); }); }); describe('when the last line of selection does not end with a valid line ending', () => { it('appends line ending to last line and moves the lines spanned by the selection to the preceding row', () => { expect(editor.lineTextForBufferRow(9)).toBe(' };'); expect(editor.lineTextForBufferRow(10)).toBe(''); expect(editor.lineTextForBufferRow(11)).toBe( ' return sort(Array.apply(this, arguments));' ); expect(editor.lineTextForBufferRow(12)).toBe('};'); editor.setSelectedBufferRange([[10, 0], [12, 2]]); editor.moveLineUp(); expect(editor.getSelectedBufferRange()).toEqual([[9, 0], [11, 2]]); expect(editor.lineTextForBufferRow(9)).toBe(''); expect(editor.lineTextForBufferRow(10)).toBe( ' return sort(Array.apply(this, arguments));' ); expect(editor.lineTextForBufferRow(11)).toBe('};'); expect(editor.lineTextForBufferRow(12)).toBe(' };'); }); }); }); describe('when there are multiple selections', () => { describe('when all the selections span different lines', () => { describe('when there is no folds', () => it('moves all lines that are spanned by a selection to the following row', () => { editor.setSelectedBufferRanges([ [[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]] ]); editor.moveLineDown(); expect(editor.getSelectedBufferRanges()).toEqual([ [[6, 2], [6, 9]], [[4, 2], [4, 9]], [[2, 2], [2, 9]] ]); expect(editor.lineTextForBufferRow(1)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(2)).toBe( ' var sort = function(items) {' ); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(4)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(5)).toBe( ' current < pivot ? left.push(current) : right.push(current);' ); expect(editor.lineTextForBufferRow(6)).toBe( ' current = items.shift();' ); })); describe('when there are many folds', () => { beforeEach(async () => { editor = await atom.workspace.open('sample-with-many-folds.js', { autoIndent: false }); }); describe('and many selections intersects folded rows', () => it('moves and preserves all the folds', () => { editor.foldBufferRowRange(2, 4); editor.foldBufferRowRange(7, 9); editor.setSelectedBufferRanges( [[[2, 0], [2, 4]], [[6, 0], [10, 4]]], { preserveFolds: true } ); editor.moveLineDown(); expect(editor.lineTextForBufferRow(2)).toEqual('6;'); expect(editor.lineTextForBufferRow(3)).toEqual( 'function f3() {' ); expect(editor.lineTextForBufferRow(6)).toEqual('12;'); expect(editor.lineTextForBufferRow(7)).toEqual('7;'); expect(editor.lineTextForBufferRow(8)).toEqual( 'function f8() {' ); expect(editor.lineTextForBufferRow(11)).toEqual('11;'); expect(editor.isFoldedAtBufferRow(2)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(7)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(10)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(11)).toBeFalsy(); })); }); describe('when there is a fold below one of the selected row', () => it('moves all lines spanned by a selection to the following row, preserving the fold', () => { editor.foldBufferRowRange(4, 7); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.setSelectedBufferRanges([ [[1, 2], [1, 6]], [[3, 0], [3, 4]], [[8, 0], [8, 3]] ]); editor.moveLineDown(); expect(editor.getSelectedBufferRanges()).toEqual([ [[9, 0], [9, 3]], [[7, 0], [7, 4]], [[2, 2], [2, 6]] ]); expect(editor.lineTextForBufferRow(2)).toBe( ' var sort = function(items) {' ); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeFalsy(); expect(editor.lineTextForBufferRow(7)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(9)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); })); describe('when there is a fold below a group of multiple selections without any lines with no selection in-between', () => it('moves all the lines below the fold, preserving the fold', () => { editor.foldBufferRowRange(4, 7); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); editor.setSelectedBufferRanges([ [[2, 2], [2, 6]], [[3, 0], [3, 4]] ]); editor.moveLineDown(); expect(editor.getSelectedBufferRanges()).toEqual([ [[7, 0], [7, 4]], [[6, 2], [6, 6]] ]); expect(editor.lineTextForBufferRow(2)).toBe( ' while(items.length > 0) {' ); expect(editor.isFoldedAtBufferRow(2)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(3)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeFalsy(); expect(editor.lineTextForBufferRow(6)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(7)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); })); }); describe('when one selection intersects a fold', () => { it('moves the lines to the previous row without breaking the fold', () => { expect(editor.lineTextForBufferRow(4)).toBe( ' while(items.length > 0) {' ); editor.foldBufferRowRange(4, 7); editor.setSelectedBufferRanges( [[[2, 2], [2, 9]], [[4, 2], [4, 9]]], { preserveFolds: true } ); expect(editor.isFoldedAtBufferRow(2)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(3)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(4)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); editor.moveLineDown(); expect(editor.getSelectedBufferRanges()).toEqual([ [[5, 2], [5, 9]], [[3, 2], [3, 9]] ]); expect(editor.lineTextForBufferRow(2)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(4)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForBufferRow(5)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(9)).toBe(' };'); expect(editor.isFoldedAtBufferRow(2)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(3)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(4)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(5)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(6)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(7)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(8)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeFalsy(); }); }); describe('when some of the selections span the same lines', () => { it('moves lines that contain multiple selections correctly', () => { editor.setSelectedBufferRanges([ [[3, 2], [3, 9]], [[3, 12], [3, 13]] ]); editor.moveLineDown(); expect(editor.getSelectedBufferRanges()).toEqual([ [[4, 12], [4, 13]], [[4, 2], [4, 9]] ]); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); }); }); describe('when the selections are above a wrapped line', () => { beforeEach(() => { editor.setSoftWrapped(true); editor.setEditorWidthInChars(80); editor.setText(dedent` 1 2 Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 3 4 `); }); it('moves the lines past the soft wrapped line', () => { editor.setSelectedBufferRanges([ [[0, 0], [0, 0]], [[1, 0], [1, 0]] ]); editor.moveLineDown(); expect(editor.lineTextForBufferRow(0)).not.toBe('2'); expect(editor.lineTextForBufferRow(1)).toBe('1'); expect(editor.lineTextForBufferRow(2)).toBe('2'); }); }); }); describe('when the line is the last buffer row', () => { it("doesn't move it", () => { editor.setText('abc\ndef'); editor.setCursorBufferPosition([1, 0]); editor.moveLineDown(); expect(editor.getText()).toBe('abc\ndef'); }); }); }); describe('.insertText(text)', () => { describe('when there is a single selection', () => { beforeEach(() => editor.setSelectedBufferRange([[1, 0], [1, 2]])); it('replaces the selection with the given text', () => { const range = editor.insertText('xxx'); expect(range).toEqual([[[1, 0], [1, 3]]]); expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {'); }); }); describe('when there are multiple empty selections', () => { describe('when the cursors are on the same line', () => { it("inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", () => { editor.setCursorScreenPosition([1, 2]); editor.addCursorAtScreenPosition([1, 5]); editor.insertText('xxx'); expect(buffer.lineForRow(1)).toBe( ' xxxvarxxx sort = function(items) {' ); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([1, 5]); expect(cursor2.getBufferPosition()).toEqual([1, 11]); }); }); describe('when the cursors are on different lines', () => { it("inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", () => { editor.setCursorScreenPosition([1, 2]); editor.addCursorAtScreenPosition([2, 4]); editor.insertText('xxx'); expect(buffer.lineForRow(1)).toBe( ' xxxvar sort = function(items) {' ); expect(buffer.lineForRow(2)).toBe( ' xxxif (items.length <= 1) return items;' ); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([1, 5]); expect(cursor2.getBufferPosition()).toEqual([2, 7]); }); }); }); describe('when there are multiple non-empty selections', () => { describe('when the selections are on the same line', () => { it('replaces each selection range with the inserted characters', () => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[0, 22], [0, 24]] ]); editor.insertText('x'); const [cursor1, cursor2] = editor.getCursors(); const [selection1, selection2] = editor.getSelections(); expect(cursor1.getScreenPosition()).toEqual([0, 5]); expect(cursor2.getScreenPosition()).toEqual([0, 15]); expect(selection1.isEmpty()).toBeTruthy(); expect(selection2.isEmpty()).toBeTruthy(); expect(editor.lineTextForBufferRow(0)).toBe('var x = functix () {'); }); }); describe('when the selections are on different lines', () => { it("replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", () => { editor.setSelectedBufferRanges([ [[1, 0], [1, 2]], [[2, 0], [2, 4]] ]); editor.insertText('xxx'); expect(buffer.lineForRow(1)).toBe( 'xxxvar sort = function(items) {' ); expect(buffer.lineForRow(2)).toBe( 'xxxif (items.length <= 1) return items;' ); const [selection1, selection2] = editor.getSelections(); expect(selection1.isEmpty()).toBeTruthy(); expect(selection1.cursor.getBufferPosition()).toEqual([1, 3]); expect(selection2.isEmpty()).toBeTruthy(); expect(selection2.cursor.getBufferPosition()).toEqual([2, 3]); }); }); }); describe('when there is a selection that ends on a folded line', () => { it('destroys the selection', () => { editor.foldBufferRowRange(2, 4); editor.setSelectedBufferRange([[1, 0], [2, 0]]); editor.insertText('holy cow'); expect(editor.isFoldedAtScreenRow(2)).toBeFalsy(); }); }); describe('when there are ::onWillInsertText and ::onDidInsertText observers', () => { beforeEach(() => editor.setSelectedBufferRange([[1, 0], [1, 2]])); it('notifies the observers when inserting text', () => { const willInsertSpy = jasmine .createSpy() .andCallFake(() => expect(buffer.lineForRow(1)).toBe( ' var sort = function(items) {' ) ); const didInsertSpy = jasmine .createSpy() .andCallFake(() => expect(buffer.lineForRow(1)).toBe( 'xxxvar sort = function(items) {' ) ); editor.onWillInsertText(willInsertSpy); editor.onDidInsertText(didInsertSpy); expect(editor.insertText('xxx')).toBeTruthy(); expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {'); expect(willInsertSpy).toHaveBeenCalled(); expect(didInsertSpy).toHaveBeenCalled(); let options = willInsertSpy.mostRecentCall.args[0]; expect(options.text).toBe('xxx'); expect(options.cancel).toBeDefined(); options = didInsertSpy.mostRecentCall.args[0]; expect(options.text).toBe('xxx'); }); it('cancels text insertion when an ::onWillInsertText observer calls cancel on an event', () => { const willInsertSpy = jasmine .createSpy() .andCallFake(({ cancel }) => cancel()); const didInsertSpy = jasmine.createSpy(); editor.onWillInsertText(willInsertSpy); editor.onDidInsertText(didInsertSpy); expect(editor.insertText('xxx')).toBe(false); expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {'); expect(willInsertSpy).toHaveBeenCalled(); expect(didInsertSpy).not.toHaveBeenCalled(); }); }); describe("when the undo option is set to 'skip'", () => { it('groups the change with the previous change for purposes of undo and redo', () => { editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]); editor.insertText('x'); editor.insertText('y', { undo: 'skip' }); editor.undo(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {'); }); }); }); describe('.insertNewline()', () => { describe('when there is a single cursor', () => { describe('when the cursor is at the beginning of a line', () => { it('inserts an empty line before it', () => { editor.setCursorScreenPosition({ row: 1, column: 0 }); editor.insertNewline(); expect(buffer.lineForRow(1)).toBe(''); expect(editor.getCursorScreenPosition()).toEqual({ row: 2, column: 0 }); }); }); describe('when the cursor is in the middle of a line', () => { it('splits the current line to form a new line', () => { editor.setCursorScreenPosition({ row: 1, column: 6 }); const originalLine = buffer.lineForRow(1); const lineBelowOriginalLine = buffer.lineForRow(2); editor.insertNewline(); expect(buffer.lineForRow(1)).toBe(originalLine.slice(0, 6)); expect(buffer.lineForRow(2)).toBe(originalLine.slice(6)); expect(buffer.lineForRow(3)).toBe(lineBelowOriginalLine); expect(editor.getCursorScreenPosition()).toEqual({ row: 2, column: 0 }); }); }); describe('when the cursor is on the end of a line', () => { it('inserts an empty line after it', () => { editor.setCursorScreenPosition({ row: 1, column: buffer.lineForRow(1).length }); editor.insertNewline(); expect(buffer.lineForRow(2)).toBe(''); expect(editor.getCursorScreenPosition()).toEqual({ row: 2, column: 0 }); }); }); }); describe('when there are multiple cursors', () => { describe('when the cursors are on the same line', () => { it('breaks the line at the cursor locations', () => { editor.setCursorScreenPosition([3, 13]); editor.addCursorAtScreenPosition([3, 38]); editor.insertNewline(); expect(editor.lineTextForBufferRow(3)).toBe(' var pivot'); expect(editor.lineTextForBufferRow(4)).toBe( ' = items.shift(), current' ); expect(editor.lineTextForBufferRow(5)).toBe( ', left = [], right = [];' ); expect(editor.lineTextForBufferRow(6)).toBe( ' while(items.length > 0) {' ); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([4, 0]); expect(cursor2.getBufferPosition()).toEqual([5, 0]); }); }); describe('when the cursors are on different lines', () => { it('inserts newlines at each cursor location', () => { editor.setCursorScreenPosition([3, 0]); editor.addCursorAtScreenPosition([6, 0]); editor.insertText('\n'); expect(editor.lineTextForBufferRow(3)).toBe(''); expect(editor.lineTextForBufferRow(4)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(5)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(6)).toBe( ' current = items.shift();' ); expect(editor.lineTextForBufferRow(7)).toBe(''); expect(editor.lineTextForBufferRow(8)).toBe( ' current < pivot ? left.push(current) : right.push(current);' ); expect(editor.lineTextForBufferRow(9)).toBe(' }'); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([4, 0]); expect(cursor2.getBufferPosition()).toEqual([8, 0]); }); }); }); }); describe('.insertNewlineBelow()', () => { describe('when the operation is undone', () => { it('places the cursor back at the previous location', () => { editor.setCursorBufferPosition([0, 2]); editor.insertNewlineBelow(); expect(editor.getCursorBufferPosition()).toEqual([1, 0]); editor.undo(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); }); }); it("inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", () => { editor.update({ autoIndent: true }); editor.insertNewlineBelow(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); expect(buffer.lineForRow(1)).toBe(' '); expect(editor.getCursorBufferPosition()).toEqual([1, 2]); }); }); describe('.insertNewlineAbove()', () => { describe('when the cursor is on first line', () => { it('inserts a newline on the first line and moves the cursor to the first line', () => { editor.setCursorBufferPosition([0]); editor.insertNewlineAbove(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); expect(editor.lineTextForBufferRow(0)).toBe(''); expect(editor.lineTextForBufferRow(1)).toBe( 'var quicksort = function () {' ); expect(editor.buffer.getLineCount()).toBe(14); }); }); describe('when the cursor is not on the first line', () => { it('inserts a newline above the current line and moves the cursor to the inserted line', () => { editor.setCursorBufferPosition([3, 4]); editor.insertNewlineAbove(); expect(editor.getCursorBufferPosition()).toEqual([3, 0]); expect(editor.lineTextForBufferRow(3)).toBe(''); expect(editor.lineTextForBufferRow(4)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.buffer.getLineCount()).toBe(14); editor.undo(); expect(editor.getCursorBufferPosition()).toEqual([3, 4]); }); }); it('indents the new line to the correct level when editor.autoIndent is true', () => { editor.update({ autoIndent: true }); editor.setText(' var test'); editor.setCursorBufferPosition([0, 2]); editor.insertNewlineAbove(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); expect(editor.lineTextForBufferRow(0)).toBe(' '); expect(editor.lineTextForBufferRow(1)).toBe(' var test'); editor.setText('\n var test'); editor.setCursorBufferPosition([1, 2]); editor.insertNewlineAbove(); expect(editor.getCursorBufferPosition()).toEqual([1, 2]); expect(editor.lineTextForBufferRow(0)).toBe(''); expect(editor.lineTextForBufferRow(1)).toBe(' '); expect(editor.lineTextForBufferRow(2)).toBe(' var test'); editor.setText('function() {\n}'); editor.setCursorBufferPosition([1, 1]); editor.insertNewlineAbove(); expect(editor.getCursorBufferPosition()).toEqual([1, 2]); expect(editor.lineTextForBufferRow(0)).toBe('function() {'); expect(editor.lineTextForBufferRow(1)).toBe(' '); expect(editor.lineTextForBufferRow(2)).toBe('}'); }); }); describe('.insertNewLine()', () => { describe('when a new line is appended before a closing tag (e.g. by pressing enter before a selection)', () => { it('moves the line down and keeps the indentation level the same when editor.autoIndent is true', () => { editor.update({ autoIndent: true }); editor.setCursorBufferPosition([9, 2]); editor.insertNewline(); expect(editor.lineTextForBufferRow(10)).toBe(' };'); }); }); describe('when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)', () => { it('indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language', () => { editor.update({ autoIndent: true }); atom.grammars.assignLanguageMode(editor, 'source.js'); editor.setText('var test = () => {\n return true;};'); editor.setCursorBufferPosition([1, 14]); editor.insertNewline(); expect(editor.indentationForBufferRow(1)).toBe(1); expect(editor.indentationForBufferRow(2)).toBe(0); }); it('indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified', () => { atom.grammars.assignLanguageMode(editor, null); editor.update({ autoIndent: true }); editor.setText(' if true'); editor.setCursorBufferPosition([0, 8]); editor.insertNewline(); expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar); expect(editor.indentationForBufferRow(0)).toBe(1); expect(editor.indentationForBufferRow(1)).toBe(1); }); it('indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language', async () => { await atom.packages.activatePackage('language-coffee-script'); editor.update({ autoIndent: true }); atom.grammars.assignLanguageMode(editor, 'source.coffee'); editor.setText('if true\n return trueelse\n return false'); editor.setCursorBufferPosition([1, 13]); editor.insertNewline(); expect(editor.indentationForBufferRow(1)).toBe(1); expect(editor.indentationForBufferRow(2)).toBe(0); expect(editor.indentationForBufferRow(3)).toBe(1); }); }); describe('when a newline is appended on a line that matches the decreaseNextIndentPattern', () => { it('indents the new line to the correct level when editor.autoIndent is true', async () => { await atom.packages.activatePackage('language-go'); editor.update({ autoIndent: true }); atom.grammars.assignLanguageMode(editor, 'source.go'); editor.setText('fmt.Printf("some%s",\n "thing")'); // eslint-disable-line no-tabs editor.setCursorBufferPosition([1, 10]); editor.insertNewline(); expect(editor.indentationForBufferRow(1)).toBe(1); expect(editor.indentationForBufferRow(2)).toBe(0); }); }); }); describe('.backspace()', () => { describe('when there is a single cursor', () => { let changeScreenRangeHandler = null; beforeEach(() => { const selection = editor.getLastSelection(); changeScreenRangeHandler = jasmine.createSpy( 'changeScreenRangeHandler' ); selection.onDidChangeRange(changeScreenRangeHandler); }); describe('when the cursor is on the middle of the line', () => { it('removes the character before the cursor', () => { editor.setCursorScreenPosition({ row: 1, column: 7 }); expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {'); editor.backspace(); const line = buffer.lineForRow(1); expect(line).toBe(' var ort = function(items) {'); expect(editor.getCursorScreenPosition()).toEqual({ row: 1, column: 6 }); expect(changeScreenRangeHandler).toHaveBeenCalled(); }); }); describe('when the cursor is at the beginning of a line', () => { it('joins it with the line above', () => { const originalLine0 = buffer.lineForRow(0); expect(originalLine0).toBe('var quicksort = function () {'); expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {'); editor.setCursorScreenPosition({ row: 1, column: 0 }); editor.backspace(); const line0 = buffer.lineForRow(0); const line1 = buffer.lineForRow(1); expect(line0).toBe( 'var quicksort = function () { var sort = function(items) {' ); expect(line1).toBe(' if (items.length <= 1) return items;'); expect(editor.getCursorScreenPosition()).toEqual([ 0, originalLine0.length ]); expect(changeScreenRangeHandler).toHaveBeenCalled(); }); }); describe('when the cursor is at the first column of the first line', () => { it("does nothing, but doesn't raise an error", () => { editor.setCursorScreenPosition({ row: 0, column: 0 }); editor.backspace(); }); }); describe('when the cursor is after a fold', () => { it('deletes the folded range', () => { editor.foldBufferRange([[4, 7], [5, 8]]); editor.setCursorBufferPosition([5, 8]); editor.backspace(); expect(buffer.lineForRow(4)).toBe(' whirrent = items.shift();'); expect(editor.isFoldedAtBufferRow(4)).toBe(false); }); }); describe('when the cursor is in the middle of a line below a fold', () => { it('backspaces as normal', () => { editor.setCursorScreenPosition([4, 0]); editor.foldCurrentRow(); editor.setCursorScreenPosition([5, 5]); editor.backspace(); expect(buffer.lineForRow(7)).toBe(' }'); expect(buffer.lineForRow(8)).toBe( ' eturn sort(left).concat(pivot).concat(sort(right));' ); }); }); describe('when the cursor is on a folded screen line', () => { it('deletes the contents of the fold before the cursor', () => { editor.setCursorBufferPosition([3, 0]); editor.foldCurrentRow(); editor.backspace(); expect(buffer.lineForRow(1)).toBe( ' var sort = function(items) var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.getCursorScreenPosition()).toEqual([1, 29]); }); }); }); describe('when there are multiple cursors', () => { describe('when cursors are on the same line', () => { it('removes the characters preceding each cursor', () => { editor.setCursorScreenPosition([3, 13]); editor.addCursorAtScreenPosition([3, 38]); editor.backspace(); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivo = items.shift(), curren, left = [], right = [];' ); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([3, 12]); expect(cursor2.getBufferPosition()).toEqual([3, 36]); const [selection1, selection2] = editor.getSelections(); expect(selection1.isEmpty()).toBeTruthy(); expect(selection2.isEmpty()).toBeTruthy(); }); }); describe('when cursors are on different lines', () => { describe('when the cursors are in the middle of their lines', () => it('removes the characters preceding each cursor', () => { editor.setCursorScreenPosition([3, 13]); editor.addCursorAtScreenPosition([4, 10]); editor.backspace(); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivo = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(4)).toBe( ' whileitems.length > 0) {' ); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([3, 12]); expect(cursor2.getBufferPosition()).toEqual([4, 9]); const [selection1, selection2] = editor.getSelections(); expect(selection1.isEmpty()).toBeTruthy(); expect(selection2.isEmpty()).toBeTruthy(); })); describe('when the cursors are on the first column of their lines', () => it('removes the newlines preceding each cursor', () => { editor.setCursorScreenPosition([3, 0]); editor.addCursorAtScreenPosition([6, 0]); editor.backspace(); expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(3)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(4)).toBe( ' current = items.shift(); current < pivot ? left.push(current) : right.push(current);' ); expect(editor.lineTextForBufferRow(5)).toBe(' }'); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([2, 40]); expect(cursor2.getBufferPosition()).toEqual([4, 30]); })); }); }); describe('when there is a single selection', () => { it('deletes the selection, but not the character before it', () => { editor.setSelectedBufferRange([[0, 5], [0, 9]]); editor.backspace(); expect(editor.buffer.lineForRow(0)).toBe('var qsort = function () {'); }); describe('when the selection ends on a folded line', () => { it('preserves the fold', () => { editor.setSelectedBufferRange([[3, 0], [4, 0]]); editor.foldBufferRow(4); editor.backspace(); expect(buffer.lineForRow(3)).toBe(' while(items.length > 0) {'); expect(editor.isFoldedAtScreenRow(3)).toBe(true); }); }); }); describe('when there are multiple selections', () => { it('removes all selected text', () => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[0, 16], [0, 24]] ]); editor.backspace(); expect(editor.lineTextForBufferRow(0)).toBe('var = () {'); }); }); }); describe('.deleteToPreviousWordBoundary()', () => { describe('when no text is selected', () => { it('deletes to the previous word boundary', () => { editor.setCursorBufferPosition([0, 16]); editor.addCursorAtBufferPosition([1, 21]); const [cursor1, cursor2] = editor.getCursors(); editor.deleteToPreviousWordBoundary(); expect(buffer.lineForRow(0)).toBe('var quicksort =function () {'); expect(buffer.lineForRow(1)).toBe(' var sort = (items) {'); expect(cursor1.getBufferPosition()).toEqual([0, 15]); expect(cursor2.getBufferPosition()).toEqual([1, 13]); editor.deleteToPreviousWordBoundary(); expect(buffer.lineForRow(0)).toBe('var quicksort function () {'); expect(buffer.lineForRow(1)).toBe(' var sort =(items) {'); expect(cursor1.getBufferPosition()).toEqual([0, 14]); expect(cursor2.getBufferPosition()).toEqual([1, 12]); }); }); describe('when text is selected', () => { it('deletes only selected text', () => { editor.setSelectedBufferRange([[1, 24], [1, 27]]); editor.deleteToPreviousWordBoundary(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {'); }); }); }); describe('.deleteToNextWordBoundary()', () => { describe('when no text is selected', () => { it('deletes to the next word boundary', () => { editor.setCursorBufferPosition([0, 15]); editor.addCursorAtBufferPosition([1, 24]); const [cursor1, cursor2] = editor.getCursors(); editor.deleteToNextWordBoundary(); expect(buffer.lineForRow(0)).toBe('var quicksort =function () {'); expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {'); expect(cursor1.getBufferPosition()).toEqual([0, 15]); expect(cursor2.getBufferPosition()).toEqual([1, 24]); editor.deleteToNextWordBoundary(); expect(buffer.lineForRow(0)).toBe('var quicksort = () {'); expect(buffer.lineForRow(1)).toBe(' var sort = function(it {'); expect(cursor1.getBufferPosition()).toEqual([0, 15]); expect(cursor2.getBufferPosition()).toEqual([1, 24]); editor.deleteToNextWordBoundary(); expect(buffer.lineForRow(0)).toBe('var quicksort =() {'); expect(buffer.lineForRow(1)).toBe(' var sort = function(it{'); expect(cursor1.getBufferPosition()).toEqual([0, 15]); expect(cursor2.getBufferPosition()).toEqual([1, 24]); }); }); describe('when text is selected', () => { it('deletes only selected text', () => { editor.setSelectedBufferRange([[1, 24], [1, 27]]); editor.deleteToNextWordBoundary(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {'); }); }); }); describe('.deleteToBeginningOfWord()', () => { describe('when no text is selected', () => { it('deletes all text between the cursor and the beginning of the word', () => { editor.setCursorBufferPosition([1, 24]); editor.addCursorAtBufferPosition([3, 5]); const [cursor1, cursor2] = editor.getCursors(); editor.deleteToBeginningOfWord(); expect(buffer.lineForRow(1)).toBe(' var sort = function(ems) {'); expect(buffer.lineForRow(3)).toBe( ' ar pivot = items.shift(), current, left = [], right = [];' ); expect(cursor1.getBufferPosition()).toEqual([1, 22]); expect(cursor2.getBufferPosition()).toEqual([3, 4]); editor.deleteToBeginningOfWord(); expect(buffer.lineForRow(1)).toBe(' var sort = functionems) {'); expect(buffer.lineForRow(2)).toBe( ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];' ); expect(cursor1.getBufferPosition()).toEqual([1, 21]); expect(cursor2.getBufferPosition()).toEqual([2, 39]); editor.deleteToBeginningOfWord(); expect(buffer.lineForRow(1)).toBe(' var sort = ems) {'); expect(buffer.lineForRow(2)).toBe( ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];' ); expect(cursor1.getBufferPosition()).toEqual([1, 13]); expect(cursor2.getBufferPosition()).toEqual([2, 34]); editor.setText(' var sort'); editor.setCursorBufferPosition([0, 2]); editor.deleteToBeginningOfWord(); expect(buffer.lineForRow(0)).toBe('var sort'); }); }); describe('when text is selected', () => { it('deletes only selected text', () => { editor.setSelectedBufferRanges([ [[1, 24], [1, 27]], [[2, 0], [2, 4]] ]); editor.deleteToBeginningOfWord(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {'); expect(buffer.lineForRow(2)).toBe( 'if (items.length <= 1) return items;' ); }); }); }); describe('.deleteToEndOfLine()', () => { describe('when no text is selected', () => { it('deletes all text between the cursor and the end of the line', () => { editor.setCursorBufferPosition([1, 24]); editor.addCursorAtBufferPosition([2, 5]); const [cursor1, cursor2] = editor.getCursors(); editor.deleteToEndOfLine(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it'); expect(buffer.lineForRow(2)).toBe(' i'); expect(cursor1.getBufferPosition()).toEqual([1, 24]); expect(cursor2.getBufferPosition()).toEqual([2, 5]); }); describe('when at the end of the line', () => { it('deletes the next newline', () => { editor.setCursorBufferPosition([1, 30]); editor.deleteToEndOfLine(); expect(buffer.lineForRow(1)).toBe( ' var sort = function(items) { if (items.length <= 1) return items;' ); }); }); }); describe('when text is selected', () => { it('deletes only the text in the selection', () => { editor.setSelectedBufferRanges([ [[1, 24], [1, 27]], [[2, 0], [2, 4]] ]); editor.deleteToEndOfLine(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {'); expect(buffer.lineForRow(2)).toBe( 'if (items.length <= 1) return items;' ); }); }); }); describe('.deleteToBeginningOfLine()', () => { describe('when no text is selected', () => { it('deletes all text between the cursor and the beginning of the line', () => { editor.setCursorBufferPosition([1, 24]); editor.addCursorAtBufferPosition([2, 5]); const [cursor1, cursor2] = editor.getCursors(); editor.deleteToBeginningOfLine(); expect(buffer.lineForRow(1)).toBe('ems) {'); expect(buffer.lineForRow(2)).toBe( 'f (items.length <= 1) return items;' ); expect(cursor1.getBufferPosition()).toEqual([1, 0]); expect(cursor2.getBufferPosition()).toEqual([2, 0]); }); describe('when at the beginning of the line', () => { it('deletes the newline', () => { editor.setCursorBufferPosition([2]); editor.deleteToBeginningOfLine(); expect(buffer.lineForRow(1)).toBe( ' var sort = function(items) { if (items.length <= 1) return items;' ); }); }); }); describe('when text is selected', () => { it('still deletes all text to beginning of the line', () => { editor.setSelectedBufferRanges([ [[1, 24], [1, 27]], [[2, 0], [2, 4]] ]); editor.deleteToBeginningOfLine(); expect(buffer.lineForRow(1)).toBe('ems) {'); expect(buffer.lineForRow(2)).toBe( ' if (items.length <= 1) return items;' ); }); }); }); describe('.delete()', () => { describe('when there is a single cursor', () => { describe('when the cursor is on the middle of a line', () => { it('deletes the character following the cursor', () => { editor.setCursorScreenPosition([1, 6]); editor.delete(); expect(buffer.lineForRow(1)).toBe(' var ort = function(items) {'); }); }); describe('when the cursor is on the end of a line', () => { it('joins the line with the following line', () => { editor.setCursorScreenPosition([1, buffer.lineForRow(1).length]); editor.delete(); expect(buffer.lineForRow(1)).toBe( ' var sort = function(items) { if (items.length <= 1) return items;' ); }); }); describe('when the cursor is on the last column of the last line', () => { it("does nothing, but doesn't raise an error", () => { editor.setCursorScreenPosition([12, buffer.lineForRow(12).length]); editor.delete(); expect(buffer.lineForRow(12)).toBe('};'); }); }); describe('when the cursor is before a fold', () => { it('only deletes the lines inside the fold', () => { editor.foldBufferRange([[3, 6], [4, 8]]); editor.setCursorScreenPosition([3, 6]); const cursorPositionBefore = editor.getCursorScreenPosition(); editor.delete(); expect(buffer.lineForRow(3)).toBe(' vae(items.length > 0) {'); expect(buffer.lineForRow(4)).toBe(' current = items.shift();'); expect(editor.getCursorScreenPosition()).toEqual( cursorPositionBefore ); }); }); describe('when the cursor is in the middle a line above a fold', () => { it('deletes as normal', () => { editor.foldBufferRow(4); editor.setCursorScreenPosition([3, 4]); editor.delete(); expect(buffer.lineForRow(3)).toBe( ' ar pivot = items.shift(), current, left = [], right = [];' ); expect(editor.isFoldedAtScreenRow(4)).toBe(true); expect(editor.getCursorScreenPosition()).toEqual([3, 4]); }); }); describe('when the cursor is inside a fold', () => { it('removes the folded content after the cursor', () => { editor.foldBufferRange([[2, 6], [6, 21]]); editor.setCursorBufferPosition([4, 9]); editor.delete(); expect(buffer.lineForRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(buffer.lineForRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(buffer.lineForRow(4)).toBe( ' while ? left.push(current) : right.push(current);' ); expect(buffer.lineForRow(5)).toBe(' }'); expect(editor.getCursorBufferPosition()).toEqual([4, 9]); }); }); }); describe('when there are multiple cursors', () => { describe('when cursors are on the same line', () => { it('removes the characters following each cursor', () => { editor.setCursorScreenPosition([3, 13]); editor.addCursorAtScreenPosition([3, 38]); editor.delete(); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot= items.shift(), current left = [], right = [];' ); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([3, 13]); expect(cursor2.getBufferPosition()).toEqual([3, 37]); const [selection1, selection2] = editor.getSelections(); expect(selection1.isEmpty()).toBeTruthy(); expect(selection2.isEmpty()).toBeTruthy(); }); }); describe('when cursors are on different lines', () => { describe('when the cursors are in the middle of the lines', () => it('removes the characters following each cursor', () => { editor.setCursorScreenPosition([3, 13]); editor.addCursorAtScreenPosition([4, 10]); editor.delete(); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot= items.shift(), current, left = [], right = [];' ); expect(editor.lineTextForBufferRow(4)).toBe( ' while(tems.length > 0) {' ); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([3, 13]); expect(cursor2.getBufferPosition()).toEqual([4, 10]); const [selection1, selection2] = editor.getSelections(); expect(selection1.isEmpty()).toBeTruthy(); expect(selection2.isEmpty()).toBeTruthy(); })); describe('when the cursors are at the end of their lines', () => it('removes the newlines following each cursor', () => { editor.setCursorScreenPosition([0, 29]); editor.addCursorAtScreenPosition([1, 30]); editor.delete(); expect(editor.lineTextForBufferRow(0)).toBe( 'var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;' ); const [cursor1, cursor2] = editor.getCursors(); expect(cursor1.getBufferPosition()).toEqual([0, 29]); expect(cursor2.getBufferPosition()).toEqual([0, 59]); })); }); }); describe('when there is a single selection', () => { it('deletes the selection, but not the character following it', () => { editor.setSelectedBufferRanges([ [[1, 24], [1, 27]], [[2, 0], [2, 4]] ]); editor.delete(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {'); expect(buffer.lineForRow(2)).toBe( 'if (items.length <= 1) return items;' ); expect(editor.getLastSelection().isEmpty()).toBeTruthy(); }); }); describe('when there are multiple selections', () => describe('when selections are on the same line', () => { it('removes all selected text', () => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[0, 16], [0, 24]] ]); editor.delete(); expect(editor.lineTextForBufferRow(0)).toBe('var = () {'); }); })); }); describe('.deleteToEndOfWord()', () => { describe('when no text is selected', () => { it('deletes to the end of the word', () => { editor.setCursorBufferPosition([1, 24]); editor.addCursorAtBufferPosition([2, 5]); const [cursor1, cursor2] = editor.getCursors(); editor.deleteToEndOfWord(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {'); expect(buffer.lineForRow(2)).toBe( ' i (items.length <= 1) return items;' ); expect(cursor1.getBufferPosition()).toEqual([1, 24]); expect(cursor2.getBufferPosition()).toEqual([2, 5]); editor.deleteToEndOfWord(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it {'); expect(buffer.lineForRow(2)).toBe( ' iitems.length <= 1) return items;' ); expect(cursor1.getBufferPosition()).toEqual([1, 24]); expect(cursor2.getBufferPosition()).toEqual([2, 5]); }); }); describe('when text is selected', () => { it('deletes only selected text', () => { editor.setSelectedBufferRange([[1, 24], [1, 27]]); editor.deleteToEndOfWord(); expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {'); }); }); }); describe('.indent()', () => { describe('when the selection is empty', () => { describe('when autoIndent is disabled', () => { describe("if 'softTabs' is true (the default)", () => { it("inserts 'tabLength' spaces into the buffer", () => { const tabRegex = new RegExp(`^[ ]{${editor.getTabLength()}}`); expect(buffer.lineForRow(0)).not.toMatch(tabRegex); editor.indent(); expect(buffer.lineForRow(0)).toMatch(tabRegex); }); it('respects the tab stops when cursor is in the middle of a tab', () => { editor.setTabLength(4); buffer.insert([12, 2], '\n '); editor.setCursorBufferPosition([13, 1]); editor.indent(); expect(buffer.lineForRow(13)).toMatch(/^\s+$/); expect(buffer.lineForRow(13).length).toBe(4); expect(editor.getCursorBufferPosition()).toEqual([13, 4]); buffer.insert([13, 0], ' '); editor.setCursorBufferPosition([13, 6]); editor.indent(); expect(buffer.lineForRow(13).length).toBe(8); }); }); describe("if 'softTabs' is false", () => it('insert a \t into the buffer', () => { editor.setSoftTabs(false); expect(buffer.lineForRow(0)).not.toMatch(/^\t/); editor.indent(); expect(buffer.lineForRow(0)).toMatch(/^\t/); })); }); describe('when autoIndent is enabled', () => { describe("when the cursor's column is less than the suggested level of indentation", () => { describe("when 'softTabs' is true (the default)", () => { it('moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation', () => { buffer.insert([5, 0], ' \n'); editor.setCursorBufferPosition([5, 0]); editor.indent({ autoIndent: true }); expect(buffer.lineForRow(5)).toMatch(/^\s+$/); expect(buffer.lineForRow(5).length).toBe(6); expect(editor.getCursorBufferPosition()).toEqual([5, 6]); }); it('respects the tab stops when cursor is in the middle of a tab', () => { editor.setTabLength(4); buffer.insert([12, 2], '\n '); editor.setCursorBufferPosition([13, 1]); editor.indent({ autoIndent: true }); expect(buffer.lineForRow(13)).toMatch(/^\s+$/); expect(buffer.lineForRow(13).length).toBe(4); expect(editor.getCursorBufferPosition()).toEqual([13, 4]); buffer.insert([13, 0], ' '); editor.setCursorBufferPosition([13, 6]); editor.indent({ autoIndent: true }); expect(buffer.lineForRow(13).length).toBe(8); }); }); describe("when 'softTabs' is false", () => { it('moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation', () => { convertToHardTabs(buffer); editor.setSoftTabs(false); buffer.insert([5, 0], '\t\n'); editor.setCursorBufferPosition([5, 0]); editor.indent({ autoIndent: true }); expect(buffer.lineForRow(5)).toMatch(/^\t\t\t$/); expect(editor.getCursorBufferPosition()).toEqual([5, 3]); }); describe('when the difference between the suggested level of indentation and the current level of indentation is greater than 0 but less than 1', () => it('inserts one tab', () => { editor.setSoftTabs(false); buffer.setText(' \ntest'); editor.setCursorBufferPosition([1, 0]); editor.indent({ autoIndent: true }); expect(buffer.lineForRow(1)).toBe('\ttest'); expect(editor.getCursorBufferPosition()).toEqual([1, 1]); })); }); }); describe("when the line's indent level is greater than the suggested level of indentation", () => { describe("when 'softTabs' is true (the default)", () => it("moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", () => { buffer.insert([7, 0], ' \n'); editor.setCursorBufferPosition([7, 2]); editor.indent({ autoIndent: true }); expect(buffer.lineForRow(7)).toMatch(/^\s+$/); expect(buffer.lineForRow(7).length).toBe(8); expect(editor.getCursorBufferPosition()).toEqual([7, 8]); })); describe("when 'softTabs' is false", () => it('moves the cursor to the end of the leading whitespace and inserts \t into the buffer', () => { convertToHardTabs(buffer); editor.setSoftTabs(false); buffer.insert([7, 0], '\t\t\t\n'); editor.setCursorBufferPosition([7, 1]); editor.indent({ autoIndent: true }); expect(buffer.lineForRow(7)).toMatch(/^\t\t\t\t$/); expect(editor.getCursorBufferPosition()).toEqual([7, 4]); })); }); }); }); describe('when the selection is not empty', () => { it('indents the selected lines', () => { editor.setSelectedBufferRange([[0, 0], [10, 0]]); const selection = editor.getLastSelection(); spyOn(selection, 'indentSelectedRows'); editor.indent(); expect(selection.indentSelectedRows).toHaveBeenCalled(); }); }); describe('if editor.softTabs is false', () => { it('inserts a tab character into the buffer', () => { editor.setSoftTabs(false); expect(buffer.lineForRow(0)).not.toMatch(/^\t/); editor.indent(); expect(buffer.lineForRow(0)).toMatch(/^\t/); expect(editor.getCursorBufferPosition()).toEqual([0, 1]); expect(editor.getCursorScreenPosition()).toEqual([ 0, editor.getTabLength() ]); editor.indent(); expect(buffer.lineForRow(0)).toMatch(/^\t\t/); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); expect(editor.getCursorScreenPosition()).toEqual([ 0, editor.getTabLength() * 2 ]); }); }); }); describe('clipboard operations', () => { describe('.cutSelectedText()', () => { it('removes the selected text from the buffer and places it on the clipboard', () => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[1, 6], [1, 10]] ]); editor.cutSelectedText(); expect(buffer.lineForRow(0)).toBe('var = function () {'); expect(buffer.lineForRow(1)).toBe(' var = function(items) {'); expect(clipboard.readText()).toBe(['quicksort', 'sort'].join(os.EOL)); }); describe('when no text is selected', () => { beforeEach(() => editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[5, 0], [5, 0]]]) ); it('cuts the lines on which there are cursors', () => { editor.cutSelectedText(); expect(buffer.getLineCount()).toBe(11); expect(buffer.lineForRow(1)).toBe( ' if (items.length <= 1) return items;' ); expect(buffer.lineForRow(4)).toBe( ' current < pivot ? left.push(current) : right.push(current);' ); expect(atom.clipboard.read()).toEqual( [ 'var quicksort = function () {', '', ' current = items.shift();', '' ].join(os.EOL) ); }); }); describe('when many selections get added in shuffle order', () => { it('cuts them in order', () => { editor.setSelectedBufferRanges([ [[2, 8], [2, 13]], [[0, 4], [0, 13]], [[1, 6], [1, 10]] ]); editor.cutSelectedText(); expect(atom.clipboard.read()).toEqual( ['quicksort', 'sort', 'items'].join(os.EOL) ); }); }); }); describe('.cutToEndOfLine()', () => { describe('when soft wrap is on', () => { it('cuts up to the end of the line', () => { editor.setSoftWrapped(true); editor.setDefaultCharWidth(1); editor.setEditorWidthInChars(25); editor.setCursorScreenPosition([2, 6]); editor.cutToEndOfLine(); expect(editor.lineTextForScreenRow(2)).toBe( ' var function(items) {' ); }); }); describe('when soft wrap is off', () => { describe('when nothing is selected', () => it('cuts up to the end of the line', () => { editor.setCursorBufferPosition([2, 20]); editor.addCursorAtBufferPosition([3, 20]); editor.cutToEndOfLine(); expect(buffer.lineForRow(2)).toBe(' if (items.length'); expect(buffer.lineForRow(3)).toBe(' var pivot = item'); expect(atom.clipboard.read()).toBe( ` <= 1) return items;${ os.EOL }s.shift(), current, left = [], right = [];` ); })); describe('when text is selected', () => it('only cuts the selected text, not to the end of the line', () => { editor.setSelectedBufferRanges([ [[2, 20], [2, 30]], [[3, 20], [3, 20]] ]); editor.cutToEndOfLine(); expect(buffer.lineForRow(2)).toBe( ' if (items.lengthurn items;' ); expect(buffer.lineForRow(3)).toBe(' var pivot = item'); expect(atom.clipboard.read()).toBe( ` <= 1) ret${os.EOL}s.shift(), current, left = [], right = [];` ); })); }); }); describe('.cutToEndOfBufferLine()', () => { beforeEach(() => { editor.setSoftWrapped(true); editor.setEditorWidthInChars(10); }); describe('when nothing is selected', () => { it('cuts up to the end of the buffer line', () => { editor.setCursorBufferPosition([2, 20]); editor.addCursorAtBufferPosition([3, 20]); editor.cutToEndOfBufferLine(); expect(buffer.lineForRow(2)).toBe(' if (items.length'); expect(buffer.lineForRow(3)).toBe(' var pivot = item'); expect(atom.clipboard.read()).toBe( ` <= 1) return items;${ os.EOL }s.shift(), current, left = [], right = [];` ); }); }); describe('when text is selected', () => { it('only cuts the selected text, not to the end of the buffer line', () => { editor.setSelectedBufferRanges([ [[2, 20], [2, 30]], [[3, 20], [3, 20]] ]); editor.cutToEndOfBufferLine(); expect(buffer.lineForRow(2)).toBe(' if (items.lengthurn items;'); expect(buffer.lineForRow(3)).toBe(' var pivot = item'); expect(atom.clipboard.read()).toBe( ` <= 1) ret${os.EOL}s.shift(), current, left = [], right = [];` ); }); }); }); describe('.copySelectedText()', () => { it('copies selected text onto the clipboard', () => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]] ]); editor.copySelectedText(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {'); expect(buffer.lineForRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(clipboard.readText()).toBe( ['quicksort', 'sort', 'items'].join(os.EOL) ); expect(atom.clipboard.read()).toEqual( ['quicksort', 'sort', 'items'].join(os.EOL) ); }); describe('when no text is selected', () => { beforeEach(() => { editor.setSelectedBufferRanges([ [[1, 5], [1, 5]], [[5, 8], [5, 8]] ]); }); it('copies the lines on which there are cursors', () => { editor.copySelectedText(); expect(atom.clipboard.read()).toEqual( [ ` var sort = function(items) {${os.EOL}`, ` current = items.shift();${os.EOL}` ].join(os.EOL) ); expect(editor.getSelectedBufferRanges()).toEqual([ [[1, 5], [1, 5]], [[5, 8], [5, 8]] ]); }); }); describe('when many selections get added in shuffle order', () => { it('copies them in order', () => { editor.setSelectedBufferRanges([ [[2, 8], [2, 13]], [[0, 4], [0, 13]], [[1, 6], [1, 10]] ]); editor.copySelectedText(); expect(atom.clipboard.read()).toEqual( ['quicksort', 'sort', 'items'].join(os.EOL) ); }); }); }); describe('.copyOnlySelectedText()', () => { describe('when thee are multiple selections', () => { it('copies selected text onto the clipboard', () => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]] ]); editor.copyOnlySelectedText(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {'); expect(buffer.lineForRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(clipboard.readText()).toBe( ['quicksort', 'sort', 'items'].join(os.EOL) ); expect(atom.clipboard.read()).toEqual( ['quicksort', 'sort', 'items'].join(os.EOL) ); }); }); describe('when no text is selected', () => { it('does not copy anything', () => { editor.setCursorBufferPosition([1, 5]); editor.copyOnlySelectedText(); expect(atom.clipboard.read()).toEqual('initial clipboard content'); }); }); }); describe('.pasteText()', () => { it('pastes text into the buffer', () => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[1, 6], [1, 10]] ]); atom.clipboard.write('first'); editor.pasteText(); expect(editor.lineTextForBufferRow(0)).toBe( 'var first = function () {' ); expect(editor.lineTextForBufferRow(1)).toBe( ' var first = function(items) {' ); }); it('notifies ::onWillInsertText observers', () => { const insertedStrings = []; editor.onWillInsertText(function({ text, cancel }) { insertedStrings.push(text); cancel(); }); atom.clipboard.write('hello'); editor.pasteText(); expect(insertedStrings).toEqual(['hello']); }); it('notifies ::onDidInsertText observers', () => { const insertedStrings = []; editor.onDidInsertText(({ text, range }) => insertedStrings.push(text) ); atom.clipboard.write('hello'); editor.pasteText(); expect(insertedStrings).toEqual(['hello']); }); describe('when `autoIndentOnPaste` is true', () => { beforeEach(() => editor.update({ autoIndentOnPaste: true })); describe('when pasting multiple lines before any non-whitespace characters', () => { it('auto-indents the lines spanned by the pasted text, based on the first pasted line', () => { atom.clipboard.write('a(x);\n b(x);\n c(x);\n', { indentBasis: 0 }); editor.setCursorBufferPosition([5, 0]); editor.pasteText(); // Adjust the indentation of the pasted lines while preserving // their indentation relative to each other. Also preserve the // indentation of the following line. expect(editor.lineTextForBufferRow(5)).toBe(' a(x);'); expect(editor.lineTextForBufferRow(6)).toBe(' b(x);'); expect(editor.lineTextForBufferRow(7)).toBe(' c(x);'); expect(editor.lineTextForBufferRow(8)).toBe( ' current = items.shift();' ); }); it('auto-indents lines with a mix of hard tabs and spaces without removing spaces', () => { editor.setSoftTabs(false); expect(editor.indentationForBufferRow(5)).toBe(3); atom.clipboard.write('/**\n\t * testing\n\t * indent\n\t **/\n', { indentBasis: 1 }); editor.setCursorBufferPosition([5, 0]); editor.pasteText(); // Do not lose the alignment spaces expect(editor.lineTextForBufferRow(5)).toBe('\t\t\t/**'); expect(editor.lineTextForBufferRow(6)).toBe('\t\t\t * testing'); expect(editor.lineTextForBufferRow(7)).toBe('\t\t\t * indent'); expect(editor.lineTextForBufferRow(8)).toBe('\t\t\t **/'); }); }); describe('when pasting line(s) above a line that matches the decreaseIndentPattern', () => it('auto-indents based on the pasted line(s) only', () => { atom.clipboard.write('a(x);\n b(x);\n c(x);\n', { indentBasis: 0 }); editor.setCursorBufferPosition([7, 0]); editor.pasteText(); expect(editor.lineTextForBufferRow(7)).toBe(' a(x);'); expect(editor.lineTextForBufferRow(8)).toBe(' b(x);'); expect(editor.lineTextForBufferRow(9)).toBe(' c(x);'); expect(editor.lineTextForBufferRow(10)).toBe(' }'); })); describe('when pasting a line of text without line ending', () => it('does not auto-indent the text', () => { atom.clipboard.write('a(x);', { indentBasis: 0 }); editor.setCursorBufferPosition([5, 0]); editor.pasteText(); expect(editor.lineTextForBufferRow(5)).toBe( 'a(x); current = items.shift();' ); expect(editor.lineTextForBufferRow(6)).toBe( ' current < pivot ? left.push(current) : right.push(current);' ); })); describe('when pasting on a line after non-whitespace characters', () => it('does not auto-indent the affected line', () => { // Before the paste, the indentation is non-standard. editor.setText(dedent`\ if (x) { y(); }\ `); atom.clipboard.write(' z();\n h();'); editor.setCursorBufferPosition([1, Infinity]); // The indentation of the non-standard line is unchanged. editor.pasteText(); expect(editor.lineTextForBufferRow(1)).toBe(' y(); z();'); expect(editor.lineTextForBufferRow(2)).toBe(' h();'); })); }); describe('when `autoIndentOnPaste` is false', () => { beforeEach(() => editor.update({ autoIndentOnPaste: false })); describe('when the cursor is indented further than the original copied text', () => it('increases the indentation of the copied lines to match', () => { editor.setSelectedBufferRange([[1, 2], [3, 0]]); editor.copySelectedText(); editor.setCursorBufferPosition([5, 6]); editor.pasteText(); expect(editor.lineTextForBufferRow(5)).toBe( ' var sort = function(items) {' ); expect(editor.lineTextForBufferRow(6)).toBe( ' if (items.length <= 1) return items;' ); })); describe('when the cursor is indented less far than the original copied text', () => it('decreases the indentation of the copied lines to match', () => { editor.setSelectedBufferRange([[6, 6], [8, 0]]); editor.copySelectedText(); editor.setCursorBufferPosition([1, 2]); editor.pasteText(); expect(editor.lineTextForBufferRow(1)).toBe( ' current < pivot ? left.push(current) : right.push(current);' ); expect(editor.lineTextForBufferRow(2)).toBe('}'); })); describe('when the first copied line has leading whitespace', () => it("preserves the line's leading whitespace", () => { editor.setSelectedBufferRange([[4, 0], [6, 0]]); editor.copySelectedText(); editor.setCursorBufferPosition([0, 0]); editor.pasteText(); expect(editor.lineTextForBufferRow(0)).toBe( ' while(items.length > 0) {' ); expect(editor.lineTextForBufferRow(1)).toBe( ' current = items.shift();' ); })); }); describe('when the clipboard has many selections', () => { beforeEach(() => { editor.update({ autoIndentOnPaste: false }); editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[1, 6], [1, 10]] ]); editor.copySelectedText(); }); it('pastes each selection in order separately into the buffer', () => { editor.setSelectedBufferRanges([ [[1, 6], [1, 10]], [[0, 4], [0, 13]] ]); editor.moveRight(); editor.insertText('_'); editor.pasteText(); expect(editor.lineTextForBufferRow(0)).toBe( 'var quicksort_quicksort = function () {' ); expect(editor.lineTextForBufferRow(1)).toBe( ' var sort_sort = function(items) {' ); }); describe('and the selections count does not match', () => { beforeEach(() => editor.setSelectedBufferRanges([[[0, 4], [0, 13]]]) ); it('pastes the whole text into the buffer', () => { editor.pasteText(); expect(editor.lineTextForBufferRow(0)).toBe('var quicksort'); expect(editor.lineTextForBufferRow(1)).toBe( 'sort = function () {' ); }); }); }); describe('when a full line was cut', () => { beforeEach(() => { editor.setCursorBufferPosition([2, 13]); editor.cutSelectedText(); editor.setCursorBufferPosition([2, 13]); }); it("pastes the line above the cursor and retains the cursor's column", () => { editor.pasteText(); expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.getCursorBufferPosition()).toEqual([3, 13]); }); }); describe('when a full line was copied', () => { beforeEach(() => { editor.setCursorBufferPosition([2, 13]); editor.copySelectedText(); }); describe('when there is a selection', () => it('overwrites the selection as with any copied text', () => { editor.setSelectedBufferRange([[1, 2], [1, Infinity]]); editor.pasteText(); expect(editor.lineTextForBufferRow(1)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(2)).toBe(''); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.getCursorBufferPosition()).toEqual([2, 0]); })); describe('when there is no selection', () => it("pastes the line above the cursor and retains the cursor's column", () => { editor.pasteText(); expect(editor.lineTextForBufferRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.lineTextForBufferRow(3)).toBe( ' if (items.length <= 1) return items;' ); expect(editor.getCursorBufferPosition()).toEqual([3, 13]); })); }); it('respects options that preserve the formatting of the pasted text', () => { editor.update({ autoIndentOnPaste: true }); atom.clipboard.write('a(x);\n b(x);\r\nc(x);\n', { indentBasis: 0 }); editor.setCursorBufferPosition([5, 0]); editor.insertText(' '); editor.pasteText({ autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false }); expect(editor.lineTextForBufferRow(5)).toBe(' a(x);'); expect(editor.lineTextForBufferRow(6)).toBe(' b(x);'); expect(editor.buffer.lineEndingForRow(6)).toBe(os.EOL); expect(editor.lineTextForBufferRow(7)).toBe('c(x);'); expect(editor.lineTextForBufferRow(8)).toBe( ' current = items.shift();' ); }); }); }); describe('.indentSelectedRows()', () => { describe('when nothing is selected', () => { describe('when softTabs is enabled', () => { it('indents line and retains selection', () => { editor.setSelectedBufferRange([[0, 3], [0, 3]]); editor.indentSelectedRows(); expect(buffer.lineForRow(0)).toBe( ' var quicksort = function () {' ); expect(editor.getSelectedBufferRange()).toEqual([ [0, 3 + editor.getTabLength()], [0, 3 + editor.getTabLength()] ]); }); }); describe('when softTabs is disabled', () => { it('indents line and retains selection', () => { convertToHardTabs(buffer); editor.setSoftTabs(false); editor.setSelectedBufferRange([[0, 3], [0, 3]]); editor.indentSelectedRows(); expect(buffer.lineForRow(0)).toBe( '\tvar quicksort = function () {' ); expect(editor.getSelectedBufferRange()).toEqual([ [0, 3 + 1], [0, 3 + 1] ]); }); }); }); describe('when one line is selected', () => { describe('when softTabs is enabled', () => { it('indents line and retains selection', () => { editor.setSelectedBufferRange([[0, 4], [0, 14]]); editor.indentSelectedRows(); expect(buffer.lineForRow(0)).toBe( `${editor.getTabText()}var quicksort = function () {` ); expect(editor.getSelectedBufferRange()).toEqual([ [0, 4 + editor.getTabLength()], [0, 14 + editor.getTabLength()] ]); }); }); describe('when softTabs is disabled', () => { it('indents line and retains selection', () => { convertToHardTabs(buffer); editor.setSoftTabs(false); editor.setSelectedBufferRange([[0, 4], [0, 14]]); editor.indentSelectedRows(); expect(buffer.lineForRow(0)).toBe( '\tvar quicksort = function () {' ); expect(editor.getSelectedBufferRange()).toEqual([ [0, 4 + 1], [0, 14 + 1] ]); }); }); }); describe('when multiple lines are selected', () => { describe('when softTabs is enabled', () => { it('indents selected lines (that are not empty) and retains selection', () => { editor.setSelectedBufferRange([[9, 1], [11, 15]]); editor.indentSelectedRows(); expect(buffer.lineForRow(9)).toBe(' };'); expect(buffer.lineForRow(10)).toBe(''); expect(buffer.lineForRow(11)).toBe( ' return sort(Array.apply(this, arguments));' ); expect(editor.getSelectedBufferRange()).toEqual([ [9, 1 + editor.getTabLength()], [11, 15 + editor.getTabLength()] ]); }); it('does not indent the last row if the selection ends at column 0', () => { editor.setSelectedBufferRange([[9, 1], [11, 0]]); editor.indentSelectedRows(); expect(buffer.lineForRow(9)).toBe(' };'); expect(buffer.lineForRow(10)).toBe(''); expect(buffer.lineForRow(11)).toBe( ' return sort(Array.apply(this, arguments));' ); expect(editor.getSelectedBufferRange()).toEqual([ [9, 1 + editor.getTabLength()], [11, 0] ]); }); }); describe('when softTabs is disabled', () => { it('indents selected lines (that are not empty) and retains selection', () => { convertToHardTabs(buffer); editor.setSoftTabs(false); editor.setSelectedBufferRange([[9, 1], [11, 15]]); editor.indentSelectedRows(); expect(buffer.lineForRow(9)).toBe('\t\t};'); expect(buffer.lineForRow(10)).toBe(''); expect(buffer.lineForRow(11)).toBe( '\t\treturn sort(Array.apply(this, arguments));' ); expect(editor.getSelectedBufferRange()).toEqual([ [9, 1 + 1], [11, 15 + 1] ]); }); }); }); }); describe('.outdentSelectedRows()', () => { describe('when nothing is selected', () => { it('outdents line and retains selection', () => { editor.setSelectedBufferRange([[1, 3], [1, 3]]); editor.outdentSelectedRows(); expect(buffer.lineForRow(1)).toBe('var sort = function(items) {'); expect(editor.getSelectedBufferRange()).toEqual([ [1, 3 - editor.getTabLength()], [1, 3 - editor.getTabLength()] ]); }); it('outdents when indent is less than a tab length', () => { editor.insertText(' '); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); }); it('outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs', () => { editor.insertText('\t\t'); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {'); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); }); it('outdents when a mix of hard tabs and soft tabs are used', () => { editor.insertText('\t '); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {'); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {'); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); }); it('outdents only up to the first non-space non-tab character', () => { editor.insertText(' \tfoo\t '); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe( '\tfoo\t var quicksort = function () {' ); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe( 'foo\t var quicksort = function () {' ); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe( 'foo\t var quicksort = function () {' ); }); }); describe('when one line is selected', () => { it('outdents line and retains editor', () => { editor.setSelectedBufferRange([[1, 4], [1, 14]]); editor.outdentSelectedRows(); expect(buffer.lineForRow(1)).toBe('var sort = function(items) {'); expect(editor.getSelectedBufferRange()).toEqual([ [1, 4 - editor.getTabLength()], [1, 14 - editor.getTabLength()] ]); }); }); describe('when multiple lines are selected', () => { it('outdents selected lines and retains editor', () => { editor.setSelectedBufferRange([[0, 1], [3, 15]]); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); expect(buffer.lineForRow(1)).toBe('var sort = function(items) {'); expect(buffer.lineForRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(buffer.lineForRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.getSelectedBufferRange()).toEqual([ [0, 1], [3, 15 - editor.getTabLength()] ]); }); it('does not outdent the last line of the selection if it ends at column 0', () => { editor.setSelectedBufferRange([[0, 1], [3, 0]]); editor.outdentSelectedRows(); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); expect(buffer.lineForRow(1)).toBe('var sort = function(items) {'); expect(buffer.lineForRow(2)).toBe( ' if (items.length <= 1) return items;' ); expect(buffer.lineForRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 0]]); }); }); }); describe('.autoIndentSelectedRows', () => { it('auto-indents the selection', () => { editor.setCursorBufferPosition([2, 0]); editor.insertText('function() {\ninside=true\n}\n i=1\n'); editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]); editor.autoIndentSelectedRows(); expect(editor.lineTextForBufferRow(2)).toBe(' function() {'); expect(editor.lineTextForBufferRow(3)).toBe(' inside=true'); expect(editor.lineTextForBufferRow(4)).toBe(' }'); expect(editor.lineTextForBufferRow(5)).toBe(' i=1'); }); }); describe('.undo() and .redo()', () => { it('undoes/redoes the last change', () => { editor.insertText('foo'); editor.undo(); expect(buffer.lineForRow(0)).not.toContain('foo'); editor.redo(); expect(buffer.lineForRow(0)).toContain('foo'); }); it('batches the undo / redo of changes caused by multiple cursors', () => { editor.setCursorScreenPosition([0, 0]); editor.addCursorAtScreenPosition([1, 0]); editor.insertText('foo'); editor.backspace(); expect(buffer.lineForRow(0)).toContain('fovar'); expect(buffer.lineForRow(1)).toContain('fo '); editor.undo(); expect(buffer.lineForRow(0)).toContain('foo'); expect(buffer.lineForRow(1)).toContain('foo'); editor.redo(); expect(buffer.lineForRow(0)).not.toContain('foo'); expect(buffer.lineForRow(0)).toContain('fovar'); }); it('restores cursors and selections to their states before and after undone and redone changes', () => { editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 3]]]); editor.insertText('abc'); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 3], [0, 3]], [[1, 3], [1, 3]] ]); editor.setCursorBufferPosition([0, 0]); editor.setSelectedBufferRanges([ [[2, 0], [2, 0]], [[3, 0], [3, 0]], [[4, 0], [4, 3]] ]); editor.insertText('def'); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 3], [2, 3]], [[3, 3], [3, 3]], [[4, 3], [4, 3]] ]); editor.setCursorBufferPosition([0, 0]); editor.undo(); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 0], [2, 0]], [[3, 0], [3, 0]], [[4, 0], [4, 3]] ]); editor.undo(); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 0], [0, 0]], [[1, 0], [1, 3]] ]); editor.redo(); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 3], [0, 3]], [[1, 3], [1, 3]] ]); editor.redo(); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 3], [2, 3]], [[3, 3], [3, 3]], [[4, 3], [4, 3]] ]); }); it('restores the selected ranges after undo and redo', () => { editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]); editor.delete(); editor.delete(); expect(buffer.lineForRow(1)).toBe(' var = function( {'); expect(editor.getSelectedBufferRanges()).toEqual([ [[1, 6], [1, 6]], [[1, 17], [1, 17]] ]); editor.undo(); expect(editor.getSelectedBufferRanges()).toEqual([ [[1, 6], [1, 6]], [[1, 18], [1, 18]] ]); editor.undo(); expect(editor.getSelectedBufferRanges()).toEqual([ [[1, 6], [1, 10]], [[1, 22], [1, 27]] ]); editor.redo(); expect(editor.getSelectedBufferRanges()).toEqual([ [[1, 6], [1, 6]], [[1, 18], [1, 18]] ]); }); xit('restores folds after undo and redo', () => { editor.foldBufferRow(1); editor.setSelectedBufferRange([[1, 0], [10, Infinity]], { preserveFolds: true }); expect(editor.isFoldedAtBufferRow(1)).toBeTruthy(); editor.insertText(dedent`\ // testing function foo() { return 1 + 2; }\ `); expect(editor.isFoldedAtBufferRow(1)).toBeFalsy(); editor.foldBufferRow(2); editor.undo(); expect(editor.isFoldedAtBufferRow(1)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(9)).toBeTruthy(); expect(editor.isFoldedAtBufferRow(10)).toBeFalsy(); editor.redo(); expect(editor.isFoldedAtBufferRow(1)).toBeFalsy(); expect(editor.isFoldedAtBufferRow(2)).toBeTruthy(); }); }); describe('::transact', () => { it('restores the selection when the transaction is undone/redone', () => { buffer.setText('1234'); editor.setSelectedBufferRange([[0, 1], [0, 3]]); editor.transact(() => { editor.delete(); editor.moveToEndOfLine(); editor.insertText('5'); expect(buffer.getText()).toBe('145'); }); editor.undo(); expect(buffer.getText()).toBe('1234'); expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 3]]); editor.redo(); expect(buffer.getText()).toBe('145'); expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 3]]); }); }); describe('undo/redo restore selections of editor which initiated original change', () => { let editor1, editor2; beforeEach(async () => { editor1 = editor; editor2 = new TextEditor({ buffer: editor1.buffer }); editor1.setText(dedent` aaaaaa bbbbbb cccccc dddddd eeeeee `); }); it('[editor.transact] restore selection of change-initiated-editor', () => { editor1.setCursorBufferPosition([0, 0]); editor1.transact(() => editor1.insertText('1')); editor2.setCursorBufferPosition([1, 0]); editor2.transact(() => editor2.insertText('2')); editor1.setCursorBufferPosition([2, 0]); editor1.transact(() => editor1.insertText('3')); editor2.setCursorBufferPosition([3, 0]); editor2.transact(() => editor2.insertText('4')); expect(editor1.getText()).toBe(dedent` 1aaaaaa 2bbbbbb 3cccccc 4dddddd eeeeee `); editor2.setCursorBufferPosition([4, 0]); editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([3, 0]); editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([2, 0]); editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([1, 0]); editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([0, 0]); expect(editor2.getCursorBufferPosition()).toEqual([4, 0]); // remain unchanged editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([0, 1]); editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([1, 1]); editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([2, 1]); editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([3, 1]); expect(editor2.getCursorBufferPosition()).toEqual([4, 0]); // remain unchanged editor1.setCursorBufferPosition([4, 0]); editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([3, 0]); editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([2, 0]); editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([1, 0]); editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([0, 0]); expect(editor1.getCursorBufferPosition()).toEqual([4, 0]); // remain unchanged editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([0, 1]); editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([1, 1]); editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([2, 1]); editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([3, 1]); expect(editor1.getCursorBufferPosition()).toEqual([4, 0]); // remain unchanged }); it('[manually group checkpoint] restore selection of change-initiated-editor', () => { const transact = (editor, fn) => { const checkpoint = editor.createCheckpoint(); fn(); editor.groupChangesSinceCheckpoint(checkpoint); }; editor1.setCursorBufferPosition([0, 0]); transact(editor1, () => editor1.insertText('1')); editor2.setCursorBufferPosition([1, 0]); transact(editor2, () => editor2.insertText('2')); editor1.setCursorBufferPosition([2, 0]); transact(editor1, () => editor1.insertText('3')); editor2.setCursorBufferPosition([3, 0]); transact(editor2, () => editor2.insertText('4')); expect(editor1.getText()).toBe(dedent` 1aaaaaa 2bbbbbb 3cccccc 4dddddd eeeeee `); editor2.setCursorBufferPosition([4, 0]); editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([3, 0]); editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([2, 0]); editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([1, 0]); editor1.undo(); expect(editor1.getCursorBufferPosition()).toEqual([0, 0]); expect(editor2.getCursorBufferPosition()).toEqual([4, 0]); // remain unchanged editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([0, 1]); editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([1, 1]); editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([2, 1]); editor1.redo(); expect(editor1.getCursorBufferPosition()).toEqual([3, 1]); expect(editor2.getCursorBufferPosition()).toEqual([4, 0]); // remain unchanged editor1.setCursorBufferPosition([4, 0]); editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([3, 0]); editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([2, 0]); editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([1, 0]); editor2.undo(); expect(editor2.getCursorBufferPosition()).toEqual([0, 0]); expect(editor1.getCursorBufferPosition()).toEqual([4, 0]); // remain unchanged editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([0, 1]); editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([1, 1]); editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([2, 1]); editor2.redo(); expect(editor2.getCursorBufferPosition()).toEqual([3, 1]); expect(editor1.getCursorBufferPosition()).toEqual([4, 0]); // remain unchanged }); }); describe('when the buffer is changed (via its direct api, rather than via than edit session)', () => { it('moves the cursor so it is in the same relative position of the buffer', () => { expect(editor.getCursorScreenPosition()).toEqual([0, 0]); editor.addCursorAtScreenPosition([0, 5]); editor.addCursorAtScreenPosition([1, 0]); const [cursor1, cursor2, cursor3] = editor.getCursors(); buffer.insert([0, 1], 'abc'); expect(cursor1.getScreenPosition()).toEqual([0, 0]); expect(cursor2.getScreenPosition()).toEqual([0, 8]); expect(cursor3.getScreenPosition()).toEqual([1, 0]); }); it('does not destroy cursors or selections when a change encompasses them', () => { const cursor = editor.getLastCursor(); cursor.setBufferPosition([3, 3]); editor.buffer.delete([[3, 1], [3, 5]]); expect(cursor.getBufferPosition()).toEqual([3, 1]); expect(editor.getCursors().indexOf(cursor)).not.toBe(-1); const selection = editor.getLastSelection(); selection.setBufferRange([[3, 5], [3, 10]]); editor.buffer.delete([[3, 3], [3, 8]]); expect(selection.getBufferRange()).toEqual([[3, 3], [3, 5]]); expect(editor.getSelections().indexOf(selection)).not.toBe(-1); }); it('merges cursors when the change causes them to overlap', () => { editor.setCursorScreenPosition([0, 0]); editor.addCursorAtScreenPosition([0, 2]); editor.addCursorAtScreenPosition([1, 2]); const [cursor1, , cursor3] = editor.getCursors(); expect(editor.getCursors().length).toBe(3); buffer.delete([[0, 0], [0, 2]]); expect(editor.getCursors().length).toBe(2); expect(editor.getCursors()).toEqual([cursor1, cursor3]); expect(cursor1.getBufferPosition()).toEqual([0, 0]); expect(cursor3.getBufferPosition()).toEqual([1, 2]); }); }); describe('.moveSelectionLeft()', () => { it('moves one active selection on one line one column to the left', () => { editor.setSelectedBufferRange([[0, 4], [0, 13]]); expect(editor.getSelectedText()).toBe('quicksort'); editor.moveSelectionLeft(); expect(editor.getSelectedText()).toBe('quicksort'); expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 12]]); }); it('moves multiple active selections on one line one column to the left', () => { editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]); const selections = editor.getSelections(); expect(selections[0].getText()).toBe('quicksort'); expect(selections[1].getText()).toBe('function'); editor.moveSelectionLeft(); expect(selections[0].getText()).toBe('quicksort'); expect(selections[1].getText()).toBe('function'); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 3], [0, 12]], [[0, 15], [0, 23]] ]); }); it('moves multiple active selections on multiple lines one column to the left', () => { editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]); const selections = editor.getSelections(); expect(selections[0].getText()).toBe('quicksort'); expect(selections[1].getText()).toBe('sort'); editor.moveSelectionLeft(); expect(selections[0].getText()).toBe('quicksort'); expect(selections[1].getText()).toBe('sort'); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 3], [0, 12]], [[1, 5], [1, 9]] ]); }); describe('when a selection is at the first column of a line', () => { it('does not change the selection', () => { editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]); const selections = editor.getSelections(); expect(selections[0].getText()).toBe('var'); expect(selections[1].getText()).toBe(' v'); editor.moveSelectionLeft(); editor.moveSelectionLeft(); expect(selections[0].getText()).toBe('var'); expect(selections[1].getText()).toBe(' v'); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 0], [0, 3]], [[1, 0], [1, 3]] ]); }); describe('when multiple selections are active on one line', () => { it('does not change the selection', () => { editor.setSelectedBufferRanges([ [[0, 0], [0, 3]], [[0, 4], [0, 13]] ]); const selections = editor.getSelections(); expect(selections[0].getText()).toBe('var'); expect(selections[1].getText()).toBe('quicksort'); editor.moveSelectionLeft(); expect(selections[0].getText()).toBe('var'); expect(selections[1].getText()).toBe('quicksort'); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 0], [0, 3]], [[0, 4], [0, 13]] ]); }); }); }); }); describe('.moveSelectionRight()', () => { it('moves one active selection on one line one column to the right', () => { editor.setSelectedBufferRange([[0, 4], [0, 13]]); expect(editor.getSelectedText()).toBe('quicksort'); editor.moveSelectionRight(); expect(editor.getSelectedText()).toBe('quicksort'); expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 14]]); }); it('moves multiple active selections on one line one column to the right', () => { editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]); const selections = editor.getSelections(); expect(selections[0].getText()).toBe('quicksort'); expect(selections[1].getText()).toBe('function'); editor.moveSelectionRight(); expect(selections[0].getText()).toBe('quicksort'); expect(selections[1].getText()).toBe('function'); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 5], [0, 14]], [[0, 17], [0, 25]] ]); }); it('moves multiple active selections on multiple lines one column to the right', () => { editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]); const selections = editor.getSelections(); expect(selections[0].getText()).toBe('quicksort'); expect(selections[1].getText()).toBe('sort'); editor.moveSelectionRight(); expect(selections[0].getText()).toBe('quicksort'); expect(selections[1].getText()).toBe('sort'); expect(editor.getSelectedBufferRanges()).toEqual([ [[0, 5], [0, 14]], [[1, 7], [1, 11]] ]); }); describe('when a selection is at the last column of a line', () => { it('does not change the selection', () => { editor.setSelectedBufferRanges([ [[2, 34], [2, 40]], [[5, 22], [5, 30]] ]); const selections = editor.getSelections(); expect(selections[0].getText()).toBe('items;'); expect(selections[1].getText()).toBe('shift();'); editor.moveSelectionRight(); editor.moveSelectionRight(); expect(selections[0].getText()).toBe('items;'); expect(selections[1].getText()).toBe('shift();'); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 34], [2, 40]], [[5, 22], [5, 30]] ]); }); describe('when multiple selections are active on one line', () => { it('does not change the selection', () => { editor.setSelectedBufferRanges([ [[2, 27], [2, 33]], [[2, 34], [2, 40]] ]); const selections = editor.getSelections(); expect(selections[0].getText()).toBe('return'); expect(selections[1].getText()).toBe('items;'); editor.moveSelectionRight(); expect(selections[0].getText()).toBe('return'); expect(selections[1].getText()).toBe('items;'); expect(editor.getSelectedBufferRanges()).toEqual([ [[2, 27], [2, 33]], [[2, 34], [2, 40]] ]); }); }); }); }); describe('when readonly', () => { beforeEach(() => { editor.setReadOnly(true); }); const modifications = [ { name: 'moveLineUp', op: (opts = {}) => { editor.setCursorBufferPosition([1, 0]); editor.moveLineUp(opts); } }, { name: 'moveLineDown', op: (opts = {}) => { editor.setCursorBufferPosition([0, 0]); editor.moveLineDown(opts); } }, { name: 'insertText', op: (opts = {}) => { editor.setSelectedBufferRange([[1, 0], [1, 2]]); editor.insertText('xxx', opts); } }, { name: 'insertNewline', op: (opts = {}) => { editor.setCursorScreenPosition({ row: 1, column: 0 }); editor.insertNewline(opts); } }, { name: 'insertNewlineBelow', op: (opts = {}) => { editor.setCursorBufferPosition([0, 2]); editor.insertNewlineBelow(opts); } }, { name: 'insertNewlineAbove', op: (opts = {}) => { editor.setCursorBufferPosition([0]); editor.insertNewlineAbove(opts); } }, { name: 'backspace', op: (opts = {}) => { editor.setCursorScreenPosition({ row: 1, column: 7 }); editor.backspace(opts); } }, { name: 'deleteToPreviousWordBoundary', op: (opts = {}) => { editor.setCursorBufferPosition([0, 16]); editor.deleteToPreviousWordBoundary(opts); } }, { name: 'deleteToNextWordBoundary', op: (opts = {}) => { editor.setCursorBufferPosition([0, 15]); editor.deleteToNextWordBoundary(opts); } }, { name: 'deleteToBeginningOfWord', op: (opts = {}) => { editor.setCursorBufferPosition([1, 24]); editor.deleteToBeginningOfWord(opts); } }, { name: 'deleteToEndOfLine', op: (opts = {}) => { editor.setCursorBufferPosition([1, 24]); editor.deleteToEndOfLine(opts); } }, { name: 'deleteToBeginningOfLine', op: (opts = {}) => { editor.setCursorBufferPosition([1, 24]); editor.deleteToBeginningOfLine(opts); } }, { name: 'delete', op: (opts = {}) => { editor.setCursorScreenPosition([1, 6]); editor.delete(opts); } }, { name: 'deleteToEndOfWord', op: (opts = {}) => { editor.setCursorBufferPosition([1, 24]); editor.deleteToEndOfWord(opts); } }, { name: 'indent', op: (opts = {}) => { editor.indent(opts); } }, { name: 'cutSelectedText', op: (opts = {}) => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[1, 6], [1, 10]] ]); editor.cutSelectedText(opts); } }, { name: 'cutToEndOfLine', op: (opts = {}) => { editor.setCursorBufferPosition([2, 20]); editor.cutToEndOfLine(opts); } }, { name: 'cutToEndOfBufferLine', op: (opts = {}) => { editor.setCursorBufferPosition([2, 20]); editor.cutToEndOfBufferLine(opts); } }, { name: 'pasteText', op: (opts = {}) => { editor.setSelectedBufferRanges([ [[0, 4], [0, 13]], [[1, 6], [1, 10]] ]); atom.clipboard.write('first'); editor.pasteText(opts); } }, { name: 'indentSelectedRows', op: (opts = {}) => { editor.setSelectedBufferRange([[0, 3], [0, 3]]); editor.indentSelectedRows(opts); } }, { name: 'outdentSelectedRows', op: (opts = {}) => { editor.setSelectedBufferRange([[1, 3], [1, 3]]); editor.outdentSelectedRows(opts); } }, { name: 'autoIndentSelectedRows', op: (opts = {}) => { editor.setCursorBufferPosition([2, 0]); editor.insertText('function() {\ninside=true\n}\n i=1\n', opts); editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]); editor.autoIndentSelectedRows(opts); } }, { name: 'undo/redo', op: (opts = {}) => { editor.insertText('foo', opts); editor.undo(opts); editor.redo(opts); } } ]; describe('without bypassReadOnly', () => { for (const { name, op } of modifications) { it(`throws an error on ${name}`, () => { expect(op).toThrow(); }); } }); describe('with bypassReadOnly', () => { for (const { name, op } of modifications) { it(`permits ${name}`, () => { op({ bypassReadOnly: true }); }); } }); }); }); describe('reading text', () => { it('.lineTextForScreenRow(row)', () => { editor.foldBufferRow(4); expect(editor.lineTextForScreenRow(5)).toEqual( ' return sort(left).concat(pivot).concat(sort(right));' ); expect(editor.lineTextForScreenRow(9)).toEqual('};'); expect(editor.lineTextForScreenRow(10)).toBeUndefined(); }); }); describe('.deleteLine()', () => { it('deletes the first line when the cursor is there', () => { editor.getLastCursor().moveToTop(); const line1 = buffer.lineForRow(1); const count = buffer.getLineCount(); expect(buffer.lineForRow(0)).not.toBe(line1); editor.deleteLine(); expect(buffer.lineForRow(0)).toBe(line1); expect(buffer.getLineCount()).toBe(count - 1); }); it('deletes the last line when the cursor is there', () => { const count = buffer.getLineCount(); const secondToLastLine = buffer.lineForRow(count - 2); expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine); editor.getLastCursor().moveToBottom(); editor.deleteLine(); const newCount = buffer.getLineCount(); expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine); expect(newCount).toBe(count - 1); }); it('deletes whole lines when partial lines are selected', () => { editor.setSelectedBufferRange([[0, 2], [1, 2]]); const line2 = buffer.lineForRow(2); const count = buffer.getLineCount(); expect(buffer.lineForRow(0)).not.toBe(line2); expect(buffer.lineForRow(1)).not.toBe(line2); editor.deleteLine(); expect(buffer.lineForRow(0)).toBe(line2); expect(buffer.getLineCount()).toBe(count - 2); }); it('restores cursor position for multiple cursors', () => { const line = '0123456789'.repeat(8); editor.setText((line + '\n').repeat(5)); editor.setCursorScreenPosition([0, 5]); editor.addCursorAtScreenPosition([2, 8]); editor.deleteLine(); const cursors = editor.getCursors(); expect(cursors.length).toBe(2); expect(cursors[0].getScreenPosition()).toEqual([0, 5]); expect(cursors[1].getScreenPosition()).toEqual([1, 8]); }); it('restores cursor position for multiple selections', () => { const line = '0123456789'.repeat(8); editor.setText((line + '\n').repeat(5)); editor.setSelectedBufferRanges([[[0, 5], [0, 8]], [[2, 4], [2, 15]]]); editor.deleteLine(); const cursors = editor.getCursors(); expect(cursors.length).toBe(2); expect(cursors[0].getScreenPosition()).toEqual([0, 5]); expect(cursors[1].getScreenPosition()).toEqual([1, 4]); }); it('deletes a line only once when multiple selections are on the same line', () => { const line1 = buffer.lineForRow(1); const count = buffer.getLineCount(); editor.setSelectedBufferRanges([[[0, 1], [0, 2]], [[0, 4], [0, 5]]]); expect(buffer.lineForRow(0)).not.toBe(line1); editor.deleteLine(); expect(buffer.lineForRow(0)).toBe(line1); expect(buffer.getLineCount()).toBe(count - 1); }); it('only deletes first line if only newline is selected on second line', () => { editor.setSelectedBufferRange([[0, 2], [1, 0]]); const line1 = buffer.lineForRow(1); const count = buffer.getLineCount(); expect(buffer.lineForRow(0)).not.toBe(line1); editor.deleteLine(); expect(buffer.lineForRow(0)).toBe(line1); expect(buffer.getLineCount()).toBe(count - 1); }); it('deletes the entire region when invoke on a folded region', () => { editor.foldBufferRow(1); editor.getLastCursor().moveToTop(); editor.getLastCursor().moveDown(); expect(buffer.getLineCount()).toBe(13); editor.deleteLine(); expect(buffer.getLineCount()).toBe(4); }); it('deletes the entire file from the bottom up', () => { const count = buffer.getLineCount(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { editor.getLastCursor().moveToBottom(); editor.deleteLine(); } expect(buffer.getLineCount()).toBe(1); expect(buffer.getText()).toBe(''); }); it('deletes the entire file from the top down', () => { const count = buffer.getLineCount(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { editor.getLastCursor().moveToTop(); editor.deleteLine(); } expect(buffer.getLineCount()).toBe(1); expect(buffer.getText()).toBe(''); }); describe('when soft wrap is enabled', () => { it('deletes the entire line that the cursor is on', () => { editor.setSoftWrapped(true); editor.setEditorWidthInChars(10); editor.setCursorBufferPosition([6]); const line7 = buffer.lineForRow(7); const count = buffer.getLineCount(); expect(buffer.lineForRow(6)).not.toBe(line7); editor.deleteLine(); expect(buffer.lineForRow(6)).toBe(line7); expect(buffer.getLineCount()).toBe(count - 1); }); }); describe('when the line being deleted precedes a fold, and the command is undone', () => { it('restores the line and preserves the fold', () => { editor.setCursorBufferPosition([4]); editor.foldCurrentRow(); expect(editor.isFoldedAtScreenRow(4)).toBeTruthy(); editor.setCursorBufferPosition([3]); editor.deleteLine(); expect(editor.isFoldedAtScreenRow(3)).toBeTruthy(); expect(buffer.lineForRow(3)).toBe(' while(items.length > 0) {'); editor.undo(); expect(editor.isFoldedAtScreenRow(4)).toBeTruthy(); expect(buffer.lineForRow(3)).toBe( ' var pivot = items.shift(), current, left = [], right = [];' ); }); }); }); describe('.replaceSelectedText(options, fn)', () => { describe('when no text is selected', () => { it('inserts the text returned from the function at the cursor position', () => { editor.replaceSelectedText({}, () => '123'); expect(buffer.lineForRow(0)).toBe('123var quicksort = function () {'); editor.setCursorBufferPosition([0]); editor.replaceSelectedText({ selectWordIfEmpty: true }, () => 'var'); expect(buffer.lineForRow(0)).toBe('var quicksort = function () {'); editor.setCursorBufferPosition([10]); editor.replaceSelectedText(null, () => ''); expect(buffer.lineForRow(10)).toBe(''); }); }); describe('when text is selected', () => { it('replaces the selected text with the text returned from the function', () => { editor.setSelectedBufferRange([[0, 1], [0, 3]]); editor.replaceSelectedText({}, () => 'ia'); expect(buffer.lineForRow(0)).toBe('via quicksort = function () {'); }); it('replaces the selected text and selects the replacement text', () => { editor.setSelectedBufferRange([[0, 4], [0, 9]]); editor.replaceSelectedText({}, () => 'whatnot'); expect(buffer.lineForRow(0)).toBe('var whatnotsort = function () {'); expect(editor.getSelectedBufferRange()).toEqual([[0, 4], [0, 11]]); }); }); }); describe('.transpose()', () => { it('swaps two characters', () => { editor.buffer.setText('abc'); editor.setCursorScreenPosition([0, 1]); editor.transpose(); expect(editor.lineTextForBufferRow(0)).toBe('bac'); }); it('reverses a selection', () => { editor.buffer.setText('xabcz'); editor.setSelectedBufferRange([[0, 1], [0, 4]]); editor.transpose(); expect(editor.lineTextForBufferRow(0)).toBe('xcbaz'); }); }); describe('.upperCase()', () => { describe('when there is no selection', () => { it('upper cases the current word', () => { editor.buffer.setText('aBc'); editor.setCursorScreenPosition([0, 1]); editor.upperCase(); expect(editor.lineTextForBufferRow(0)).toBe('ABC'); expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]); }); }); describe('when there is a selection', () => { it('upper cases the current selection', () => { editor.buffer.setText('abc'); editor.setSelectedBufferRange([[0, 0], [0, 2]]); editor.upperCase(); expect(editor.lineTextForBufferRow(0)).toBe('ABc'); expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 2]]); }); }); }); describe('.lowerCase()', () => { describe('when there is no selection', () => { it('lower cases the current word', () => { editor.buffer.setText('aBC'); editor.setCursorScreenPosition([0, 1]); editor.lowerCase(); expect(editor.lineTextForBufferRow(0)).toBe('abc'); expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]); }); }); describe('when there is a selection', () => { it('lower cases the current selection', () => { editor.buffer.setText('ABC'); editor.setSelectedBufferRange([[0, 0], [0, 2]]); editor.lowerCase(); expect(editor.lineTextForBufferRow(0)).toBe('abC'); expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 2]]); }); }); }); describe('.setTabLength(tabLength)', () => { it('clips atomic soft tabs to the given tab length', () => { expect(editor.getTabLength()).toBe(2); expect( editor.clipScreenPosition([5, 1], { clipDirection: 'forward' }) ).toEqual([5, 2]); editor.setTabLength(6); expect(editor.getTabLength()).toBe(6); expect( editor.clipScreenPosition([5, 1], { clipDirection: 'forward' }) ).toEqual([5, 6]); const changeHandler = jasmine.createSpy('changeHandler'); editor.onDidChange(changeHandler); editor.setTabLength(6); expect(changeHandler).not.toHaveBeenCalled(); }); it('does not change its tab length when the given tab length is null', () => { editor.setTabLength(4); editor.setTabLength(null); expect(editor.getTabLength()).toBe(4); }); }); describe('.indentLevelForLine(line)', () => { it('returns the indent level when the line has only leading whitespace', () => { expect(editor.indentLevelForLine(' hello')).toBe(2); expect(editor.indentLevelForLine(' hello')).toBe(1.5); }); it('returns the indent level when the line has only leading tabs', () => expect(editor.indentLevelForLine('\t\thello')).toBe(2)); it('returns the indent level based on the character starting the line when the leading whitespace contains both spaces and tabs', () => { expect(editor.indentLevelForLine('\t hello')).toBe(2); expect(editor.indentLevelForLine(' \thello')).toBe(2); expect(editor.indentLevelForLine(' \t hello')).toBe(2.5); expect(editor.indentLevelForLine(' \t \thello')).toBe(4); expect(editor.indentLevelForLine(' \t \thello')).toBe(4); expect(editor.indentLevelForLine(' \t \t hello')).toBe(4.5); }); }); describe("when the buffer's language mode changes", () => { beforeEach(() => { atom.config.set('core.useTreeSitterParsers', false); }); it('notifies onDidTokenize observers when retokenization is finished', async () => { // Exercise the full `tokenizeInBackground` code path, which bails out early if // `.setVisible` has not been called with `true`. jasmine.unspy(TextMateLanguageMode.prototype, 'tokenizeInBackground'); jasmine.attachToDOM(editor.getElement()); const events = []; editor.onDidTokenize(event => events.push(event)); await atom.packages.activatePackage('language-c'); expect( atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.c') ).toBe(true); advanceClock(1); expect(events.length).toBe(1); }); it('notifies onDidChangeGrammar observers', async () => { const events = []; editor.onDidChangeGrammar(grammar => events.push(grammar)); await atom.packages.activatePackage('language-c'); expect( atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.c') ).toBe(true); expect(events.length).toBe(1); expect(events[0].name).toBe('C'); }); }); describe('editor.autoIndent', () => { describe('when editor.autoIndent is false (default)', () => { describe('when `indent` is triggered', () => { it('does not auto-indent the line', () => { editor.setCursorBufferPosition([1, 30]); editor.insertText('\n '); expect(editor.lineTextForBufferRow(2)).toBe(' '); editor.update({ autoIndent: false }); editor.indent(); expect(editor.lineTextForBufferRow(2)).toBe(' '); }); }); }); describe('when editor.autoIndent is true', () => { beforeEach(() => editor.update({ autoIndent: true })); describe('when `indent` is triggered', () => { it('auto-indents the line', () => { editor.setCursorBufferPosition([1, 30]); editor.insertText('\n '); expect(editor.lineTextForBufferRow(2)).toBe(' '); editor.update({ autoIndent: true }); editor.indent(); expect(editor.lineTextForBufferRow(2)).toBe(' '); }); }); describe('when a newline is added', () => { describe('when the line preceding the newline adds a new level of indentation', () => { it('indents the newline to one additional level of indentation beyond the preceding line', () => { editor.setCursorBufferPosition([1, Infinity]); editor.insertText('\n'); expect(editor.indentationForBufferRow(2)).toBe( editor.indentationForBufferRow(1) + 1 ); }); }); describe("when the line preceding the newline doesn't add a level of indentation", () => { it('indents the new line to the same level as the preceding line', () => { editor.setCursorBufferPosition([5, 14]); editor.insertText('\n'); expect(editor.indentationForBufferRow(6)).toBe( editor.indentationForBufferRow(5) ); }); }); describe('when the line preceding the newline is a comment', () => { it('maintains the indent of the commented line', () => { editor.setCursorBufferPosition([0, 0]); editor.insertText(' //'); editor.setCursorBufferPosition([0, Infinity]); editor.insertText('\n'); expect(editor.indentationForBufferRow(1)).toBe(2); }); }); describe('when the line preceding the newline contains only whitespace', () => { it("bases the new line's indentation on only the preceding line", () => { editor.setCursorBufferPosition([6, Infinity]); editor.insertText('\n '); expect(editor.getCursorBufferPosition()).toEqual([7, 2]); editor.insertNewline(); expect(editor.lineTextForBufferRow(8)).toBe(' '); }); }); it('does not indent the line preceding the newline', () => { editor.setCursorBufferPosition([2, 0]); editor.insertText(' var this-line-should-be-indented-more\n'); expect(editor.indentationForBufferRow(1)).toBe(1); editor.update({ autoIndent: true }); editor.setCursorBufferPosition([2, Infinity]); editor.insertText('\n'); expect(editor.indentationForBufferRow(1)).toBe(1); expect(editor.indentationForBufferRow(2)).toBe(1); }); describe('when the cursor is before whitespace', () => { it('retains the whitespace following the cursor on the new line', () => { editor.setText(' var sort = function() {}'); editor.setCursorScreenPosition([0, 12]); editor.insertNewline(); expect(buffer.lineForRow(0)).toBe(' var sort ='); expect(buffer.lineForRow(1)).toBe(' function() {}'); expect(editor.getCursorScreenPosition()).toEqual([1, 2]); }); }); }); describe('when inserted text matches a decrease indent pattern', () => { describe('when the preceding line matches an increase indent pattern', () => { it('decreases the indentation to match that of the preceding line', () => { editor.setCursorBufferPosition([1, Infinity]); editor.insertText('\n'); expect(editor.indentationForBufferRow(2)).toBe( editor.indentationForBufferRow(1) + 1 ); editor.insertText('}'); expect(editor.indentationForBufferRow(2)).toBe( editor.indentationForBufferRow(1) ); }); }); describe("when the preceding line doesn't match an increase indent pattern", () => { it('decreases the indentation to be one level below that of the preceding line', () => { editor.setCursorBufferPosition([3, Infinity]); editor.insertText('\n '); expect(editor.indentationForBufferRow(4)).toBe( editor.indentationForBufferRow(3) ); editor.insertText('}'); expect(editor.indentationForBufferRow(4)).toBe( editor.indentationForBufferRow(3) - 1 ); }); it("doesn't break when decreasing the indentation on a row that has no indentation", () => { editor.setCursorBufferPosition([12, Infinity]); editor.insertText('\n}; # too many closing brackets!'); expect(editor.lineTextForBufferRow(13)).toBe( '}; # too many closing brackets!' ); }); }); }); describe('when inserted text does not match a decrease indent pattern', () => { it('does not decrease the indentation', () => { editor.setCursorBufferPosition([12, 0]); editor.insertText(' '); expect(editor.lineTextForBufferRow(12)).toBe(' };'); editor.insertText('\t\t'); expect(editor.lineTextForBufferRow(12)).toBe(' \t\t};'); }); }); describe('when the current line does not match a decrease indent pattern', () => { it('leaves the line unchanged', () => { editor.setCursorBufferPosition([2, 4]); expect(editor.indentationForBufferRow(2)).toBe( editor.indentationForBufferRow(1) + 1 ); editor.insertText('foo'); expect(editor.indentationForBufferRow(2)).toBe( editor.indentationForBufferRow(1) + 1 ); }); }); }); }); describe('atomic soft tabs', () => { it('skips tab-length runs of leading whitespace when moving the cursor', () => { editor.update({ tabLength: 4, atomicSoftTabs: true }); editor.setCursorScreenPosition([2, 3]); expect(editor.getCursorScreenPosition()).toEqual([2, 4]); editor.update({ atomicSoftTabs: false }); editor.setCursorScreenPosition([2, 3]); expect(editor.getCursorScreenPosition()).toEqual([2, 3]); editor.update({ atomicSoftTabs: true }); editor.setCursorScreenPosition([2, 3]); expect(editor.getCursorScreenPosition()).toEqual([2, 4]); }); }); describe('.destroy()', () => { it('destroys marker layers associated with the text editor', () => { buffer.retain(); const selectionsMarkerLayerId = editor.selectionsMarkerLayer.id; const foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id; editor.destroy(); expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined(); expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined(); buffer.release(); }); it('notifies ::onDidDestroy observers when the editor is destroyed', () => { let destroyObserverCalled = false; editor.onDidDestroy(() => (destroyObserverCalled = true)); editor.destroy(); expect(destroyObserverCalled).toBe(true); }); it('does not blow up when query methods are called afterward', () => { editor.destroy(); editor.getGrammar(); editor.getLastCursor(); editor.lineTextForBufferRow(0); }); it("emits the destroy event after destroying the editor's buffer", () => { const events = []; editor.getBuffer().onDidDestroy(() => { expect(editor.isDestroyed()).toBe(true); events.push('buffer-destroyed'); }); editor.onDidDestroy(() => { expect(buffer.isDestroyed()).toBe(true); events.push('editor-destroyed'); }); editor.destroy(); expect(events).toEqual(['buffer-destroyed', 'editor-destroyed']); }); }); describe('.joinLines()', () => { describe('when no text is selected', () => { describe("when the line below isn't empty", () => { it('joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up', () => { editor.setCursorBufferPosition([0, Infinity]); editor.insertText(' '); editor.setCursorBufferPosition([0]); editor.joinLines(); expect(editor.lineTextForBufferRow(0)).toBe( 'var quicksort = function () { var sort = function(items) {' ); expect(editor.getCursorBufferPosition()).toEqual([0, 29]); }); }); describe('when the line below is empty', () => { it('deletes the line below and moves the cursor to the end of the line', () => { editor.setCursorBufferPosition([9]); editor.joinLines(); expect(editor.lineTextForBufferRow(9)).toBe(' };'); expect(editor.lineTextForBufferRow(10)).toBe( ' return sort(Array.apply(this, arguments));' ); expect(editor.getCursorBufferPosition()).toEqual([9, 4]); }); }); describe('when the cursor is on the last row', () => { it('does nothing', () => { editor.setCursorBufferPosition([Infinity, Infinity]); editor.joinLines(); expect(editor.lineTextForBufferRow(12)).toBe('};'); }); }); describe('when the line is empty', () => { it('joins the line below with the current line with no added space', () => { editor.setCursorBufferPosition([10]); editor.joinLines(); expect(editor.lineTextForBufferRow(10)).toBe( 'return sort(Array.apply(this, arguments));' ); expect(editor.getCursorBufferPosition()).toEqual([10, 0]); }); }); }); describe('when text is selected', () => { describe('when the selection does not span multiple lines', () => { it('joins the line below with the current line separated by a space and retains the selected text', () => { editor.setSelectedBufferRange([[0, 1], [0, 3]]); editor.joinLines(); expect(editor.lineTextForBufferRow(0)).toBe( 'var quicksort = function () { var sort = function(items) {' ); expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 3]]); }); }); describe('when the selection spans multiple lines', () => { it('joins all selected lines separated by a space and retains the selected text', () => { editor.setSelectedBufferRange([[9, 3], [12, 1]]); editor.joinLines(); expect(editor.lineTextForBufferRow(9)).toBe( ' }; return sort(Array.apply(this, arguments)); };' ); expect(editor.getSelectedBufferRange()).toEqual([[9, 3], [9, 49]]); }); }); }); }); describe('.duplicateLines()', () => { it('for each selection, duplicates all buffer lines intersected by the selection', () => { editor.foldBufferRow(4); editor.setCursorBufferPosition([2, 5]); editor.addSelectionForBufferRange([[3, 0], [8, 0]], { preserveFolds: true }); editor.duplicateLines(); expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe( dedent` if (items.length <= 1) return items; if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = []; while(items.length > 0) { current = items.shift(); current < pivot ? left.push(current) : right.push(current); } var pivot = items.shift(), current, left = [], right = []; while(items.length > 0) { current = items.shift(); current < pivot ? left.push(current) : right.push(current); }\ ` .split('\n') .map(l => ` ${l}`) .join('\n') ); expect(editor.getSelectedBufferRanges()).toEqual([ [[3, 5], [3, 5]], [[9, 0], [14, 0]] ]); // folds are also duplicated expect(editor.isFoldedAtScreenRow(5)).toBe(true); expect(editor.isFoldedAtScreenRow(7)).toBe(true); expect(editor.lineTextForScreenRow(7)).toBe( ` while(items.length > 0) {${editor.displayLayer.foldCharacter}}` ); expect(editor.lineTextForScreenRow(8)).toBe( ' return sort(left).concat(pivot).concat(sort(right));' ); }); it('duplicates all folded lines for empty selections on lines containing folds', () => { editor.foldBufferRow(4); editor.setCursorBufferPosition([4, 0]); editor.duplicateLines(); expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe( dedent` if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = []; while(items.length > 0) { current = items.shift(); current < pivot ? left.push(current) : right.push(current); } while(items.length > 0) { current = items.shift(); current < pivot ? left.push(current) : right.push(current); } ` .split('\n') .map(l => ` ${l}`) .join('\n') ); expect(editor.getSelectedBufferRange()).toEqual([[8, 0], [8, 0]]); }); it('can duplicate the last line of the buffer', () => { editor.setSelectedBufferRange([[11, 0], [12, 2]]); editor.duplicateLines(); expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe( ' ' + dedent` return sort(Array.apply(this, arguments)); }; return sort(Array.apply(this, arguments)); }; `.trim() ); expect(editor.getSelectedBufferRange()).toEqual([[13, 0], [14, 2]]); }); it('only duplicates lines containing multiple selections once', () => { editor.setText(dedent` aaaaaa bbbbbb cccccc dddddd `); editor.setSelectedBufferRanges([ [[0, 1], [0, 2]], [[0, 3], [0, 4]], [[2, 1], [2, 2]], [[2, 3], [3, 1]], [[3, 3], [3, 4]] ]); editor.duplicateLines(); expect(editor.getText()).toBe(dedent` aaaaaa aaaaaa bbbbbb cccccc dddddd cccccc dddddd `); expect(editor.getSelectedBufferRanges()).toEqual([ [[1, 1], [1, 2]], [[1, 3], [1, 4]], [[5, 1], [5, 2]], [[5, 3], [6, 1]], [[6, 3], [6, 4]] ]); }); }); describe('when the editor contains surrogate pair characters', () => { it('correctly backspaces over them', () => { editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97'); editor.moveToBottom(); editor.backspace(); expect(editor.getText()).toBe('\uD835\uDF97\uD835\uDF97'); editor.backspace(); expect(editor.getText()).toBe('\uD835\uDF97'); editor.backspace(); expect(editor.getText()).toBe(''); }); it('correctly deletes over them', () => { editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97'); editor.moveToTop(); editor.delete(); expect(editor.getText()).toBe('\uD835\uDF97\uD835\uDF97'); editor.delete(); expect(editor.getText()).toBe('\uD835\uDF97'); editor.delete(); expect(editor.getText()).toBe(''); }); it('correctly moves over them', () => { editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n'); editor.moveToTop(); editor.moveRight(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); editor.moveRight(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.moveRight(); expect(editor.getCursorBufferPosition()).toEqual([0, 6]); editor.moveRight(); expect(editor.getCursorBufferPosition()).toEqual([1, 0]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([0, 6]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); }); describe('when the editor contains variation sequence character pairs', () => { it('correctly backspaces over them', () => { editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E'); editor.moveToBottom(); editor.backspace(); expect(editor.getText()).toBe('\u2714\uFE0E\u2714\uFE0E'); editor.backspace(); expect(editor.getText()).toBe('\u2714\uFE0E'); editor.backspace(); expect(editor.getText()).toBe(''); }); it('correctly deletes over them', () => { editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E'); editor.moveToTop(); editor.delete(); expect(editor.getText()).toBe('\u2714\uFE0E\u2714\uFE0E'); editor.delete(); expect(editor.getText()).toBe('\u2714\uFE0E'); editor.delete(); expect(editor.getText()).toBe(''); }); it('correctly moves over them', () => { editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E\n'); editor.moveToTop(); editor.moveRight(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); editor.moveRight(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.moveRight(); expect(editor.getCursorBufferPosition()).toEqual([0, 6]); editor.moveRight(); expect(editor.getCursorBufferPosition()).toEqual([1, 0]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([0, 6]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([0, 4]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([0, 2]); editor.moveLeft(); expect(editor.getCursorBufferPosition()).toEqual([0, 0]); }); }); describe('.setIndentationForBufferRow', () => { describe('when the editor uses soft tabs but the row has hard tabs', () => { it('only replaces whitespace characters', () => { editor.setSoftWrapped(true); editor.setText('\t1\n\t2'); editor.setCursorBufferPosition([0, 0]); editor.setIndentationForBufferRow(0, 2); expect(editor.getText()).toBe(' 1\n\t2'); }); }); describe('when the indentation level is a non-integer', () => { it('does not throw an exception', () => { editor.setSoftWrapped(true); editor.setText('\t1\n\t2'); editor.setCursorBufferPosition([0, 0]); editor.setIndentationForBufferRow(0, 2.1); expect(editor.getText()).toBe(' 1\n\t2'); }); }); }); describe("when the editor's grammar has an injection selector", () => { beforeEach(async () => { atom.config.set('core.useTreeSitterParsers', false); await atom.packages.activatePackage('language-text'); await atom.packages.activatePackage('language-javascript'); }); it("includes the grammar's patterns when the selector matches the current scope in other grammars", async () => { await atom.packages.activatePackage('language-hyperlink'); const grammar = atom.grammars.selectGrammar('text.js'); const { line, tags } = grammar.tokenizeLine( 'var i; // http://github.com' ); const tokens = atom.grammars.decodeTokens(line, tags); expect(tokens[0].value).toBe('var'); expect(tokens[0].scopes).toEqual(['source.js', 'storage.type.var.js']); expect(tokens[6].value).toBe('http://github.com'); expect(tokens[6].scopes).toEqual([ 'source.js', 'comment.line.double-slash.js', 'markup.underline.link.http.hyperlink' ]); }); describe('when the grammar is added', () => { it('retokenizes existing buffers that contain tokens that match the injection selector', async () => { editor = await atom.workspace.open('sample.js'); editor.setText('// http://github.com'); let tokens = editor.tokensForScreenRow(0); expect(tokens).toEqual([ { text: '//', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js' ] }, { text: ' http://github.com', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js' ] } ]); await atom.packages.activatePackage('language-hyperlink'); tokens = editor.tokensForScreenRow(0); expect(tokens).toEqual([ { text: '//', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js' ] }, { text: ' ', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js' ] }, { text: 'http://github.com', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink' ] } ]); }); describe('when the grammar is updated', () => { it('retokenizes existing buffers that contain tokens that match the injection selector', async () => { editor = await atom.workspace.open('sample.js'); editor.setText('// SELECT * FROM OCTOCATS'); let tokens = editor.tokensForScreenRow(0); expect(tokens).toEqual([ { text: '//', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js' ] }, { text: ' SELECT * FROM OCTOCATS', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js' ] } ]); await atom.packages.activatePackage( 'package-with-injection-selector' ); tokens = editor.tokensForScreenRow(0); expect(tokens).toEqual([ { text: '//', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js' ] }, { text: ' SELECT * FROM OCTOCATS', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js' ] } ]); await atom.packages.activatePackage('language-sql'); tokens = editor.tokensForScreenRow(0); expect(tokens).toEqual([ { text: '//', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js' ] }, { text: ' ', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js' ] }, { text: 'SELECT', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql' ] }, { text: ' ', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js' ] }, { text: '*', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql' ] }, { text: ' ', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js' ] }, { text: 'FROM', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql' ] }, { text: ' OCTOCATS', scopes: [ 'syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js' ] } ]); }); }); }); }); describe('.normalizeTabsInBufferRange()', () => { it("normalizes tabs depending on the editor's soft tab/tab length settings", () => { editor.setTabLength(1); editor.setSoftTabs(true); editor.setText('\t\t\t'); editor.normalizeTabsInBufferRange([[0, 0], [0, 1]]); expect(editor.getText()).toBe(' \t\t'); editor.setTabLength(2); editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]); expect(editor.getText()).toBe(' '); editor.setSoftTabs(false); editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]); expect(editor.getText()).toBe(' '); }); }); describe('.pageUp/Down()', () => { it('moves the cursor down one page length', () => { editor.update({ autoHeight: false }); const element = editor.getElement(); jasmine.attachToDOM(element); element.style.height = element.component.getLineHeight() * 5 + 'px'; element.measureDimensions(); expect(editor.getCursorBufferPosition().row).toBe(0); editor.pageDown(); expect(editor.getCursorBufferPosition().row).toBe(5); editor.pageDown(); expect(editor.getCursorBufferPosition().row).toBe(10); editor.pageUp(); expect(editor.getCursorBufferPosition().row).toBe(5); editor.pageUp(); expect(editor.getCursorBufferPosition().row).toBe(0); }); }); describe('.selectPageUp/Down()', () => { it('selects one screen height of text up or down', () => { editor.update({ autoHeight: false }); const element = editor.getElement(); jasmine.attachToDOM(element); element.style.height = element.component.getLineHeight() * 5 + 'px'; element.measureDimensions(); expect(editor.getCursorBufferPosition().row).toBe(0); editor.selectPageDown(); expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [5, 0]]]); editor.selectPageDown(); expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [10, 0]]]); editor.selectPageDown(); expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [12, 2]]]); editor.moveToBottom(); editor.selectPageUp(); expect(editor.getSelectedBufferRanges()).toEqual([[[7, 0], [12, 2]]]); editor.selectPageUp(); expect(editor.getSelectedBufferRanges()).toEqual([[[2, 0], [12, 2]]]); editor.selectPageUp(); expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [12, 2]]]); }); }); describe('::scrollToScreenPosition(position, [options])', () => { it('triggers ::onDidRequestAutoscroll with the logical coordinates along with the options', () => { const scrollSpy = jasmine.createSpy('::onDidRequestAutoscroll'); editor.onDidRequestAutoscroll(scrollSpy); editor.scrollToScreenPosition([8, 20]); editor.scrollToScreenPosition([8, 20], { center: true }); editor.scrollToScreenPosition([8, 20], { center: false, reversed: true }); expect(scrollSpy).toHaveBeenCalledWith({ screenRange: [[8, 20], [8, 20]], options: {} }); expect(scrollSpy).toHaveBeenCalledWith({ screenRange: [[8, 20], [8, 20]], options: { center: true } }); expect(scrollSpy).toHaveBeenCalledWith({ screenRange: [[8, 20], [8, 20]], options: { center: false, reversed: true } }); }); }); describe('scroll past end', () => { it('returns false by default but can be customized', () => { expect(editor.getScrollPastEnd()).toBe(false); editor.update({ scrollPastEnd: true }); expect(editor.getScrollPastEnd()).toBe(true); editor.update({ scrollPastEnd: false }); expect(editor.getScrollPastEnd()).toBe(false); }); it('always returns false when autoHeight is on', () => { editor.update({ autoHeight: true, scrollPastEnd: true }); expect(editor.getScrollPastEnd()).toBe(false); editor.update({ autoHeight: false }); expect(editor.getScrollPastEnd()).toBe(true); }); }); describe('auto height', () => { it('returns true by default but can be customized', () => { editor = new TextEditor(); expect(editor.getAutoHeight()).toBe(true); editor.update({ autoHeight: false }); expect(editor.getAutoHeight()).toBe(false); editor.update({ autoHeight: true }); expect(editor.getAutoHeight()).toBe(true); editor.destroy(); }); }); describe('auto width', () => { it('returns false by default but can be customized', () => { expect(editor.getAutoWidth()).toBe(false); editor.update({ autoWidth: true }); expect(editor.getAutoWidth()).toBe(true); editor.update({ autoWidth: false }); expect(editor.getAutoWidth()).toBe(false); }); }); describe('.get/setPlaceholderText()', () => { it('can be created with placeholderText', () => { const newEditor = new TextEditor({ mini: true, placeholderText: 'yep' }); expect(newEditor.getPlaceholderText()).toBe('yep'); }); it('models placeholderText and emits an event when changed', () => { let handler; editor.onDidChangePlaceholderText((handler = jasmine.createSpy())); expect(editor.getPlaceholderText()).toBeUndefined(); editor.setPlaceholderText('OK'); expect(handler).toHaveBeenCalledWith('OK'); expect(editor.getPlaceholderText()).toBe('OK'); }); }); describe('gutters', () => { describe('the TextEditor constructor', () => { it('creates a line-number gutter', () => { expect(editor.getGutters().length).toBe(1); const lineNumberGutter = editor.gutterWithName('line-number'); expect(lineNumberGutter.name).toBe('line-number'); expect(lineNumberGutter.priority).toBe(0); }); }); describe('::addGutter', () => { it('can add a gutter', () => { expect(editor.getGutters().length).toBe(1); // line-number gutter const options = { name: 'test-gutter', priority: 1 }; const gutter = editor.addGutter(options); expect(editor.getGutters().length).toBe(2); expect(editor.getGutters()[1]).toBe(gutter); expect(gutter.type).toBe('decorated'); }); it('can add a custom line-number gutter', () => { expect(editor.getGutters().length).toBe(1); const options = { name: 'another-gutter', priority: 2, type: 'line-number' }; const gutter = editor.addGutter(options); expect(editor.getGutters().length).toBe(2); expect(editor.getGutters()[1]).toBe(gutter); expect(gutter.type).toBe('line-number'); }); it("does not allow a custom gutter with the 'line-number' name.", () => expect( editor.addGutter.bind(editor, { name: 'line-number' }) ).toThrow()); }); describe('::decorateMarker', () => { let marker; beforeEach(() => (marker = editor.markBufferRange([[1, 0], [1, 0]]))); it('reflects an added decoration when one of its custom gutters is decorated.', () => { const gutter = editor.addGutter({ name: 'custom-gutter' }); const decoration = gutter.decorateMarker(marker, { class: 'custom-class' }); const gutterDecorations = editor.getDecorations({ type: 'gutter', gutterName: 'custom-gutter', class: 'custom-class' }); expect(gutterDecorations.length).toBe(1); expect(gutterDecorations[0]).toBe(decoration); }); it('reflects an added decoration when its line-number gutter is decorated.', () => { const decoration = editor .gutterWithName('line-number') .decorateMarker(marker, { class: 'test-class' }); const gutterDecorations = editor.getDecorations({ type: 'line-number', gutterName: 'line-number', class: 'test-class' }); expect(gutterDecorations.length).toBe(1); expect(gutterDecorations[0]).toBe(decoration); }); }); describe('::observeGutters', () => { let payloads, callback; beforeEach(() => { payloads = []; callback = payload => payloads.push(payload); }); it('calls the callback immediately with each existing gutter, and with each added gutter after that.', () => { const lineNumberGutter = editor.gutterWithName('line-number'); editor.observeGutters(callback); expect(payloads).toEqual([lineNumberGutter]); const gutter1 = editor.addGutter({ name: 'test-gutter-1' }); expect(payloads).toEqual([lineNumberGutter, gutter1]); const gutter2 = editor.addGutter({ name: 'test-gutter-2' }); expect(payloads).toEqual([lineNumberGutter, gutter1, gutter2]); }); it('does not call the callback when a gutter is removed.', () => { const gutter = editor.addGutter({ name: 'test-gutter' }); editor.observeGutters(callback); payloads = []; gutter.destroy(); expect(payloads).toEqual([]); }); it('does not call the callback after the subscription has been disposed.', () => { const subscription = editor.observeGutters(callback); payloads = []; subscription.dispose(); editor.addGutter({ name: 'test-gutter' }); expect(payloads).toEqual([]); }); }); describe('::onDidAddGutter', () => { let payloads, callback; beforeEach(() => { payloads = []; callback = payload => payloads.push(payload); }); it('calls the callback with each newly-added gutter, but not with existing gutters.', () => { editor.onDidAddGutter(callback); expect(payloads).toEqual([]); const gutter = editor.addGutter({ name: 'test-gutter' }); expect(payloads).toEqual([gutter]); }); it('does not call the callback after the subscription has been disposed.', () => { const subscription = editor.onDidAddGutter(callback); payloads = []; subscription.dispose(); editor.addGutter({ name: 'test-gutter' }); expect(payloads).toEqual([]); }); }); describe('::onDidRemoveGutter', () => { let payloads, callback; beforeEach(() => { payloads = []; callback = payload => payloads.push(payload); }); it('calls the callback when a gutter is removed.', () => { const gutter = editor.addGutter({ name: 'test-gutter' }); editor.onDidRemoveGutter(callback); expect(payloads).toEqual([]); gutter.destroy(); expect(payloads).toEqual(['test-gutter']); }); it('does not call the callback after the subscription has been disposed.', () => { const gutter = editor.addGutter({ name: 'test-gutter' }); const subscription = editor.onDidRemoveGutter(callback); subscription.dispose(); gutter.destroy(); expect(payloads).toEqual([]); }); }); }); describe('decorations', () => { describe('::decorateMarker', () => { it('includes the decoration in the object returned from ::decorationsStateForScreenRowRange', () => { const marker = editor.markBufferRange([[2, 4], [6, 8]]); const decoration = editor.decorateMarker(marker, { type: 'highlight', class: 'foo' }); expect( editor.decorationsStateForScreenRowRange(0, 5)[decoration.id] ).toEqual({ properties: { id: decoration.id, order: Infinity, type: 'highlight', class: 'foo' }, screenRange: marker.getScreenRange(), bufferRange: marker.getBufferRange(), rangeIsReversed: false }); }); it("does not throw errors after the marker's containing layer is destroyed", () => { const layer = editor.addMarkerLayer(); layer.markBufferRange([[2, 4], [6, 8]]); layer.destroy(); editor.decorationsStateForScreenRowRange(0, 5); }); }); describe('::decorateMarkerLayer', () => { it('based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange', () => { const layer1 = editor.getBuffer().addMarkerLayer(); const marker1 = layer1.markRange([[2, 4], [6, 8]]); const marker2 = layer1.markRange([[11, 0], [11, 12]]); const layer2 = editor.getBuffer().addMarkerLayer(); const marker3 = layer2.markRange([[8, 0], [9, 0]]); const layer1Decoration1 = editor.decorateMarkerLayer(layer1, { type: 'highlight', class: 'foo' }); const layer1Decoration2 = editor.decorateMarkerLayer(layer1, { type: 'highlight', class: 'bar' }); const layer2Decoration = editor.decorateMarkerLayer(layer2, { type: 'highlight', class: 'baz' }); let decorationState = editor.decorationsStateForScreenRowRange(0, 13); expect( decorationState[`${layer1Decoration1.id}-${marker1.id}`] ).toEqual({ properties: { type: 'highlight', class: 'foo' }, screenRange: marker1.getRange(), bufferRange: marker1.getRange(), rangeIsReversed: false }); expect( decorationState[`${layer1Decoration1.id}-${marker2.id}`] ).toEqual({ properties: { type: 'highlight', class: 'foo' }, screenRange: marker2.getRange(), bufferRange: marker2.getRange(), rangeIsReversed: false }); expect( decorationState[`${layer1Decoration2.id}-${marker1.id}`] ).toEqual({ properties: { type: 'highlight', class: 'bar' }, screenRange: marker1.getRange(), bufferRange: marker1.getRange(), rangeIsReversed: false }); expect( decorationState[`${layer1Decoration2.id}-${marker2.id}`] ).toEqual({ properties: { type: 'highlight', class: 'bar' }, screenRange: marker2.getRange(), bufferRange: marker2.getRange(), rangeIsReversed: false }); expect(decorationState[`${layer2Decoration.id}-${marker3.id}`]).toEqual( { properties: { type: 'highlight', class: 'baz' }, screenRange: marker3.getRange(), bufferRange: marker3.getRange(), rangeIsReversed: false } ); layer1Decoration1.destroy(); decorationState = editor.decorationsStateForScreenRowRange(0, 12); expect( decorationState[`${layer1Decoration1.id}-${marker1.id}`] ).toBeUndefined(); expect( decorationState[`${layer1Decoration1.id}-${marker2.id}`] ).toBeUndefined(); expect( decorationState[`${layer1Decoration2.id}-${marker1.id}`] ).toEqual({ properties: { type: 'highlight', class: 'bar' }, screenRange: marker1.getRange(), bufferRange: marker1.getRange(), rangeIsReversed: false }); expect( decorationState[`${layer1Decoration2.id}-${marker2.id}`] ).toEqual({ properties: { type: 'highlight', class: 'bar' }, screenRange: marker2.getRange(), bufferRange: marker2.getRange(), rangeIsReversed: false }); expect(decorationState[`${layer2Decoration.id}-${marker3.id}`]).toEqual( { properties: { type: 'highlight', class: 'baz' }, screenRange: marker3.getRange(), bufferRange: marker3.getRange(), rangeIsReversed: false } ); layer1Decoration2.setPropertiesForMarker(marker1, { type: 'highlight', class: 'quux' }); decorationState = editor.decorationsStateForScreenRowRange(0, 12); expect( decorationState[`${layer1Decoration2.id}-${marker1.id}`] ).toEqual({ properties: { type: 'highlight', class: 'quux' }, screenRange: marker1.getRange(), bufferRange: marker1.getRange(), rangeIsReversed: false }); layer1Decoration2.setPropertiesForMarker(marker1, null); decorationState = editor.decorationsStateForScreenRowRange(0, 12); expect( decorationState[`${layer1Decoration2.id}-${marker1.id}`] ).toEqual({ properties: { type: 'highlight', class: 'bar' }, screenRange: marker1.getRange(), bufferRange: marker1.getRange(), rangeIsReversed: false }); }); }); }); describe('invisibles', () => { beforeEach(() => { editor.update({ showInvisibles: true }); }); it('substitutes invisible characters according to the given rules', () => { const previousLineText = editor.lineTextForScreenRow(0); editor.update({ invisibles: { eol: '?' } }); expect(editor.lineTextForScreenRow(0)).not.toBe(previousLineText); expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true); expect(editor.getInvisibles()).toEqual({ eol: '?' }); }); it('does not use invisibles if showInvisibles is set to false', () => { editor.update({ invisibles: { eol: '?' } }); expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true); editor.update({ showInvisibles: false }); expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(false); }); }); describe('indent guides', () => { it('shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini', () => { editor.update({ showIndentGuide: false }); expect(editor.tokensForScreenRow(1).slice(0, 3)).toEqual([ { text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace'] }, { text: 'var', scopes: ['syntax--source syntax--js', 'syntax--storage syntax--type'] }, { text: ' sort ', scopes: ['syntax--source syntax--js'] } ]); editor.update({ showIndentGuide: true }); expect(editor.tokensForScreenRow(1).slice(0, 3)).toEqual([ { text: ' ', scopes: [ 'syntax--source syntax--js', 'leading-whitespace indent-guide' ] }, { text: 'var', scopes: ['syntax--source syntax--js', 'syntax--storage syntax--type'] }, { text: ' sort ', scopes: ['syntax--source syntax--js'] } ]); editor.setMini(true); expect(editor.tokensForScreenRow(1).slice(0, 3)).toEqual([ { text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace'] }, { text: 'var', scopes: ['syntax--source syntax--js', 'syntax--storage syntax--type'] }, { text: ' sort ', scopes: ['syntax--source syntax--js'] } ]); }); }); describe('softWrapAtPreferredLineLength', () => { it('soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini', () => { editor.update({ editorWidthInChars: 30, softWrapped: true, softWrapAtPreferredLineLength: true, preferredLineLength: 20 }); expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = '); editor.update({ editorWidthInChars: 10 }); expect(editor.lineTextForScreenRow(0)).toBe('var '); editor.update({ mini: true }); expect(editor.lineTextForScreenRow(0)).toBe( 'var quicksort = function () {' ); }); }); describe('softWrapHangingIndentLength', () => { it('controls how much extra indentation is applied to soft-wrapped lines', () => { editor.setText('123456789'); editor.update({ editorWidthInChars: 8, softWrapped: true, softWrapHangingIndentLength: 2 }); expect(editor.lineTextForScreenRow(1)).toEqual(' 9'); editor.update({ softWrapHangingIndentLength: 4 }); expect(editor.lineTextForScreenRow(1)).toEqual(' 9'); }); }); describe('::getElement', () => { it('returns an element', () => expect(editor.getElement() instanceof HTMLElement).toBe(true)); }); describe('setMaxScreenLineLength', () => { it('sets the maximum line length in the editor before soft wrapping is forced', () => { expect(editor.getSoftWrapColumn()).toBe(500); editor.update({ maxScreenLineLength: 1500 }); expect(editor.getSoftWrapColumn()).toBe(1500); }); }); }); describe('TextEditor', () => { let editor; afterEach(() => { editor.destroy(); }); describe('.scopeDescriptorForBufferPosition(position)', () => { it('returns a default scope descriptor when no language mode is assigned', () => { editor = new TextEditor({ buffer: new TextBuffer() }); const scopeDescriptor = editor.scopeDescriptorForBufferPosition([0, 0]); expect(scopeDescriptor.getScopesArray()).toEqual(['text']); }); }); describe('.syntaxTreeScopeDescriptorForBufferPosition(position)', () => { it('returns the result of scopeDescriptorForBufferPosition() when textmate language mode is used', async () => { atom.config.set('core.useTreeSitterParsers', false); editor = await atom.workspace.open('sample.js', { autoIndent: false }); await atom.packages.activatePackage('language-javascript'); let buffer = editor.getBuffer(); let languageMode = new TextMateLanguageMode({ buffer, grammar: atom.grammars.grammarForScopeName('source.js') }); buffer.setLanguageMode(languageMode); languageMode.startTokenizing(); while (languageMode.firstInvalidRow() != null) { advanceClock(); } const syntaxTreeeScopeDescriptor = editor.syntaxTreeScopeDescriptorForBufferPosition( [4, 17] ); expect(syntaxTreeeScopeDescriptor.getScopesArray()).toEqual([ 'source.js', 'support.variable.property.js' ]); }); it('returns the result of syntaxTreeScopeDescriptorForBufferPosition() when tree-sitter language mode is used', async () => { editor = await atom.workspace.open('sample.js', { autoIndent: false }); await atom.packages.activatePackage('language-javascript'); let buffer = editor.getBuffer(); buffer.setLanguageMode( new TreeSitterLanguageMode({ buffer, grammar: atom.grammars.grammarForScopeName('source.js') }) ); const syntaxTreeeScopeDescriptor = editor.syntaxTreeScopeDescriptorForBufferPosition( [4, 17] ); expect(syntaxTreeeScopeDescriptor.getScopesArray()).toEqual([ 'source.js', 'program', 'variable_declaration', 'variable_declarator', 'function', 'statement_block', 'variable_declaration', 'variable_declarator', 'function', 'statement_block', 'while_statement', 'parenthesized_expression', 'binary_expression', 'member_expression', 'property_identifier' ]); }); }); describe('.shouldPromptToSave()', () => { beforeEach(async () => { editor = await atom.workspace.open('sample.js'); jasmine.unspy(editor, 'shouldPromptToSave'); spyOn(atom.stateStore, 'isConnected').andReturn(true); }); 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', () => { atom.grammars.assignLanguageMode(editor, null); 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'); }); it('does not select the new delimiters', () => { editor.setText(''); let delimLength = '' }, injectionRegExp: 'html', injectionPoints: [SCRIPT_TAG_INJECTION_POINT] } ); atom.grammars.addGrammar(jsGrammar); atom.grammars.addGrammar(htmlGrammar); const languageMode = new TreeSitterLanguageMode({ buffer, grammar: htmlGrammar, grammars: atom.grammars }); buffer.setLanguageMode(languageMode); buffer.setText( `
          hi
          `.trim() ); const htmlCommentStrings = { commentStartString: '' }; const jsCommentStrings = { commentStartString: '//', commentEndString: undefined }; expect(languageMode.commentStringsForPosition(new Point(0, 0))).toEqual( htmlCommentStrings ); expect(languageMode.commentStringsForPosition(new Point(1, 0))).toEqual( htmlCommentStrings ); expect(languageMode.commentStringsForPosition(new Point(2, 0))).toEqual( jsCommentStrings ); expect(languageMode.commentStringsForPosition(new Point(3, 0))).toEqual( jsCommentStrings ); expect(languageMode.commentStringsForPosition(new Point(4, 0))).toEqual( htmlCommentStrings ); expect(languageMode.commentStringsForPosition(new Point(5, 0))).toEqual( jsCommentStrings ); expect(languageMode.commentStringsForPosition(new Point(6, 0))).toEqual( htmlCommentStrings ); }); }); describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => { it('expands and contracts the selection based on the syntax tree', async () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', scopes: { program: 'source' } }); buffer.setText(dedent` function a (b, c, d) { eee.f() g() } `); buffer.setLanguageMode(new TreeSitterLanguageMode({ buffer, grammar })); editor.setCursorBufferPosition([1, 3]); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('eee'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('eee.f'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('eee.f()'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('{\n eee.f()\n g()\n}'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe( 'function a (b, c, d) {\n eee.f()\n g()\n}' ); editor.selectSmallerSyntaxNode(); expect(editor.getSelectedText()).toBe('{\n eee.f()\n g()\n}'); editor.selectSmallerSyntaxNode(); expect(editor.getSelectedText()).toBe('eee.f()'); editor.selectSmallerSyntaxNode(); expect(editor.getSelectedText()).toBe('eee.f'); editor.selectSmallerSyntaxNode(); expect(editor.getSelectedText()).toBe('eee'); editor.selectSmallerSyntaxNode(); expect(editor.getSelectedBufferRange()).toEqual([[1, 3], [1, 3]]); }); it('handles injected languages', async () => { const jsGrammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { scopeName: 'javascript', parser: 'tree-sitter-javascript', scopes: { property_identifier: 'property', 'call_expression > identifier': 'function', template_string: 'string', 'template_substitution > "${"': 'interpolation', 'template_substitution > "}"': 'interpolation' }, injectionRegExp: 'javascript', injectionPoints: [HTML_TEMPLATE_LITERAL_INJECTION_POINT] }); const htmlGrammar = new TreeSitterGrammar( atom.grammars, htmlGrammarPath, { scopeName: 'html', parser: 'tree-sitter-html', scopes: { fragment: 'html', tag_name: 'tag', attribute_name: 'attr' }, injectionRegExp: 'html' } ); atom.grammars.addGrammar(htmlGrammar); buffer.setText('a = html ` c${def()}e${f}g `'); const languageMode = new TreeSitterLanguageMode({ buffer, grammar: jsGrammar, grammars: atom.grammars }); buffer.setLanguageMode(languageMode); editor.setCursorBufferPosition({ row: 0, column: buffer.getText().indexOf('ef()') }); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('def'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('def()'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('${def()}'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('c${def()}e${f}g'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('c${def()}e${f}g'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe(' c${def()}e${f}g '); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('` c${def()}e${f}g `'); editor.selectLargerSyntaxNode(); expect(editor.getSelectedText()).toBe('html ` c${def()}e${f}g `'); }); }); describe('.tokenizedLineForRow(row)', () => { it('returns a shimmed TokenizedLine with tokens', () => { const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, { parser: 'tree-sitter-javascript', scopes: { program: 'source', 'call_expression > identifier': 'function', property_identifier: 'property', 'call_expression > member_expression > property_identifier': 'method', identifier: 'variable' } }); buffer.setText('aa.bbb = cc(d.eee());\n\n \n b'); const languageMode = new TreeSitterLanguageMode({ buffer, grammar }); buffer.setLanguageMode(languageMode); expect(languageMode.tokenizedLineForRow(0).tokens).toEqual([ { value: 'aa', scopes: ['source', 'variable'] }, { value: '.', scopes: ['source'] }, { value: 'bbb', scopes: ['source', 'property'] }, { value: ' = ', scopes: ['source'] }, { value: 'cc', scopes: ['source', 'function'] }, { value: '(', scopes: ['source'] }, { value: 'd', scopes: ['source', 'variable'] }, { value: '.', scopes: ['source'] }, { value: 'eee', scopes: ['source', 'method'] }, { value: '());', scopes: ['source'] } ]); expect(languageMode.tokenizedLineForRow(1).tokens).toEqual([]); expect(languageMode.tokenizedLineForRow(2).tokens).toEqual([ { value: ' ', scopes: ['source'] } ]); expect(languageMode.tokenizedLineForRow(3).tokens).toEqual([ { value: ' ', scopes: ['source'] }, { value: 'b', scopes: ['source', 'variable'] } ]); }); }); }); function nextHighlightingUpdate(languageMode) { return new Promise(resolve => { const subscription = languageMode.onDidChangeHighlighting(() => { subscription.dispose(); resolve(); }); }); } function getDisplayText(editor) { return editor.displayLayer.getText(); } function expectTokensToEqual(editor, expectedTokenLines) { const lastRow = editor.getLastScreenRow(); // Assert that the correct tokens are returned regardless of which row // the highlighting iterator starts on. for (let startRow = 0; startRow <= lastRow; startRow++) { // Clear the screen line cache between iterations, but not on the first // iteration, so that the first iteration tests that the cache has been // correctly invalidated by any changes. if (startRow > 0) { editor.displayLayer.clearSpatialIndex(); } editor.displayLayer.getScreenLines(startRow, Infinity); const tokenLines = []; for (let row = startRow; row <= lastRow; row++) { tokenLines[row] = editor .tokensForScreenRow(row) .map(({ text, scopes }) => ({ text, scopes: scopes.map(scope => scope .split(' ') .map(className => className.replace('syntax--', '')) .join(' ') ) })); } for (let row = startRow; row <= lastRow; row++) { const tokenLine = tokenLines[row]; const expectedTokenLine = expectedTokenLines[row]; expect(tokenLine.length).toEqual(expectedTokenLine.length); for (let i = 0; i < tokenLine.length; i++) { expect(tokenLine[i]).toEqual( expectedTokenLine[i], `Token ${i}, startRow: ${startRow}` ); } } } // Fully populate the screen line cache again so that cache invalidation // due to subsequent edits can be tested. editor.displayLayer.getScreenLines(0, Infinity); } const HTML_TEMPLATE_LITERAL_INJECTION_POINT = { type: 'call_expression', language(node) { if ( node.lastChild.type === 'template_string' && node.firstChild.type === 'identifier' ) { return node.firstChild.text; } }, content(node) { return node.lastChild; } }; const SCRIPT_TAG_INJECTION_POINT = { type: 'script_element', language() { return 'javascript'; }, content(node) { return node.child(1); } }; const JSDOC_INJECTION_POINT = { type: 'comment', language(comment) { if (comment.text.startsWith('/**')) return 'jsdoc'; }, content(comment) { return comment; } }; ================================================ FILE: spec/typescript-spec.js ================================================ describe('TypeScript transpiler support', function() { describe('when there is a .ts file', () => it('transpiles it using typescript', function() { const transpiled = require('./fixtures/typescript/valid.ts'); expect(transpiled(3)).toBe(4); })); describe('when the .ts file is invalid', () => it('does not transpile', () => expect(() => require('./fixtures/typescript/invalid.ts')).toThrow())); }); ================================================ FILE: spec/update-process-env-spec.js ================================================ /** @babel */ import path from 'path'; import childProcess from 'child_process'; import { updateProcessEnv, shouldGetEnvFromShell } from '../src/update-process-env'; import mockSpawn from 'mock-spawn'; const temp = require('temp').track(); describe('updateProcessEnv(launchEnv)', function() { let originalProcessEnv, originalProcessPlatform, originalSpawn, spawn; beforeEach(function() { originalSpawn = childProcess.spawn; spawn = mockSpawn(); childProcess.spawn = spawn; originalProcessEnv = process.env; originalProcessPlatform = process.platform; process.env = {}; }); afterEach(function() { if (originalSpawn) { childProcess.spawn = originalSpawn; } process.env = originalProcessEnv; process.platform = originalProcessPlatform; try { temp.cleanupSync(); } catch (e) { // Do nothing } }); describe('when the launch environment appears to come from a shell', function() { it('updates process.env to match the launch environment because PWD is set', async function() { process.env = { WILL_BE_DELETED: 'hi', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }; const initialProcessEnv = process.env; await updateProcessEnv({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', TERM: 'xterm-something', KEY1: 'value1', KEY2: 'value2' }); expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', TERM: 'xterm-something', KEY1: 'value1', KEY2: 'value2', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); // See #11302. On Windows, `process.env` is a magic object that offers // case-insensitive environment variable matching, so we cannot replace it // with another object. expect(process.env).toBe(initialProcessEnv); }); it('updates process.env to match the launch environment because PROMPT is set', async function() { process.env = { WILL_BE_DELETED: 'hi', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }; const initialProcessEnv = process.env; await updateProcessEnv({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PROMPT: '$P$G', KEY1: 'value1', KEY2: 'value2' }); expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PROMPT: '$P$G', KEY1: 'value1', KEY2: 'value2', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); // See #11302. On Windows, `process.env` is a magic object that offers // case-insensitive environment variable matching, so we cannot replace it // with another object. expect(process.env).toBe(initialProcessEnv); }); it('updates process.env to match the launch environment because PSModulePath is set', async function() { process.env = { WILL_BE_DELETED: 'hi', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }; const initialProcessEnv = process.env; await updateProcessEnv({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PSModulePath: 'C:\\Program Files\\WindowsPowerShell\\Modules;C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules\\', KEY1: 'value1', KEY2: 'value2' }); expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PSModulePath: 'C:\\Program Files\\WindowsPowerShell\\Modules;C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules\\', KEY1: 'value1', KEY2: 'value2', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); // See #11302. On Windows, `process.env` is a magic object that offers // case-insensitive environment variable matching, so we cannot replace it // with another object. expect(process.env).toBe(initialProcessEnv); }); it('allows ATOM_HOME to be overwritten only if the new value is a valid path', async function() { let newAtomHomePath = temp.mkdirSync('atom-home'); process.env = { WILL_BE_DELETED: 'hi', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }; await updateProcessEnv({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir' }); expect(process.env).toEqual({ PWD: '/the/dir', ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); await updateProcessEnv({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', ATOM_HOME: path.join(newAtomHomePath, 'non-existent') }); expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); await updateProcessEnv({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', ATOM_HOME: newAtomHomePath }); expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: newAtomHomePath }); }); it('allows ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT to be preserved if set', async function() { process.env = { WILL_BE_DELETED: 'hi', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }; await updateProcessEnv({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); await updateProcessEnv({ PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); expect(process.env).toEqual({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', PWD: '/the/dir', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }); }); it('allows an existing env variable to be updated', async function() { process.env = { WILL_BE_UPDATED: 'old-value', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home' }; await updateProcessEnv(process.env); expect(process.env).toEqual(process.env); let updatedEnv = { ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', WILL_BE_UPDATED: 'new-value', NODE_ENV: 'the-node-env', NODE_PATH: '/the/node/path', ATOM_HOME: '/the/atom/home', PWD: '/the/dir' }; await updateProcessEnv(updatedEnv); expect(process.env).toEqual(updatedEnv); }); }); describe('when the launch environment does not come from a shell', function() { describe('on macOS', function() { it("updates process.env to match the environment in the user's login shell", async function() { if (process.platform === 'win32') return; // TestsThatFailOnWin32 process.platform = 'darwin'; process.env.SHELL = '/my/custom/bash'; spawn.setDefault( spawn.simple( 0, 'FOO=BAR=BAZ=QUUX\0MULTILINE\nNAME=multiline\nvalue\0TERM=xterm-something\0PATH=/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path' ) ); await updateProcessEnv(process.env); expect(spawn.calls.length).toBe(1); expect(spawn.calls[0].command).toBe('/my/custom/bash'); expect(spawn.calls[0].args).toEqual([ '-ilc', 'command awk \'BEGIN{for(v in ENVIRON) printf("%s=%s%c", v, ENVIRON[v], 0)}\'' ]); expect(process.env).toEqual({ FOO: 'BAR=BAZ=QUUX', 'MULTILINE\nNAME': 'multiline\nvalue', TERM: 'xterm-something', PATH: '/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path' }); // Doesn't error await updateProcessEnv(null); }); }); describe('on linux', function() { it("updates process.env to match the environment in the user's login shell", async function() { if (process.platform === 'win32') return; // TestsThatFailOnWin32 process.platform = 'linux'; process.env.SHELL = '/my/custom/bash'; spawn.setDefault( spawn.simple( 0, 'FOO=BAR=BAZ=QUUX\0MULTILINE\nNAME=multiline\nvalue\0TERM=xterm-something\0PATH=/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path' ) ); await updateProcessEnv(process.env); expect(spawn.calls.length).toBe(1); expect(spawn.calls[0].command).toBe('/my/custom/bash'); expect(spawn.calls[0].args).toEqual([ '-ilc', 'command awk \'BEGIN{for(v in ENVIRON) printf("%s=%s%c", v, ENVIRON[v], 0)}\'' ]); expect(process.env).toEqual({ FOO: 'BAR=BAZ=QUUX', 'MULTILINE\nNAME': 'multiline\nvalue', TERM: 'xterm-something', PATH: '/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path' }); // Doesn't error await updateProcessEnv(null); }); }); describe('on windows', function() { it('does not update process.env', async function() { process.platform = 'win32'; spyOn(childProcess, 'spawn'); process.env = { FOO: 'bar' }; await updateProcessEnv(process.env); expect(childProcess.spawn).not.toHaveBeenCalled(); expect(process.env).toEqual({ FOO: 'bar' }); }); }); describe('shouldGetEnvFromShell()', function() { it('indicates when the environment should be fetched from the shell', function() { if (process.platform === 'win32') return; // TestsThatFailOnWin32 process.platform = 'darwin'; expect(shouldGetEnvFromShell({ SHELL: '/bin/sh' })).toBe(true); expect(shouldGetEnvFromShell({ SHELL: '/usr/local/bin/sh' })).toBe( true ); expect(shouldGetEnvFromShell({ SHELL: '/bin/bash' })).toBe(true); expect(shouldGetEnvFromShell({ SHELL: '/usr/local/bin/bash' })).toBe( true ); expect(shouldGetEnvFromShell({ SHELL: '/bin/zsh' })).toBe(true); expect(shouldGetEnvFromShell({ SHELL: '/usr/local/bin/zsh' })).toBe( true ); expect(shouldGetEnvFromShell({ SHELL: '/bin/fish' })).toBe(true); expect(shouldGetEnvFromShell({ SHELL: '/usr/local/bin/fish' })).toBe( true ); process.platform = 'linux'; expect(shouldGetEnvFromShell({ SHELL: '/bin/sh' })).toBe(true); expect(shouldGetEnvFromShell({ SHELL: '/usr/local/bin/sh' })).toBe( true ); expect(shouldGetEnvFromShell({ SHELL: '/bin/bash' })).toBe(true); expect(shouldGetEnvFromShell({ SHELL: '/usr/local/bin/bash' })).toBe( true ); expect(shouldGetEnvFromShell({ SHELL: '/bin/zsh' })).toBe(true); expect(shouldGetEnvFromShell({ SHELL: '/usr/local/bin/zsh' })).toBe( true ); expect(shouldGetEnvFromShell({ SHELL: '/bin/fish' })).toBe(true); expect(shouldGetEnvFromShell({ SHELL: '/usr/local/bin/fish' })).toBe( true ); }); it('returns false when the environment indicates that Atom was launched from a shell', function() { process.platform = 'darwin'; expect( shouldGetEnvFromShell({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', SHELL: '/bin/sh' }) ).toBe(false); process.platform = 'linux'; expect( shouldGetEnvFromShell({ ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT: 'true', SHELL: '/bin/sh' }) ).toBe(false); }); it('returns false when the shell is undefined or empty', function() { process.platform = 'darwin'; expect(shouldGetEnvFromShell(undefined)).toBe(false); expect(shouldGetEnvFromShell({})).toBe(false); process.platform = 'linux'; expect(shouldGetEnvFromShell(undefined)).toBe(false); expect(shouldGetEnvFromShell({})).toBe(false); }); }); }); }); ================================================ FILE: spec/uri-handler-registry-spec.js ================================================ /** @babel */ import url from 'url'; import URIHandlerRegistry from '../src/uri-handler-registry'; describe('URIHandlerRegistry', () => { let registry; beforeEach(() => { registry = new URIHandlerRegistry(5); }); it('handles URIs on a per-host basis', async () => { const testPackageSpy = jasmine.createSpy(); const otherPackageSpy = jasmine.createSpy(); registry.registerHostHandler('test-package', testPackageSpy); registry.registerHostHandler('other-package', otherPackageSpy); await registry.handleURI('atom://yet-another-package/path'); expect(testPackageSpy).not.toHaveBeenCalled(); expect(otherPackageSpy).not.toHaveBeenCalled(); await registry.handleURI('atom://test-package/path'); expect(testPackageSpy).toHaveBeenCalledWith( url.parse('atom://test-package/path', true), 'atom://test-package/path' ); expect(otherPackageSpy).not.toHaveBeenCalled(); await 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', async () => { 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' ]; for (const u of uris) { await 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() ); await 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', async () => { const invalidUris = [ 'atom:package/path', 'atom:8080://package/path', 'user:pass@atom://package/path', 'smth://package/path' ]; let numErrors = 0; for (const uri of invalidUris) { try { await registry.handleURI(uri); expect(uri).toBe('throwing an error'); } catch (ex) { numErrors++; } } expect(numErrors).toBe(invalidUris.length); }); }); ================================================ FILE: spec/view-registry-spec.js ================================================ /* * 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; }); }); })); }); ================================================ FILE: spec/window-event-handler-spec.js ================================================ const KeymapManager = require('atom-keymap'); const WindowEventHandler = require('../src/window-event-handler'); const { conditionPromise } = require('./async-spec-helpers'); 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('resize event', () => it('calls storeWindowDimensions', async () => { jasmine.useRealClock(); spyOn(atom, 'storeWindowDimensions'); window.dispatchEvent(new CustomEvent('resize')); await conditionPromise(() => atom.storeWindowDimensions.callCount > 0); })); 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(); }); it('opens the "atom://" links with URL handler', () => { const uriHandler = windowEventHandler.atomEnvironment.uriHandlerRegistry; expect(uriHandler).toBeDefined(); spyOn(uriHandler, 'handleURI'); const link = document.createElement('a'); const linkChild = document.createElement('span'); link.appendChild(linkChild); link.href = 'atom://github.com'; jasmine.attachToDOM(link); const fakeEvent = { target: linkChild, currentTarget: link, preventDefault: () => {} }; windowEventHandler.handleLinkClick(fakeEvent); expect(uriHandler.handleURI).toHaveBeenCalled(); expect(uriHandler.handleURI.argsForCall[0][0]).toBe('atom://github.com'); }); }); 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(); })); }); ================================================ FILE: spec/workspace-center-spec.js ================================================ /** @babel */ const TextEditor = require('../src/text-editor'); describe('WorkspaceCenter', () => { describe('.observeTextEditors()', () => { it('invokes the observer with current and future text editors', () => { const workspaceCenter = atom.workspace.getCenter(); const pane = workspaceCenter.getActivePane(); const observed = []; const editorAddedBeforeRegisteringObserver = new TextEditor(); const nonEditorItemAddedBeforeRegisteringObserver = document.createElement( 'div' ); pane.activateItem(editorAddedBeforeRegisteringObserver); pane.activateItem(nonEditorItemAddedBeforeRegisteringObserver); workspaceCenter.observeTextEditors(editor => observed.push(editor)); const editorAddedAfterRegisteringObserver = new TextEditor(); const nonEditorItemAddedAfterRegisteringObserver = document.createElement( 'div' ); pane.activateItem(editorAddedAfterRegisteringObserver); pane.activateItem(nonEditorItemAddedAfterRegisteringObserver); expect(observed).toEqual([ editorAddedBeforeRegisteringObserver, editorAddedAfterRegisteringObserver ]); }); }); }); ================================================ FILE: spec/workspace-element-spec.js ================================================ /** @babel */ const { ipcRenderer } = require('electron'); const etch = require('etch'); const path = require('path'); const temp = require('temp').track(); const { Disposable } = require('event-kit'); const getNextUpdatePromise = () => etch.getScheduler().nextUpdatePromise; describe('WorkspaceElement', () => { afterEach(() => { try { temp.cleanupSync(); } catch (e) { // Do nothing } }); describe('when the workspace element is focused', () => { it('transfers focus to the active pane', () => { const workspaceElement = atom.workspace.getElement(); jasmine.attachToDOM(workspaceElement); const activePaneElement = atom.workspace.getActivePane().getElement(); document.body.focus(); expect(document.activeElement).not.toBe(activePaneElement); workspaceElement.focus(); expect(document.activeElement).toBe(activePaneElement); }); }); describe('when the active pane of an inactive pane container is focused', () => { it('changes the active pane container', () => { const dock = atom.workspace.getLeftDock(); dock.show(); jasmine.attachToDOM(atom.workspace.getElement()); expect(atom.workspace.getActivePaneContainer()).toBe( atom.workspace.getCenter() ); dock .getActivePane() .getElement() .focus(); expect(atom.workspace.getActivePaneContainer()).toBe(dock); }); }); describe('finding the nearest visible pane in a specific direction', () => { let nearestPaneElement, pane1, pane2, pane3, pane4, pane5, pane6, pane7, pane8, leftDockPane, rightDockPane, bottomDockPane, workspace, workspaceElement; beforeEach(function() { atom.config.set('core.destroyEmptyPanes', false); expect(document.hasFocus()).toBe( true, 'Document needs to be focused to run this test' ); workspace = atom.workspace; // Set up a workspace center with a grid of 9 panes, in the following // arrangement, where the numbers correspond to the variable names below. // // ------- // |1|2|3| // ------- // |4|5|6| // ------- // |7|8|9| // ------- const container = workspace.getActivePaneContainer(); expect(container.getLocation()).toEqual('center'); expect(container.getPanes().length).toEqual(1); pane1 = container.getActivePane(); pane4 = pane1.splitDown(); pane7 = pane4.splitDown(); pane2 = pane1.splitRight(); pane3 = pane2.splitRight(); pane5 = pane4.splitRight(); pane6 = pane5.splitRight(); pane8 = pane7.splitRight(); pane8.splitRight(); const leftDock = workspace.getLeftDock(); const rightDock = workspace.getRightDock(); const bottomDock = workspace.getBottomDock(); expect(leftDock.isVisible()).toBe(false); expect(rightDock.isVisible()).toBe(false); expect(bottomDock.isVisible()).toBe(false); expect(leftDock.getPanes().length).toBe(1); expect(rightDock.getPanes().length).toBe(1); expect(bottomDock.getPanes().length).toBe(1); leftDockPane = leftDock.getPanes()[0]; rightDockPane = rightDock.getPanes()[0]; bottomDockPane = bottomDock.getPanes()[0]; workspaceElement = atom.workspace.getElement(); workspaceElement.style.height = '400px'; workspaceElement.style.width = '400px'; jasmine.attachToDOM(workspaceElement); }); describe('finding the nearest pane above', () => { describe('when there are multiple rows above the pane', () => { it('returns the pane in the adjacent row above', () => { nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'above', pane8 ); expect(nearestPaneElement).toBe(pane5.getElement()); }); }); describe('when there are no rows above the pane', () => { it('returns null', () => { nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'above', pane2 ); expect(nearestPaneElement).toBeUndefined(); // TODO Expect toBeNull() }); }); describe('when the bottom dock contains the pane', () => { it('returns the pane in the adjacent row above', () => { workspace.getBottomDock().show(); nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'above', bottomDockPane ); expect(nearestPaneElement).toBe(pane7.getElement()); }); }); }); describe('finding the nearest pane below', () => { describe('when there are multiple rows below the pane', () => { it('returns the pane in the adjacent row below', () => { nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'below', pane2 ); expect(nearestPaneElement).toBe(pane5.getElement()); }); }); describe('when there are no rows below the pane', () => { it('returns null', () => { nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'below', pane8 ); expect(nearestPaneElement).toBeUndefined(); // TODO Expect toBeNull() }); }); describe('when the bottom dock is visible', () => { describe("when the workspace center's bottommost row contains the pane", () => { it("returns the pane in the bottom dock's adjacent row below", () => { workspace.getBottomDock().show(); nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'below', pane8 ); expect(nearestPaneElement).toBe(bottomDockPane.getElement()); }); }); }); }); describe('finding the nearest pane to the left', () => { describe('when there are multiple columns to the left of the pane', () => { it('returns the pane in the adjacent column to the left', () => { nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'left', pane6 ); expect(nearestPaneElement).toBe(pane5.getElement()); }); }); describe('when there are no columns to the left of the pane', () => { it('returns null', () => { nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'left', pane4 ); expect(nearestPaneElement).toBeUndefined(); // TODO Expect toBeNull() }); }); describe('when the right dock contains the pane', () => { it('returns the pane in the adjacent column to the left', () => { workspace.getRightDock().show(); nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'left', rightDockPane ); expect(nearestPaneElement).toBe(pane3.getElement()); }); }); describe('when the left dock is visible', () => { describe("when the workspace center's leftmost column contains the pane", () => { it("returns the pane in the left dock's adjacent column to the left", () => { workspace.getLeftDock().show(); nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'left', pane4 ); expect(nearestPaneElement).toBe(leftDockPane.getElement()); }); }); describe('when the bottom dock contains the pane', () => { it("returns the pane in the left dock's adjacent column to the left", () => { workspace.getLeftDock().show(); workspace.getBottomDock().show(); nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'left', bottomDockPane ); expect(nearestPaneElement).toBe(leftDockPane.getElement()); }); }); }); }); describe('finding the nearest pane to the right', () => { describe('when there are multiple columns to the right of the pane', () => { it('returns the pane in the adjacent column to the right', () => { nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'right', pane4 ); expect(nearestPaneElement).toBe(pane5.getElement()); }); }); describe('when there are no columns to the right of the pane', () => { it('returns null', () => { nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'right', pane6 ); expect(nearestPaneElement).toBeUndefined(); // TODO Expect toBeNull() }); }); describe('when the left dock contains the pane', () => { it('returns the pane in the adjacent column to the right', () => { workspace.getLeftDock().show(); nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'right', leftDockPane ); expect(nearestPaneElement).toBe(pane1.getElement()); }); }); describe('when the right dock is visible', () => { describe("when the workspace center's rightmost column contains the pane", () => { it("returns the pane in the right dock's adjacent column to the right", () => { workspace.getRightDock().show(); nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'right', pane6 ); expect(nearestPaneElement).toBe(rightDockPane.getElement()); }); }); describe('when the bottom dock contains the pane', () => { it("returns the pane in the right dock's adjacent column to the right", () => { workspace.getRightDock().show(); workspace.getBottomDock().show(); nearestPaneElement = workspaceElement.nearestVisiblePaneInDirection( 'right', bottomDockPane ); expect(nearestPaneElement).toBe(rightDockPane.getElement()); }); }); }); }); }); describe('changing focus, copying, and moving items directionally between panes', function() { let workspace, workspaceElement, startingPane; beforeEach(function() { atom.config.set('core.destroyEmptyPanes', false); expect(document.hasFocus()).toBe( true, 'Document needs to be focused to run this test' ); workspace = atom.workspace; expect(workspace.getLeftDock().isVisible()).toBe(false); expect(workspace.getRightDock().isVisible()).toBe(false); expect(workspace.getBottomDock().isVisible()).toBe(false); const panes = workspace.getCenter().getPanes(); expect(panes.length).toEqual(1); startingPane = panes[0]; workspaceElement = atom.workspace.getElement(); workspaceElement.style.height = '400px'; workspaceElement.style.width = '400px'; jasmine.attachToDOM(workspaceElement); }); describe('::focusPaneViewAbove()', function() { describe('when there is a row above the focused pane', () => it('focuses up to the adjacent row', function() { const paneAbove = startingPane.splitUp(); startingPane.activate(); workspaceElement.focusPaneViewAbove(); expect(document.activeElement).toBe(paneAbove.getElement()); })); describe('when there are no rows above the focused pane', () => it('keeps the current pane focused', function() { startingPane.activate(); workspaceElement.focusPaneViewAbove(); expect(document.activeElement).toBe(startingPane.getElement()); })); }); describe('::focusPaneViewBelow()', function() { describe('when there is a row below the focused pane', () => it('focuses down to the adjacent row', function() { const paneBelow = startingPane.splitDown(); startingPane.activate(); workspaceElement.focusPaneViewBelow(); expect(document.activeElement).toBe(paneBelow.getElement()); })); describe('when there are no rows below the focused pane', () => it('keeps the current pane focused', function() { startingPane.activate(); workspaceElement.focusPaneViewBelow(); expect(document.activeElement).toBe(startingPane.getElement()); })); }); describe('::focusPaneViewOnLeft()', function() { describe('when there is a column to the left of the focused pane', () => it('focuses left to the adjacent column', function() { const paneOnLeft = startingPane.splitLeft(); startingPane.activate(); workspaceElement.focusPaneViewOnLeft(); expect(document.activeElement).toBe(paneOnLeft.getElement()); })); describe('when there are no columns to the left of the focused pane', () => it('keeps the current pane focused', function() { startingPane.activate(); workspaceElement.focusPaneViewOnLeft(); expect(document.activeElement).toBe(startingPane.getElement()); })); }); describe('::focusPaneViewOnRight()', function() { describe('when there is a column to the right of the focused pane', () => it('focuses right to the adjacent column', function() { const paneOnRight = startingPane.splitRight(); startingPane.activate(); workspaceElement.focusPaneViewOnRight(); expect(document.activeElement).toBe(paneOnRight.getElement()); })); describe('when there are no columns to the right of the focused pane', () => it('keeps the current pane focused', function() { startingPane.activate(); workspaceElement.focusPaneViewOnRight(); expect(document.activeElement).toBe(startingPane.getElement()); })); }); describe('::moveActiveItemToPaneAbove(keepOriginal)', function() { describe('when there is a row above the focused pane', () => it('moves the active item up to the adjacent row', function() { const item = document.createElement('div'); const paneAbove = startingPane.splitUp(); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToPaneAbove(); expect(workspace.paneForItem(item)).toBe(paneAbove); expect(paneAbove.getActiveItem()).toBe(item); })); describe('when there are no rows above the focused pane', () => it('keeps the active pane focused', function() { const item = document.createElement('div'); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToPaneAbove(); expect(workspace.paneForItem(item)).toBe(startingPane); })); describe('when `keepOriginal: true` is passed in the params', () => it('keeps the item and adds a copy of it to the adjacent pane', function() { const itemA = document.createElement('div'); const itemB = document.createElement('div'); itemA.copy = () => itemB; const paneAbove = startingPane.splitUp(); startingPane.activate(); startingPane.activateItem(itemA); workspaceElement.moveActiveItemToPaneAbove({ keepOriginal: true }); expect(workspace.paneForItem(itemA)).toBe(startingPane); expect(paneAbove.getActiveItem()).toBe(itemB); })); }); describe('::moveActiveItemToPaneBelow(keepOriginal)', function() { describe('when there is a row below the focused pane', () => it('moves the active item down to the adjacent row', function() { const item = document.createElement('div'); const paneBelow = startingPane.splitDown(); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToPaneBelow(); expect(workspace.paneForItem(item)).toBe(paneBelow); expect(paneBelow.getActiveItem()).toBe(item); })); describe('when there are no rows below the focused pane', () => it('keeps the active item in the focused pane', function() { const item = document.createElement('div'); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToPaneBelow(); expect(workspace.paneForItem(item)).toBe(startingPane); })); describe('when `keepOriginal: true` is passed in the params', () => it('keeps the item and adds a copy of it to the adjacent pane', function() { const itemA = document.createElement('div'); const itemB = document.createElement('div'); itemA.copy = () => itemB; const paneBelow = startingPane.splitDown(); startingPane.activate(); startingPane.activateItem(itemA); workspaceElement.moveActiveItemToPaneBelow({ keepOriginal: true }); expect(workspace.paneForItem(itemA)).toBe(startingPane); expect(paneBelow.getActiveItem()).toBe(itemB); })); }); describe('::moveActiveItemToPaneOnLeft(keepOriginal)', function() { describe('when there is a column to the left of the focused pane', () => it('moves the active item left to the adjacent column', function() { const item = document.createElement('div'); const paneOnLeft = startingPane.splitLeft(); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToPaneOnLeft(); expect(workspace.paneForItem(item)).toBe(paneOnLeft); expect(paneOnLeft.getActiveItem()).toBe(item); })); describe('when there are no columns to the left of the focused pane', () => it('keeps the active item in the focused pane', function() { const item = document.createElement('div'); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToPaneOnLeft(); expect(workspace.paneForItem(item)).toBe(startingPane); })); describe('when `keepOriginal: true` is passed in the params', () => it('keeps the item and adds a copy of it to the adjacent pane', function() { const itemA = document.createElement('div'); const itemB = document.createElement('div'); itemA.copy = () => itemB; const paneOnLeft = startingPane.splitLeft(); startingPane.activate(); startingPane.activateItem(itemA); workspaceElement.moveActiveItemToPaneOnLeft({ keepOriginal: true }); expect(workspace.paneForItem(itemA)).toBe(startingPane); expect(paneOnLeft.getActiveItem()).toBe(itemB); })); }); describe('::moveActiveItemToPaneOnRight(keepOriginal)', function() { describe('when there is a column to the right of the focused pane', () => it('moves the active item right to the adjacent column', function() { const item = document.createElement('div'); const paneOnRight = startingPane.splitRight(); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToPaneOnRight(); expect(workspace.paneForItem(item)).toBe(paneOnRight); expect(paneOnRight.getActiveItem()).toBe(item); })); describe('when there are no columns to the right of the focused pane', () => it('keeps the active item in the focused pane', function() { const item = document.createElement('div'); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToPaneOnRight(); expect(workspace.paneForItem(item)).toBe(startingPane); })); describe('when `keepOriginal: true` is passed in the params', () => it('keeps the item and adds a copy of it to the adjacent pane', function() { const itemA = document.createElement('div'); const itemB = document.createElement('div'); itemA.copy = () => itemB; const paneOnRight = startingPane.splitRight(); startingPane.activate(); startingPane.activateItem(itemA); workspaceElement.moveActiveItemToPaneOnRight({ keepOriginal: true }); expect(workspace.paneForItem(itemA)).toBe(startingPane); expect(paneOnRight.getActiveItem()).toBe(itemB); })); }); describe('::moveActiveItemToNearestPaneInDirection(direction, params)', () => { describe('when the item is not allowed in nearest pane in the given direction', () => { it('does not move or copy the active item', function() { const item = { element: document.createElement('div'), getAllowedLocations: () => ['left', 'right'] }; workspace.getBottomDock().show(); startingPane.activate(); startingPane.activateItem(item); workspaceElement.moveActiveItemToNearestPaneInDirection('below', { keepOriginal: false }); expect(workspace.paneForItem(item)).toBe(startingPane); workspaceElement.moveActiveItemToNearestPaneInDirection('below', { keepOriginal: true }); expect(workspace.paneForItem(item)).toBe(startingPane); }); }); describe("when the item doesn't implement a `copy` function", () => { it('does not copy the active item', function() { const item = document.createElement('div'); const paneBelow = startingPane.splitDown(); expect(paneBelow.getItems().length).toEqual(0); startingPane.activate(); startingPane.activateItem(item); workspaceElement.focusPaneViewAbove(); workspaceElement.moveActiveItemToNearestPaneInDirection('below', { keepOriginal: true }); expect(workspace.paneForItem(item)).toBe(startingPane); expect(paneBelow.getItems().length).toEqual(0); }); }); }); }); describe('mousing over docks', () => { let workspaceElement; let originalTimeout = jasmine.getEnv().defaultTimeoutInterval; beforeEach(() => { workspaceElement = atom.workspace.getElement(); workspaceElement.style.width = '600px'; workspaceElement.style.height = '300px'; jasmine.attachToDOM(workspaceElement); // To isolate this test from unintended events happening on the host machine, // we remove any listener that could cause interferences. window.removeEventListener( 'mousemove', workspaceElement.handleEdgesMouseMove ); workspaceElement.htmlElement.removeEventListener( 'mouseleave', workspaceElement.handleCenterLeave ); jasmine.getEnv().defaultTimeoutInterval = 10000; }); afterEach(() => { jasmine.getEnv().defaultTimeoutInterval = originalTimeout; window.addEventListener( 'mousemove', workspaceElement.handleEdgesMouseMove ); workspaceElement.htmlElement.addEventListener( 'mouseleave', workspaceElement.handleCenterLeave ); }); it('shows the toggle button when the dock is open', async () => { await Promise.all([ atom.workspace.open({ element: document.createElement('div'), getDefaultLocation() { return 'left'; }, getPreferredWidth() { return 150; } }), atom.workspace.open({ element: document.createElement('div'), getDefaultLocation() { return 'right'; }, getPreferredWidth() { return 150; } }), atom.workspace.open({ element: document.createElement('div'), getDefaultLocation() { return 'bottom'; }, getPreferredHeight() { return 100; } }) ]); const leftDock = atom.workspace.getLeftDock(); const rightDock = atom.workspace.getRightDock(); const bottomDock = atom.workspace.getBottomDock(); expect(leftDock.isVisible()).toBe(true); expect(rightDock.isVisible()).toBe(true); expect(bottomDock.isVisible()).toBe(true); expectToggleButtonHidden(leftDock); expectToggleButtonHidden(rightDock); expectToggleButtonHidden(bottomDock); // --- Right Dock --- // Mouse over where the toggle button would be if the dock were hovered moveMouse({ clientX: 440, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); expectToggleButtonHidden(rightDock); expectToggleButtonHidden(bottomDock); // Mouse over the dock moveMouse({ clientX: 460, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); expectToggleButtonVisible(rightDock, 'icon-chevron-right'); expectToggleButtonHidden(bottomDock); // Mouse over the toggle button moveMouse({ clientX: 440, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); expectToggleButtonVisible(rightDock, 'icon-chevron-right'); expectToggleButtonHidden(bottomDock); // Click the toggle button rightDock.refs.toggleButton.refs.innerElement.click(); await getNextUpdatePromise(); expect(rightDock.isVisible()).toBe(false); expectToggleButtonHidden(rightDock); // Mouse to edge of the window moveMouse({ clientX: 575, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonHidden(rightDock); moveMouse({ clientX: 598, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonVisible(rightDock, 'icon-chevron-left'); // Click the toggle button again rightDock.refs.toggleButton.refs.innerElement.click(); await getNextUpdatePromise(); expect(rightDock.isVisible()).toBe(true); expectToggleButtonVisible(rightDock, 'icon-chevron-right'); // --- Left Dock --- // Mouse over where the toggle button would be if the dock were hovered moveMouse({ clientX: 160, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); expectToggleButtonHidden(rightDock); expectToggleButtonHidden(bottomDock); // Mouse over the dock moveMouse({ clientX: 140, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonVisible(leftDock, 'icon-chevron-left'); expectToggleButtonHidden(rightDock); expectToggleButtonHidden(bottomDock); // Mouse over the toggle button moveMouse({ clientX: 160, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonVisible(leftDock, 'icon-chevron-left'); expectToggleButtonHidden(rightDock); expectToggleButtonHidden(bottomDock); // Click the toggle button leftDock.refs.toggleButton.refs.innerElement.click(); await getNextUpdatePromise(); expect(leftDock.isVisible()).toBe(false); expectToggleButtonHidden(leftDock); // Mouse to edge of the window moveMouse({ clientX: 25, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); moveMouse({ clientX: 2, clientY: 150 }); await getNextUpdatePromise(); expectToggleButtonVisible(leftDock, 'icon-chevron-right'); // Click the toggle button again leftDock.refs.toggleButton.refs.innerElement.click(); await getNextUpdatePromise(); expect(leftDock.isVisible()).toBe(true); expectToggleButtonVisible(leftDock, 'icon-chevron-left'); // --- Bottom Dock --- // Mouse over where the toggle button would be if the dock were hovered moveMouse({ clientX: 300, clientY: 190 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); expectToggleButtonHidden(rightDock); expectToggleButtonHidden(bottomDock); // Mouse over the dock moveMouse({ clientX: 300, clientY: 210 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); expectToggleButtonHidden(rightDock); expectToggleButtonVisible(bottomDock, 'icon-chevron-down'); // Mouse over the toggle button moveMouse({ clientX: 300, clientY: 195 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); expectToggleButtonHidden(rightDock); expectToggleButtonVisible(bottomDock, 'icon-chevron-down'); // Click the toggle button bottomDock.refs.toggleButton.refs.innerElement.click(); await getNextUpdatePromise(); expect(bottomDock.isVisible()).toBe(false); expectToggleButtonHidden(bottomDock); // Mouse to edge of the window moveMouse({ clientX: 300, clientY: 290 }); await getNextUpdatePromise(); expectToggleButtonHidden(leftDock); moveMouse({ clientX: 300, clientY: 299 }); await getNextUpdatePromise(); expectToggleButtonVisible(bottomDock, 'icon-chevron-up'); // Click the toggle button again bottomDock.refs.toggleButton.refs.innerElement.click(); await getNextUpdatePromise(); expect(bottomDock.isVisible()).toBe(true); expectToggleButtonVisible(bottomDock, 'icon-chevron-down'); }); function moveMouse(coordinates) { // Simulate a mouse move event by calling the method that handles that event. workspaceElement.updateHoveredDock({ x: coordinates.clientX, y: coordinates.clientY }); advanceClock(100); } function expectToggleButtonHidden(dock) { expect(dock.refs.toggleButton.element).not.toHaveClass( 'atom-dock-toggle-button-visible' ); } function expectToggleButtonVisible(dock, iconClass) { expect(dock.refs.toggleButton.element).toHaveClass( 'atom-dock-toggle-button-visible' ); expect(dock.refs.toggleButton.refs.iconElement).toHaveClass(iconClass); } }); describe('the scrollbar visibility class', () => { it('has a class based on the style of the scrollbar', () => { let observeCallback; const scrollbarStyle = require('scrollbar-style'); spyOn(scrollbarStyle, 'observePreferredScrollbarStyle').andCallFake( cb => { observeCallback = cb; return new Disposable(() => {}); } ); const workspaceElement = atom.workspace.getElement(); observeCallback('legacy'); expect(workspaceElement.className).toMatch('scrollbars-visible-always'); observeCallback('overlay'); expect(workspaceElement).toHaveClass('scrollbars-visible-when-scrolling'); }); }); describe('editor font styling', () => { let editor, editorElement, workspaceElement; beforeEach(async () => { await atom.workspace.open('sample.js'); workspaceElement = atom.workspace.getElement(); jasmine.attachToDOM(workspaceElement); editor = atom.workspace.getActiveTextEditor(); editorElement = editor.getElement(); }); it("updates the font-size based on the 'editor.fontSize' config value", async () => { const initialCharWidth = editor.getDefaultCharWidth(); expect(getComputedStyle(editorElement).fontSize).toBe( atom.config.get('editor.fontSize') + 'px' ); atom.config.set( 'editor.fontSize', atom.config.get('editor.fontSize') + 5 ); await editorElement.component.getNextUpdatePromise(); expect(getComputedStyle(editorElement).fontSize).toBe( atom.config.get('editor.fontSize') + 'px' ); expect(editor.getDefaultCharWidth()).toBeGreaterThan(initialCharWidth); }); it("updates the font-family based on the 'editor.fontFamily' config value", async () => { const initialCharWidth = editor.getDefaultCharWidth(); let fontFamily = atom.config.get('editor.fontFamily'); expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily); atom.config.set('editor.fontFamily', 'sans-serif'); fontFamily = atom.config.get('editor.fontFamily'); await editorElement.component.getNextUpdatePromise(); expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily); expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth); }); it("updates the line-height based on the 'editor.lineHeight' config value", async () => { const initialLineHeight = editor.getLineHeightInPixels(); atom.config.set('editor.lineHeight', '30px'); await editorElement.component.getNextUpdatePromise(); expect(getComputedStyle(editorElement).lineHeight).toBe( atom.config.get('editor.lineHeight') ); expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeight); }); it('increases or decreases the font size when a ctrl-mousewheel event occurs', () => { atom.config.set('editor.zoomFontWhenCtrlScrolling', true); atom.config.set('editor.fontSize', 12); // Zoom out editorElement.querySelector('span').dispatchEvent( new WheelEvent('mousewheel', { wheelDeltaY: -10, ctrlKey: true }) ); expect(atom.config.get('editor.fontSize')).toBe(11); // Zoom in editorElement.querySelector('span').dispatchEvent( new WheelEvent('mousewheel', { wheelDeltaY: 10, ctrlKey: true }) ); expect(atom.config.get('editor.fontSize')).toBe(12); // Not on an atom-text-editor workspaceElement.dispatchEvent( new WheelEvent('mousewheel', { wheelDeltaY: 10, ctrlKey: true }) ); expect(atom.config.get('editor.fontSize')).toBe(12); // No ctrl key editorElement.querySelector('span').dispatchEvent( new WheelEvent('mousewheel', { wheelDeltaY: 10 }) ); expect(atom.config.get('editor.fontSize')).toBe(12); atom.config.set('editor.zoomFontWhenCtrlScrolling', false); editorElement.querySelector('span').dispatchEvent( new WheelEvent('mousewheel', { wheelDeltaY: 10, ctrlKey: true }) ); expect(atom.config.get('editor.fontSize')).toBe(12); }); }); describe('panel containers', () => { it('inserts panel container elements in the correct places in the DOM', () => { const workspaceElement = atom.workspace.getElement(); const leftContainer = workspaceElement.querySelector( 'atom-panel-container.left' ); const rightContainer = workspaceElement.querySelector( 'atom-panel-container.right' ); expect(leftContainer.nextSibling).toBe(workspaceElement.verticalAxis); expect(rightContainer.previousSibling).toBe( workspaceElement.verticalAxis ); const topContainer = workspaceElement.querySelector( 'atom-panel-container.top' ); const bottomContainer = workspaceElement.querySelector( 'atom-panel-container.bottom' ); expect(topContainer.nextSibling).toBe(workspaceElement.paneContainer); expect(bottomContainer.previousSibling).toBe( workspaceElement.paneContainer ); const headerContainer = workspaceElement.querySelector( 'atom-panel-container.header' ); const footerContainer = workspaceElement.querySelector( 'atom-panel-container.footer' ); expect(headerContainer.nextSibling).toBe(workspaceElement.horizontalAxis); expect(footerContainer.previousSibling).toBe( workspaceElement.horizontalAxis ); const modalContainer = workspaceElement.querySelector( 'atom-panel-container.modal' ); expect(modalContainer.parentNode).toBe(workspaceElement); }); it('stretches header/footer panels to the workspace width', () => { const workspaceElement = atom.workspace.getElement(); jasmine.attachToDOM(workspaceElement); expect(workspaceElement.offsetWidth).toBeGreaterThan(0); const headerItem = document.createElement('div'); atom.workspace.addHeaderPanel({ item: headerItem }); expect(headerItem.offsetWidth).toEqual(workspaceElement.offsetWidth); const footerItem = document.createElement('div'); atom.workspace.addFooterPanel({ item: footerItem }); expect(footerItem.offsetWidth).toEqual(workspaceElement.offsetWidth); }); it('shrinks horizontal axis according to header/footer panels height', () => { const workspaceElement = atom.workspace.getElement(); workspaceElement.style.height = '100px'; const horizontalAxisElement = workspaceElement.querySelector( 'atom-workspace-axis.horizontal' ); jasmine.attachToDOM(workspaceElement); const originalHorizontalAxisHeight = horizontalAxisElement.offsetHeight; expect(workspaceElement.offsetHeight).toBeGreaterThan(0); expect(originalHorizontalAxisHeight).toBeGreaterThan(0); const headerItem = document.createElement('div'); headerItem.style.height = '10px'; atom.workspace.addHeaderPanel({ item: headerItem }); expect(headerItem.offsetHeight).toBeGreaterThan(0); const footerItem = document.createElement('div'); footerItem.style.height = '15px'; atom.workspace.addFooterPanel({ item: footerItem }); expect(footerItem.offsetHeight).toBeGreaterThan(0); expect(horizontalAxisElement.offsetHeight).toEqual( originalHorizontalAxisHeight - headerItem.offsetHeight - footerItem.offsetHeight ); }); }); describe("the 'window:toggle-invisibles' command", () => { it('shows/hides invisibles in all open and future editors', () => { const workspaceElement = atom.workspace.getElement(); expect(atom.config.get('editor.showInvisibles')).toBe(false); atom.commands.dispatch(workspaceElement, 'window:toggle-invisibles'); expect(atom.config.get('editor.showInvisibles')).toBe(true); atom.commands.dispatch(workspaceElement, 'window:toggle-invisibles'); expect(atom.config.get('editor.showInvisibles')).toBe(false); }); }); describe("the 'window:run-package-specs' command", () => { it("runs the package specs for the active item's project path, or the first project path", () => { const workspaceElement = atom.workspace.getElement(); spyOn(ipcRenderer, 'send'); // No project paths. Don't try to run specs. atom.commands.dispatch(workspaceElement, 'window:run-package-specs'); expect(ipcRenderer.send).not.toHaveBeenCalledWith('run-package-specs'); const projectPaths = [temp.mkdirSync('dir1-'), temp.mkdirSync('dir2-')]; atom.project.setPaths(projectPaths); // No active item. Use first project directory. atom.commands.dispatch(workspaceElement, 'window:run-package-specs'); expect(ipcRenderer.send).toHaveBeenCalledWith( 'run-package-specs', path.join(projectPaths[0], 'spec'), {} ); ipcRenderer.send.reset(); // Active item doesn't implement ::getPath(). Use first project directory. const item = document.createElement('div'); atom.workspace.getActivePane().activateItem(item); atom.commands.dispatch(workspaceElement, 'window:run-package-specs'); expect(ipcRenderer.send).toHaveBeenCalledWith( 'run-package-specs', path.join(projectPaths[0], 'spec'), {} ); ipcRenderer.send.reset(); // Active item has no path. Use first project directory. item.getPath = () => null; atom.commands.dispatch(workspaceElement, 'window:run-package-specs'); expect(ipcRenderer.send).toHaveBeenCalledWith( 'run-package-specs', path.join(projectPaths[0], 'spec'), {} ); ipcRenderer.send.reset(); // Active item has path. Use project path for item path. item.getPath = () => path.join(projectPaths[1], 'a-file.txt'); atom.commands.dispatch(workspaceElement, 'window:run-package-specs'); expect(ipcRenderer.send).toHaveBeenCalledWith( 'run-package-specs', path.join(projectPaths[1], 'spec'), {} ); ipcRenderer.send.reset(); }); it('passes additional options to the spec window', () => { const workspaceElement = atom.workspace.getElement(); spyOn(ipcRenderer, 'send'); const projectPath = temp.mkdirSync('dir1-'); atom.project.setPaths([projectPath]); workspaceElement.runPackageSpecs({ env: { ATOM_GITHUB_BABEL_ENV: 'coverage' } }); expect(ipcRenderer.send).toHaveBeenCalledWith( 'run-package-specs', path.join(projectPath, 'spec'), { env: { ATOM_GITHUB_BABEL_ENV: 'coverage' } } ); }); }); }); ================================================ FILE: spec/workspace-spec.js ================================================ const path = require('path'); const temp = require('temp').track(); const dedent = require('dedent'); const TextBuffer = require('text-buffer'); const TextEditor = require('../src/text-editor'); const Workspace = require('../src/workspace'); const Project = require('../src/project'); const platform = require('./spec-helper-platform'); const _ = require('underscore-plus'); const fstream = require('fstream'); const fs = require('fs-plus'); const AtomEnvironment = require('../src/atom-environment'); const { conditionPromise } = require('./async-spec-helpers'); describe('Workspace', () => { let workspace; let setDocumentEdited; beforeEach(() => { workspace = atom.workspace; workspace.resetFontSize(); spyOn(atom.applicationDelegate, 'confirm'); setDocumentEdited = spyOn( atom.applicationDelegate, 'setWindowDocumentEdited' ); atom.project.setPaths([atom.project.getDirectories()[0].resolve('dir')]); waits(1); waitsForPromise(() => atom.workspace.itemLocationStore.clear()); }); afterEach(() => { try { temp.cleanupSync(); } catch (e) { // Do nothing } }); function simulateReload() { waitsForPromise(() => { const workspaceState = workspace.serialize(); const projectState = atom.project.serialize({ isUnloading: true }); workspace.destroy(); atom.project.destroy(); atom.project = new Project({ notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate, grammarRegistry: atom.grammars }); return atom.project.deserialize(projectState).then(() => { workspace = atom.workspace = new Workspace({ config: atom.config, project: atom.project, packageManager: atom.packages, grammarRegistry: atom.grammars, styleManager: atom.styles, deserializerManager: atom.deserializers, notificationManager: atom.notifications, applicationDelegate: atom.applicationDelegate, viewRegistry: atom.views, assert: atom.assert.bind(atom), textEditorRegistry: atom.textEditors }); workspace.deserialize(workspaceState, atom.deserializers); }); }); } describe('serialization', () => { describe('when the workspace contains text editors', () => { it('constructs the view with the same panes', () => { const pane1 = atom.workspace.getActivePane(); const pane2 = pane1.splitRight({ copyActiveItem: true }); const pane3 = pane2.splitRight({ copyActiveItem: true }); let pane4 = null; waitsForPromise(() => atom.workspace .open(null) .then(editor => editor.setText('An untitled editor.')) ); waitsForPromise(() => atom.workspace .open('b') .then(editor => pane2.activateItem(editor.copy())) ); waitsForPromise(() => atom.workspace .open('../sample.js') .then(editor => pane3.activateItem(editor)) ); runs(() => { pane3.activeItem.setCursorScreenPosition([2, 4]); pane4 = pane2.splitDown(); }); waitsForPromise(() => atom.workspace .open('../sample.txt') .then(editor => pane4.activateItem(editor)) ); runs(() => { pane4.getActiveItem().setCursorScreenPosition([0, 2]); pane2.activate(); }); simulateReload(); runs(() => { expect(atom.workspace.getTextEditors().length).toBe(5); const [ editor1, editor2, untitledEditor, editor3, editor4 ] = atom.workspace.getTextEditors(); const firstDirectory = atom.project.getDirectories()[0]; expect(firstDirectory).toBeDefined(); expect(editor1.getPath()).toBe(firstDirectory.resolve('b')); expect(editor2.getPath()).toBe( firstDirectory.resolve('../sample.txt') ); expect(editor2.getCursorScreenPosition()).toEqual([0, 2]); expect(editor3.getPath()).toBe(firstDirectory.resolve('b')); expect(editor4.getPath()).toBe( firstDirectory.resolve('../sample.js') ); expect(editor4.getCursorScreenPosition()).toEqual([2, 4]); expect(untitledEditor.getPath()).toBeUndefined(); expect(untitledEditor.getText()).toBe('An untitled editor.'); expect(atom.workspace.getActiveTextEditor().getPath()).toBe( editor3.getPath() ); const pathEscaped = fs.tildify( escapeStringRegex(atom.project.getPaths()[0]) ); expect(document.title).toMatch( new RegExp( `^${path.basename(editor3.getLongTitle())} \\u2014 ${pathEscaped}` ) ); }); }); }); describe('where there are no open panes or editors', () => { it('constructs the view with no open editors', () => { atom.workspace.getActivePane().destroy(); expect(atom.workspace.getTextEditors().length).toBe(0); simulateReload(); runs(() => { expect(atom.workspace.getTextEditors().length).toBe(0); }); }); }); }); describe('::open(itemOrURI, options)', () => { let openEvents = null; beforeEach(() => { openEvents = []; workspace.onDidOpen(event => openEvents.push(event)); spyOn(workspace.getActivePane(), 'activate').andCallThrough(); }); describe("when the 'searchAllPanes' option is false (default)", () => { describe('when called without a uri or item', () => { it('adds and activates an empty editor on the active pane', () => { let editor1; let editor2; waitsForPromise(() => workspace.open().then(editor => { editor1 = editor; }) ); runs(() => { expect(editor1.getPath()).toBeUndefined(); expect(workspace.getActivePane().items).toEqual([editor1]); expect(workspace.getActivePaneItem()).toBe(editor1); expect(workspace.getActivePane().activate).toHaveBeenCalled(); expect(openEvents).toEqual([ { uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0 } ]); openEvents = []; }); waitsForPromise(() => workspace.open().then(editor => { editor2 = editor; }) ); runs(() => { expect(editor2.getPath()).toBeUndefined(); expect(workspace.getActivePane().items).toEqual([editor1, editor2]); expect(workspace.getActivePaneItem()).toBe(editor2); expect(workspace.getActivePane().activate).toHaveBeenCalled(); expect(openEvents).toEqual([ { uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1 } ]); }); }); }); describe('when called with a uri', () => { describe('when the active pane already has an editor for the given uri', () => { it('activates the existing editor on the active pane', () => { let editor = null; let editor1 = null; let editor2 = null; waitsForPromise(() => workspace.open('a').then(o => { editor1 = o; return workspace.open('b').then(o => { editor2 = o; return workspace.open('a').then(o => { editor = o; }); }); }) ); runs(() => { expect(editor).toBe(editor1); expect(workspace.getActivePaneItem()).toBe(editor); expect(workspace.getActivePane().activate).toHaveBeenCalled(); const firstDirectory = atom.project.getDirectories()[0]; expect(firstDirectory).toBeDefined(); expect(openEvents).toEqual([ { uri: firstDirectory.resolve('a'), item: editor1, pane: atom.workspace.getActivePane(), index: 0 }, { uri: firstDirectory.resolve('b'), item: editor2, pane: atom.workspace.getActivePane(), index: 1 }, { uri: firstDirectory.resolve('a'), item: editor1, pane: atom.workspace.getActivePane(), index: 0 } ]); }); }); it('finds items in docks', () => { const dock = atom.workspace.getRightDock(); const ITEM_URI = 'atom://test'; const item = { getURI: () => ITEM_URI, getDefaultLocation: () => 'left', getElement: () => document.createElement('div') }; dock.getActivePane().addItem(item); expect(dock.getPaneItems()).toHaveLength(1); waitsForPromise(() => atom.workspace.open(ITEM_URI, { searchAllPanes: true }) ); runs(() => { expect(atom.workspace.getPaneItems()).toHaveLength(1); expect(dock.getPaneItems()).toHaveLength(1); expect(dock.getPaneItems()[0]).toBe(item); }); }); }); describe("when the 'activateItem' option is false", () => { it('adds the item to the workspace', () => { let editor; waitsForPromise(() => workspace.open('a')); waitsForPromise(() => workspace.open('b', { activateItem: false }).then(o => { editor = o; }) ); runs(() => { expect(workspace.getPaneItems()).toContain(editor); expect(workspace.getActivePaneItem()).not.toBe(editor); }); }); }); describe('when the active pane does not have an editor for the given uri', () => { beforeEach(() => { atom.workspace.enablePersistence = true; }); afterEach(async () => { await atom.workspace.itemLocationStore.clear(); atom.workspace.enablePersistence = false; }); it('adds and activates a new editor for the given path on the active pane', () => { let editor = null; waitsForPromise(() => workspace.open('a').then(o => { editor = o; }) ); runs(() => { const firstDirectory = atom.project.getDirectories()[0]; expect(firstDirectory).toBeDefined(); expect(editor.getURI()).toBe(firstDirectory.resolve('a')); expect(workspace.getActivePaneItem()).toBe(editor); expect(workspace.getActivePane().items).toEqual([editor]); expect(workspace.getActivePane().activate).toHaveBeenCalled(); }); }); it('discovers existing editors that are still opening', () => { let editor0 = null; let editor1 = null; waitsForPromise(() => Promise.all([ workspace.open('spartacus.txt').then(o0 => { editor0 = o0; }), workspace.open('spartacus.txt').then(o1 => { editor1 = o1; }) ]) ); runs(() => { expect(editor0).toEqual(editor1); expect(workspace.getActivePane().items).toEqual([editor0]); }); }); it("uses the location specified by the model's `getDefaultLocation()` method", () => { const item = { getDefaultLocation: jasmine.createSpy().andReturn('right'), getElement: () => document.createElement('div') }; const opener = jasmine.createSpy().andReturn(item); const dock = atom.workspace.getRightDock(); spyOn(atom.workspace.itemLocationStore, 'load').andReturn( Promise.resolve() ); spyOn(atom.workspace, 'getOpeners').andReturn([opener]); expect(dock.getPaneItems()).toHaveLength(0); waitsForPromise(() => atom.workspace.open('a')); runs(() => { expect(dock.getPaneItems()).toHaveLength(1); expect(opener).toHaveBeenCalled(); expect(item.getDefaultLocation).toHaveBeenCalled(); }); }); it('prefers the last location the user used for that item', () => { const ITEM_URI = 'atom://test'; const item = { getURI: () => ITEM_URI, getDefaultLocation: () => 'left', getElement: () => document.createElement('div') }; const opener = uri => (uri === ITEM_URI ? item : null); const dock = atom.workspace.getRightDock(); spyOn(atom.workspace.itemLocationStore, 'load').andCallFake(uri => uri === 'atom://test' ? Promise.resolve('right') : Promise.resolve() ); spyOn(atom.workspace, 'getOpeners').andReturn([opener]); expect(dock.getPaneItems()).toHaveLength(0); waitsForPromise(() => atom.workspace.open(ITEM_URI)); runs(() => { expect(dock.getPaneItems()).toHaveLength(1); expect(dock.getPaneItems()[0]).toBe(item); }); }); }); }); describe('when an item with the given uri exists in an inactive pane container', () => { it("activates that item if it is in that container's active pane", async () => { const item = await atom.workspace.open('a'); atom.workspace.getLeftDock().activate(); expect( await atom.workspace.open('a', { searchAllPanes: false }) ).toBe(item); expect(atom.workspace.getActivePaneContainer().getLocation()).toBe( 'center' ); expect(atom.workspace.getPaneItems()).toEqual([item]); atom.workspace.getActivePane().splitRight(); atom.workspace.getLeftDock().activate(); const item2 = await atom.workspace.open('a', { searchAllPanes: false }); expect(item2).not.toBe(item); expect(atom.workspace.getActivePaneContainer().getLocation()).toBe( 'center' ); expect(atom.workspace.getPaneItems()).toEqual([item, item2]); }); }); }); describe("when the 'searchAllPanes' option is true", () => { describe('when an editor for the given uri is already open on an inactive pane', () => { it('activates the existing editor on the inactive pane, then activates that pane', () => { let editor1 = null; let editor2 = null; const pane1 = workspace.getActivePane(); const pane2 = workspace.getActivePane().splitRight(); waitsForPromise(() => { pane1.activate(); return workspace.open('a').then(o => { editor1 = o; }); }); waitsForPromise(() => { pane2.activate(); return workspace.open('b').then(o => { editor2 = o; }); }); runs(() => expect(workspace.getActivePaneItem()).toBe(editor2)); waitsForPromise(() => workspace.open('a', { searchAllPanes: true })); runs(() => { expect(workspace.getActivePane()).toBe(pane1); expect(workspace.getActivePaneItem()).toBe(editor1); }); }); it('discovers existing editors that are still opening in an inactive pane', () => { let editor0 = null; let editor1 = null; const pane0 = workspace.getActivePane(); const pane1 = workspace.getActivePane().splitRight(); pane0.activate(); const promise0 = workspace .open('spartacus.txt', { searchAllPanes: true }) .then(o0 => { editor0 = o0; }); pane1.activate(); const promise1 = workspace .open('spartacus.txt', { searchAllPanes: true }) .then(o1 => { editor1 = o1; }); waitsForPromise(() => Promise.all([promise0, promise1])); runs(() => { expect(editor0).toBeDefined(); expect(editor1).toBeDefined(); expect(editor0).toEqual(editor1); expect(workspace.getActivePane().items).toEqual([editor0]); }); }); it('activates the pane in the dock with the matching item', () => { const dock = atom.workspace.getRightDock(); const ITEM_URI = 'atom://test'; const item = { getURI: () => ITEM_URI, getDefaultLocation: jasmine.createSpy().andReturn('left'), getElement: () => document.createElement('div') }; dock.getActivePane().addItem(item); spyOn(dock.paneForItem(item), 'activate'); waitsForPromise(() => atom.workspace.open(ITEM_URI, { searchAllPanes: true }) ); runs(() => expect(dock.paneForItem(item).activate).toHaveBeenCalled() ); }); }); describe('when no editor for the given uri is open in any pane', () => { it('opens an editor for the given uri in the active pane', () => { let editor = null; waitsForPromise(() => workspace.open('a', { searchAllPanes: true }).then(o => { editor = o; }) ); runs(() => expect(workspace.getActivePaneItem()).toBe(editor)); }); }); }); describe('when attempting to open an editor in a dock', () => { it('opens the editor in the workspace center', async () => { await atom.workspace.open('sample.txt', { location: 'right' }); expect( atom.workspace .getCenter() .getActivePaneItem() .getFileName() ).toEqual('sample.txt'); }); }); describe('when called with an item rather than a URI', () => { it('adds the item itself to the workspace', async () => { const item = document.createElement('div'); await atom.workspace.open(item); expect(atom.workspace.getActivePaneItem()).toBe(item); }); describe('when the active pane already contains the item', () => { it('activates the item', async () => { const item = document.createElement('div'); await atom.workspace.open(item); await atom.workspace.open(); expect(atom.workspace.getActivePaneItem()).not.toBe(item); expect(atom.workspace.getActivePane().getItems().length).toBe(2); await atom.workspace.open(item); expect(atom.workspace.getActivePaneItem()).toBe(item); expect(atom.workspace.getActivePane().getItems().length).toBe(2); }); }); describe('when the item already exists in another pane', () => { it('rejects the promise', async () => { const item = document.createElement('div'); await atom.workspace.open(item); await atom.workspace.open(null, { split: 'right' }); expect(atom.workspace.getActivePaneItem()).not.toBe(item); expect(atom.workspace.getActivePane().getItems().length).toBe(1); let rejection; try { await atom.workspace.open(item); } catch (error) { rejection = error; } expect(rejection.message).toMatch( /The workspace can only contain one instance of item/ ); }); }); }); describe("when the 'split' option is set", () => { describe("when the 'split' option is 'left'", () => { it('opens the editor in the leftmost pane of the current pane axis', () => { const pane1 = workspace.getActivePane(); const pane2 = pane1.splitRight(); expect(workspace.getActivePane()).toBe(pane2); let editor = null; waitsForPromise(() => workspace.open('a', { split: 'left' }).then(o => { editor = o; }) ); runs(() => { expect(workspace.getActivePane()).toBe(pane1); expect(pane1.items).toEqual([editor]); expect(pane2.items).toEqual([]); }); // Focus right pane and reopen the file on the left waitsForPromise(() => { pane2.focus(); return workspace.open('a', { split: 'left' }).then(o => { editor = o; }); }); runs(() => { expect(workspace.getActivePane()).toBe(pane1); expect(pane1.items).toEqual([editor]); expect(pane2.items).toEqual([]); }); }); }); describe('when a pane axis is the leftmost sibling of the current pane', () => { it('opens the new item in the current pane', () => { let editor = null; const pane1 = workspace.getActivePane(); const pane2 = pane1.splitLeft(); pane2.splitDown(); pane1.activate(); expect(workspace.getActivePane()).toBe(pane1); waitsForPromise(() => workspace.open('a', { split: 'left' }).then(o => { editor = o; }) ); runs(() => { expect(workspace.getActivePane()).toBe(pane1); expect(pane1.items).toEqual([editor]); }); }); }); describe("when the 'split' option is 'right'", () => { it('opens the editor in the rightmost pane of the current pane axis', () => { let editor = null; const pane1 = workspace.getActivePane(); let pane2 = null; waitsForPromise(() => workspace.open('a', { split: 'right' }).then(o => { editor = o; }) ); runs(() => { pane2 = workspace.getPanes().filter(p => p !== pane1)[0]; expect(workspace.getActivePane()).toBe(pane2); expect(pane1.items).toEqual([]); expect(pane2.items).toEqual([editor]); }); // Focus right pane and reopen the file on the right waitsForPromise(() => { pane1.focus(); return workspace.open('a', { split: 'right' }).then(o => { editor = o; }); }); runs(() => { expect(workspace.getActivePane()).toBe(pane2); expect(pane1.items).toEqual([]); expect(pane2.items).toEqual([editor]); }); }); describe('when a pane axis is the rightmost sibling of the current pane', () => { it('opens the new item in a new pane split to the right of the current pane', () => { let editor = null; const pane1 = workspace.getActivePane(); const pane2 = pane1.splitRight(); pane2.splitDown(); pane1.activate(); expect(workspace.getActivePane()).toBe(pane1); let pane4 = null; waitsForPromise(() => workspace.open('a', { split: 'right' }).then(o => { editor = o; }) ); runs(() => { pane4 = workspace.getPanes().filter(p => p !== pane1)[0]; expect(workspace.getActivePane()).toBe(pane4); expect(pane4.items).toEqual([editor]); expect(workspace.getCenter().paneContainer.root.children[0]).toBe( pane1 ); expect(workspace.getCenter().paneContainer.root.children[1]).toBe( pane4 ); }); }); }); }); describe("when the 'split' option is 'up'", () => { it('opens the editor in the topmost pane of the current pane axis', () => { const pane1 = workspace.getActivePane(); const pane2 = pane1.splitDown(); expect(workspace.getActivePane()).toBe(pane2); let editor = null; waitsForPromise(() => workspace.open('a', { split: 'up' }).then(o => { editor = o; }) ); runs(() => { expect(workspace.getActivePane()).toBe(pane1); expect(pane1.items).toEqual([editor]); expect(pane2.items).toEqual([]); }); // Focus bottom pane and reopen the file on the top waitsForPromise(() => { pane2.focus(); return workspace.open('a', { split: 'up' }).then(o => { editor = o; }); }); runs(() => { expect(workspace.getActivePane()).toBe(pane1); expect(pane1.items).toEqual([editor]); expect(pane2.items).toEqual([]); }); }); }); describe('when a pane axis is the topmost sibling of the current pane', () => { it('opens the new item in the current pane', () => { let editor = null; const pane1 = workspace.getActivePane(); const pane2 = pane1.splitUp(); pane2.splitRight(); pane1.activate(); expect(workspace.getActivePane()).toBe(pane1); waitsForPromise(() => workspace.open('a', { split: 'up' }).then(o => { editor = o; }) ); runs(() => { expect(workspace.getActivePane()).toBe(pane1); expect(pane1.items).toEqual([editor]); }); }); }); describe("when the 'split' option is 'down'", () => { it('opens the editor in the bottommost pane of the current pane axis', () => { let editor = null; const pane1 = workspace.getActivePane(); let pane2 = null; waitsForPromise(() => workspace.open('a', { split: 'down' }).then(o => { editor = o; }) ); runs(() => { pane2 = workspace.getPanes().filter(p => p !== pane1)[0]; expect(workspace.getActivePane()).toBe(pane2); expect(pane1.items).toEqual([]); expect(pane2.items).toEqual([editor]); }); // Focus bottom pane and reopen the file on the right waitsForPromise(() => { pane1.focus(); return workspace.open('a', { split: 'down' }).then(o => { editor = o; }); }); runs(() => { expect(workspace.getActivePane()).toBe(pane2); expect(pane1.items).toEqual([]); expect(pane2.items).toEqual([editor]); }); }); describe('when a pane axis is the bottommost sibling of the current pane', () => { it('opens the new item in a new pane split to the bottom of the current pane', () => { let editor = null; const pane1 = workspace.getActivePane(); const pane2 = pane1.splitDown(); pane1.activate(); expect(workspace.getActivePane()).toBe(pane1); let pane4 = null; waitsForPromise(() => workspace.open('a', { split: 'down' }).then(o => { editor = o; }) ); runs(() => { pane4 = workspace.getPanes().filter(p => p !== pane1)[0]; expect(workspace.getActivePane()).toBe(pane4); expect(pane4.items).toEqual([editor]); expect(workspace.getCenter().paneContainer.root.children[0]).toBe( pane1 ); expect(workspace.getCenter().paneContainer.root.children[1]).toBe( pane2 ); }); }); }); }); }); describe('when an initialLine and initialColumn are specified', () => { it('moves the cursor to the indicated location', () => { waitsForPromise(() => workspace.open('a', { initialLine: 1, initialColumn: 5 }) ); runs(() => expect( workspace.getActiveTextEditor().getCursorBufferPosition() ).toEqual([1, 5]) ); waitsForPromise(() => workspace.open('a', { initialLine: 2, initialColumn: 4 }) ); runs(() => expect( workspace.getActiveTextEditor().getCursorBufferPosition() ).toEqual([2, 4]) ); waitsForPromise(() => workspace.open('a', { initialLine: 0, initialColumn: 0 }) ); runs(() => expect( workspace.getActiveTextEditor().getCursorBufferPosition() ).toEqual([0, 0]) ); waitsForPromise(() => workspace.open('a', { initialLine: NaN, initialColumn: 4 }) ); runs(() => expect( workspace.getActiveTextEditor().getCursorBufferPosition() ).toEqual([0, 4]) ); waitsForPromise(() => workspace.open('a', { initialLine: 2, initialColumn: NaN }) ); runs(() => expect( workspace.getActiveTextEditor().getCursorBufferPosition() ).toEqual([2, 0]) ); waitsForPromise(() => workspace.open('a', { initialLine: Infinity, initialColumn: Infinity }) ); runs(() => expect( workspace.getActiveTextEditor().getCursorBufferPosition() ).toEqual([2, 11]) ); }); it('unfolds the fold containing the line', async () => { let editor; await workspace.open('../sample-with-many-folds.js'); editor = workspace.getActiveTextEditor(); editor.foldBufferRow(2); expect(editor.isFoldedAtBufferRow(2)).toBe(true); expect(editor.isFoldedAtBufferRow(3)).toBe(true); await workspace.open('../sample-with-many-folds.js', { initialLine: 2 }); expect(editor.isFoldedAtBufferRow(2)).toBe(false); expect(editor.isFoldedAtBufferRow(3)).toBe(false); }); }); describe('when the file size is over the limit defined in `core.warnOnLargeFileLimit`', () => { const shouldPromptForFileOfSize = async (size, shouldPrompt) => { spyOn(fs, 'getSizeSync').andReturn(size * 1048577); let selectedButtonIndex = 1; // cancel atom.applicationDelegate.confirm.andCallFake((options, callback) => callback(selectedButtonIndex) ); let editor = await workspace.open('sample.js'); if (shouldPrompt) { expect(editor).toBeUndefined(); expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); atom.applicationDelegate.confirm.reset(); selectedButtonIndex = 0; // open the file editor = await workspace.open('sample.js'); expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); } else { expect(editor).not.toBeUndefined(); } }; it('prompts before opening the file', async () => { atom.config.set('core.warnOnLargeFileLimit', 20); await shouldPromptForFileOfSize(20, true); }); it("doesn't prompt on files below the limit", async () => { atom.config.set('core.warnOnLargeFileLimit', 30); await shouldPromptForFileOfSize(20, false); }); it('prompts for smaller files with a lower limit', async () => { atom.config.set('core.warnOnLargeFileLimit', 5); await shouldPromptForFileOfSize(10, true); }); }); describe('when passed a path that matches a custom opener', () => { it('returns the resource returned by the custom opener', () => { const fooOpener = (pathToOpen, options) => { if (pathToOpen != null ? pathToOpen.match(/\.foo/) : undefined) { return { foo: pathToOpen, options }; } }; const barOpener = pathToOpen => { if (pathToOpen != null ? pathToOpen.match(/^bar:\/\//) : undefined) { return { bar: pathToOpen }; } }; workspace.addOpener(fooOpener); workspace.addOpener(barOpener); waitsForPromise(() => { const pathToOpen = atom.project.getDirectories()[0].resolve('a.foo'); return workspace.open(pathToOpen, { hey: 'there' }).then(item => expect(item).toEqual({ foo: pathToOpen, options: { hey: 'there' } }) ); }); waitsForPromise(() => workspace .open('bar://baz') .then(item => expect(item).toEqual({ bar: 'bar://baz' })) ); }); }); it("adds the file to the application's recent documents list", () => { if (process.platform !== 'darwin') { return; } // Feature only supported on macOS spyOn(atom.applicationDelegate, 'addRecentDocument'); waitsForPromise(() => workspace.open()); runs(() => expect( atom.applicationDelegate.addRecentDocument ).not.toHaveBeenCalled() ); waitsForPromise(() => workspace.open('something://a/url')); runs(() => expect( atom.applicationDelegate.addRecentDocument ).not.toHaveBeenCalled() ); waitsForPromise(() => workspace.open(__filename)); runs(() => expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith( __filename ) ); }); it('notifies ::onDidAddTextEditor observers', () => { const absolutePath = require.resolve('./fixtures/dir/a'); const newEditorHandler = jasmine.createSpy('newEditorHandler'); workspace.onDidAddTextEditor(newEditorHandler); let editor = null; waitsForPromise(() => workspace.open(absolutePath).then(e => { editor = e; }) ); runs(() => expect(newEditorHandler.argsForCall[0][0].textEditor).toBe(editor) ); }); describe('when there is an error opening the file', () => { let notificationSpy = null; beforeEach(() => atom.notifications.onDidAddNotification( (notificationSpy = jasmine.createSpy()) ) ); describe('when a file does not exist', () => { it('creates an empty buffer for the specified path', () => { waitsForPromise(() => workspace.open('not-a-file.md')); runs(() => { const editor = workspace.getActiveTextEditor(); expect(notificationSpy).not.toHaveBeenCalled(); expect(editor.getPath()).toContain('not-a-file.md'); }); }); }); describe('when the user does not have access to the file', () => { beforeEach(() => spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EACCES, permission denied '${path}'`); error.path = path; error.code = 'EACCES'; throw error; }) ); it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')); runs(() => { expect(notificationSpy).toHaveBeenCalled(); const notification = notificationSpy.mostRecentCall.args[0]; expect(notification.getType()).toBe('warning'); expect(notification.getMessage()).toContain('Permission denied'); expect(notification.getMessage()).toContain('file1'); }); }); }); describe('when the the operation is not permitted', () => { beforeEach(() => spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EPERM, operation not permitted '${path}'`); error.path = path; error.code = 'EPERM'; throw error; }) ); it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')); runs(() => { expect(notificationSpy).toHaveBeenCalled(); const notification = notificationSpy.mostRecentCall.args[0]; expect(notification.getType()).toBe('warning'); expect(notification.getMessage()).toContain('Unable to open'); expect(notification.getMessage()).toContain('file1'); }); }); }); describe('when the the file is already open in windows', () => { beforeEach(() => spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EBUSY, resource busy or locked '${path}'`); error.path = path; error.code = 'EBUSY'; throw error; }) ); it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')); runs(() => { expect(notificationSpy).toHaveBeenCalled(); const notification = notificationSpy.mostRecentCall.args[0]; expect(notification.getType()).toBe('warning'); expect(notification.getMessage()).toContain('Unable to open'); expect(notification.getMessage()).toContain('file1'); }); }); }); describe('when there is an unhandled error', () => { beforeEach(() => spyOn(fs, 'openSync').andCallFake(path => { throw new Error('I dont even know what is happening right now!!'); }) ); it('rejects the promise', () => { waitsFor(done => { workspace.open('file1').catch(error => { expect(error.message).toBe( 'I dont even know what is happening right now!!' ); done(); }); }); }); }); }); describe('when the file is already open in pending state', () => { it('should terminate the pending state', () => { let editor = null; let pane = null; waitsForPromise(() => atom.workspace.open('sample.js', { pending: true }).then(o => { editor = o; pane = atom.workspace.getActivePane(); }) ); runs(() => expect(pane.getPendingItem()).toEqual(editor)); waitsForPromise(() => atom.workspace.open('sample.js')); runs(() => expect(pane.getPendingItem()).toBeNull()); }); }); describe('when opening will switch from a pending tab to a permanent tab', () => { it('keeps the pending tab open', () => { let editor1 = null; let editor2 = null; waitsForPromise(() => atom.workspace.open('sample.txt').then(o => { editor1 = o; }) ); waitsForPromise(() => atom.workspace.open('sample2.txt', { pending: true }).then(o => { editor2 = o; }) ); runs(() => { const pane = atom.workspace.getActivePane(); pane.activateItem(editor1); expect(pane.getItems().length).toBe(2); expect(pane.getItems()).toEqual([editor1, editor2]); }); }); }); describe('when replacing a pending item which is the last item in a second pane', () => { it('does not destroy the pane even if core.destroyEmptyPanes is on', () => { atom.config.set('core.destroyEmptyPanes', true); let editor1 = null; let editor2 = null; const leftPane = atom.workspace.getActivePane(); let rightPane = null; waitsForPromise(() => atom.workspace .open('sample.js', { pending: true, split: 'right' }) .then(o => { editor1 = o; rightPane = atom.workspace.getActivePane(); spyOn(rightPane, 'destroy').andCallThrough(); }) ); runs(() => { expect(leftPane).not.toBe(rightPane); expect(atom.workspace.getActivePane()).toBe(rightPane); expect(atom.workspace.getActivePane().getItems().length).toBe(1); expect(rightPane.getPendingItem()).toBe(editor1); }); waitsForPromise(() => atom.workspace.open('sample.txt', { pending: true }).then(o => { editor2 = o; }) ); runs(() => { expect(rightPane.getPendingItem()).toBe(editor2); expect(rightPane.destroy.callCount).toBe(0); }); }); }); describe("when opening an editor with a buffer that isn't part of the project", () => { it('adds the buffer to the project', async () => { const buffer = new TextBuffer(); const editor = new TextEditor({ buffer }); await atom.workspace.open(editor); expect(atom.project.getBuffers().map(buffer => buffer.id)).toContain( buffer.id ); expect(buffer.getLanguageMode().getLanguageId()).toBe( 'text.plain.null-grammar' ); }); }); }); describe('finding items in the workspace', () => { it('can identify the pane and pane container for a given item or URI', () => { const uri = 'atom://test-pane-for-item'; const item = { element: document.createElement('div'), getURI() { return uri; } }; atom.workspace.getActivePane().activateItem(item); expect(atom.workspace.paneForItem(item)).toBe( atom.workspace.getCenter().getActivePane() ); expect(atom.workspace.paneContainerForItem(item)).toBe( atom.workspace.getCenter() ); expect(atom.workspace.paneForURI(uri)).toBe( atom.workspace.getCenter().getActivePane() ); expect(atom.workspace.paneContainerForURI(uri)).toBe( atom.workspace.getCenter() ); atom.workspace.getActivePane().destroyActiveItem(); atom.workspace .getLeftDock() .getActivePane() .activateItem(item); expect(atom.workspace.paneForItem(item)).toBe( atom.workspace.getLeftDock().getActivePane() ); expect(atom.workspace.paneContainerForItem(item)).toBe( atom.workspace.getLeftDock() ); expect(atom.workspace.paneForURI(uri)).toBe( atom.workspace.getLeftDock().getActivePane() ); expect(atom.workspace.paneContainerForURI(uri)).toBe( atom.workspace.getLeftDock() ); }); }); describe('::hide(uri)', () => { let item; const URI = 'atom://hide-test'; beforeEach(() => { const el = document.createElement('div'); item = { getTitle: () => 'Item', getElement: () => el, getURI: () => URI }; }); describe('when called with a URI', () => { it('if the item for the given URI is in the center, removes it', () => { const pane = atom.workspace.getActivePane(); pane.addItem(item); atom.workspace.hide(URI); expect(pane.getItems().length).toBe(0); }); it('if the item for the given URI is in a dock, hides the dock', () => { const dock = atom.workspace.getLeftDock(); const pane = dock.getActivePane(); pane.addItem(item); dock.activate(); expect(dock.isVisible()).toBe(true); const itemFound = atom.workspace.hide(URI); expect(itemFound).toBe(true); expect(dock.isVisible()).toBe(false); }); }); describe('when called with an item', () => { it('if the item is in the center, removes it', () => { const pane = atom.workspace.getActivePane(); pane.addItem(item); atom.workspace.hide(item); expect(pane.getItems().length).toBe(0); }); it('if the item is in a dock, hides the dock', () => { const dock = atom.workspace.getLeftDock(); const pane = dock.getActivePane(); pane.addItem(item); dock.activate(); expect(dock.isVisible()).toBe(true); const itemFound = atom.workspace.hide(item); expect(itemFound).toBe(true); expect(dock.isVisible()).toBe(false); }); }); }); describe('::toggle(itemOrUri)', () => { describe('when the location resolves to a dock', () => { it('adds or shows the item and its dock if it is not currently visible, and otherwise hides the containing dock', async () => { const item1 = { getDefaultLocation() { return 'left'; }, getElement() { return (this.element = document.createElement('div')); } }; const item2 = { getDefaultLocation() { return 'left'; }, getElement() { return (this.element = document.createElement('div')); } }; const dock = workspace.getLeftDock(); expect(dock.isVisible()).toBe(false); await workspace.toggle(item1); expect(dock.isVisible()).toBe(true); expect(dock.getActivePaneItem()).toBe(item1); await workspace.toggle(item2); expect(dock.isVisible()).toBe(true); expect(dock.getActivePaneItem()).toBe(item2); await workspace.toggle(item1); expect(dock.isVisible()).toBe(true); expect(dock.getActivePaneItem()).toBe(item1); await workspace.toggle(item1); expect(dock.isVisible()).toBe(false); expect(dock.getActivePaneItem()).toBe(item1); await workspace.toggle(item2); expect(dock.isVisible()).toBe(true); expect(dock.getActivePaneItem()).toBe(item2); }); }); describe('when the location resolves to the center', () => { it('adds or shows the item if it is not currently the active pane item, and otherwise removes the item', async () => { const item1 = { getDefaultLocation() { return 'center'; }, getElement() { return (this.element = document.createElement('div')); } }; const item2 = { getDefaultLocation() { return 'center'; }, getElement() { return (this.element = document.createElement('div')); } }; expect(workspace.getActivePaneItem()).toBeUndefined(); await workspace.toggle(item1); expect(workspace.getActivePaneItem()).toBe(item1); await workspace.toggle(item2); expect(workspace.getActivePaneItem()).toBe(item2); await workspace.toggle(item1); expect(workspace.getActivePaneItem()).toBe(item1); await workspace.toggle(item1); expect(workspace.paneForItem(item1)).toBeUndefined(); expect(workspace.getActivePaneItem()).toBe(item2); }); }); }); describe('active pane containers', () => { it('maintains the active pane and item globally across active pane containers', () => { const leftDock = workspace.getLeftDock(); const leftItem1 = { element: document.createElement('div') }; const leftItem2 = { element: document.createElement('div') }; const leftItem3 = { element: document.createElement('div') }; const leftPane1 = leftDock.getActivePane(); leftPane1.addItems([leftItem1, leftItem2]); const leftPane2 = leftPane1.splitDown({ items: [leftItem3] }); const rightDock = workspace.getRightDock(); const rightItem1 = { element: document.createElement('div') }; const rightItem2 = { element: document.createElement('div') }; const rightItem3 = { element: document.createElement('div') }; const rightPane1 = rightDock.getActivePane(); rightPane1.addItems([rightItem1, rightItem2]); const rightPane2 = rightPane1.splitDown({ items: [rightItem3] }); const bottomDock = workspace.getBottomDock(); const bottomItem1 = { element: document.createElement('div') }; const bottomItem2 = { element: document.createElement('div') }; const bottomItem3 = { element: document.createElement('div') }; const bottomPane1 = bottomDock.getActivePane(); bottomPane1.addItems([bottomItem1, bottomItem2]); const bottomPane2 = bottomPane1.splitDown({ items: [bottomItem3] }); const center = workspace.getCenter(); const centerItem1 = { element: document.createElement('div') }; const centerItem2 = { element: document.createElement('div') }; const centerItem3 = { element: document.createElement('div') }; const centerPane1 = center.getActivePane(); centerPane1.addItems([centerItem1, centerItem2]); const centerPane2 = centerPane1.splitDown({ items: [centerItem3] }); const activePaneContainers = []; const activePanes = []; const activeItems = []; workspace.onDidChangeActivePaneContainer(container => activePaneContainers.push(container) ); workspace.onDidChangeActivePane(pane => activePanes.push(pane)); workspace.onDidChangeActivePaneItem(item => activeItems.push(item)); function clearEvents() { activePaneContainers.length = 0; activePanes.length = 0; activeItems.length = 0; } expect(workspace.getActivePaneContainer()).toBe(center); expect(workspace.getActivePane()).toBe(centerPane2); expect(workspace.getActivePaneItem()).toBe(centerItem3); leftDock.activate(); expect(workspace.getActivePaneContainer()).toBe(leftDock); expect(workspace.getActivePane()).toBe(leftPane2); expect(workspace.getActivePaneItem()).toBe(leftItem3); expect(activePaneContainers).toEqual([leftDock]); expect(activePanes).toEqual([leftPane2]); expect(activeItems).toEqual([leftItem3]); clearEvents(); leftPane1.activate(); leftPane1.activate(); expect(workspace.getActivePaneContainer()).toBe(leftDock); expect(workspace.getActivePane()).toBe(leftPane1); expect(workspace.getActivePaneItem()).toBe(leftItem1); expect(activePaneContainers).toEqual([]); expect(activePanes).toEqual([leftPane1]); expect(activeItems).toEqual([leftItem1]); clearEvents(); leftPane1.activateItem(leftItem2); leftPane1.activateItem(leftItem2); expect(workspace.getActivePaneContainer()).toBe(leftDock); expect(workspace.getActivePane()).toBe(leftPane1); expect(workspace.getActivePaneItem()).toBe(leftItem2); expect(activePaneContainers).toEqual([]); expect(activePanes).toEqual([]); expect(activeItems).toEqual([leftItem2]); clearEvents(); expect(rightDock.getActivePane()).toBe(rightPane2); rightPane1.activate(); rightPane1.activate(); expect(workspace.getActivePaneContainer()).toBe(rightDock); expect(workspace.getActivePane()).toBe(rightPane1); expect(workspace.getActivePaneItem()).toBe(rightItem1); expect(activePaneContainers).toEqual([rightDock]); expect(activePanes).toEqual([rightPane1]); expect(activeItems).toEqual([rightItem1]); clearEvents(); rightPane1.activateItem(rightItem2); expect(workspace.getActivePaneContainer()).toBe(rightDock); expect(workspace.getActivePane()).toBe(rightPane1); expect(workspace.getActivePaneItem()).toBe(rightItem2); expect(activePaneContainers).toEqual([]); expect(activePanes).toEqual([]); expect(activeItems).toEqual([rightItem2]); clearEvents(); expect(bottomDock.getActivePane()).toBe(bottomPane2); bottomPane2.activate(); bottomPane2.activate(); expect(workspace.getActivePaneContainer()).toBe(bottomDock); expect(workspace.getActivePane()).toBe(bottomPane2); expect(workspace.getActivePaneItem()).toBe(bottomItem3); expect(activePaneContainers).toEqual([bottomDock]); expect(activePanes).toEqual([bottomPane2]); expect(activeItems).toEqual([bottomItem3]); clearEvents(); center.activate(); center.activate(); expect(workspace.getActivePaneContainer()).toBe(center); expect(workspace.getActivePane()).toBe(centerPane2); expect(workspace.getActivePaneItem()).toBe(centerItem3); expect(activePaneContainers).toEqual([center]); expect(activePanes).toEqual([centerPane2]); expect(activeItems).toEqual([centerItem3]); clearEvents(); centerPane1.activate(); centerPane1.activate(); expect(workspace.getActivePaneContainer()).toBe(center); expect(workspace.getActivePane()).toBe(centerPane1); expect(workspace.getActivePaneItem()).toBe(centerItem1); expect(activePaneContainers).toEqual([]); expect(activePanes).toEqual([centerPane1]); expect(activeItems).toEqual([centerItem1]); }); }); describe('::onDidStopChangingActivePaneItem()', () => { it('invokes observers when the active item of the active pane stops changing', () => { const pane1 = atom.workspace.getCenter().getActivePane(); const pane2 = pane1.splitRight({ items: [document.createElement('div'), document.createElement('div')] }); atom.workspace .getLeftDock() .getActivePane() .addItem(document.createElement('div')); const emittedItems = []; atom.workspace.onDidStopChangingActivePaneItem(item => emittedItems.push(item) ); pane2.activateNextItem(); pane2.activateNextItem(); pane1.activate(); atom.workspace.getLeftDock().activate(); advanceClock(100); expect(emittedItems).toEqual([ atom.workspace.getLeftDock().getActivePaneItem() ]); }); }); describe('the grammar-used hook', () => { it('fires when opening a file or changing the grammar of an open file', async () => { await atom.packages.activatePackage('language-javascript'); await atom.packages.activatePackage('language-coffee-script'); const observeTextEditorsSpy = jasmine.createSpy('observeTextEditors'); const javascriptGrammarUsed = jasmine.createSpy('javascript'); const coffeeScriptGrammarUsed = jasmine.createSpy('coffeescript'); atom.packages.triggerDeferredActivationHooks(); atom.packages.onDidTriggerActivationHook( 'language-javascript:grammar-used', () => { atom.workspace.observeTextEditors(observeTextEditorsSpy); javascriptGrammarUsed(); } ); atom.packages.onDidTriggerActivationHook( 'language-coffee-script:grammar-used', coffeeScriptGrammarUsed ); expect(javascriptGrammarUsed).not.toHaveBeenCalled(); expect(observeTextEditorsSpy).not.toHaveBeenCalled(); const editor = await atom.workspace.open('sample.js', { autoIndent: false }); expect(javascriptGrammarUsed).toHaveBeenCalled(); expect(observeTextEditorsSpy.callCount).toBe(1); expect(coffeeScriptGrammarUsed).not.toHaveBeenCalled(); atom.grammars.assignLanguageMode(editor, 'source.coffee'); expect(coffeeScriptGrammarUsed).toHaveBeenCalled(); }); }); describe('the root-scope-used hook', () => { it('fires when opening a file or changing the grammar of an open file', async () => { await atom.packages.activatePackage('language-javascript'); await atom.packages.activatePackage('language-coffee-script'); const observeTextEditorsSpy = jasmine.createSpy('observeTextEditors'); const javascriptGrammarUsed = jasmine.createSpy('javascript'); const coffeeScriptGrammarUsed = jasmine.createSpy('coffeescript'); atom.packages.triggerDeferredActivationHooks(); atom.packages.onDidTriggerActivationHook( 'source.js:root-scope-used', () => { atom.workspace.observeTextEditors(observeTextEditorsSpy); javascriptGrammarUsed(); } ); atom.packages.onDidTriggerActivationHook( 'source.coffee:root-scope-used', coffeeScriptGrammarUsed ); expect(javascriptGrammarUsed).not.toHaveBeenCalled(); expect(observeTextEditorsSpy).not.toHaveBeenCalled(); const editor = await atom.workspace.open('sample.js', { autoIndent: false }); expect(javascriptGrammarUsed).toHaveBeenCalled(); expect(observeTextEditorsSpy.callCount).toBe(1); expect(coffeeScriptGrammarUsed).not.toHaveBeenCalled(); atom.grammars.assignLanguageMode(editor, 'source.coffee'); expect(coffeeScriptGrammarUsed).toHaveBeenCalled(); }); }); describe('::reopenItem()', () => { it("opens the uri associated with the last closed pane that isn't currently open", () => { const pane = workspace.getActivePane(); waitsForPromise(() => workspace .open('a') .then(() => workspace .open('b') .then(() => workspace.open('file1').then(() => workspace.open())) ) ); runs(() => { // does not reopen items with no uri expect(workspace.getActivePaneItem().getURI()).toBeUndefined(); pane.destroyActiveItem(); }); waitsForPromise(() => workspace.reopenItem()); const firstDirectory = atom.project.getDirectories()[0]; expect(firstDirectory).toBeDefined(); runs(() => { expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined(); // destroy all items expect(workspace.getActivePaneItem().getURI()).toBe( firstDirectory.resolve('file1') ); pane.destroyActiveItem(); expect(workspace.getActivePaneItem().getURI()).toBe( firstDirectory.resolve('b') ); pane.destroyActiveItem(); expect(workspace.getActivePaneItem().getURI()).toBe( firstDirectory.resolve('a') ); pane.destroyActiveItem(); // reopens items with uris expect(workspace.getActivePaneItem()).toBeUndefined(); }); waitsForPromise(() => workspace.reopenItem()); runs(() => expect(workspace.getActivePaneItem().getURI()).toBe( firstDirectory.resolve('a') ) ); // does not reopen items that are already open waitsForPromise(() => workspace.open('b')); runs(() => expect(workspace.getActivePaneItem().getURI()).toBe( firstDirectory.resolve('b') ) ); waitsForPromise(() => workspace.reopenItem()); runs(() => expect(workspace.getActivePaneItem().getURI()).toBe( firstDirectory.resolve('file1') ) ); }); }); describe('::increase/decreaseFontSize()', () => { it('increases/decreases the font size without going below 1', () => { atom.config.set('editor.fontSize', 1); workspace.increaseFontSize(); expect(atom.config.get('editor.fontSize')).toBe(2); workspace.increaseFontSize(); expect(atom.config.get('editor.fontSize')).toBe(3); workspace.decreaseFontSize(); expect(atom.config.get('editor.fontSize')).toBe(2); workspace.decreaseFontSize(); expect(atom.config.get('editor.fontSize')).toBe(1); workspace.decreaseFontSize(); expect(atom.config.get('editor.fontSize')).toBe(1); }); }); describe('::resetFontSize()', () => { it("resets the font size to the window's default font size", () => { const defaultFontSize = atom.config.get('editor.defaultFontSize'); workspace.increaseFontSize(); expect(atom.config.get('editor.fontSize')).toBe(defaultFontSize + 1); workspace.resetFontSize(); expect(atom.config.get('editor.fontSize')).toBe(defaultFontSize); workspace.decreaseFontSize(); expect(atom.config.get('editor.fontSize')).toBe(defaultFontSize - 1); workspace.resetFontSize(); expect(atom.config.get('editor.fontSize')).toBe(defaultFontSize); }); it('resets the font size the default font size when it is changed', () => { const defaultFontSize = atom.config.get('editor.defaultFontSize'); workspace.increaseFontSize(); expect(atom.config.get('editor.fontSize')).toBe(defaultFontSize + 1); atom.config.set('editor.defaultFontSize', 14); workspace.resetFontSize(); expect(atom.config.get('editor.fontSize')).toBe(14); }); it('does nothing if the font size has not been changed', () => { const originalFontSize = atom.config.get('editor.fontSize'); workspace.resetFontSize(); expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); }); it("resets the font size when the editor's font size changes", () => { const originalFontSize = atom.config.get('editor.fontSize'); atom.config.set('editor.fontSize', originalFontSize + 1); workspace.resetFontSize(); expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); atom.config.set('editor.fontSize', originalFontSize - 1); workspace.resetFontSize(); expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); }); }); describe('::openLicense()', () => { it('opens the license as plain-text in a buffer', () => { waitsForPromise(() => workspace.openLicense()); runs(() => expect(workspace.getActivePaneItem().getText()).toMatch(/Copyright/) ); }); }); describe('::isTextEditor(obj)', () => { it('returns true when the passed object is an instance of `TextEditor`', () => { expect(workspace.isTextEditor(new TextEditor())).toBe(true); expect(workspace.isTextEditor({ getText: () => null })).toBe(false); expect(workspace.isTextEditor(null)).toBe(false); expect(workspace.isTextEditor(undefined)).toBe(false); }); }); describe('::getActiveTextEditor()', () => { describe("when the workspace center's active pane item is a text editor", () => { describe('when the workspace center has focus', () => { it('returns the text editor', () => { const workspaceCenter = workspace.getCenter(); const editor = new TextEditor(); workspaceCenter.getActivePane().activateItem(editor); workspaceCenter.activate(); expect(workspace.getActiveTextEditor()).toBe(editor); }); }); describe('when a dock has focus', () => { it('returns the text editor', () => { const workspaceCenter = workspace.getCenter(); const editor = new TextEditor(); workspaceCenter.getActivePane().activateItem(editor); workspace.getLeftDock().activate(); expect(workspace.getActiveTextEditor()).toBe(editor); }); }); }); describe("when the workspace center's active pane item is not a text editor", () => { it('returns undefined', () => { const workspaceCenter = workspace.getCenter(); const nonEditorItem = document.createElement('div'); workspaceCenter.getActivePane().activateItem(nonEditorItem); expect(workspace.getActiveTextEditor()).toBeUndefined(); }); }); }); describe('::observeTextEditors()', () => { it('invokes the observer with current and future text editors', () => { const observed = []; waitsForPromise(() => workspace.open()); waitsForPromise(() => workspace.open()); waitsForPromise(() => workspace.openLicense()); runs(() => workspace.observeTextEditors(editor => observed.push(editor))); waitsForPromise(() => workspace.open()); expect(observed).toEqual(workspace.getTextEditors()); }); }); describe('::observeActiveTextEditor()', () => { it('invokes the observer with current active text editor and each time a different text editor becomes active', () => { const pane = workspace.getCenter().getActivePane(); const observed = []; const inactiveEditorBeforeRegisteringObserver = new TextEditor(); const activeEditorBeforeRegisteringObserver = new TextEditor(); pane.activateItem(inactiveEditorBeforeRegisteringObserver); pane.activateItem(activeEditorBeforeRegisteringObserver); workspace.observeActiveTextEditor(editor => observed.push(editor)); const editorAddedAfterRegisteringObserver = new TextEditor(); pane.activateItem(editorAddedAfterRegisteringObserver); expect(observed).toEqual([ activeEditorBeforeRegisteringObserver, editorAddedAfterRegisteringObserver ]); }); }); describe('::onDidChangeActiveTextEditor()', () => { let center, pane, observed; beforeEach(() => { center = workspace.getCenter(); pane = center.getActivePane(); observed = []; }); it("invokes the observer when a text editor becomes the workspace center's active pane item while a dock has focus", () => { workspace.onDidChangeActiveTextEditor(editor => observed.push(editor)); const dock = workspace.getLeftDock(); dock.activate(); expect(atom.workspace.getActivePaneContainer()).toBe(dock); const editor = new TextEditor(); center.getActivePane().activateItem(editor); expect(atom.workspace.getActivePaneContainer()).toBe(dock); expect(observed).toEqual([editor]); }); it('invokes the observer when the last text editor is closed', () => { const editor = new TextEditor(); pane.activateItem(editor); workspace.onDidChangeActiveTextEditor(editor => observed.push(editor)); pane.destroyItem(editor); expect(observed).toEqual([undefined]); }); it("invokes the observer when the workspace center's active pane item changes from an editor item to a non-editor item", () => { const editor = new TextEditor(); const nonEditorItem = document.createElement('div'); pane.activateItem(editor); workspace.onDidChangeActiveTextEditor(editor => observed.push(editor)); pane.activateItem(nonEditorItem); expect(observed).toEqual([undefined]); }); it("does not invoke the observer when the workspace center's active pane item changes from a non-editor item to another non-editor item", () => { workspace.onDidChangeActiveTextEditor(editor => observed.push(editor)); const nonEditorItem1 = document.createElement('div'); const nonEditorItem2 = document.createElement('div'); pane.activateItem(nonEditorItem1); pane.activateItem(nonEditorItem2); expect(observed).toEqual([]); }); it('invokes the observer when closing the one and only text editor after deserialization', async () => { pane.activateItem(new TextEditor()); simulateReload(); runs(() => { workspace.onDidChangeActiveTextEditor(editor => observed.push(editor)); workspace.closeActivePaneItemOrEmptyPaneOrWindow(); expect(observed).toEqual([undefined]); }); }); }); describe('when an editor is destroyed', () => { it('removes the editor', async () => { const editor = await workspace.open('a'); expect(workspace.getTextEditors()).toHaveLength(1); editor.destroy(); expect(workspace.getTextEditors()).toHaveLength(0); }); }); describe('when an editor is copied because its pane is split', () => { it('sets up the new editor to be configured by the text editor registry', async () => { await atom.packages.activatePackage('language-javascript'); const editor = await workspace.open('a'); atom.grammars.assignLanguageMode(editor, 'source.js'); expect(editor.getGrammar().name).toBe('JavaScript'); workspace.getActivePane().splitRight({ copyActiveItem: true }); const newEditor = workspace.getActiveTextEditor(); expect(newEditor).not.toBe(editor); expect(newEditor.getGrammar().name).toBe('JavaScript'); }); }); it('stores the active grammars used by all the open editors', () => { waitsForPromise(() => atom.packages.activatePackage('language-javascript')); waitsForPromise(() => atom.packages.activatePackage('language-coffee-script') ); waitsForPromise(() => atom.packages.activatePackage('language-todo')); waitsForPromise(() => atom.workspace.open('sample.coffee')); runs(() => { atom.workspace.getActiveTextEditor().setText(dedent` i = /test/; #FIXME\ `); const atom2 = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate }); atom2.initialize({ window: document.createElement('div'), document: Object.assign(document.createElement('div'), { body: document.createElement('div'), head: document.createElement('div') }) }); atom2.packages.loadPackage('language-javascript'); atom2.packages.loadPackage('language-coffee-script'); atom2.packages.loadPackage('language-todo'); atom2.project.deserialize(atom.project.serialize()); atom2.workspace.deserialize( atom.workspace.serialize(), atom2.deserializers ); expect( atom2.grammars .getGrammars({ includeTreeSitter: true }) .map(grammar => grammar.scopeName) .sort() ).toEqual([ 'source.coffee', 'source.js', // Tree-sitter grammars also load 'source.js', 'source.js.regexp', 'source.js.regexp', 'source.js.regexp.replacement', 'source.jsdoc', 'source.jsdoc', 'source.litcoffee', 'text.plain.null-grammar', 'text.todo' ]); atom2.destroy(); }); }); describe('document.title', () => { describe('when there is no item open', () => { it('sets the title to the project path', () => expect(document.title).toMatch( escapeStringRegex(fs.tildify(atom.project.getPaths()[0])) )); it("sets the title to 'untitled' if there is no project path", () => { atom.project.setPaths([]); expect(document.title).toMatch(/^untitled/); }); }); describe("when the active pane item's path is not inside a project path", () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('b').then(() => atom.project.setPaths([])) ) ); it("sets the title to the pane item's title plus the item's path", () => { const item = atom.workspace.getActivePaneItem(); const pathEscaped = fs.tildify( escapeStringRegex(path.dirname(item.getPath())) ); expect(document.title).toMatch( new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`) ); }); describe('when the title of the active pane item changes', () => { it("updates the window title based on the item's new title", () => { const editor = atom.workspace.getActivePaneItem(); editor.buffer.setPath(path.join(temp.dir, 'hi')); const pathEscaped = fs.tildify( escapeStringRegex(path.dirname(editor.getPath())) ); expect(document.title).toMatch( new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`) ); }); }); describe("when the active pane's item changes", () => { it("updates the title to the new item's title plus the project path", () => { atom.workspace.getActivePane().activateNextItem(); const item = atom.workspace.getActivePaneItem(); const pathEscaped = fs.tildify( escapeStringRegex(path.dirname(item.getPath())) ); expect(document.title).toMatch( new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`) ); }); }); describe("when an inactive pane's item changes", () => { it('does not update the title', () => { const pane = atom.workspace.getActivePane(); pane.splitRight(); const initialTitle = document.title; pane.activateNextItem(); expect(document.title).toBe(initialTitle); }); }); }); describe('when the active pane item is inside a project path', () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('b'))); describe('when there is an active pane item', () => { it("sets the title to the pane item's title plus the project path", () => { const item = atom.workspace.getActivePaneItem(); const pathEscaped = fs.tildify( escapeStringRegex(atom.project.getPaths()[0]) ); expect(document.title).toMatch( new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`) ); }); }); describe('when the title of the active pane item changes', () => { it("updates the window title based on the item's new title", () => { const editor = atom.workspace.getActivePaneItem(); editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')); const pathEscaped = fs.tildify( escapeStringRegex(atom.project.getPaths()[0]) ); expect(document.title).toMatch( new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`) ); }); }); describe("when the active pane's item changes", () => { it("updates the title to the new item's title plus the project path", () => { atom.workspace.getActivePane().activateNextItem(); const item = atom.workspace.getActivePaneItem(); const pathEscaped = fs.tildify( escapeStringRegex(atom.project.getPaths()[0]) ); expect(document.title).toMatch( new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`) ); }); }); describe('when the last pane item is removed', () => { it("updates the title to the project's first path", () => { atom.workspace.getActivePane().destroy(); expect(atom.workspace.getActivePaneItem()).toBeUndefined(); expect(document.title).toMatch( escapeStringRegex(fs.tildify(atom.project.getPaths()[0])) ); }); }); describe("when an inactive pane's item changes", () => { it('does not update the title', () => { const pane = atom.workspace.getActivePane(); pane.splitRight(); const initialTitle = document.title; pane.activateNextItem(); expect(document.title).toBe(initialTitle); }); }); }); describe('when the workspace is deserialized', () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('a'))); it("updates the title to contain the project's path", () => { document.title = null; const atom2 = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate }); atom2.initialize({ window: document.createElement('div'), document: Object.assign(document.createElement('div'), { body: document.createElement('div'), head: document.createElement('div') }) }); waitsForPromise(() => atom2.project.deserialize(atom.project.serialize()) ); runs(() => { atom2.workspace.deserialize( atom.workspace.serialize(), atom2.deserializers ); const item = atom2.workspace.getActivePaneItem(); const pathEscaped = fs.tildify( escapeStringRegex(atom.project.getPaths()[0]) ); expect(document.title).toMatch( new RegExp(`^${item.getLongTitle()} \\u2014 ${pathEscaped}`) ); atom2.destroy(); }); }); }); }); describe('document edited status', () => { let item1; let item2; beforeEach(() => { waitsForPromise(() => atom.workspace.open('a')); waitsForPromise(() => atom.workspace.open('b')); runs(() => { [item1, item2] = atom.workspace.getPaneItems(); }); }); it('calls setDocumentEdited when the active item changes', () => { expect(atom.workspace.getActivePaneItem()).toBe(item2); item1.insertText('a'); expect(item1.isModified()).toBe(true); atom.workspace.getActivePane().activateNextItem(); expect(setDocumentEdited).toHaveBeenCalledWith(true); }); it("calls atom.setDocumentEdited when the active item's modified status changes", () => { expect(atom.workspace.getActivePaneItem()).toBe(item2); item2.insertText('a'); advanceClock(item2.getBuffer().getStoppedChangingDelay()); expect(item2.isModified()).toBe(true); expect(setDocumentEdited).toHaveBeenCalledWith(true); item2.undo(); advanceClock(item2.getBuffer().getStoppedChangingDelay()); expect(item2.isModified()).toBe(false); expect(setDocumentEdited).toHaveBeenCalledWith(false); }); }); describe('adding panels', () => { class TestItem {} // Don't use ES6 classes because then we'll have to call `super()` which we can't do with // HTMLElement function TestItemElement() { this.constructor = TestItemElement; } function Ctor() { this.constructor = TestItemElement; } Ctor.prototype = HTMLElement.prototype; TestItemElement.prototype = new Ctor(); TestItemElement.__super__ = HTMLElement.prototype; TestItemElement.prototype.initialize = function(model) { this.model = model; return this; }; TestItemElement.prototype.getModel = function() { return this.model; }; beforeEach(() => atom.views.addViewProvider(TestItem, model => new TestItemElement().initialize(model) ) ); describe('::addLeftPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy; expect(atom.workspace.getLeftPanels().length).toBe(0); atom.workspace.panelContainers.left.onDidAddPanel( (addPanelSpy = jasmine.createSpy()) ); const model = new TestItem(); const panel = atom.workspace.addLeftPanel({ item: model }); expect(panel).toBeDefined(); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); const itemView = atom.views.getView( atom.workspace.getLeftPanels()[0].getItem() ); expect(itemView instanceof TestItemElement).toBe(true); expect(itemView.getModel()).toBe(model); }); }); describe('::addRightPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy; expect(atom.workspace.getRightPanels().length).toBe(0); atom.workspace.panelContainers.right.onDidAddPanel( (addPanelSpy = jasmine.createSpy()) ); const model = new TestItem(); const panel = atom.workspace.addRightPanel({ item: model }); expect(panel).toBeDefined(); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); const itemView = atom.views.getView( atom.workspace.getRightPanels()[0].getItem() ); expect(itemView instanceof TestItemElement).toBe(true); expect(itemView.getModel()).toBe(model); }); }); describe('::addTopPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy; expect(atom.workspace.getTopPanels().length).toBe(0); atom.workspace.panelContainers.top.onDidAddPanel( (addPanelSpy = jasmine.createSpy()) ); const model = new TestItem(); const panel = atom.workspace.addTopPanel({ item: model }); expect(panel).toBeDefined(); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); const itemView = atom.views.getView( atom.workspace.getTopPanels()[0].getItem() ); expect(itemView instanceof TestItemElement).toBe(true); expect(itemView.getModel()).toBe(model); }); }); describe('::addBottomPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy; expect(atom.workspace.getBottomPanels().length).toBe(0); atom.workspace.panelContainers.bottom.onDidAddPanel( (addPanelSpy = jasmine.createSpy()) ); const model = new TestItem(); const panel = atom.workspace.addBottomPanel({ item: model }); expect(panel).toBeDefined(); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); const itemView = atom.views.getView( atom.workspace.getBottomPanels()[0].getItem() ); expect(itemView instanceof TestItemElement).toBe(true); expect(itemView.getModel()).toBe(model); }); }); describe('::addHeaderPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy; expect(atom.workspace.getHeaderPanels().length).toBe(0); atom.workspace.panelContainers.header.onDidAddPanel( (addPanelSpy = jasmine.createSpy()) ); const model = new TestItem(); const panel = atom.workspace.addHeaderPanel({ item: model }); expect(panel).toBeDefined(); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); const itemView = atom.views.getView( atom.workspace.getHeaderPanels()[0].getItem() ); expect(itemView instanceof TestItemElement).toBe(true); expect(itemView.getModel()).toBe(model); }); }); describe('::addFooterPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy; expect(atom.workspace.getFooterPanels().length).toBe(0); atom.workspace.panelContainers.footer.onDidAddPanel( (addPanelSpy = jasmine.createSpy()) ); const model = new TestItem(); const panel = atom.workspace.addFooterPanel({ item: model }); expect(panel).toBeDefined(); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); const itemView = atom.views.getView( atom.workspace.getFooterPanels()[0].getItem() ); expect(itemView instanceof TestItemElement).toBe(true); expect(itemView.getModel()).toBe(model); }); }); describe('::addModalPanel(model)', () => { it('adds a panel to the correct panel container', () => { let addPanelSpy; expect(atom.workspace.getModalPanels().length).toBe(0); atom.workspace.panelContainers.modal.onDidAddPanel( (addPanelSpy = jasmine.createSpy()) ); const model = new TestItem(); const panel = atom.workspace.addModalPanel({ item: model }); expect(panel).toBeDefined(); expect(addPanelSpy).toHaveBeenCalledWith({ panel, index: 0 }); const itemView = atom.views.getView( atom.workspace.getModalPanels()[0].getItem() ); expect(itemView instanceof TestItemElement).toBe(true); expect(itemView.getModel()).toBe(model); }); }); describe('::panelForItem(item)', () => { it('returns the panel associated with the item', () => { const item = new TestItem(); const panel = atom.workspace.addLeftPanel({ item }); const itemWithNoPanel = new TestItem(); expect(atom.workspace.panelForItem(item)).toBe(panel); expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe(null); }); }); }); for (const ripgrep of [true, false]) { describe(`::scan(regex, options, callback) { ripgrep: ${ripgrep} }`, () => { function scan(regex, options, iterator) { return atom.workspace.scan(regex, { ...options, ripgrep }, iterator); } describe('when called with a regex', () => { it('calls the callback with all regex results in all files in the project', async () => { const results = []; await scan( /(a)+/, { leadingContextLineCount: 1, trailingContextLineCount: 1 }, result => results.push(result) ); results.sort((a, b) => a.filePath.localeCompare(b.filePath)); expect(results.length).toBeGreaterThan(0); expect(results[0].filePath).toBe( atom.project.getDirectories()[0].resolve('a') ); expect(results[0].matches).toHaveLength(3); expect(results[0].matches[0]).toEqual({ matchText: 'aaa', lineText: 'aaa bbb', lineTextOffset: 0, range: [[0, 0], [0, 3]], leadingContextLines: [], trailingContextLines: ['cc aa cc'] }); }); it('works with with escaped literals (like $ and ^)', async () => { const results = []; await scan( /\$\w+/, { leadingContextLineCount: 1, trailingContextLineCount: 1 }, result => results.push(result) ); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe(atom.project.getDirectories()[0].resolve('a')); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: '$bill', lineText: 'dollar$bill', lineTextOffset: 0, range: [[2, 6], [2, 11]], leadingContextLines: ['cc aa cc'], trailingContextLines: [] }); }); it('works on evil filenames', async () => { atom.config.set('core.excludeVcsIgnoredPaths', false); platform.generateEvilFiles(); atom.project.setPaths([ path.join(__dirname, 'fixtures', 'evil-files') ]); const paths = []; let matches = []; await scan(/evil/, {}, result => { paths.push(result.filePath); matches = matches.concat(result.matches); }); // Sort the paths to make the test deterministic. paths.sort(); _.each(matches, m => expect(m.matchText).toEqual('evil')); if (platform.isWindows()) { expect(paths.length).toBe(3); expect(paths[0]).toMatch(/a_file_with_utf8.txt$/); expect(paths[1]).toMatch(/file with spaces.txt$/); expect(path.basename(paths[2])).toBe('utfa\u0306.md'); } else { expect(paths.length).toBe(5); expect(paths[0]).toMatch(/a_file_with_utf8.txt$/); expect(paths[1]).toMatch(/file with spaces.txt$/); expect(paths[2]).toMatch(/goddam\nnewlines$/m); expect(paths[3]).toMatch(/quote".txt$/m); expect(path.basename(paths[4])).toBe('utfa\u0306.md'); } }); it('ignores case if the regex includes the `i` flag', async () => { const results = []; await scan(/DOLLAR/i, {}, result => results.push(result)); expect(results).toHaveLength(1); }); if (ripgrep) { it('returns empty text matches', async () => { const results = []; await scan( /^\s{0}/, { paths: [`oh-git`] }, result => results.push(result) ); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project .getDirectories()[0] .resolve(path.join('a-dir', 'oh-git')) ); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: '', lineText: 'bbb aaaa', lineTextOffset: 0, range: [[0, 0], [0, 0]], leadingContextLines: [], trailingContextLines: [] }); }); describe('newlines on regexps', async () => { it('returns multiline results from regexps', async () => { const results = []; await scan(/first\nsecond/, {}, result => results.push(result)); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project .getDirectories()[0] .resolve('file-with-newline-literal') ); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: 'first\nsecond', lineText: 'first\nsecond\\nthird', lineTextOffset: 0, range: [[3, 0], [4, 6]], leadingContextLines: [], trailingContextLines: [] }); }); it('returns correctly the context lines', async () => { const results = []; await scan( /first\nsecond/, { leadingContextLineCount: 2, trailingContextLineCount: 2 }, result => results.push(result) ); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project .getDirectories()[0] .resolve('file-with-newline-literal') ); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: 'first\nsecond', lineText: 'first\nsecond\\nthird', lineTextOffset: 0, range: [[3, 0], [4, 6]], leadingContextLines: ['newline2', 'newline3'], trailingContextLines: ['newline4', 'newline5'] }); }); it('returns multiple results from the same line', async () => { const results = []; await scan(/line\d\nne/, {}, result => results.push(result)); results.sort((a, b) => a.filePath.localeCompare(b.filePath)); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project .getDirectories()[0] .resolve('file-with-newline-literal') ); expect(matches).toHaveLength(3); expect(matches[0]).toEqual({ matchText: 'line1\nne', lineText: 'newline1\nnewline2', lineTextOffset: 0, range: [[0, 3], [1, 2]], leadingContextLines: [], trailingContextLines: [] }); expect(matches[1]).toEqual({ matchText: 'line2\nne', lineText: 'newline2\nnewline3', lineTextOffset: 0, range: [[1, 3], [2, 2]], leadingContextLines: [], trailingContextLines: [] }); expect(matches[2]).toEqual({ matchText: 'line4\nne', lineText: 'newline4\nnewline5', lineTextOffset: 0, range: [[5, 3], [6, 2]], leadingContextLines: [], trailingContextLines: [] }); }); it('works with escaped newlines', async () => { const results = []; await scan(/second\\nthird/, {}, result => results.push(result)); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project .getDirectories()[0] .resolve('file-with-newline-literal') ); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: 'second\\nthird', lineText: 'second\\nthird', lineTextOffset: 0, range: [[4, 0], [4, 13]], leadingContextLines: [], trailingContextLines: [] }); }); it('matches a regexp ending with a newline', async () => { const results = []; await scan(/newline3\n/, {}, result => results.push(result)); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project .getDirectories()[0] .resolve('file-with-newline-literal') ); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: 'newline3\n', lineText: 'newline3', lineTextOffset: 0, range: [[2, 0], [3, 0]], leadingContextLines: [], trailingContextLines: [] }); }); }); describe('pcre2 enabled', async () => { it('supports lookbehind searches', async () => { const results = []; await scan(/(? results.push(result) ); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project.getDirectories()[0].resolve('a') ); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: 'aa', lineText: 'cc aa cc', lineTextOffset: 0, range: [[1, 3], [1, 5]], leadingContextLines: [], trailingContextLines: [] }); }); }); } it('returns results on lines with unicode strings', async () => { const results = []; await scan(/line with unico/, {}, result => results.push(result)); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project.getDirectories()[0].resolve('file-with-unicode') ); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: 'line with unico', lineText: 'ДДДДДДДДДДДДДДДДДД line with unicode', lineTextOffset: 0, range: [[0, 19], [0, 34]], leadingContextLines: [], trailingContextLines: [] }); }); it('returns results on files detected as binary', async () => { const results = []; await scan( /asciiProperty=Foo/, { trailingContextLineCount: 2 }, result => results.push(result) ); expect(results.length).toBe(1); const { filePath, matches } = results[0]; expect(filePath).toBe( atom.project.getDirectories()[0].resolve('file-detected-as-binary') ); expect(matches).toHaveLength(1); expect(matches[0]).toEqual({ matchText: 'asciiProperty=Foo', lineText: 'asciiProperty=Foo', lineTextOffset: 0, range: [[0, 0], [0, 17]], leadingContextLines: [], trailingContextLines: ['utf8Property=Fòò', 'latin1Property=F��'] }); }); describe('when the core.excludeVcsIgnoredPaths config is used', () => { let projectPath; let ignoredPath; beforeEach(async () => { const sourceProjectPath = path.join( __dirname, 'fixtures', 'git', 'working-dir' ); projectPath = path.join(temp.mkdirSync('atom')); const writerStream = fstream.Writer(projectPath); fstream.Reader(sourceProjectPath).pipe(writerStream); await new Promise(resolve => { writerStream.on('close', resolve); writerStream.on('error', resolve); }); fs.renameSync( path.join(projectPath, 'git.git'), path.join(projectPath, '.git') ); ignoredPath = path.join(projectPath, 'ignored.txt'); fs.writeFileSync(ignoredPath, 'this match should not be included'); }); afterEach(() => { if (fs.existsSync(projectPath)) { fs.removeSync(projectPath); } }); it('excludes ignored files when core.excludeVcsIgnoredPaths is true', async () => { atom.project.setPaths([projectPath]); atom.config.set('core.excludeVcsIgnoredPaths', true); const resultHandler = jasmine.createSpy('result found'); await scan(/match/, {}, ({ filePath }) => resultHandler(filePath)); expect(resultHandler).not.toHaveBeenCalled(); }); it('does not exclude ignored files when core.excludeVcsIgnoredPaths is false', async () => { atom.project.setPaths([projectPath]); atom.config.set('core.excludeVcsIgnoredPaths', false); const resultHandler = jasmine.createSpy('result found'); await scan(/match/, {}, ({ filePath }) => resultHandler(filePath)); expect(resultHandler).toHaveBeenCalledWith( path.join(projectPath, 'ignored.txt') ); }); it('does not exclude files when searching on an ignored folder even when core.excludeVcsIgnoredPaths is true', async () => { fs.mkdirSync(path.join(projectPath, 'poop')); ignoredPath = path.join( path.join(projectPath, 'poop', 'whatever.txt') ); fs.writeFileSync(ignoredPath, 'this match should be included'); atom.project.setPaths([projectPath]); atom.config.set('core.excludeVcsIgnoredPaths', true); const resultHandler = jasmine.createSpy('result found'); await scan(/match/, { paths: ['poop'] }, ({ filePath }) => resultHandler(filePath) ); expect(resultHandler).toHaveBeenCalledWith(ignoredPath); }); }); describe('when the core.followSymlinks config is used', () => { let projectPath; beforeEach(async () => { const sourceProjectPath = path.join( __dirname, 'fixtures', 'dir', 'a-dir' ); projectPath = path.join(temp.mkdirSync('atom')); const writerStream = fstream.Writer(projectPath); fstream.Reader(sourceProjectPath).pipe(writerStream); await new Promise(resolve => { writerStream.on('close', resolve); writerStream.on('error', resolve); }); fs.symlinkSync( path.join(__dirname, 'fixtures', 'dir', 'b'), path.join(projectPath, 'symlink') ); }); afterEach(() => { if (fs.existsSync(projectPath)) { fs.removeSync(projectPath); } }); it('follows symlinks when core.followSymlinks is true', async () => { atom.project.setPaths([projectPath]); atom.config.set('core.followSymlinks', true); const resultHandler = jasmine.createSpy('result found'); await scan(/ccc/, {}, ({ filePath }) => resultHandler(filePath)); expect(resultHandler).toHaveBeenCalledWith( path.join(projectPath, 'symlink') ); }); it('does not follow symlinks when core.followSymlinks is false', async () => { atom.project.setPaths([projectPath]); atom.config.set('core.followSymlinks', false); const resultHandler = jasmine.createSpy('result found'); await scan(/ccc/, {}, ({ filePath }) => resultHandler(filePath)); expect(resultHandler).not.toHaveBeenCalled(); }); }); describe('when there are hidden files', () => { let projectPath; beforeEach(async () => { const sourceProjectPath = path.join( __dirname, 'fixtures', 'dir', 'a-dir' ); projectPath = path.join(temp.mkdirSync('atom')); const writerStream = fstream.Writer(projectPath); fstream.Reader(sourceProjectPath).pipe(writerStream); await new Promise(resolve => { writerStream.on('close', resolve); writerStream.on('error', resolve); }); // Note: This won't create a hidden file on Windows, in order to more // accurately test this behaviour there, we should either use a package // like `fswin` or manually spawn an `ATTRIB` command. fs.writeFileSync(path.join(projectPath, '.hidden'), 'ccc'); }); afterEach(() => { if (fs.existsSync(projectPath)) { fs.removeSync(projectPath); } }); it('searches on hidden files', async () => { atom.project.setPaths([projectPath]); const resultHandler = jasmine.createSpy('result found'); await scan(/ccc/, {}, ({ filePath }) => resultHandler(filePath)); expect(resultHandler).toHaveBeenCalledWith( path.join(projectPath, '.hidden') ); }); }); it('includes only files when a directory filter is specified', async () => { const projectPath = path.join( path.join(__dirname, 'fixtures', 'dir') ); atom.project.setPaths([projectPath]); const filePath = path.join(projectPath, 'a-dir', 'oh-git'); const paths = []; let matches = []; await scan(/aaa/, { paths: [`a-dir${path.sep}`] }, result => { paths.push(result.filePath); matches = matches.concat(result.matches); }); expect(paths.length).toBe(1); expect(paths[0]).toBe(filePath); expect(matches.length).toBe(1); }); it("includes files and folders that begin with a '.'", async () => { const projectPath = temp.mkdirSync('atom-spec-workspace'); const filePath = path.join(projectPath, '.text'); fs.writeFileSync(filePath, 'match this'); atom.project.setPaths([projectPath]); const paths = []; let matches = []; await scan(/match this/, {}, result => { paths.push(result.filePath); matches = matches.concat(result.matches); }); expect(paths.length).toBe(1); expect(paths[0]).toBe(filePath); expect(matches.length).toBe(1); }); it('excludes values in core.ignoredNames', async () => { const ignoredNames = atom.config.get('core.ignoredNames'); ignoredNames.push('a'); atom.config.set('core.ignoredNames', ignoredNames); const resultHandler = jasmine.createSpy('result found'); await scan(/dollar/, {}, () => resultHandler()); expect(resultHandler).not.toHaveBeenCalled(); }); it('scans buffer contents if the buffer is modified', async () => { const results = []; const editor = await atom.workspace.open('a'); editor.setText('Elephant'); await scan(/a|Elephant/, {}, result => results.push(result)); expect(results.length).toBeGreaterThan(0); const resultForA = _.find( results, ({ filePath }) => path.basename(filePath) === 'a' ); expect(resultForA.matches).toHaveLength(1); expect(resultForA.matches[0].matchText).toBe('Elephant'); }); it('ignores buffers outside the project', async () => { const results = []; const editor = await atom.workspace.open(temp.openSync().path); editor.setText('Elephant'); await scan(/Elephant/, {}, result => results.push(result)); expect(results).toHaveLength(0); }); describe('when the project has multiple root directories', () => { let dir1; let dir2; let file1; let file2; beforeEach(() => { dir1 = atom.project.getPaths()[0]; file1 = path.join(dir1, 'a-dir', 'oh-git'); dir2 = temp.mkdirSync('a-second-dir'); const aDir2 = path.join(dir2, 'a-dir'); file2 = path.join(aDir2, 'a-file'); fs.mkdirSync(aDir2); fs.writeFileSync(file2, 'ccc aaaa'); atom.project.addPath(dir2); }); it("searches matching files in all of the project's root directories", async () => { const resultPaths = []; await scan(/aaaa/, {}, ({ filePath }) => resultPaths.push(filePath) ); expect(resultPaths.sort()).toEqual([file1, file2].sort()); }); describe('when an inclusion path starts with the basename of a root directory', () => { it('interprets the inclusion path as starting from that directory', async () => { let resultPaths = []; await scan(/aaaa/, { paths: ['dir'] }, ({ filePath }) => { if (!resultPaths.includes(filePath)) { resultPaths.push(filePath); } }); expect(resultPaths).toEqual([file1]); resultPaths = []; await scan( /aaaa/, { paths: [path.join('dir', 'a-dir')] }, ({ filePath }) => { if (!resultPaths.includes(filePath)) { resultPaths.push(filePath); } } ); expect(resultPaths).toEqual([file1]); resultPaths = []; await scan( /aaaa/, { paths: [path.basename(dir2)] }, ({ filePath }) => { if (!resultPaths.includes(filePath)) { resultPaths.push(filePath); } } ); expect(resultPaths).toEqual([file2]); resultPaths = []; await scan( /aaaa/, { paths: [path.join(path.basename(dir2), 'a-dir')] }, ({ filePath }) => { if (!resultPaths.includes(filePath)) { resultPaths.push(filePath); } } ); expect(resultPaths).toEqual([file2]); }); }); describe('when a custom directory searcher is registered', () => { let fakeSearch = null; // Function that is invoked once all of the fields on fakeSearch are set. let onFakeSearchCreated = null; class FakeSearch { constructor(options) { // Note that hoisting resolve and reject in this way is generally frowned upon. this.options = options; this.promise = new Promise((resolve, reject) => { this.hoistedResolve = resolve; this.hoistedReject = reject; if (typeof onFakeSearchCreated === 'function') { onFakeSearchCreated(this); } }); } then(...args) { return this.promise.then.apply(this.promise, args); } cancel() { this.cancelled = true; // According to the spec for a DirectorySearcher, invoking `cancel()` should // resolve the thenable rather than reject it. this.hoistedResolve(); } } beforeEach(() => { fakeSearch = null; onFakeSearchCreated = null; atom.packages.serviceHub.provide( 'atom.directory-searcher', '0.1.0', { canSearchDirectory(directory) { return directory.getPath() === dir1; }, search(directory, regex, options) { fakeSearch = new FakeSearch(options); return fakeSearch; } } ); waitsFor(() => atom.workspace.directorySearchers.length > 0); }); it('can override the DefaultDirectorySearcher on a per-directory basis', async () => { const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt'; const numPathsSearchedInDir2 = 1; const numPathsToPretendToSearchInCustomDirectorySearcher = 10; const searchResult = { filePath: foreignFilePath, matches: [ { lineText: 'Hello world', lineTextOffset: 0, matchText: 'Hello', range: [[0, 0], [0, 5]] } ] }; onFakeSearchCreated = fakeSearch => { fakeSearch.options.didMatch(searchResult); fakeSearch.options.didSearchPaths( numPathsToPretendToSearchInCustomDirectorySearcher ); fakeSearch.hoistedResolve(); }; const resultPaths = []; const onPathsSearched = jasmine.createSpy('onPathsSearched'); await scan(/aaaa/, { onPathsSearched }, ({ filePath }) => resultPaths.push(filePath) ); expect(resultPaths.sort()).toEqual( [foreignFilePath, file2].sort() ); // onPathsSearched should be called once by each DirectorySearcher. The order is not // guaranteed, so we can only verify the total number of paths searched is correct // after the second call. expect(onPathsSearched.callCount).toBe(2); expect(onPathsSearched.mostRecentCall.args[0]).toBe( numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2 ); }); it('can be cancelled when the object returned by scan() has its cancel() method invoked', async () => { const thenable = scan(/aaaa/, {}, () => {}); let resultOfPromiseSearch = null; waitsFor('fakeSearch to be defined', () => fakeSearch != null); runs(() => { expect(fakeSearch.cancelled).toBe(undefined); thenable.cancel(); expect(fakeSearch.cancelled).toBe(true); }); waitsForPromise(() => thenable.then(promiseResult => { resultOfPromiseSearch = promiseResult; }) ); runs(() => expect(resultOfPromiseSearch).toBe('cancelled')); }); it('will have the side-effect of failing the overall search if it fails', () => { // This provider's search should be cancelled when the first provider fails let cancelableSearch; let fakeSearch2 = null; atom.packages.serviceHub.provide( 'atom.directory-searcher', '0.1.0', { canSearchDirectory(directory) { return directory.getPath() === dir2; }, search(directory, regex, options) { fakeSearch2 = new FakeSearch(options); return fakeSearch2; } } ); let didReject = false; const promise = (cancelableSearch = scan(/aaaa/, () => {})); waitsFor('fakeSearch to be defined', () => fakeSearch != null); runs(() => fakeSearch.hoistedReject()); waitsForPromise(() => cancelableSearch.catch(() => { didReject = true; }) ); waitsFor(done => promise.then(null, done)); runs(() => { expect(didReject).toBe(true); expect(fakeSearch2.cancelled).toBe(true); }); }); }); }); }); describe('leadingContextLineCount and trailingContextLineCount options', () => { async function search({ leadingContextLineCount, trailingContextLineCount }) { const results = []; await scan( /result/, { leadingContextLineCount, trailingContextLineCount }, result => results.push(result) ); return { leadingContext: results[0].matches.map( result => result.leadingContextLines ), trailingContext: results[0].matches.map( result => result.trailingContextLines ) }; } const expectedLeadingContext = [ ['line 1', 'line 2', 'line 3', 'line 4', 'line 5'], ['line 6', 'line 7', 'line 8', 'line 9', 'line 10'], ['line 7', 'line 8', 'line 9', 'line 10', 'result 2'], ['line 10', 'result 2', 'result 3', 'line 11', 'line 12'] ]; const expectedTrailingContext = [ ['line 6', 'line 7', 'line 8', 'line 9', 'line 10'], ['result 3', 'line 11', 'line 12', 'result 4', 'line 13'], ['line 11', 'line 12', 'result 4', 'line 13', 'line 14'], ['line 13', 'line 14', 'line 15'] ]; it('returns valid contexts no matter how many lines are requested', async () => { expect(await search({})).toEqual({ leadingContext: [[], [], [], []], trailingContext: [[], [], [], []] }); expect( await search({ leadingContextLineCount: 1, trailingContextLineCount: 1 }) ).toEqual({ leadingContext: expectedLeadingContext.map(result => result.slice(-1) ), trailingContext: expectedTrailingContext.map(result => result.slice(0, 1) ) }); expect( await search({ leadingContextLineCount: 2, trailingContextLineCount: 2 }) ).toEqual({ leadingContext: expectedLeadingContext.map(result => result.slice(-2) ), trailingContext: expectedTrailingContext.map(result => result.slice(0, 2) ) }); expect( await search({ leadingContextLineCount: 5, trailingContextLineCount: 5 }) ).toEqual({ leadingContext: expectedLeadingContext.map(result => result.slice(-5) ), trailingContext: expectedTrailingContext.map(result => result.slice(0, 5) ) }); expect( await search({ leadingContextLineCount: 2, trailingContextLineCount: 3 }) ).toEqual({ leadingContext: expectedLeadingContext.map(result => result.slice(-2) ), trailingContext: expectedTrailingContext.map(result => result.slice(0, 3) ) }); }); }); }); // Cancels other ongoing searches } describe('::replace(regex, replacementText, paths, iterator)', () => { let fixturesDir, projectDir; beforeEach(() => { fixturesDir = path.dirname(atom.project.getPaths()[0]); projectDir = temp.mkdirSync('atom'); atom.project.setPaths([projectDir]); }); describe("when a file doesn't exist", () => { it('calls back with an error', () => { const errors = []; const missingPath = path.resolve('/not-a-file.js'); expect(fs.existsSync(missingPath)).toBeFalsy(); waitsForPromise(() => atom.workspace.replace( /items/gi, 'items', [missingPath], (result, error) => errors.push(error) ) ); runs(() => { expect(errors).toHaveLength(1); expect(errors[0].path).toBe(missingPath); }); }); }); describe('when called with unopened files', () => { it('replaces properly', () => { const filePath = path.join(projectDir, 'sample.js'); fs.copyFileSync(path.join(fixturesDir, 'sample.js'), filePath); const results = []; waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result) ) ); runs(() => { expect(results).toHaveLength(1); expect(results[0].filePath).toBe(filePath); expect(results[0].replacements).toBe(6); }); }); it('does not discard the multiline flag', () => { const filePath = path.join(projectDir, 'sample.js'); fs.copyFileSync(path.join(fixturesDir, 'sample.js'), filePath); const results = []; waitsForPromise(() => atom.workspace.replace(/;$/gim, 'items', [filePath], result => results.push(result) ) ); runs(() => { expect(results).toHaveLength(1); expect(results[0].filePath).toBe(filePath); expect(results[0].replacements).toBe(8); }); }); }); describe('when a buffer is already open', () => { it('replaces properly and saves when not modified', () => { const filePath = path.join(projectDir, 'sample.js'); fs.copyFileSync( path.join(fixturesDir, 'sample.js'), path.join(projectDir, 'sample.js') ); let editor = null; const results = []; waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o; }) ); runs(() => expect(editor.isModified()).toBeFalsy()); waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result) ) ); runs(() => { expect(results).toHaveLength(1); expect(results[0].filePath).toBe(filePath); expect(results[0].replacements).toBe(6); expect(editor.isModified()).toBeFalsy(); }); }); it('does not replace when the path is not specified', () => { const filePath = path.join(projectDir, 'sample.js'); const commentFilePath = path.join( projectDir, 'sample-with-comments.js' ); fs.copyFileSync(path.join(fixturesDir, 'sample.js'), filePath); fs.copyFileSync( path.join(fixturesDir, 'sample-with-comments.js'), path.join(projectDir, 'sample-with-comments.js') ); const results = []; waitsForPromise(() => atom.workspace.open('sample-with-comments.js')); waitsForPromise(() => atom.workspace.replace( /items/gi, 'items', [commentFilePath], result => results.push(result) ) ); runs(() => { expect(results).toHaveLength(1); expect(results[0].filePath).toBe(commentFilePath); }); }); it('does NOT save when modified', () => { const filePath = path.join(projectDir, 'sample.js'); fs.copyFileSync(path.join(fixturesDir, 'sample.js'), filePath); let editor = null; const results = []; waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o; }) ); runs(() => { editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg'); expect(editor.isModified()).toBeTruthy(); }); waitsForPromise(() => atom.workspace.replace(/items/gi, 'okthen', [filePath], result => results.push(result) ) ); runs(() => { expect(results).toHaveLength(1); expect(results[0].filePath).toBe(filePath); expect(results[0].replacements).toBe(6); expect(editor.isModified()).toBeTruthy(); }); }); }); }); describe('::saveActivePaneItem()', () => { let editor, notificationSpy; beforeEach(() => { waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o; }) ); notificationSpy = jasmine.createSpy('did-add-notification'); atom.notifications.onDidAddNotification(notificationSpy); }); describe('when there is an error', () => { it('emits a warning notification when the file cannot be saved', () => { spyOn(editor, 'save').andCallFake(() => { throw new Error("'/some/file' is a directory"); }); waitsForPromise(() => atom.workspace.saveActivePaneItem().then(() => { expect(notificationSpy).toHaveBeenCalled(); expect(notificationSpy.mostRecentCall.args[0].getType()).toBe( 'warning' ); expect( notificationSpy.mostRecentCall.args[0].getMessage() ).toContain('Unable to save'); }) ); }); it('emits a warning notification when the directory cannot be written to', () => { spyOn(editor, 'save').andCallFake(() => { throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'"); }); waitsForPromise(() => atom.workspace.saveActivePaneItem().then(() => { expect(notificationSpy).toHaveBeenCalled(); expect(notificationSpy.mostRecentCall.args[0].getType()).toBe( 'warning' ); expect( notificationSpy.mostRecentCall.args[0].getMessage() ).toContain('Unable to save'); }) ); }); it('emits a warning notification when the user does not have permission', () => { spyOn(editor, 'save').andCallFake(() => { const error = new Error( "EACCES, permission denied '/Some/dir/and-a-file.js'" ); error.code = 'EACCES'; error.path = '/Some/dir/and-a-file.js'; throw error; }); waitsForPromise(() => atom.workspace.saveActivePaneItem().then(() => { expect(notificationSpy).toHaveBeenCalled(); expect(notificationSpy.mostRecentCall.args[0].getType()).toBe( 'warning' ); expect( notificationSpy.mostRecentCall.args[0].getMessage() ).toContain('Unable to save'); }) ); }); it('emits a warning notification when the operation is not permitted', () => { spyOn(editor, 'save').andCallFake(() => { const error = new Error( "EPERM, operation not permitted '/Some/dir/and-a-file.js'" ); error.code = 'EPERM'; error.path = '/Some/dir/and-a-file.js'; throw error; }); waitsForPromise(() => atom.workspace.saveActivePaneItem().then(() => { expect(notificationSpy).toHaveBeenCalled(); expect(notificationSpy.mostRecentCall.args[0].getType()).toBe( 'warning' ); expect( notificationSpy.mostRecentCall.args[0].getMessage() ).toContain('Unable to save'); }) ); }); it('emits a warning notification when the file is already open by another app', () => { spyOn(editor, 'save').andCallFake(() => { const error = new Error( "EBUSY, resource busy or locked '/Some/dir/and-a-file.js'" ); error.code = 'EBUSY'; error.path = '/Some/dir/and-a-file.js'; throw error; }); waitsForPromise(() => atom.workspace.saveActivePaneItem().then(() => { expect(notificationSpy).toHaveBeenCalled(); expect(notificationSpy.mostRecentCall.args[0].getType()).toBe( 'warning' ); expect( notificationSpy.mostRecentCall.args[0].getMessage() ).toContain('Unable to save'); }) ); }); it('emits a warning notification when the file system is read-only', () => { spyOn(editor, 'save').andCallFake(() => { const error = new Error( "EROFS, read-only file system '/Some/dir/and-a-file.js'" ); error.code = 'EROFS'; error.path = '/Some/dir/and-a-file.js'; throw error; }); waitsForPromise(() => atom.workspace.saveActivePaneItem().then(() => { expect(notificationSpy).toHaveBeenCalled(); expect(notificationSpy.mostRecentCall.args[0].getType()).toBe( 'warning' ); expect( notificationSpy.mostRecentCall.args[0].getMessage() ).toContain('Unable to save'); }) ); }); it('emits a warning notification when the file cannot be saved', () => { spyOn(editor, 'save').andCallFake(() => { throw new Error('no one knows'); }); waitsForPromise({ shouldReject: true }, () => atom.workspace.saveActivePaneItem() ); }); }); }); describe('::closeActivePaneItemOrEmptyPaneOrWindow', () => { beforeEach(() => { spyOn(atom, 'close'); waitsForPromise(() => atom.workspace.open()); }); it('closes the active center pane item, or the active center pane if it is empty, or the current window if there is only the empty root pane in the center', async () => { atom.config.set('core.destroyEmptyPanes', false); const pane1 = atom.workspace.getActivePane(); const pane2 = pane1.splitRight({ copyActiveItem: true }); expect(atom.workspace.getCenter().getPanes().length).toBe(2); expect(pane2.getItems().length).toBe(1); atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); expect(atom.workspace.getCenter().getPanes().length).toBe(2); expect(pane2.getItems().length).toBe(0); atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); expect(atom.workspace.getCenter().getPanes().length).toBe(1); expect(pane1.getItems().length).toBe(1); atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); expect(atom.workspace.getCenter().getPanes().length).toBe(1); expect(pane1.getItems().length).toBe(0); expect(atom.workspace.getCenter().getPanes().length).toBe(1); // The dock items should not be closed await atom.workspace.open({ getTitle: () => 'Permanent Dock Item', element: document.createElement('div'), getDefaultLocation: () => 'left', isPermanentDockItem: () => true }); await atom.workspace.open({ getTitle: () => 'Impermanent Dock Item', element: document.createElement('div'), getDefaultLocation: () => 'left' }); expect(atom.workspace.getLeftDock().getPaneItems().length).toBe(2); atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); expect(atom.close).toHaveBeenCalled(); }); }); describe('::activateNextPane', () => { describe('when the active workspace pane is inside a dock', () => { it('activates the next pane in the dock', () => { const dock = atom.workspace.getLeftDock(); const dockPane1 = dock.getPanes()[0]; const dockPane2 = dockPane1.splitRight(); dockPane2.focus(); expect(atom.workspace.getActivePane()).toBe(dockPane2); atom.workspace.activateNextPane(); expect(atom.workspace.getActivePane()).toBe(dockPane1); }); }); describe('when the active workspace pane is inside the workspace center', () => { it('activates the next pane in the workspace center', () => { const center = atom.workspace.getCenter(); const centerPane1 = center.getPanes()[0]; const centerPane2 = centerPane1.splitRight(); centerPane2.focus(); expect(atom.workspace.getActivePane()).toBe(centerPane2); atom.workspace.activateNextPane(); expect(atom.workspace.getActivePane()).toBe(centerPane1); }); }); }); describe('::activatePreviousPane', () => { describe('when the active workspace pane is inside a dock', () => { it('activates the previous pane in the dock', () => { const dock = atom.workspace.getLeftDock(); const dockPane1 = dock.getPanes()[0]; const dockPane2 = dockPane1.splitRight(); dockPane1.focus(); expect(atom.workspace.getActivePane()).toBe(dockPane1); atom.workspace.activatePreviousPane(); expect(atom.workspace.getActivePane()).toBe(dockPane2); }); }); describe('when the active workspace pane is inside the workspace center', () => { it('activates the previous pane in the workspace center', () => { const center = atom.workspace.getCenter(); const centerPane1 = center.getPanes()[0]; const centerPane2 = centerPane1.splitRight(); centerPane1.focus(); expect(atom.workspace.getActivePane()).toBe(centerPane1); atom.workspace.activatePreviousPane(); expect(atom.workspace.getActivePane()).toBe(centerPane2); }); }); }); describe('::getVisiblePanes', () => { it('returns all panes in visible pane containers', () => { const center = workspace.getCenter(); const leftDock = workspace.getLeftDock(); const rightDock = workspace.getRightDock(); const bottomDock = workspace.getBottomDock(); const centerPane = center.getPanes()[0]; const leftDockPane = leftDock.getPanes()[0]; const rightDockPane = rightDock.getPanes()[0]; const bottomDockPane = bottomDock.getPanes()[0]; leftDock.hide(); rightDock.hide(); bottomDock.hide(); expect(workspace.getVisiblePanes()).toContain(centerPane); expect(workspace.getVisiblePanes()).not.toContain(leftDockPane); expect(workspace.getVisiblePanes()).not.toContain(rightDockPane); expect(workspace.getVisiblePanes()).not.toContain(bottomDockPane); leftDock.show(); expect(workspace.getVisiblePanes()).toContain(centerPane); expect(workspace.getVisiblePanes()).toContain(leftDockPane); expect(workspace.getVisiblePanes()).not.toContain(rightDockPane); expect(workspace.getVisiblePanes()).not.toContain(bottomDockPane); rightDock.show(); expect(workspace.getVisiblePanes()).toContain(centerPane); expect(workspace.getVisiblePanes()).toContain(leftDockPane); expect(workspace.getVisiblePanes()).toContain(rightDockPane); expect(workspace.getVisiblePanes()).not.toContain(bottomDockPane); bottomDock.show(); expect(workspace.getVisiblePanes()).toContain(centerPane); expect(workspace.getVisiblePanes()).toContain(leftDockPane); expect(workspace.getVisiblePanes()).toContain(rightDockPane); expect(workspace.getVisiblePanes()).toContain(bottomDockPane); }); }); describe('::getVisiblePaneContainers', () => { it('returns all visible pane containers', () => { const center = workspace.getCenter(); const leftDock = workspace.getLeftDock(); const rightDock = workspace.getRightDock(); const bottomDock = workspace.getBottomDock(); leftDock.hide(); rightDock.hide(); bottomDock.hide(); expect(workspace.getVisiblePaneContainers()).toEqual([center]); leftDock.show(); expect(workspace.getVisiblePaneContainers().sort()).toEqual([ center, leftDock ]); rightDock.show(); expect(workspace.getVisiblePaneContainers().sort()).toEqual([ center, leftDock, rightDock ]); bottomDock.show(); expect(workspace.getVisiblePaneContainers().sort()).toEqual([ center, leftDock, rightDock, bottomDock ]); }); }); 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); waitsForPromise(() => atom.workspace.open('sample.js', { pending: true }).then(() => { pane = atom.workspace.getActivePane(); }) ); runs(() => expect(pane.getPendingItem()).toBeFalsy()); }); }); describe('grammar activation', () => { it('notifies the workspace of which grammar is used', async () => { atom.packages.triggerDeferredActivationHooks(); const javascriptGrammarUsed = jasmine.createSpy('js grammar used'); const rubyGrammarUsed = jasmine.createSpy('ruby grammar used'); const cGrammarUsed = jasmine.createSpy('c grammar used'); atom.packages.onDidTriggerActivationHook( 'language-javascript:grammar-used', javascriptGrammarUsed ); atom.packages.onDidTriggerActivationHook( 'language-ruby:grammar-used', rubyGrammarUsed ); atom.packages.onDidTriggerActivationHook( 'language-c:grammar-used', cGrammarUsed ); await atom.packages.activatePackage('language-ruby'); await atom.packages.activatePackage('language-javascript'); await atom.packages.activatePackage('language-c'); await atom.workspace.open('sample-with-comments.js'); // Hooks are triggered when opening new editors expect(javascriptGrammarUsed).toHaveBeenCalled(); // Hooks are triggered when changing existing editors grammars atom.grammars.assignLanguageMode( atom.workspace.getActiveTextEditor(), 'source.c' ); expect(cGrammarUsed).toHaveBeenCalled(); // Hooks are triggered when editors are added in other ways. atom.workspace.getActivePane().splitRight({ copyActiveItem: true }); atom.grammars.assignLanguageMode( atom.workspace.getActiveTextEditor(), 'source.ruby' ); expect(rubyGrammarUsed).toHaveBeenCalled(); }); }); describe('.checkoutHeadRevision()', () => { let editor = null; beforeEach(async () => { jasmine.useRealClock(); atom.config.set('editor.confirmCheckoutHeadRevision', false); editor = await atom.workspace.open('sample-with-comments.js'); }); it('reverts to the version of its file checked into the project repository', async () => { editor.setCursorBufferPosition([0, 0]); editor.insertText('---\n'); expect(editor.lineTextForBufferRow(0)).toBe('---'); atom.workspace.checkoutHeadRevision(editor); await conditionPromise(() => editor.lineTextForBufferRow(0) === ''); }); describe("when there's no repository for the editor's file", () => { it("doesn't do anything", async () => { editor = new TextEditor(); editor.setText('stuff'); atom.workspace.checkoutHeadRevision(editor); atom.workspace.checkoutHeadRevision(editor); }); }); }); describe('when an item is moved', () => { beforeEach(() => { atom.workspace.enablePersistence = true; }); afterEach(async () => { await atom.workspace.itemLocationStore.clear(); atom.workspace.enablePersistence = false; }); it("stores the new location if it's not the default", () => { const ITEM_URI = 'atom://test'; const item = { getURI: () => ITEM_URI, getDefaultLocation: () => 'left', getElement: () => document.createElement('div') }; const centerPane = workspace.getActivePane(); centerPane.addItem(item); const dockPane = atom.workspace.getRightDock().getActivePane(); spyOn(workspace.itemLocationStore, 'save'); centerPane.moveItemToPane(item, dockPane); expect(workspace.itemLocationStore.save).toHaveBeenCalledWith( ITEM_URI, 'right' ); }); it("clears the location if it's the default", () => { const ITEM_URI = 'atom://test'; const item = { getURI: () => ITEM_URI, getDefaultLocation: () => 'right', getElement: () => document.createElement('div') }; const centerPane = workspace.getActivePane(); centerPane.addItem(item); const dockPane = atom.workspace.getRightDock().getActivePane(); spyOn(workspace.itemLocationStore, 'save'); spyOn(workspace.itemLocationStore, 'delete'); centerPane.moveItemToPane(item, dockPane); expect(workspace.itemLocationStore.delete).toHaveBeenCalledWith(ITEM_URI); expect(workspace.itemLocationStore.save).not.toHaveBeenCalled(); }); }); }); function escapeStringRegex(string) { return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); } ================================================ FILE: src/application-delegate.js ================================================ const { ipcRenderer, remote, shell } = require('electron'); const ipcHelpers = require('./ipc-helpers'); const { Emitter, Disposable } = require('event-kit'); const getWindowLoadSettings = require('./get-window-load-settings'); module.exports = class ApplicationDelegate { constructor() { this.pendingSettingsUpdateCount = 0; this._ipcMessageEmitter = null; } ipcMessageEmitter() { if (!this._ipcMessageEmitter) { this._ipcMessageEmitter = new Emitter(); ipcRenderer.on('message', (event, message, detail) => { this._ipcMessageEmitter.emit(message, detail); }); } return this._ipcMessageEmitter; } getWindowLoadSettings() { return getWindowLoadSettings(); } open(params) { return ipcRenderer.send('open', params); } pickFolder(callback) { const responseChannel = 'atom-pick-folder-response'; ipcRenderer.on(responseChannel, function(event, path) { ipcRenderer.removeAllListeners(responseChannel); return callback(path); }); return ipcRenderer.send('pick-folder', responseChannel); } getCurrentWindow() { return remote.getCurrentWindow(); } closeWindow() { return ipcHelpers.call('window-method', 'close'); } async getTemporaryWindowState() { const stateJSON = await ipcHelpers.call('get-temporary-window-state'); return stateJSON && JSON.parse(stateJSON); } setTemporaryWindowState(state) { return ipcHelpers.call('set-temporary-window-state', JSON.stringify(state)); } getWindowSize() { const [width, height] = Array.from(remote.getCurrentWindow().getSize()); return { width, height }; } setWindowSize(width, height) { return ipcHelpers.call('set-window-size', width, height); } getWindowPosition() { const [x, y] = Array.from(remote.getCurrentWindow().getPosition()); return { x, y }; } setWindowPosition(x, y) { return ipcHelpers.call('set-window-position', x, y); } centerWindow() { return ipcHelpers.call('center-window'); } focusWindow() { return ipcHelpers.call('focus-window'); } showWindow() { return ipcHelpers.call('show-window'); } hideWindow() { return ipcHelpers.call('hide-window'); } reloadWindow() { return ipcHelpers.call('window-method', 'reload'); } restartApplication() { return ipcRenderer.send('restart-application'); } minimizeWindow() { return ipcHelpers.call('window-method', 'minimize'); } isWindowMaximized() { return remote.getCurrentWindow().isMaximized(); } maximizeWindow() { return ipcHelpers.call('window-method', 'maximize'); } unmaximizeWindow() { return ipcHelpers.call('window-method', 'unmaximize'); } isWindowFullScreen() { return remote.getCurrentWindow().isFullScreen(); } setWindowFullScreen(fullScreen = false) { return ipcHelpers.call('window-method', 'setFullScreen', fullScreen); } onDidEnterFullScreen(callback) { return ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback); } onDidLeaveFullScreen(callback) { return ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback); } async openWindowDevTools() { // Defer DevTools interaction to the next tick, because using them during // event handling causes some wrong input events to be triggered on // `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). await new Promise(process.nextTick); return ipcHelpers.call('window-method', 'openDevTools'); } async closeWindowDevTools() { // Defer DevTools interaction to the next tick, because using them during // event handling causes some wrong input events to be triggered on // `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). await new Promise(process.nextTick); return ipcHelpers.call('window-method', 'closeDevTools'); } async toggleWindowDevTools() { // Defer DevTools interaction to the next tick, because using them during // event handling causes some wrong input events to be triggered on // `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697). await new Promise(process.nextTick); return ipcHelpers.call('window-method', 'toggleDevTools'); } executeJavaScriptInWindowDevTools(code) { return ipcRenderer.send('execute-javascript-in-dev-tools', code); } didClosePathWithWaitSession(path) { return ipcHelpers.call( 'window-method', 'didClosePathWithWaitSession', path ); } setWindowDocumentEdited(edited) { return ipcHelpers.call('window-method', 'setDocumentEdited', edited); } setRepresentedFilename(filename) { return ipcHelpers.call('window-method', 'setRepresentedFilename', filename); } addRecentDocument(filename) { return ipcRenderer.send('add-recent-document', filename); } setProjectRoots(paths) { return ipcHelpers.call('window-method', 'setProjectRoots', paths); } setAutoHideWindowMenuBar(autoHide) { return ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide); } setWindowMenuBarVisibility(visible) { return remote.getCurrentWindow().setMenuBarVisibility(visible); } getPrimaryDisplayWorkAreaSize() { return remote.screen.getPrimaryDisplay().workAreaSize; } getUserDefault(key, type) { return remote.systemPreferences.getUserDefault(key, type); } async setUserSettings(config, configFilePath) { this.pendingSettingsUpdateCount++; try { await ipcHelpers.call( 'set-user-settings', JSON.stringify(config), configFilePath ); } finally { this.pendingSettingsUpdateCount--; } } onDidChangeUserSettings(callback) { return this.ipcMessageEmitter().on('did-change-user-settings', detail => { if (this.pendingSettingsUpdateCount === 0) callback(detail); }); } onDidFailToReadUserSettings(callback) { return this.ipcMessageEmitter().on( 'did-fail-to-read-user-setting', callback ); } confirm(options, callback) { if (typeof callback === 'function') { // Async version: pass options directly to Electron but set sane defaults options = Object.assign( { type: 'info', normalizeAccessKeys: true }, options ); remote.dialog .showMessageBox(remote.getCurrentWindow(), options) .then(result => { callback(result.response, result.checkboxChecked); }); } else { // Legacy sync version: options can only have `message`, // `detailedMessage` (optional), and buttons array or object (optional) let { message, detailedMessage, buttons } = options; let buttonLabels; if (!buttons) buttons = {}; if (Array.isArray(buttons)) { buttonLabels = buttons; } else { buttonLabels = Object.keys(buttons); } const chosen = remote.dialog.showMessageBoxSync( remote.getCurrentWindow(), { type: 'info', message, detail: detailedMessage, buttons: buttonLabels, normalizeAccessKeys: true } ); if (Array.isArray(buttons)) { return chosen; } else { const callback = buttons[buttonLabels[chosen]]; if (typeof callback === 'function') return callback(); } } } showMessageDialog(params) {} showSaveDialog(options, callback) { if (typeof callback === 'function') { // Async this.getCurrentWindow().showSaveDialog(options, callback); } else { // Sync if (typeof options === 'string') { options = { defaultPath: options }; } return this.getCurrentWindow().showSaveDialog(options); } } playBeepSound() { return shell.beep(); } onDidOpenLocations(callback) { return this.ipcMessageEmitter().on('open-locations', callback); } onUpdateAvailable(callback) { // TODO: Yes, this is strange that `onUpdateAvailable` is listening for // `did-begin-downloading-update`. We currently have no mechanism to know // if there is an update, so begin of downloading is a good proxy. return this.ipcMessageEmitter().on( 'did-begin-downloading-update', callback ); } onDidBeginDownloadingUpdate(callback) { return this.onUpdateAvailable(callback); } onDidBeginCheckingForUpdate(callback) { return this.ipcMessageEmitter().on('checking-for-update', callback); } onDidCompleteDownloadingUpdate(callback) { return this.ipcMessageEmitter().on('update-available', callback); } onUpdateNotAvailable(callback) { return this.ipcMessageEmitter().on('update-not-available', callback); } onUpdateError(callback) { return this.ipcMessageEmitter().on('update-error', callback); } onApplicationMenuCommand(handler) { const outerCallback = (event, ...args) => handler(...args); ipcRenderer.on('command', outerCallback); return new Disposable(() => ipcRenderer.removeListener('command', outerCallback) ); } onContextMenuCommand(handler) { const outerCallback = (event, ...args) => handler(...args); ipcRenderer.on('context-command', outerCallback); return new Disposable(() => ipcRenderer.removeListener('context-command', outerCallback) ); } onURIMessage(handler) { const outerCallback = (event, ...args) => handler(...args); ipcRenderer.on('uri-message', outerCallback); return new Disposable(() => ipcRenderer.removeListener('uri-message', outerCallback) ); } onDidRequestUnload(callback) { const outerCallback = async (event, message) => { const shouldUnload = await callback(event); ipcRenderer.send('did-prepare-to-unload', shouldUnload); }; ipcRenderer.on('prepare-to-unload', outerCallback); return new Disposable(() => ipcRenderer.removeListener('prepare-to-unload', outerCallback) ); } onDidChangeHistoryManager(callback) { const outerCallback = (event, message) => callback(event); ipcRenderer.on('did-change-history-manager', outerCallback); return new Disposable(() => ipcRenderer.removeListener('did-change-history-manager', outerCallback) ); } didChangeHistoryManager() { return ipcRenderer.send('did-change-history-manager'); } openExternal(url) { return shell.openExternal(url); } checkForUpdate() { return ipcRenderer.send('command', 'application:check-for-update'); } restartAndInstallUpdate() { return ipcRenderer.send('command', 'application:install-update'); } getAutoUpdateManagerState() { return ipcRenderer.sendSync('get-auto-update-manager-state'); } getAutoUpdateManagerErrorMessage() { return ipcRenderer.sendSync('get-auto-update-manager-error'); } emitWillSavePath(path) { return ipcHelpers.call('will-save-path', path); } emitDidSavePath(path) { return ipcHelpers.call('did-save-path', path); } resolveProxy(requestId, url) { return ipcRenderer.send('resolve-proxy', requestId, url); } onDidResolveProxy(callback) { const outerCallback = (event, requestId, proxy) => callback(requestId, proxy); ipcRenderer.on('did-resolve-proxy', outerCallback); return new Disposable(() => ipcRenderer.removeListener('did-resolve-proxy', outerCallback) ); } }; ================================================ FILE: src/atom-environment.js ================================================ const crypto = require('crypto'); const path = require('path'); const util = require('util'); const { ipcRenderer } = require('electron'); const _ = require('underscore-plus'); const { deprecate } = require('grim'); const { CompositeDisposable, Disposable, Emitter } = require('event-kit'); const fs = require('fs-plus'); const { mapSourcePosition } = require('@atom/source-map-support'); const WindowEventHandler = require('./window-event-handler'); const StateStore = require('./state-store'); const registerDefaultCommands = require('./register-default-commands'); const { updateProcessEnv } = require('./update-process-env'); const ConfigSchema = require('./config-schema'); const DeserializerManager = require('./deserializer-manager'); const ViewRegistry = require('./view-registry'); const NotificationManager = require('./notification-manager'); const Config = require('./config'); const KeymapManager = require('./keymap-extensions'); const TooltipManager = require('./tooltip-manager'); const CommandRegistry = require('./command-registry'); const URIHandlerRegistry = require('./uri-handler-registry'); const GrammarRegistry = require('./grammar-registry'); const { HistoryManager } = require('./history-manager'); const ReopenProjectMenuManager = require('./reopen-project-menu-manager'); const StyleManager = require('./style-manager'); const PackageManager = require('./package-manager'); const ThemeManager = require('./theme-manager'); const MenuManager = require('./menu-manager'); const ContextMenuManager = require('./context-menu-manager'); const CommandInstaller = require('./command-installer'); const CoreURIHandlers = require('./core-uri-handlers'); const ProtocolHandlerInstaller = require('./protocol-handler-installer'); const Project = require('./project'); const TitleBar = require('./title-bar'); const Workspace = require('./workspace'); const PaneContainer = require('./pane-container'); const PaneAxis = require('./pane-axis'); const Pane = require('./pane'); const Dock = require('./dock'); const TextEditor = require('./text-editor'); const TextBuffer = require('text-buffer'); const TextEditorRegistry = require('./text-editor-registry'); const AutoUpdateManager = require('./auto-update-manager'); const StartupTime = require('./startup-time'); const getReleaseChannel = require('./get-release-channel'); const stat = util.promisify(fs.stat); let nextId = 0; // Essential: Atom global for dealing with packages, themes, menus, and the window. // // An instance of this class is always available as the `atom` global. class AtomEnvironment { /* Section: Properties */ constructor(params = {}) { this.id = params.id != null ? params.id : nextId++; // Public: A {Clipboard} instance this.clipboard = params.clipboard; this.updateProcessEnv = params.updateProcessEnv || updateProcessEnv; this.enablePersistence = params.enablePersistence; this.applicationDelegate = params.applicationDelegate; this.nextProxyRequestId = 0; this.unloading = false; this.loadTime = null; this.emitter = new Emitter(); this.disposables = new CompositeDisposable(); this.pathsWithWaitSessions = new Set(); // Public: A {DeserializerManager} instance this.deserializers = new DeserializerManager(this); this.deserializeTimings = {}; // Public: A {ViewRegistry} instance this.views = new ViewRegistry(this); // Public: A {NotificationManager} instance this.notifications = new NotificationManager(); this.stateStore = new StateStore('AtomEnvironments', 1); // Public: A {Config} instance this.config = new Config({ saveCallback: settings => { if (this.enablePersistence) { this.applicationDelegate.setUserSettings( settings, this.config.getUserConfigPath() ); } } }); this.config.setSchema(null, { type: 'object', properties: _.clone(ConfigSchema) }); // Public: A {KeymapManager} instance this.keymaps = new KeymapManager({ notificationManager: this.notifications }); // Public: A {TooltipManager} instance this.tooltips = new TooltipManager({ keymapManager: this.keymaps, viewRegistry: this.views }); // Public: A {CommandRegistry} instance this.commands = new CommandRegistry(); this.uriHandlerRegistry = new URIHandlerRegistry(); // Public: A {GrammarRegistry} instance this.grammars = new GrammarRegistry({ config: this.config }); // Public: A {StyleManager} instance this.styles = new StyleManager(); // Public: A {PackageManager} instance this.packages = new PackageManager({ config: this.config, styleManager: this.styles, commandRegistry: this.commands, keymapManager: this.keymaps, notificationManager: this.notifications, grammarRegistry: this.grammars, deserializerManager: this.deserializers, viewRegistry: this.views, uriHandlerRegistry: this.uriHandlerRegistry }); // Public: A {ThemeManager} instance this.themes = new ThemeManager({ packageManager: this.packages, config: this.config, styleManager: this.styles, notificationManager: this.notifications, viewRegistry: this.views }); // Public: A {MenuManager} instance this.menu = new MenuManager({ keymapManager: this.keymaps, packageManager: this.packages }); // Public: A {ContextMenuManager} instance this.contextMenu = new ContextMenuManager({ keymapManager: this.keymaps }); this.packages.setMenuManager(this.menu); this.packages.setContextMenuManager(this.contextMenu); this.packages.setThemeManager(this.themes); // Public: A {Project} instance this.project = new Project({ notificationManager: this.notifications, packageManager: this.packages, grammarRegistry: this.grammars, config: this.config, applicationDelegate: this.applicationDelegate }); this.commandInstaller = new CommandInstaller(this.applicationDelegate); this.protocolHandlerInstaller = new ProtocolHandlerInstaller(); // Public: A {TextEditorRegistry} instance this.textEditors = new TextEditorRegistry({ config: this.config, grammarRegistry: this.grammars, assert: this.assert.bind(this), packageManager: this.packages }); // Public: A {Workspace} instance this.workspace = new Workspace({ config: this.config, project: this.project, packageManager: this.packages, grammarRegistry: this.grammars, deserializerManager: this.deserializers, notificationManager: this.notifications, applicationDelegate: this.applicationDelegate, viewRegistry: this.views, assert: this.assert.bind(this), textEditorRegistry: this.textEditors, styleManager: this.styles, enablePersistence: this.enablePersistence }); this.themes.workspace = this.workspace; this.autoUpdater = new AutoUpdateManager({ applicationDelegate: this.applicationDelegate }); if (this.keymaps.canLoadBundledKeymapsFromMemory()) { this.keymaps.loadBundledKeymaps(); } this.registerDefaultCommands(); this.registerDefaultOpeners(); this.registerDefaultDeserializers(); this.windowEventHandler = new WindowEventHandler({ atomEnvironment: this, applicationDelegate: this.applicationDelegate }); // Public: A {HistoryManager} instance this.history = new HistoryManager({ project: this.project, commands: this.commands, stateStore: this.stateStore }); // Keep instances of HistoryManager in sync this.disposables.add( this.history.onDidChangeProjects(event => { if (!event.reloaded) this.applicationDelegate.didChangeHistoryManager(); }) ); } initialize(params = {}) { // This will force TextEditorElement to register the custom element, so that // using `document.createElement('atom-text-editor')` works if it's called // before opening a buffer. require('./text-editor-element'); this.window = params.window; this.document = params.document; this.blobStore = params.blobStore; this.configDirPath = params.configDirPath; const { devMode, safeMode, resourcePath, userSettings, projectSpecification } = this.getLoadSettings(); ConfigSchema.projectHome = { type: 'string', default: path.join(fs.getHomeDirectory(), 'github'), description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.' }; this.config.initialize({ mainSource: this.enablePersistence && path.join(this.configDirPath, 'config.cson'), projectHomeSchema: ConfigSchema.projectHome }); this.config.resetUserSettings(userSettings); if (projectSpecification != null && projectSpecification.config != null) { this.project.replace(projectSpecification); } this.menu.initialize({ resourcePath }); this.contextMenu.initialize({ resourcePath, devMode }); this.keymaps.configDirPath = this.configDirPath; this.keymaps.resourcePath = resourcePath; this.keymaps.devMode = devMode; if (!this.keymaps.canLoadBundledKeymapsFromMemory()) { this.keymaps.loadBundledKeymaps(); } this.commands.attach(this.window); this.styles.initialize({ configDirPath: this.configDirPath }); this.packages.initialize({ devMode, configDirPath: this.configDirPath, resourcePath, safeMode }); this.themes.initialize({ configDirPath: this.configDirPath, resourcePath, safeMode, devMode }); this.commandInstaller.initialize(this.getVersion()); this.uriHandlerRegistry.registerHostHandler( 'core', CoreURIHandlers.create(this) ); this.autoUpdater.initialize(); this.protocolHandlerInstaller.initialize(this.config, this.notifications); this.themes.loadBaseStylesheets(); this.initialStyleElements = this.styles.getSnapshot(); if (params.onlyLoadBaseStyleSheets) this.themes.initialLoadComplete = true; this.setBodyPlatformClass(); this.stylesElement = this.styles.buildStylesElement(); this.document.head.appendChild(this.stylesElement); this.keymaps.subscribeToFileReadFailure(); this.installUncaughtErrorHandler(); this.attachSaveStateListeners(); this.windowEventHandler.initialize(this.window, this.document); this.workspace.initialize(); const didChangeStyles = this.didChangeStyles.bind(this); this.disposables.add(this.styles.onDidAddStyleElement(didChangeStyles)); this.disposables.add(this.styles.onDidUpdateStyleElement(didChangeStyles)); this.disposables.add(this.styles.onDidRemoveStyleElement(didChangeStyles)); this.observeAutoHideMenuBar(); this.disposables.add( this.applicationDelegate.onDidChangeHistoryManager(() => this.history.loadState() ) ); } preloadPackages() { return this.packages.preloadPackages(); } attachSaveStateListeners() { const saveState = _.debounce(() => { this.window.requestIdleCallback(() => { if (!this.unloading) this.saveState({ isUnloading: false }); }); }, this.saveStateDebounceInterval); this.document.addEventListener('mousedown', saveState, { capture: true }); this.document.addEventListener('keydown', saveState, { capture: true }); this.disposables.add( new Disposable(() => { this.document.removeEventListener('mousedown', saveState, { capture: true }); this.document.removeEventListener('keydown', saveState, { capture: true }); }) ); } registerDefaultDeserializers() { this.deserializers.add(Workspace); this.deserializers.add(PaneContainer); this.deserializers.add(PaneAxis); this.deserializers.add(Pane); this.deserializers.add(Dock); this.deserializers.add(Project); this.deserializers.add(TextEditor); this.deserializers.add(TextBuffer); } registerDefaultCommands() { registerDefaultCommands({ commandRegistry: this.commands, config: this.config, commandInstaller: this.commandInstaller, notificationManager: this.notifications, project: this.project, clipboard: this.clipboard }); } registerDefaultOpeners() { this.workspace.addOpener(uri => { switch (uri) { case 'atom://.atom/stylesheet': return this.workspace.openTextFile( this.styles.getUserStyleSheetPath() ); case 'atom://.atom/keymap': return this.workspace.openTextFile(this.keymaps.getUserKeymapPath()); case 'atom://.atom/config': return this.workspace.openTextFile(this.config.getUserConfigPath()); case 'atom://.atom/init-script': return this.workspace.openTextFile(this.getUserInitScriptPath()); } }); } registerDefaultTargetForKeymaps() { this.keymaps.defaultTarget = this.workspace.getElement(); } observeAutoHideMenuBar() { this.disposables.add( this.config.onDidChange('core.autoHideMenuBar', ({ newValue }) => { this.setAutoHideMenuBar(newValue); }) ); if (this.config.get('core.autoHideMenuBar')) this.setAutoHideMenuBar(true); } async reset() { this.deserializers.clear(); this.registerDefaultDeserializers(); this.config.clear(); this.config.setSchema(null, { type: 'object', properties: _.clone(ConfigSchema) }); this.keymaps.clear(); this.keymaps.loadBundledKeymaps(); this.commands.clear(); this.registerDefaultCommands(); this.styles.restoreSnapshot(this.initialStyleElements); this.menu.clear(); this.clipboard.reset(); this.notifications.clear(); this.contextMenu.clear(); await this.packages.reset(); this.workspace.reset(this.packages); this.registerDefaultOpeners(); this.project.reset(this.packages); this.workspace.initialize(); this.grammars.clear(); this.textEditors.clear(); this.views.clear(); this.pathsWithWaitSessions.clear(); } destroy() { if (!this.project) return; this.disposables.dispose(); if (this.workspace) this.workspace.destroy(); this.workspace = null; this.themes.workspace = null; if (this.project) this.project.destroy(); this.project = null; this.commands.clear(); if (this.stylesElement) this.stylesElement.remove(); this.autoUpdater.destroy(); this.uriHandlerRegistry.destroy(); this.uninstallWindowEventHandler(); } /* Section: Event Subscription */ // Extended: Invoke the given callback whenever {::beep} is called. // // * `callback` {Function} to be called whenever {::beep} is called. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidBeep(callback) { return this.emitter.on('did-beep', callback); } // Extended: Invoke the given callback when there is an unhandled error, but // before the devtools pop open // // * `callback` {Function} to be called whenever there is an unhandled error // * `event` {Object} // * `originalError` {Object} the original error object // * `message` {String} the original error object // * `url` {String} Url to the file where the error originated. // * `line` {Number} // * `column` {Number} // * `preventDefault` {Function} call this to avoid popping up the dev tools. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onWillThrowError(callback) { return this.emitter.on('will-throw-error', callback); } // Extended: Invoke the given callback whenever there is an unhandled error. // // * `callback` {Function} to be called whenever there is an unhandled error // * `event` {Object} // * `originalError` {Object} the original error object // * `message` {String} the original error object // * `url` {String} Url to the file where the error originated. // * `line` {Number} // * `column` {Number} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidThrowError(callback) { return this.emitter.on('did-throw-error', callback); } // TODO: Make this part of the public API. We should make onDidThrowError // match the interface by only yielding an exception object to the handler // and deprecating the old behavior. onDidFailAssertion(callback) { return this.emitter.on('did-fail-assertion', callback); } // Extended: Invoke the given callback as soon as the shell environment is // loaded (or immediately if it was already loaded). // // * `callback` {Function} to be called whenever there is an unhandled error whenShellEnvironmentLoaded(callback) { if (this.shellEnvironmentLoaded) { callback(); return new Disposable(); } else { return this.emitter.once('loaded-shell-environment', callback); } } /* Section: Atom Details */ // Public: Returns a {Boolean} that is `true` if the current window is in development mode. inDevMode() { if (this.devMode == null) this.devMode = this.getLoadSettings().devMode; return this.devMode; } // Public: Returns a {Boolean} that is `true` if the current window is in safe mode. inSafeMode() { if (this.safeMode == null) this.safeMode = this.getLoadSettings().safeMode; return this.safeMode; } // Public: Returns a {Boolean} that is `true` if the current window is running specs. inSpecMode() { if (this.specMode == null) this.specMode = this.getLoadSettings().isSpec; return this.specMode; } // Returns a {Boolean} indicating whether this the first time the window's been // loaded. isFirstLoad() { if (this.firstLoad == null) this.firstLoad = this.getLoadSettings().firstLoad; return this.firstLoad; } // Public: Get the full name of this Atom release (e.g. "Atom", "Atom Beta") // // Returns the app name {String}. getAppName() { if (this.appName == null) this.appName = this.getLoadSettings().appName; return this.appName; } // Public: Get the version of the Atom application. // // Returns the version text {String}. getVersion() { if (this.appVersion == null) this.appVersion = this.getLoadSettings().appVersion; return this.appVersion; } // Public: Gets the release channel of the Atom application. // // Returns the release channel as a {String}. Will return a specific release channel // name like 'beta' or 'nightly' if one is found in the Atom version or 'stable' // otherwise. getReleaseChannel() { return getReleaseChannel(this.getVersion()); } // Public: Returns a {Boolean} that is `true` if the current version is an official release. isReleasedVersion() { return this.getReleaseChannel().match(/stable|beta|nightly/) != null; } // Public: Get the time taken to completely load the current window. // // This time include things like loading and activating packages, creating // DOM elements for the editor, and reading the config. // // Returns the {Number} of milliseconds taken to load the window or null // if the window hasn't finished loading yet. getWindowLoadTime() { return this.loadTime; } // Public: Get the all the markers with the information about startup time. // // Returns an array of timing markers. // Each timing is an object with two keys: // * `label`: string // * `time`: Time since the `startTime` (in milliseconds). getStartupMarkers() { const data = StartupTime.exportData(); return data ? data.markers : []; } // Public: Get the load settings for the current window. // // Returns an {Object} containing all the load setting key/value pairs. getLoadSettings() { return this.applicationDelegate.getWindowLoadSettings(); } /* Section: Managing The Atom Window */ // Essential: Open a new Atom window using the given options. // // Calling this method without an options parameter will open a prompt to pick // a file/folder to open in the new window. // // * `params` An {Object} with the following keys: // * `pathsToOpen` An {Array} of {String} paths to open. // * `newWindow` A {Boolean}, true to always open a new window instead of // reusing existing windows depending on the paths to open. // * `devMode` A {Boolean}, true to open the window in development mode. // Development mode loads the Atom source from the locally cloned // repository and also loads all the packages in ~/.atom/dev/packages // * `safeMode` A {Boolean}, true to open the window in safe mode. Safe // mode prevents all packages installed to ~/.atom/packages from loading. open(params) { return this.applicationDelegate.open(params); } // Extended: Prompt the user to select one or more folders. // // * `callback` A {Function} to call once the user has confirmed the selection. // * `paths` An {Array} of {String} paths that the user selected, or `null` // if the user dismissed the dialog. pickFolder(callback) { return this.applicationDelegate.pickFolder(callback); } // Essential: Close the current window. close() { return this.applicationDelegate.closeWindow(); } // Essential: Get the size of current window. // // Returns an {Object} in the format `{width: 1000, height: 700}` getSize() { return this.applicationDelegate.getWindowSize(); } // Essential: Set the size of current window. // // * `width` The {Number} of pixels. // * `height` The {Number} of pixels. setSize(width, height) { return this.applicationDelegate.setWindowSize(width, height); } // Essential: Get the position of current window. // // Returns an {Object} in the format `{x: 10, y: 20}` getPosition() { return this.applicationDelegate.getWindowPosition(); } // Essential: Set the position of current window. // // * `x` The {Number} of pixels. // * `y` The {Number} of pixels. setPosition(x, y) { return this.applicationDelegate.setWindowPosition(x, y); } // Extended: Get the current window getCurrentWindow() { return this.applicationDelegate.getCurrentWindow(); } // Extended: Move current window to the center of the screen. center() { return this.applicationDelegate.centerWindow(); } // Extended: Focus the current window. focus() { this.applicationDelegate.focusWindow(); return this.window.focus(); } // Extended: Show the current window. show() { return this.applicationDelegate.showWindow(); } // Extended: Hide the current window. hide() { return this.applicationDelegate.hideWindow(); } // Extended: Reload the current window. reload() { return this.applicationDelegate.reloadWindow(); } // Extended: Relaunch the entire application. restartApplication() { return this.applicationDelegate.restartApplication(); } // Extended: Returns a {Boolean} that is `true` if the current window is maximized. isMaximized() { return this.applicationDelegate.isWindowMaximized(); } maximize() { return this.applicationDelegate.maximizeWindow(); } // Extended: Returns a {Boolean} that is `true` if the current window is in full screen mode. isFullScreen() { return this.applicationDelegate.isWindowFullScreen(); } // Extended: Set the full screen state of the current window. setFullScreen(fullScreen = false) { return this.applicationDelegate.setWindowFullScreen(fullScreen); } // Extended: Toggle the full screen state of the current window. toggleFullScreen() { return this.setFullScreen(!this.isFullScreen()); } // Restore the window to its previous dimensions and show it. // // Restores the full screen and maximized state after the window has resized to // prevent resize glitches. async displayWindow() { await this.restoreWindowDimensions(); const steps = [this.restoreWindowBackground(), this.show(), this.focus()]; if (this.windowDimensions && this.windowDimensions.fullScreen) { steps.push(this.setFullScreen(true)); } if ( this.windowDimensions && this.windowDimensions.maximized && process.platform !== 'darwin' ) { steps.push(this.maximize()); } await Promise.all(steps); } // Get the dimensions of this window. // // Returns an {Object} with the following keys: // * `x` The window's x-position {Number}. // * `y` The window's y-position {Number}. // * `width` The window's width {Number}. // * `height` The window's height {Number}. getWindowDimensions() { const browserWindow = this.getCurrentWindow(); const [x, y] = browserWindow.getPosition(); const [width, height] = browserWindow.getSize(); const maximized = browserWindow.isMaximized(); return { x, y, width, height, maximized }; } // Set the dimensions of the window. // // The window will be centered if either the x or y coordinate is not set // in the dimensions parameter. If x or y are omitted the window will be // centered. If height or width are omitted only the position will be changed. // // * `dimensions` An {Object} with the following keys: // * `x` The new x coordinate. // * `y` The new y coordinate. // * `width` The new width. // * `height` The new height. setWindowDimensions({ x, y, width, height }) { const steps = []; if (width != null && height != null) { steps.push(this.setSize(width, height)); } if (x != null && y != null) { steps.push(this.setPosition(x, y)); } else { steps.push(this.center()); } return Promise.all(steps); } // Returns true if the dimensions are useable, false if they should be ignored. // Work around for https://github.com/atom/atom-shell/issues/473 isValidDimensions({ x, y, width, height } = {}) { return width > 0 && height > 0 && x + width > 0 && y + height > 0; } storeWindowDimensions() { this.windowDimensions = this.getWindowDimensions(); if (this.isValidDimensions(this.windowDimensions)) { localStorage.setItem( 'defaultWindowDimensions', JSON.stringify(this.windowDimensions) ); } } getDefaultWindowDimensions() { const { windowDimensions } = this.getLoadSettings(); if (windowDimensions) return windowDimensions; let dimensions; try { dimensions = JSON.parse(localStorage.getItem('defaultWindowDimensions')); } catch (error) { console.warn('Error parsing default window dimensions', error); localStorage.removeItem('defaultWindowDimensions'); } if (dimensions && this.isValidDimensions(dimensions)) { return dimensions; } else { const { width, height } = this.applicationDelegate.getPrimaryDisplayWorkAreaSize(); return { x: 0, y: 0, width: Math.min(1024, width), height }; } } async restoreWindowDimensions() { if ( !this.windowDimensions || !this.isValidDimensions(this.windowDimensions) ) { this.windowDimensions = this.getDefaultWindowDimensions(); } await this.setWindowDimensions(this.windowDimensions); return this.windowDimensions; } restoreWindowBackground() { const backgroundColor = window.localStorage.getItem( 'atom:window-background-color' ); if (backgroundColor) { this.backgroundStylesheet = document.createElement('style'); this.backgroundStylesheet.type = 'text/css'; this.backgroundStylesheet.innerText = `html, body { background: ${backgroundColor} !important; }`; document.head.appendChild(this.backgroundStylesheet); } } storeWindowBackground() { if (this.inSpecMode()) return; const backgroundColor = this.window.getComputedStyle( this.workspace.getElement() )['background-color']; this.window.localStorage.setItem( 'atom:window-background-color', backgroundColor ); } // Call this method when establishing a real application window. async startEditorWindow() { StartupTime.addMarker('window:environment:start-editor-window:start'); if (this.getLoadSettings().clearWindowState) { await this.stateStore.clear(); } this.unloading = false; const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks(); const loadStatePromise = this.loadState().then(async state => { this.windowDimensions = state && state.windowDimensions; if (!this.getLoadSettings().headless) { StartupTime.addMarker( 'window:environment:start-editor-window:display-window' ); await this.displayWindow(); } this.commandInstaller.installAtomCommand(false, error => { if (error) console.warn(error.message); }); this.commandInstaller.installApmCommand(false, error => { if (error) console.warn(error.message); }); this.disposables.add( this.applicationDelegate.onDidChangeUserSettings(settings => this.config.resetUserSettings(settings) ) ); this.disposables.add( this.applicationDelegate.onDidFailToReadUserSettings(message => this.notifications.addError(message) ) ); this.disposables.add( this.applicationDelegate.onDidOpenLocations( this.openLocations.bind(this) ) ); this.disposables.add( this.applicationDelegate.onApplicationMenuCommand( this.dispatchApplicationMenuCommand.bind(this) ) ); this.disposables.add( this.applicationDelegate.onContextMenuCommand( this.dispatchContextMenuCommand.bind(this) ) ); this.disposables.add( this.applicationDelegate.onURIMessage( this.dispatchURIMessage.bind(this) ) ); this.disposables.add( this.applicationDelegate.onDidRequestUnload( this.prepareToUnloadEditorWindow.bind(this) ) ); this.listenForUpdates(); this.registerDefaultTargetForKeymaps(); StartupTime.addMarker( 'window:environment:start-editor-window:load-packages' ); this.packages.loadPackages(); const startTime = Date.now(); StartupTime.addMarker( 'window:environment:start-editor-window:deserialize-state' ); await this.deserialize(state); this.deserializeTimings.atom = Date.now() - startTime; if ( process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom' ) { this.workspace.addHeaderPanel({ item: new TitleBar({ workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate }) }); this.document.body.classList.add('custom-title-bar'); } if ( process.platform === 'darwin' && this.config.get('core.titleBar') === 'custom-inset' ) { this.workspace.addHeaderPanel({ item: new TitleBar({ workspace: this.workspace, themes: this.themes, applicationDelegate: this.applicationDelegate }) }); this.document.body.classList.add('custom-inset-title-bar'); } if ( process.platform === 'darwin' && this.config.get('core.titleBar') === 'hidden' ) { this.document.body.classList.add('hidden-title-bar'); } this.document.body.appendChild(this.workspace.getElement()); if (this.backgroundStylesheet) this.backgroundStylesheet.remove(); let previousProjectPaths = this.project.getPaths(); this.disposables.add( this.project.onDidChangePaths(newPaths => { for (let path of previousProjectPaths) { if ( this.pathsWithWaitSessions.has(path) && !newPaths.includes(path) ) { this.applicationDelegate.didClosePathWithWaitSession(path); } } previousProjectPaths = newPaths; this.applicationDelegate.setProjectRoots(newPaths); }) ); this.disposables.add( this.workspace.onDidDestroyPaneItem(({ item }) => { const path = item.getPath && item.getPath(); if (this.pathsWithWaitSessions.has(path)) { this.applicationDelegate.didClosePathWithWaitSession(path); } }) ); StartupTime.addMarker( 'window:environment:start-editor-window:activate-packages' ); this.packages.activate(); this.keymaps.loadUserKeymap(); if (!this.getLoadSettings().safeMode) this.requireUserInitScript(); this.menu.update(); StartupTime.addMarker( 'window:environment:start-editor-window:open-editor' ); await this.openInitialEmptyEditorIfNecessary(); }); const loadHistoryPromise = this.history.loadState().then(() => { this.reopenProjectMenuManager = new ReopenProjectMenuManager({ menu: this.menu, commands: this.commands, history: this.history, config: this.config, open: paths => this.open({ pathsToOpen: paths, safeMode: this.inSafeMode(), devMode: this.inDevMode() }) }); this.reopenProjectMenuManager.update(); }); const output = await Promise.all([ loadStatePromise, loadHistoryPromise, updateProcessEnvPromise ]); StartupTime.addMarker('window:environment:start-editor-window:end'); return output; } serialize(options) { return { version: this.constructor.version, project: this.project.serialize(options), workspace: this.workspace.serialize(), packageStates: this.packages.serialize(), grammars: this.grammars.serialize(), fullScreen: this.isFullScreen(), windowDimensions: this.windowDimensions }; } async prepareToUnloadEditorWindow() { try { await this.saveState({ isUnloading: true }); } catch (error) { console.error(error); } const closing = !this.workspace || (await this.workspace.confirmClose({ windowCloseRequested: true, projectHasPaths: this.project.getPaths().length > 0 })); if (closing) { this.unloading = true; await this.packages.deactivatePackages(); } return closing; } unloadEditorWindow() { if (!this.project) return; this.storeWindowBackground(); this.saveBlobStoreSync(); } saveBlobStoreSync() { if (this.enablePersistence) { this.blobStore.save(); } } openInitialEmptyEditorIfNecessary() { if (!this.config.get('core.openEmptyEditorOnStart')) return; const { hasOpenFiles } = this.getLoadSettings(); if (!hasOpenFiles && this.workspace.getPaneItems().length === 0) { return this.workspace.open(null, { pending: true }); } } installUncaughtErrorHandler() { this.previousWindowErrorHandler = this.window.onerror; this.window.onerror = (message, url, line, column, originalError) => { const mapping = mapSourcePosition({ source: url, line, column }); line = mapping.line; column = mapping.column; if (url === '') url = mapping.source; const eventObject = { message, url, line, column, originalError }; let openDevTools = true; eventObject.preventDefault = () => { openDevTools = false; }; this.emitter.emit('will-throw-error', eventObject); if (openDevTools) { this.openDevTools().then(() => this.executeJavaScriptInDevTools('DevToolsAPI.showPanel("console")') ); } this.emitter.emit('did-throw-error', { message, url, line, column, originalError }); }; } uninstallUncaughtErrorHandler() { this.window.onerror = this.previousWindowErrorHandler; } installWindowEventHandler() { this.windowEventHandler = new WindowEventHandler({ atomEnvironment: this, applicationDelegate: this.applicationDelegate }); this.windowEventHandler.initialize(this.window, this.document); } uninstallWindowEventHandler() { if (this.windowEventHandler) { this.windowEventHandler.unsubscribe(); } this.windowEventHandler = null; } didChangeStyles(styleElement) { TextEditor.didUpdateStyles(); if (styleElement.textContent.indexOf('scrollbar') >= 0) { TextEditor.didUpdateScrollbarStyles(); } } async updateProcessEnvAndTriggerHooks() { await this.updateProcessEnv(this.getLoadSettings().env); this.shellEnvironmentLoaded = true; this.emitter.emit('loaded-shell-environment'); this.packages.triggerActivationHook('core:loaded-shell-environment'); } /* Section: Messaging the User */ // Essential: Visually and audibly trigger a beep. beep() { if (this.config.get('core.audioBeep')) this.applicationDelegate.playBeepSound(); this.emitter.emit('did-beep'); } // Essential: A flexible way to open a dialog akin to an alert dialog. // // While both async and sync versions are provided, it is recommended to use the async version // such that the renderer process is not blocked while the dialog box is open. // // The async version accepts the same options as Electron's `dialog.showMessageBox`. // For convenience, it sets `type` to `'info'` and `normalizeAccessKeys` to `true` by default. // // 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 // // ```js // // Async version (recommended) // atom.confirm({ // message: 'How you feeling?', // detail: 'Be honest.', // buttons: ['Good', 'Bad'] // }, response => { // if (response === 0) { // window.alert('good to hear') // } else { // window.alert('bummer') // } // }) // ``` // // ```js // // Legacy sync version // const chosen = atom.confirm({ // message: 'How you feeling?', // detailedMessage: 'Be honest.', // buttons: { // Good: () => window.alert('good to hear'), // Bad: () => window.alert('bummer') // } // }) // ``` // // * `options` An options {Object}. If the callback argument is also supplied, see the documentation at // https://electronjs.org/docs/api/dialog#dialogshowmessageboxbrowserwindow-options-callback for the list of // available options. Otherwise, only the following keys are accepted: // * `message` The {String} message to display. // * `detailedMessage` (optional) The {String} detailed message to display. // * `buttons` (optional) Either an {Array} of {String}s or an {Object} where keys are // button names and the values are callback {Function}s to invoke when clicked. // * `callback` (optional) A {Function} that will be called with the index of the chosen option. // If a callback is supplied, the dialog will be non-blocking. This argument is recommended. // // 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. // If a callback function is supplied, returns `undefined`. confirm(options = {}, callback) { if (callback) { // Async: no return value this.applicationDelegate.confirm(options, callback); } else { return this.applicationDelegate.confirm(options); } } /* Section: Managing the Dev Tools */ // Extended: Open the dev tools for the current window. // // Returns a {Promise} that resolves when the DevTools have been opened. openDevTools() { return this.applicationDelegate.openWindowDevTools(); } // Extended: Toggle the visibility of the dev tools for the current window. // // Returns a {Promise} that resolves when the DevTools have been opened or // closed. toggleDevTools() { return this.applicationDelegate.toggleWindowDevTools(); } // Extended: Execute code in dev tools. executeJavaScriptInDevTools(code) { return this.applicationDelegate.executeJavaScriptInWindowDevTools(code); } /* Section: Private */ assert(condition, message, callbackOrMetadata) { if (condition) return true; const error = new Error(`Assertion failed: ${message}`); Error.captureStackTrace(error, this.assert); if (callbackOrMetadata) { if (typeof callbackOrMetadata === 'function') { callbackOrMetadata(error); } else { error.metadata = callbackOrMetadata; } } this.emitter.emit('did-fail-assertion', error); if (!this.isReleasedVersion()) throw error; return false; } loadThemes() { return this.themes.load(); } setDocumentEdited(edited) { if ( typeof this.applicationDelegate.setWindowDocumentEdited === 'function' ) { this.applicationDelegate.setWindowDocumentEdited(edited); } } setRepresentedFilename(filename) { if ( typeof this.applicationDelegate.setWindowRepresentedFilename === 'function' ) { this.applicationDelegate.setWindowRepresentedFilename(filename); } } addProjectFolder() { return new Promise(resolve => { this.pickFolder(selectedPaths => { this.addToProject(selectedPaths || []).then(resolve); }); }); } async addToProject(projectPaths) { const state = await this.loadState(this.getStateKey(projectPaths)); if (state && this.project.getPaths().length === 0) { this.attemptRestoreProjectStateForPaths(state, projectPaths); } else { projectPaths.map(folder => this.project.addPath(folder)); } } async attemptRestoreProjectStateForPaths( state, projectPaths, filesToOpen = [] ) { const center = this.workspace.getCenter(); const windowIsUnused = () => { for (let container of this.workspace.getPaneContainers()) { for (let item of container.getPaneItems()) { if (item instanceof TextEditor) { if (item.getPath() || item.isModified()) return false; } else { if (container === center) return false; } } } return true; }; if (windowIsUnused()) { await this.restoreStateIntoThisEnvironment(state); return Promise.all(filesToOpen.map(file => this.workspace.open(file))); } else { let resolveDiscardStatePromise = null; const discardStatePromise = new Promise(resolve => { resolveDiscardStatePromise = resolve; }); const nouns = projectPaths.length === 1 ? 'folder' : 'folders'; this.confirm( { message: 'Previous automatically-saved project state detected', detail: `There is previously saved state for the selected ${nouns}. ` + `Would you like to add the ${nouns} to this window, permanently discarding the saved state, ` + `or open the ${nouns} in a new window, restoring the saved state?`, buttons: [ '&Open in new window and recover state', '&Add to this window and discard state' ] }, response => { if (response === 0) { this.open({ pathsToOpen: projectPaths.concat(filesToOpen), newWindow: true, devMode: this.inDevMode(), safeMode: this.inSafeMode() }); resolveDiscardStatePromise(Promise.resolve(null)); } else if (response === 1) { for (let selectedPath of projectPaths) { this.project.addPath(selectedPath); } resolveDiscardStatePromise( Promise.all(filesToOpen.map(file => this.workspace.open(file))) ); } } ); return discardStatePromise; } } restoreStateIntoThisEnvironment(state) { state.fullScreen = this.isFullScreen(); for (let pane of this.workspace.getPanes()) { pane.destroy(); } return this.deserialize(state); } showSaveDialogSync(options = {}) { deprecate(`atom.showSaveDialogSync is deprecated and will be removed soon. Please, implement ::saveAs and ::getSaveDialogOptions instead for pane items or use Pane::saveItemAs for programmatic saving.`); return this.applicationDelegate.showSaveDialog(options); } async saveState(options, storageKey) { if (this.enablePersistence && this.project) { const state = this.serialize(options); if (!storageKey) storageKey = this.getStateKey(this.project && this.project.getPaths()); if (storageKey) { await this.stateStore.save(storageKey, state); } else { await this.applicationDelegate.setTemporaryWindowState(state); } } } loadState(stateKey) { if (this.enablePersistence) { if (!stateKey) stateKey = this.getStateKey(this.getLoadSettings().initialProjectRoots); if (stateKey) { return this.stateStore.load(stateKey); } else { return this.applicationDelegate.getTemporaryWindowState(); } } else { return Promise.resolve(null); } } async deserialize(state) { if (!state) return Promise.resolve(); this.setFullScreen(state.fullScreen); const missingProjectPaths = []; this.packages.packageStates = state.packageStates || {}; let startTime = Date.now(); if (state.project) { try { await this.project.deserialize(state.project, this.deserializers); } catch (error) { // We handle the missingProjectPaths case in openLocations(). if (!error.missingProjectPaths) { this.notifications.addError('Unable to deserialize project', { description: error.message, stack: error.stack }); } } } this.deserializeTimings.project = Date.now() - startTime; if (state.grammars) this.grammars.deserialize(state.grammars); startTime = Date.now(); if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers); this.deserializeTimings.workspace = Date.now() - startTime; if (missingProjectPaths.length > 0) { const count = missingProjectPaths.length === 1 ? '' : missingProjectPaths.length + ' '; const noun = missingProjectPaths.length === 1 ? 'folder' : 'folders'; const toBe = missingProjectPaths.length === 1 ? 'is' : 'are'; const escaped = missingProjectPaths.map( projectPath => `\`${projectPath}\`` ); let group; switch (escaped.length) { case 1: group = escaped[0]; break; case 2: group = `${escaped[0]} and ${escaped[1]}`; break; default: group = escaped.slice(0, -1).join(', ') + `, and ${escaped[escaped.length - 1]}`; } this.notifications.addError(`Unable to open ${count}project ${noun}`, { description: `Project ${noun} ${group} ${toBe} no longer on disk.` }); } } getStateKey(paths) { if (paths && paths.length > 0) { const sha1 = crypto .createHash('sha1') .update( paths .slice() .sort() .join('\n') ) .digest('hex'); return `editor-${sha1}`; } else { return null; } } getConfigDirPath() { if (!this.configDirPath) this.configDirPath = process.env.ATOM_HOME; return this.configDirPath; } getUserInitScriptPath() { const initScriptPath = fs.resolve(this.getConfigDirPath(), 'init', [ 'js', 'coffee' ]); return initScriptPath || path.join(this.getConfigDirPath(), 'init.coffee'); } requireUserInitScript() { const userInitScriptPath = this.getUserInitScriptPath(); if (userInitScriptPath) { try { if (fs.isFileSync(userInitScriptPath)) require(userInitScriptPath); } catch (error) { this.notifications.addError( `Failed to load \`${userInitScriptPath}\``, { detail: error.message, dismissable: true } ); } } } // TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead onUpdateAvailable(callback) { return this.emitter.on('update-available', callback); } updateAvailable(details) { return this.emitter.emit('update-available', details); } listenForUpdates() { // listen for updates available locally (that have been successfully downloaded) this.disposables.add( this.autoUpdater.onDidCompleteDownloadingUpdate( this.updateAvailable.bind(this) ) ); } setBodyPlatformClass() { this.document.body.classList.add(`platform-${process.platform}`); } setAutoHideMenuBar(autoHide) { this.applicationDelegate.setAutoHideWindowMenuBar(autoHide); this.applicationDelegate.setWindowMenuBarVisibility(!autoHide); } dispatchApplicationMenuCommand(command, arg) { let { activeElement } = this.document; // Use the workspace element if body has focus if (activeElement === this.document.body) { activeElement = this.workspace.getElement(); } this.commands.dispatch(activeElement, command, arg); } dispatchContextMenuCommand(command, ...args) { this.commands.dispatch(this.contextMenu.activeElement, command, args); } dispatchURIMessage(uri) { if (this.packages.hasLoadedInitialPackages()) { this.uriHandlerRegistry.handleURI(uri); } else { let subscription = this.packages.onDidLoadInitialPackages(() => { subscription.dispose(); this.uriHandlerRegistry.handleURI(uri); }); } } async openLocations(locations) { const needsProjectPaths = this.project && this.project.getPaths().length === 0; const foldersToAddToProject = new Set(); const fileLocationsToOpen = []; const missingFolders = []; // Asynchronously fetch stat information about each requested path to open. const locationStats = await Promise.all( locations.map(async location => { const stats = location.pathToOpen ? await stat(location.pathToOpen).catch(() => null) : null; return { location, stats }; }) ); for (const { location, stats } of locationStats) { const { pathToOpen } = location; if (!pathToOpen) { // Untitled buffer fileLocationsToOpen.push(location); continue; } if (stats !== null) { // Path exists if (stats.isDirectory()) { // Directory: add as a project folder foldersToAddToProject.add( this.project.getDirectoryForProjectPath(pathToOpen).getPath() ); } else if (stats.isFile()) { if (location.isDirectory) { // File: no longer a directory missingFolders.push(location); } else { // File: add as a file location fileLocationsToOpen.push(location); } } } else { // Path does not exist // Attempt to interpret as a URI from a non-default directory provider const directory = this.project.getProvidedDirectoryForProjectPath( pathToOpen ); if (directory) { // Found: add as a project folder foldersToAddToProject.add(directory.getPath()); } else if (location.isDirectory) { // Not found and must be a directory: add to missing list and use to derive state key missingFolders.push(location); } else { // Not found: open as a new file fileLocationsToOpen.push(location); } } if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen); } let restoredState = false; if (foldersToAddToProject.size > 0 || missingFolders.length > 0) { // Include missing folders in the state key so that sessions restored with no-longer-present project root folders // don't lose data. const foldersForStateKey = Array.from(foldersToAddToProject).concat( missingFolders.map(location => location.pathToOpen) ); const state = await this.loadState( this.getStateKey(Array.from(foldersForStateKey)) ); // only restore state if this is the first path added to the project if (state && needsProjectPaths) { const files = fileLocationsToOpen.map(location => location.pathToOpen); await this.attemptRestoreProjectStateForPaths( state, Array.from(foldersToAddToProject), files ); restoredState = true; } else { for (let folder of foldersToAddToProject) { this.project.addPath(folder); } } } if (!restoredState) { const fileOpenPromises = []; for (const { pathToOpen, initialLine, initialColumn } of fileLocationsToOpen) { fileOpenPromises.push( this.workspace && this.workspace.open(pathToOpen, { initialLine, initialColumn }) ); } await Promise.all(fileOpenPromises); } if (missingFolders.length > 0) { let message = 'Unable to open project folder'; if (missingFolders.length > 1) { message += 's'; } let description = 'The '; if (missingFolders.length === 1) { description += 'directory `'; description += missingFolders[0].pathToOpen; description += '` does not exist.'; } else if (missingFolders.length === 2) { description += `directories \`${missingFolders[0].pathToOpen}\` `; description += `and \`${missingFolders[1].pathToOpen}\` do not exist.`; } else { description += 'directories '; description += missingFolders .slice(0, -1) .map(location => location.pathToOpen) .map(pathToOpen => '`' + pathToOpen + '`, ') .join(''); description += 'and `' + missingFolders[missingFolders.length - 1].pathToOpen + '` do not exist.'; } this.notifications.addWarning(message, { description }); } ipcRenderer.send('window-command', 'window:locations-opened'); } resolveProxy(url) { return new Promise((resolve, reject) => { const requestId = this.nextProxyRequestId++; const disposable = this.applicationDelegate.onDidResolveProxy( (id, proxy) => { if (id === requestId) { disposable.dispose(); resolve(proxy); } } ); return this.applicationDelegate.resolveProxy(requestId, url); }); } } AtomEnvironment.version = 1; AtomEnvironment.prototype.saveStateDebounceInterval = 1000; module.exports = AtomEnvironment; /* eslint-disable */ // Preserve this deprecation until 2.0. Sorry. Should have removed Q sooner. Promise.prototype.done = function (callback) { deprecate('Atom now uses ES6 Promises instead of Q. Call promise.then instead of promise.done') return this.then(callback) } /* eslint-enable */ ================================================ FILE: src/atom-paths.js ================================================ const fs = require('fs-plus'); const path = require('path'); const hasWriteAccess = dir => { const testFilePath = path.join(dir, 'write.test'); try { fs.writeFileSync(testFilePath, new Date().toISOString(), { flag: 'w+' }); fs.unlinkSync(testFilePath); return true; } catch (err) { return false; } }; const getAppDirectory = () => { switch (process.platform) { case 'darwin': return process.execPath.substring( 0, process.execPath.indexOf('.app') + 4 ); case 'linux': case 'win32': return path.join(process.execPath, '..'); } }; module.exports = { setAtomHome: homePath => { // When a read-writeable .atom folder exists above app use that const portableHomePath = path.join(getAppDirectory(), '..', '.atom'); if (fs.existsSync(portableHomePath)) { if (hasWriteAccess(portableHomePath)) { process.env.ATOM_HOME = portableHomePath; } else { // A path exists so it was intended to be used but we didn't have rights, so warn. console.log( `Insufficient permission to portable Atom home "${portableHomePath}".` ); } } // Check ATOM_HOME environment variable next if (process.env.ATOM_HOME !== undefined) { return; } // Fall back to default .atom folder in users home folder process.env.ATOM_HOME = path.join(homePath, '.atom'); }, setUserData: app => { const electronUserDataPath = path.join( process.env.ATOM_HOME, 'electronUserData' ); if (fs.existsSync(electronUserDataPath)) { if (hasWriteAccess(electronUserDataPath)) { app.setPath('userData', electronUserDataPath); } else { // A path exists so it was intended to be used but we didn't have rights, so warn. console.log( `Insufficient permission to Electron user data "${electronUserDataPath}".` ); } } }, getAppDirectory: getAppDirectory }; ================================================ FILE: src/auto-update-manager.js ================================================ const { Emitter, CompositeDisposable } = require('event-kit'); module.exports = class AutoUpdateManager { constructor({ applicationDelegate }) { this.applicationDelegate = applicationDelegate; this.subscriptions = new CompositeDisposable(); this.emitter = new Emitter(); } initialize() { this.subscriptions.add( this.applicationDelegate.onDidBeginCheckingForUpdate(() => { this.emitter.emit('did-begin-checking-for-update'); }), this.applicationDelegate.onDidBeginDownloadingUpdate(() => { this.emitter.emit('did-begin-downloading-update'); }), this.applicationDelegate.onDidCompleteDownloadingUpdate(details => { this.emitter.emit('did-complete-downloading-update', details); }), this.applicationDelegate.onUpdateNotAvailable(() => { this.emitter.emit('update-not-available'); }), this.applicationDelegate.onUpdateError(() => { this.emitter.emit('update-error'); }) ); } destroy() { this.subscriptions.dispose(); this.emitter.dispose(); } checkForUpdate() { this.applicationDelegate.checkForUpdate(); } restartAndInstallUpdate() { this.applicationDelegate.restartAndInstallUpdate(); } getState() { return this.applicationDelegate.getAutoUpdateManagerState(); } getErrorMessage() { return this.applicationDelegate.getAutoUpdateManagerErrorMessage(); } platformSupportsUpdates() { return ( atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported' ); } onDidBeginCheckingForUpdate(callback) { return this.emitter.on('did-begin-checking-for-update', callback); } onDidBeginDownloadingUpdate(callback) { return this.emitter.on('did-begin-downloading-update', callback); } onDidCompleteDownloadingUpdate(callback) { return this.emitter.on('did-complete-downloading-update', callback); } // TODO: When https://github.com/atom/electron/issues/4587 is closed, we can // add an update-available event. // onUpdateAvailable (callback) { // return this.emitter.on('update-available', callback) // } onUpdateNotAvailable(callback) { return this.emitter.on('update-not-available', callback); } onUpdateError(callback) { return this.emitter.on('update-error', callback); } getPlatform() { return process.platform; } }; ================================================ FILE: src/babel.js ================================================ 'use strict'; const crypto = require('crypto'); const path = require('path'); const defaultOptions = require('../static/babelrc.json'); let babel = null; let babelVersionDirectory = null; const PREFIXES = [ '/** @babel */', '"use babel"', "'use babel'", '/* @flow */', '// @flow' ]; const PREFIX_LENGTH = Math.max.apply( Math, PREFIXES.map(function(prefix) { return prefix.length; }) ); exports.shouldCompile = function(sourceCode) { const start = sourceCode.substr(0, PREFIX_LENGTH); return PREFIXES.some(function(prefix) { return start.indexOf(prefix) === 0; }); }; exports.getCachePath = function(sourceCode) { if (babelVersionDirectory == null) { const babelVersion = require('babel-core/package.json').version; babelVersionDirectory = path.join( 'js', 'babel', createVersionAndOptionsDigest(babelVersion, defaultOptions) ); } return path.join( babelVersionDirectory, crypto .createHash('sha1') .update(sourceCode, 'utf8') .digest('hex') + '.js' ); }; exports.compile = function(sourceCode, filePath) { if (!babel) { babel = require('babel-core'); const Logger = require('babel-core/lib/transformation/file/logger'); const noop = function() {}; Logger.prototype.debug = noop; Logger.prototype.verbose = noop; } if (process.platform === 'win32') { filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/'); } const options = { filename: filePath }; for (const key in defaultOptions) { options[key] = defaultOptions[key]; } return babel.transform(sourceCode, options).code; }; function createVersionAndOptionsDigest(version, options) { return crypto .createHash('sha1') .update('babel-core', 'utf8') .update('\0', 'utf8') .update(version, 'utf8') .update('\0', 'utf8') .update(JSON.stringify(options), 'utf8') .digest('hex'); } ================================================ FILE: src/buffered-node-process.js ================================================ const BufferedProcess = require('./buffered-process'); // Extended: Like {BufferedProcess}, but accepts a Node script as the command // to run. // // This is necessary on Windows since it doesn't support shebang `#!` lines. // // ## Examples // // ```js // const {BufferedNodeProcess} = require('atom') // ``` module.exports = class BufferedNodeProcess extends BufferedProcess { // Public: Runs the given Node script by spawning a new child process. // // * `options` An {Object} with the following keys: // * `command` The {String} path to the JavaScript script to execute. // * `args` The {Array} of arguments to pass to the script (optional). // * `options` The options {Object} to pass to Node's `ChildProcess.spawn` // method (optional). // * `stdout` The callback {Function} that receives a single argument which // contains the standard output from the command. The callback is // called as data is received but it's buffered to ensure only // complete lines are passed until the source stream closes. After // the source stream has closed all remaining data is sent in a // final call (optional). // * `stderr` The callback {Function} that receives a single argument which // contains the standard error output from the command. The // callback is called as data is received but it's buffered to // ensure only complete lines are passed until the source stream // closes. After the source stream has closed all remaining data // is sent in a final call (optional). // * `exit` The callback {Function} which receives a single argument // containing the exit status (optional). constructor({ command, args, options = {}, stdout, stderr, exit }) { options.env = options.env || Object.create(process.env); options.env.ELECTRON_RUN_AS_NODE = 1; options.env.ELECTRON_NO_ATTACH_CONSOLE = 1; args = args ? args.slice() : []; args.unshift(command); args.unshift('--no-deprecation'); super({ command: process.execPath, args, options, stdout, stderr, exit }); } }; ================================================ FILE: src/buffered-process.js ================================================ const _ = require('underscore-plus'); const ChildProcess = require('child_process'); const { Emitter } = require('event-kit'); const path = require('path'); // Extended: A wrapper which provides standard error/output line buffering for // Node's ChildProcess. // // ## Examples // // ```js // {BufferedProcess} = require('atom') // // const command = 'ps' // const args = ['-ef'] // const stdout = (output) => console.log(output) // const exit = (code) => console.log("ps -ef exited with #{code}") // const process = new BufferedProcess({command, args, stdout, exit}) // ``` module.exports = class BufferedProcess { /* Section: Construction */ // Public: Runs the given command by spawning a new child process. // // * `options` An {Object} with the following keys: // * `command` The {String} command to execute. // * `args` The {Array} of arguments to pass to the command (optional). // * `options` {Object} (optional) The options {Object} to pass to Node's // `ChildProcess.spawn` method. // * `stdout` {Function} (optional) The callback that receives a single // argument which contains the standard output from the command. The // callback is called as data is received but it's buffered to ensure only // complete lines are passed until the source stream closes. After the // source stream has closed all remaining data is sent in a final call. // * `data` {String} // * `stderr` {Function} (optional) The callback that receives a single // argument which contains the standard error output from the command. The // callback is called as data is received but it's buffered to ensure only // complete lines are passed until the source stream closes. After the // source stream has closed all remaining data is sent in a final call. // * `data` {String} // * `exit` {Function} (optional) The callback which receives a single // argument containing the exit status. // * `code` {Number} // * `autoStart` {Boolean} (optional) Whether the command will automatically start // when this BufferedProcess is created. Defaults to true. When set to false you // must call the `start` method to start the process. constructor({ command, args, options = {}, stdout, stderr, exit, autoStart = true } = {}) { this.emitter = new Emitter(); this.command = command; this.args = args; this.options = options; this.stdout = stdout; this.stderr = stderr; this.exit = exit; if (autoStart === true) { this.start(); } this.killed = false; } start() { if (this.started === true) return; this.started = true; // Related to joyent/node#2318 if (process.platform === 'win32' && this.options.shell === undefined) { this.spawnWithEscapedWindowsArgs(this.command, this.args, this.options); } else { this.spawn(this.command, this.args, this.options); } this.handleEvents(this.stdout, this.stderr, this.exit); } // Windows has a bunch of special rules that node still doesn't take care of for you spawnWithEscapedWindowsArgs(command, args, options) { let cmdArgs = []; // Quote all arguments and escapes inner quotes if (args) { cmdArgs = args .filter(arg => arg != null) .map(arg => { if (this.isExplorerCommand(command) && /^\/[a-zA-Z]+,.*$/.test(arg)) { // Don't wrap /root,C:\folder style arguments to explorer calls in // quotes since they will not be interpreted correctly if they are return arg; } else { // Escape double quotes by putting a backslash in front of them return `"${arg.toString().replace(/"/g, '\\"')}"`; } }); } // The command itself is quoted if it contains spaces, &, ^, | or # chars cmdArgs.unshift( /\s|&|\^|\(|\)|\||#/.test(command) ? `"${command}"` : command ); const cmdOptions = _.clone(options); cmdOptions.windowsVerbatimArguments = true; this.spawn( this.getCmdPath(), ['/s', '/d', '/c', `"${cmdArgs.join(' ')}"`], cmdOptions ); } /* Section: Event Subscription */ // Public: Will call your callback when an error will be raised by the process. // Usually this is due to the command not being available or not on the PATH. // You can call `handle()` on the object passed to your callback to indicate // that you have handled this error. // // * `callback` {Function} callback // * `errorObject` {Object} // * `error` {Object} the error object // * `handle` {Function} call this to indicate you have handled the error. // The error will not be thrown if this function is called. // // Returns a {Disposable} onWillThrowError(callback) { return this.emitter.on('will-throw-error', callback); } /* Section: Helper Methods */ // Helper method to pass data line by line. // // * `stream` The Stream to read from. // * `onLines` The callback to call with each line of data. // * `onDone` The callback to call when the stream has closed. bufferStream(stream, onLines, onDone) { stream.setEncoding('utf8'); let buffered = ''; stream.on('data', data => { if (this.killed) return; let bufferedLength = buffered.length; buffered += data; let lastNewlineIndex = data.lastIndexOf('\n'); if (lastNewlineIndex !== -1) { let lineLength = lastNewlineIndex + bufferedLength + 1; onLines(buffered.substring(0, lineLength)); buffered = buffered.substring(lineLength); } }); stream.on('close', () => { if (this.killed) return; if (buffered.length > 0) onLines(buffered); onDone(); }); } // Kill all child processes of the spawned cmd.exe process on Windows. // // This is required since killing the cmd.exe does not terminate child // processes. killOnWindows() { if (!this.process) return; const parentPid = this.process.pid; const cmd = 'wmic'; const args = [ 'process', 'where', `(ParentProcessId=${parentPid})`, 'get', 'processid' ]; let wmicProcess; try { wmicProcess = ChildProcess.spawn(cmd, args); } catch (spawnError) { this.killProcess(); return; } wmicProcess.on('error', () => {}); // ignore errors let output = ''; wmicProcess.stdout.on('data', data => { output += data; }); wmicProcess.stdout.on('close', () => { for (let pid of output.split(/\s+/)) { if (!/^\d{1,10}$/.test(pid)) continue; pid = parseInt(pid, 10); if (!pid || pid === parentPid) continue; try { process.kill(pid); } catch (error) {} } this.killProcess(); }); } killProcess() { if (this.process) this.process.kill(); this.process = null; } isExplorerCommand(command) { if (command === 'explorer.exe' || command === 'explorer') { return true; } else if (process.env.SystemRoot) { return ( command === path.join(process.env.SystemRoot, 'explorer.exe') || command === path.join(process.env.SystemRoot, 'explorer') ); } else { return false; } } getCmdPath() { if (process.env.comspec) { return process.env.comspec; } else if (process.env.SystemRoot) { return path.join(process.env.SystemRoot, 'System32', 'cmd.exe'); } else { return 'cmd.exe'; } } // Public: Terminate the process. kill() { if (this.killed) return; this.killed = true; if (process.platform === 'win32') { this.killOnWindows(); } else { this.killProcess(); } } spawn(command, args, options) { try { this.process = ChildProcess.spawn(command, args, options); } catch (spawnError) { process.nextTick(() => this.handleError(spawnError)); } } handleEvents(stdout, stderr, exit) { if (!this.process) return; const triggerExitCallback = () => { if (this.killed) return; if ( stdoutClosed && stderrClosed && processExited && typeof exit === 'function' ) { exit(exitCode); } }; let stdoutClosed = true; let stderrClosed = true; let processExited = true; let exitCode = 0; if (stdout) { stdoutClosed = false; this.bufferStream(this.process.stdout, stdout, () => { stdoutClosed = true; triggerExitCallback(); }); } if (stderr) { stderrClosed = false; this.bufferStream(this.process.stderr, stderr, () => { stderrClosed = true; triggerExitCallback(); }); } if (exit) { processExited = false; this.process.on('exit', code => { exitCode = code; processExited = true; triggerExitCallback(); }); } this.process.on('error', error => { this.handleError(error); }); } handleError(error) { let handled = false; const handle = () => { handled = true; }; this.emitter.emit('will-throw-error', { error, handle }); if (error.code === 'ENOENT' && error.syscall.indexOf('spawn') === 0) { error = new Error( `Failed to spawn command \`${this.command}\`. Make sure \`${ this.command }\` is installed and on your PATH`, error.path ); error.name = 'BufferedProcessError'; } if (!handled) throw error; } }; ================================================ FILE: src/clipboard.js ================================================ const crypto = require('crypto'); const { clipboard } = require('electron'); // Extended: Represents the clipboard used for copying and pasting in Atom. // // An instance of this class is always available as the `atom.clipboard` global. // // ## Examples // // ```js // atom.clipboard.write('hello') // // console.log(atom.clipboard.read()) // 'hello' // ``` module.exports = class Clipboard { constructor() { this.reset(); } reset() { this.metadata = null; this.signatureForMetadata = null; } // Creates an `md5` hash of some text. // // * `text` A {String} to hash. // // Returns a hashed {String}. md5(text) { return crypto .createHash('md5') .update(text, 'utf8') .digest('hex'); } // Public: Write the given text to the clipboard. // // The metadata associated with the text is available by calling // {::readWithMetadata}. // // * `text` The {String} to store. // * `metadata` (optional) The additional info to associate with the text. write(text, metadata) { text = text.replace(/\r?\n/g, process.platform === 'win32' ? '\r\n' : '\n'); this.signatureForMetadata = this.md5(text); this.metadata = metadata; clipboard.writeText(text); } // Public: Read the text from the clipboard. // // Returns a {String}. read() { return clipboard.readText(); } // Public: Write the given text to the macOS find pasteboard writeFindText(text) { clipboard.writeFindText(text); } // Public: Read the text from the macOS find pasteboard. // // Returns a {String}. readFindText() { return clipboard.readFindText(); } // Public: Read the text from the clipboard and return both the text and the // associated metadata. // // Returns an {Object} with the following keys: // * `text` The {String} clipboard text. // * `metadata` The metadata stored by an earlier call to {::write}. readWithMetadata() { const text = this.read(); if (this.signatureForMetadata === this.md5(text)) { return { text, metadata: this.metadata }; } else { return { text }; } } }; ================================================ FILE: src/coffee-script.js ================================================ 'use strict'; const crypto = require('crypto'); const path = require('path'); let CoffeeScript = null; exports.shouldCompile = function() { return true; }; exports.getCachePath = function(sourceCode) { return path.join( 'coffee', crypto .createHash('sha1') .update(sourceCode, 'utf8') .digest('hex') + '.js' ); }; exports.compile = function(sourceCode, filePath) { if (!CoffeeScript) { const previousPrepareStackTrace = Error.prepareStackTrace; CoffeeScript = require('coffee-script'); // When it loads, coffee-script reassigns Error.prepareStackTrace. We have // already reassigned it via the 'source-map-support' module, so we need // to set it back. Error.prepareStackTrace = previousPrepareStackTrace; } if (process.platform === 'win32') { filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/'); } const output = CoffeeScript.compile(sourceCode, { filename: filePath, sourceFiles: [filePath], inlineMap: true }); // Strip sourceURL from output so there wouldn't be duplicate entries // in devtools. return output.replace(/\/\/# sourceURL=[^'"\n]+\s*$/, ''); }; ================================================ FILE: src/color.js ================================================ let ParsedColor = null; // Essential: A simple color class returned from {Config::get} when the value // at the key path is of type 'color'. module.exports = class Color { // Essential: Parse a {String} or {Object} into a {Color}. // // * `value` A {String} such as `'white'`, `#ff00ff`, or // `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`, // and `alpha` properties. // // Returns a {Color} or `null` if it cannot be parsed. static parse(value) { switch (typeof value) { case 'string': break; case 'object': if (Array.isArray(value)) { return null; } value = Object.values(value); break; default: return null; } if (!ParsedColor) { ParsedColor = require('color'); } try { var parsedColor = ParsedColor(value); } catch (error) { return null; } return new Color( parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha() ); } constructor(red, green, blue, alpha) { this.red = red; this.green = green; this.blue = blue; this.alpha = alpha; } set red(red) { this._red = parseColor(red); } set green(green) { this._green = parseColor(green); } set blue(blue) { this._blue = parseColor(blue); } set alpha(alpha) { this._alpha = parseAlpha(alpha); } get red() { return this._red; } get green() { return this._green; } get blue() { return this._blue; } get alpha() { return this._alpha; } // Essential: Returns a {String} in the form `'#abcdef'`. toHexString() { return `#${numberToHexString(this.red)}${numberToHexString( this.green )}${numberToHexString(this.blue)}`; } // Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`. toRGBAString() { return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`; } toJSON() { return this.alpha === 1 ? this.toHexString() : this.toRGBAString(); } toString() { return this.toRGBAString(); } isEqual(color) { if (this === color) { return true; } if (!(color instanceof Color)) { color = Color.parse(color); } if (color == null) { return false; } return ( color.red === this.red && color.blue === this.blue && color.green === this.green && color.alpha === this.alpha ); } clone() { return new Color(this.red, this.green, this.blue, this.alpha); } }; function parseColor(colorString) { const color = parseInt(colorString, 10); return isNaN(color) ? 0 : Math.min(Math.max(color, 0), 255); } function parseAlpha(alphaString) { const alpha = parseFloat(alphaString); return isNaN(alpha) ? 1 : Math.min(Math.max(alpha, 0), 1); } function numberToHexString(number) { const hex = number.toString(16); return number < 16 ? `0${hex}` : hex; } ================================================ FILE: src/command-installer.js ================================================ const path = require('path'); const fs = require('fs-plus'); module.exports = class CommandInstaller { constructor(applicationDelegate) { this.applicationDelegate = applicationDelegate; } initialize(appVersion) { this.appVersion = appVersion; } getInstallDirectory() { return '/usr/local/bin'; } getResourcesDirectory() { return process.resourcesPath; } installShellCommandsInteractively() { const showErrorDialog = error => { this.applicationDelegate.confirm( { message: 'Failed to install shell commands', detail: error.message }, () => {} ); }; this.installAtomCommand(true, (error, atomCommandName) => { if (error) return showErrorDialog(error); this.installApmCommand(true, (error, apmCommandName) => { if (error) return showErrorDialog(error); this.applicationDelegate.confirm( { message: 'Commands installed.', detail: `The shell commands \`${atomCommandName}\` and \`${apmCommandName}\` are installed.` }, () => {} ); }); }); } getCommandNameForChannel(commandName) { let channelMatch = this.appVersion.match(/beta|nightly/); let channel = channelMatch ? channelMatch[0] : ''; switch (channel) { case 'beta': return `${commandName}-beta`; case 'nightly': return `${commandName}-nightly`; default: return commandName; } } installAtomCommand(askForPrivilege, callback) { this.installCommand( path.join(this.getResourcesDirectory(), 'app', 'atom.sh'), this.getCommandNameForChannel('atom'), askForPrivilege, callback ); } installApmCommand(askForPrivilege, callback) { this.installCommand( path.join( this.getResourcesDirectory(), 'app', 'apm', 'node_modules', '.bin', 'apm' ), this.getCommandNameForChannel('apm'), askForPrivilege, callback ); } installCommand(commandPath, commandName, askForPrivilege, callback) { if (process.platform !== 'darwin') return callback(); const destinationPath = path.join(this.getInstallDirectory(), commandName); fs.readlink(destinationPath, (error, realpath) => { if (error && error.code !== 'ENOENT') return callback(error); if (realpath === commandPath) return callback(null, commandName); this.createSymlink(fs, commandPath, destinationPath, error => { if (error && error.code === 'EACCES' && askForPrivilege) { const fsAdmin = require('fs-admin'); this.createSymlink(fsAdmin, commandPath, destinationPath, error => { callback(error, commandName); }); } else { callback(error); } }); }); } createSymlink(fs, sourcePath, destinationPath, callback) { fs.unlink(destinationPath, error => { if (error && error.code !== 'ENOENT') return callback(error); fs.makeTree(path.dirname(destinationPath), error => { if (error) return callback(error); fs.symlink(sourcePath, destinationPath, callback); }); }); } }; ================================================ FILE: src/command-registry.js ================================================ 'use strict'; const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); const { calculateSpecificity, validateSelector } = require('clear-cut'); const _ = require('underscore-plus'); let SequenceCount = 0; // Public: Associates listener functions with commands in a // context-sensitive way using CSS selectors. You can access a global instance of // this class via `atom.commands`, and commands registered there will be // presented in the command palette. // // The global command registry facilitates a style of event handling known as // *event delegation* that was popularized by jQuery. Atom commands are expressed // as custom DOM events that can be invoked on the currently focused element via // a key binding or manually via the command palette. Rather than binding // listeners for command events directly to DOM nodes, you instead register // command event listeners globally on `atom.commands` and constrain them to // specific kinds of elements with CSS selectors. // // Command names must follow the `namespace:action` pattern, where `namespace` // will typically be the name of your package, and `action` describes the // behavior of your command. If either part consists of multiple words, these // must be separated by hyphens. E.g. `awesome-package:turn-it-up-to-eleven`. // All words should be lowercased. // // As the event bubbles upward through the DOM, all registered event listeners // with matching selectors are invoked in order of specificity. In the event of a // specificity tie, the most recently registered listener is invoked first. This // mirrors the "cascade" semantics of CSS. Event listeners are invoked in the // context of the current DOM node, meaning `this` always points at // `event.currentTarget`. As is normally the case with DOM events, // `stopPropagation` and `stopImmediatePropagation` can be used to terminate the // bubbling process and prevent invocation of additional listeners. // // ## Example // // Here is a command that inserts the current date in an editor: // // ```coffee // atom.commands.add 'atom-text-editor', // 'user:insert-date': (event) -> // editor = @getModel() // editor.insertText(new Date().toLocaleString()) // ``` module.exports = class CommandRegistry { constructor() { this.handleCommandEvent = this.handleCommandEvent.bind(this); this.rootNode = null; this.clear(); } clear() { this.registeredCommands = {}; this.selectorBasedListenersByCommandName = {}; this.inlineListenersByCommandName = {}; this.emitter = new Emitter(); } attach(rootNode) { this.rootNode = rootNode; for (const command in this.selectorBasedListenersByCommandName) { this.commandRegistered(command); } for (const command in this.inlineListenersByCommandName) { this.commandRegistered(command); } } destroy() { for (const commandName in this.registeredCommands) { this.rootNode.removeEventListener( commandName, this.handleCommandEvent, true ); } } // Public: Add one or more command listeners associated with a selector. // // ## Arguments: Registering One Command // // * `target` A {String} containing a CSS selector or a DOM element. If you // pass a selector, the command will be globally associated with all matching // elements. The `,` combinator is not currently supported. If you pass a // DOM element, the command will be associated with just that element. // * `commandName` A {String} containing the name of a command you want to // handle such as `user:insert-date`. // * `listener` A listener which handles the event. Either a {Function} to // call when the given command is invoked on an element matching the // selector, or an {Object} with a `didDispatch` property which is such a // function. // // The function (`listener` itself if it is a function, or the `didDispatch` // method if `listener` is an object) will be called with `this` referencing // the matching DOM node and the following argument: // * `event`: A standard DOM event instance. Call `stopPropagation` or // `stopImmediatePropagation` to terminate bubbling early. // // Additionally, `listener` may have additional properties which are returned // to those who query using `atom.commands.findCommands`, as well as several // meaningful metadata properties: // * `displayName`: Overrides any generated `displayName` that would // otherwise be generated from the event name. // * `description`: Used by consumers to display detailed information about // the command. // * `hiddenInCommandPalette`: If `true`, this command will not appear in // the bundled command palette by default, but can still be shown with. // the `Command Palette: Show Hidden Commands` command. This is a good // option when you need to register large numbers of commands that don't // make sense to be executed from the command palette. Please use this // option conservatively, as it could reduce the discoverability of your // package's commands. // // ## Arguments: Registering Multiple Commands // // * `target` A {String} containing a CSS selector or a DOM element. If you // pass a selector, the commands will be globally associated with all // matching elements. The `,` combinator is not currently supported. // If you pass a DOM element, the command will be associated with just that // element. // * `commands` An {Object} mapping command names like `user:insert-date` to // listener {Function}s. // // Returns a {Disposable} on which `.dispose()` can be called to remove the // added command handler(s). add(target, commandName, listener, throwOnInvalidSelector = true) { if (typeof commandName === 'object') { const commands = commandName; throwOnInvalidSelector = listener; const disposable = new CompositeDisposable(); for (commandName in commands) { listener = commands[commandName]; disposable.add( this.add(target, commandName, listener, throwOnInvalidSelector) ); } return disposable; } if (listener == null) { throw new Error('Cannot register a command with a null listener.'); } // type Listener = ((e: CustomEvent) => void) | { // displayName?: string, // description?: string, // didDispatch(e: CustomEvent): void, // } if ( typeof listener !== 'function' && typeof listener.didDispatch !== 'function' ) { throw new Error( 'Listener must be a callback function or an object with a didDispatch method.' ); } if (typeof target === 'string') { if (throwOnInvalidSelector) { validateSelector(target); } return this.addSelectorBasedListener(target, commandName, listener); } else { return this.addInlineListener(target, commandName, listener); } } addSelectorBasedListener(selector, commandName, listener) { if (this.selectorBasedListenersByCommandName[commandName] == null) { this.selectorBasedListenersByCommandName[commandName] = []; } const listenersForCommand = this.selectorBasedListenersByCommandName[ commandName ]; const selectorListener = new SelectorBasedListener( selector, commandName, listener ); listenersForCommand.push(selectorListener); this.commandRegistered(commandName); return new Disposable(() => { listenersForCommand.splice( listenersForCommand.indexOf(selectorListener), 1 ); if (listenersForCommand.length === 0) { delete this.selectorBasedListenersByCommandName[commandName]; } }); } addInlineListener(element, commandName, listener) { if (this.inlineListenersByCommandName[commandName] == null) { this.inlineListenersByCommandName[commandName] = new WeakMap(); } const listenersForCommand = this.inlineListenersByCommandName[commandName]; let listenersForElement = listenersForCommand.get(element); if (!listenersForElement) { listenersForElement = []; listenersForCommand.set(element, listenersForElement); } const inlineListener = new InlineListener(commandName, listener); listenersForElement.push(inlineListener); this.commandRegistered(commandName); return new Disposable(() => { listenersForElement.splice( listenersForElement.indexOf(inlineListener), 1 ); if (listenersForElement.length === 0) { listenersForCommand.delete(element); } }); } // Public: Find all registered commands matching a query. // // * `params` An {Object} containing one or more of the following keys: // * `target` A DOM node that is the hypothetical target of a given command. // // Returns an {Array} of `CommandDescriptor` {Object}s containing the following keys: // * `name` The name of the command. For example, `user:insert-date`. // * `displayName` The display name of the command. For example, // `User: Insert Date`. // Additional metadata may also be present in the returned descriptor: // * `description` a {String} describing the function of the command in more // detail than the title // * `tags` an {Array} of {String}s that describe keywords related to the // command // Any additional nonstandard metadata provided when the command was `add`ed // may also be present in the returned descriptor. findCommands({ target }) { const commandNames = new Set(); const commands = []; let currentTarget = target; while (true) { let listeners; for (const name in this.inlineListenersByCommandName) { listeners = this.inlineListenersByCommandName[name]; if (listeners.has(currentTarget) && !commandNames.has(name)) { commandNames.add(name); const targetListeners = listeners.get(currentTarget); commands.push( ...targetListeners.map(listener => listener.descriptor) ); } } for (const commandName in this.selectorBasedListenersByCommandName) { listeners = this.selectorBasedListenersByCommandName[commandName]; for (const listener of listeners) { if (listener.matchesTarget(currentTarget)) { if (!commandNames.has(commandName)) { commandNames.add(commandName); commands.push(listener.descriptor); } } } } if (currentTarget === window) { break; } currentTarget = currentTarget.parentNode || window; } return commands; } // Public: Simulate the dispatch of a command on a DOM node. // // This can be useful for testing when you want to simulate the invocation of a // command on a detached DOM node. Otherwise, the DOM node in question needs to // be attached to the document so the event bubbles up to the root node to be // processed. // // * `target` The DOM node at which to start bubbling the command event. // * `commandName` {String} indicating the name of the command to dispatch. dispatch(target, commandName, detail) { const event = new CustomEvent(commandName, { bubbles: true, detail }); Object.defineProperty(event, 'target', { value: target }); return this.handleCommandEvent(event); } // Public: Invoke the given callback before dispatching a command event. // // * `callback` {Function} to be called before dispatching each command // * `event` The Event that will be dispatched onWillDispatch(callback) { return this.emitter.on('will-dispatch', callback); } // Public: Invoke the given callback after dispatching a command event. // // * `callback` {Function} to be called after dispatching each command // * `event` The Event that was dispatched onDidDispatch(callback) { return this.emitter.on('did-dispatch', callback); } getSnapshot() { const snapshot = {}; for (const commandName in this.selectorBasedListenersByCommandName) { const listeners = this.selectorBasedListenersByCommandName[commandName]; snapshot[commandName] = listeners.slice(); } return snapshot; } restoreSnapshot(snapshot) { this.selectorBasedListenersByCommandName = {}; for (const commandName in snapshot) { const listeners = snapshot[commandName]; this.selectorBasedListenersByCommandName[commandName] = listeners.slice(); } } handleCommandEvent(event) { let propagationStopped = false; let immediatePropagationStopped = false; let matched = []; let currentTarget = event.target; const dispatchedEvent = new CustomEvent(event.type, { bubbles: true, detail: event.detail }); Object.defineProperty(dispatchedEvent, 'eventPhase', { value: Event.BUBBLING_PHASE }); Object.defineProperty(dispatchedEvent, 'currentTarget', { get() { return currentTarget; } }); Object.defineProperty(dispatchedEvent, 'target', { value: currentTarget }); Object.defineProperty(dispatchedEvent, 'preventDefault', { value() { return event.preventDefault(); } }); Object.defineProperty(dispatchedEvent, 'stopPropagation', { value() { event.stopPropagation(); propagationStopped = true; } }); Object.defineProperty(dispatchedEvent, 'stopImmediatePropagation', { value() { event.stopImmediatePropagation(); propagationStopped = true; immediatePropagationStopped = true; } }); Object.defineProperty(dispatchedEvent, 'abortKeyBinding', { value() { if (typeof event.abortKeyBinding === 'function') { event.abortKeyBinding(); } } }); for (const key of Object.keys(event)) { if (!(key in dispatchedEvent)) { dispatchedEvent[key] = event[key]; } } this.emitter.emit('will-dispatch', dispatchedEvent); while (true) { const commandInlineListeners = this.inlineListenersByCommandName[ event.type ] ? this.inlineListenersByCommandName[event.type].get(currentTarget) : null; let listeners = commandInlineListeners || []; if (currentTarget.webkitMatchesSelector != null) { const selectorBasedListeners = ( this.selectorBasedListenersByCommandName[event.type] || [] ) .filter(listener => listener.matchesTarget(currentTarget)) .sort((a, b) => a.compare(b)); listeners = selectorBasedListeners.concat(listeners); } // Call inline listeners first in reverse registration order, // and selector-based listeners by specificity and reverse // registration order. for (let i = listeners.length - 1; i >= 0; i--) { const listener = listeners[i]; if (immediatePropagationStopped) { break; } matched.push(listener.didDispatch.call(currentTarget, dispatchedEvent)); } if (currentTarget === window) { break; } if (propagationStopped) { break; } currentTarget = currentTarget.parentNode || window; } this.emitter.emit('did-dispatch', dispatchedEvent); return matched.length > 0 ? Promise.all(matched) : null; } commandRegistered(commandName) { if (this.rootNode != null && !this.registeredCommands[commandName]) { this.rootNode.addEventListener(commandName, this.handleCommandEvent, { capture: true }); return (this.registeredCommands[commandName] = true); } } }; // type Listener = { // descriptor: CommandDescriptor, // extractDidDispatch: (e: CustomEvent) => void, // }; class SelectorBasedListener { constructor(selector, commandName, listener) { this.selector = selector; this.didDispatch = extractDidDispatch(listener); this.descriptor = extractDescriptor(commandName, listener); this.specificity = calculateSpecificity(this.selector); this.sequenceNumber = SequenceCount++; } compare(other) { return ( this.specificity - other.specificity || this.sequenceNumber - other.sequenceNumber ); } matchesTarget(target) { return ( target.webkitMatchesSelector && target.webkitMatchesSelector(this.selector) ); } } class InlineListener { constructor(commandName, listener) { this.didDispatch = extractDidDispatch(listener); this.descriptor = extractDescriptor(commandName, listener); } } // type CommandDescriptor = { // name: string, // displayName: string, // }; function extractDescriptor(name, listener) { return Object.assign(_.omit(listener, 'didDispatch'), { name, displayName: listener.displayName ? listener.displayName : _.humanizeEventName(name) }); } function extractDidDispatch(listener) { return typeof listener === 'function' ? listener : listener.didDispatch; } ================================================ FILE: src/compile-cache.js ================================================ 'use strict'; // Atom's compile-cache when installing or updating packages, called by apm's Node-js const path = require('path'); const fs = require('fs-plus'); const sourceMapSupport = require('@atom/source-map-support'); const PackageTranspilationRegistry = require('./package-transpilation-registry'); let CSON = null; const packageTranspilationRegistry = new PackageTranspilationRegistry(); const COMPILERS = { '.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')), '.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')), '.tsx': packageTranspilationRegistry.wrapTranspiler(require('./typescript')), '.coffee': packageTranspilationRegistry.wrapTranspiler( require('./coffee-script') ) }; exports.addTranspilerConfigForPath = function( packagePath, packageName, packageMeta, config ) { packagePath = fs.realpathSync(packagePath); packageTranspilationRegistry.addTranspilerConfigForPath( packagePath, packageName, packageMeta, config ); }; exports.removeTranspilerConfigForPath = function(packagePath) { packagePath = fs.realpathSync(packagePath); packageTranspilationRegistry.removeTranspilerConfigForPath(packagePath); }; const cacheStats = {}; let cacheDirectory = null; exports.setAtomHomeDirectory = function(atomHome) { let cacheDir = path.join(atomHome, 'compile-cache'); if ( process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER ) { cacheDir = path.join(cacheDir, 'root'); } this.setCacheDirectory(cacheDir); }; exports.setCacheDirectory = function(directory) { cacheDirectory = directory; }; exports.getCacheDirectory = function() { return cacheDirectory; }; exports.addPathToCache = function(filePath, atomHome) { this.setAtomHomeDirectory(atomHome); const extension = path.extname(filePath); if (extension === '.cson') { if (!CSON) { CSON = require('season'); CSON.setCacheDir(this.getCacheDirectory()); } return CSON.readFileSync(filePath); } else { const compiler = COMPILERS[extension]; if (compiler) { return compileFileAtPath(compiler, filePath, extension); } } }; exports.getCacheStats = function() { return cacheStats; }; exports.resetCacheStats = function() { Object.keys(COMPILERS).forEach(function(extension) { cacheStats[extension] = { hits: 0, misses: 0 }; }); }; function compileFileAtPath(compiler, filePath, extension) { const sourceCode = fs.readFileSync(filePath, 'utf8'); if (compiler.shouldCompile(sourceCode, filePath)) { const cachePath = compiler.getCachePath(sourceCode, filePath); let compiledCode = readCachedJavaScript(cachePath); if (compiledCode != null) { cacheStats[extension].hits++; } else { cacheStats[extension].misses++; compiledCode = compiler.compile(sourceCode, filePath); writeCachedJavaScript(cachePath, compiledCode); } return compiledCode; } return sourceCode; } function readCachedJavaScript(relativeCachePath) { const cachePath = path.join(cacheDirectory, relativeCachePath); if (fs.isFileSync(cachePath)) { try { return fs.readFileSync(cachePath, 'utf8'); } catch (error) {} } return null; } function writeCachedJavaScript(relativeCachePath, code) { const cachePath = path.join(cacheDirectory, relativeCachePath); fs.writeFileSync(cachePath, code, 'utf8'); } const INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/gm; exports.install = function(resourcesPath, nodeRequire) { const snapshotSourceMapConsumer = { originalPositionFor({ line, column }) { const { relativePath, row } = snapshotResult.translateSnapshotRow(line); return { column, line: row, source: path.join(resourcesPath, 'app', 'static', relativePath), name: null }; } }; sourceMapSupport.install({ handleUncaughtExceptions: false, // Most of this logic is the same as the default implementation in the // source-map-support module, but we've overridden it to read the javascript // code from our cache directory. retrieveSourceMap: function(filePath) { if (filePath === '') { return { map: snapshotSourceMapConsumer }; } if (!cacheDirectory || !fs.isFileSync(filePath)) { return null; } try { var sourceCode = fs.readFileSync(filePath, 'utf8'); } catch (error) { console.warn('Error reading source file', error.stack); return null; } let compiler = COMPILERS[path.extname(filePath)]; if (!compiler) compiler = COMPILERS['.js']; try { var fileData = readCachedJavaScript( compiler.getCachePath(sourceCode, filePath) ); } catch (error) { console.warn('Error reading compiled file', error.stack); return null; } if (fileData == null) { return null; } let match, lastMatch; INLINE_SOURCE_MAP_REGEXP.lastIndex = 0; while ((match = INLINE_SOURCE_MAP_REGEXP.exec(fileData))) { lastMatch = match; } if (lastMatch == null) { return null; } const sourceMappingURL = lastMatch[1]; const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1); try { var sourceMap = JSON.parse(Buffer.from(rawData, 'base64')); } catch (error) { console.warn('Error parsing source map', error.stack); return null; } return { map: sourceMap, url: null }; } }); const prepareStackTraceWithSourceMapping = Error.prepareStackTrace; var prepareStackTrace = prepareStackTraceWithSourceMapping; function prepareStackTraceWithRawStackAssignment(error, frames) { if (error.rawStack) { // avoid infinite recursion return prepareStackTraceWithSourceMapping(error, frames); } else { error.rawStack = frames; return prepareStackTrace(error, frames); } } Error.stackTraceLimit = 30; Object.defineProperty(Error, 'prepareStackTrace', { get: function() { return prepareStackTraceWithRawStackAssignment; }, set: function(newValue) { prepareStackTrace = newValue; process.nextTick(function() { prepareStackTrace = prepareStackTraceWithSourceMapping; }); } }); // eslint-disable-next-line no-extend-native Error.prototype.getRawStack = function() { // Access this.stack to ensure prepareStackTrace has been run on this error // because it assigns this.rawStack as a side-effect this.stack; // eslint-disable-line no-unused-expressions return this.rawStack; }; Object.keys(COMPILERS).forEach(function(extension) { const compiler = COMPILERS[extension]; Object.defineProperty(nodeRequire.extensions, extension, { enumerable: true, writable: false, value: function(module, filePath) { const code = compileFileAtPath(compiler, filePath, extension); return module._compile(code, filePath); } }); }); }; exports.supportedExtensions = Object.keys(COMPILERS); exports.resetCacheStats(); ================================================ FILE: src/config-file.js ================================================ const _ = require('underscore-plus'); const fs = require('fs-plus'); const dedent = require('dedent'); const { Disposable, Emitter } = require('event-kit'); const { watchPath } = require('./path-watcher'); const CSON = require('season'); const Path = require('path'); const asyncQueue = require('async/queue'); const EVENT_TYPES = new Set(['created', 'modified', 'renamed']); module.exports = class ConfigFile { static at(path) { if (!this._known) { this._known = new Map(); } const existing = this._known.get(path); if (existing) { return existing; } const created = new ConfigFile(path); this._known.set(path, created); return created; } constructor(path) { this.path = path; this.emitter = new Emitter(); this.value = {}; this.reloadCallbacks = []; // Use a queue to prevent multiple concurrent write to the same file. const writeQueue = asyncQueue((data, callback) => CSON.writeFile(this.path, data, error => { if (error) { this.emitter.emit( 'did-error', dedent` Failed to write \`${Path.basename(this.path)}\`. ${error.message} ` ); } callback(); }) ); this.requestLoad = _.debounce(() => this.reload(), 200); this.requestSave = _.debounce(data => writeQueue.push(data), 200); } get() { return this.value; } update(value) { return new Promise(resolve => { this.requestSave(value); this.reloadCallbacks.push(resolve); }); } async watch(callback) { if (!fs.existsSync(this.path)) { fs.makeTreeSync(Path.dirname(this.path)); CSON.writeFileSync(this.path, {}, { flag: 'wx' }); } await this.reload(); try { return await watchPath(this.path, {}, events => { if (events.some(event => EVENT_TYPES.has(event.action))) this.requestLoad(); }); } catch (error) { this.emitter.emit( 'did-error', dedent` Unable to watch path: \`${Path.basename(this.path)}\`. Make sure you have permissions to \`${this.path}\`. On linux there are currently problems with watch sizes. See [this document][watches] for more info. [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\ ` ); return new Disposable(); } } onDidChange(callback) { return this.emitter.on('did-change', callback); } onDidError(callback) { return this.emitter.on('did-error', callback); } reload() { return new Promise(resolve => { CSON.readFile(this.path, (error, data) => { if (error) { this.emitter.emit( 'did-error', `Failed to load \`${Path.basename(this.path)}\` - ${error.message}` ); } else { this.value = data || {}; this.emitter.emit('did-change', this.value); for (const callback of this.reloadCallbacks) callback(); this.reloadCallbacks.length = 0; } resolve(); }); }); } }; ================================================ FILE: src/config-schema.js ================================================ // This is loaded by atom-environment.coffee. See // https://atom.io/docs/api/latest/Config for more information about config // schemas. const configSchema = { core: { type: 'object', properties: { ignoredNames: { type: 'array', default: [ '.git', '.hg', '.svn', '.DS_Store', '._*', 'Thumbs.db', 'desktop.ini' ], items: { type: 'string' }, description: 'List of [glob patterns](https://en.wikipedia.org/wiki/Glob_%28programming%29). Files and directories matching these patterns will be ignored by some packages, such as the fuzzy finder and tree view. Individual packages might have additional config settings for ignoring names.' }, excludeVcsIgnoredPaths: { type: 'boolean', default: true, title: 'Exclude VCS Ignored Paths', 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', default: true, description: 'Follow symbolic links when searching files and when opening files with the fuzzy finder.' }, disabledPackages: { type: 'array', default: [], items: { type: 'string' }, description: 'List of names of installed packages which are not loaded at startup.' }, titleBar: { type: 'string', default: 'native', enum: ['native', 'hidden'], description: 'Experimental: The title bar can be completely `hidden`.
          This setting will require a relaunch of Atom to take effect.' }, versionPinnedPackages: { type: 'array', default: [], items: { type: 'string' }, description: 'List of names of installed packages which are not automatically updated.' }, customFileTypes: { type: 'object', default: {}, description: 'Associates scope names (e.g. `"source.js"`) with arrays of file extensions and file names (e.g. `["Somefile", ".js2"]`)', additionalProperties: { type: 'array', items: { type: 'string' } } }, 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'], items: { type: 'string' }, description: 'Names of UI and syntax themes which will be used when Atom starts.' }, audioBeep: { type: 'boolean', default: true, description: "Trigger the system's beep sound when certain actions cannot be executed or there are no results." }, closeDeletedFileTabs: { type: 'boolean', default: false, title: 'Close Deleted File Tabs', description: 'Close corresponding editors when a file is deleted outside Atom.' }, destroyEmptyPanes: { type: 'boolean', default: true, title: 'Remove Empty Panes', description: 'When the last tab of a pane is closed, remove that pane as well.' }, closeEmptyWindows: { type: 'boolean', default: true, description: "When a window with no open tabs or panes is given the 'Close Tab' command, close that window." }, fileEncoding: { description: 'Default character set encoding to use when reading and writing files.', type: 'string', default: 'utf8', enum: [ { value: 'iso88596', description: 'Arabic (ISO 8859-6)' }, { value: 'windows1256', description: 'Arabic (Windows 1256)' }, { value: 'iso88594', description: 'Baltic (ISO 8859-4)' }, { value: 'windows1257', description: 'Baltic (Windows 1257)' }, { value: 'iso885914', description: 'Celtic (ISO 8859-14)' }, { value: 'iso88592', description: 'Central European (ISO 8859-2)' }, { value: 'windows1250', description: 'Central European (Windows 1250)' }, { value: 'gb18030', description: 'Chinese (GB18030)' }, { value: 'gbk', description: 'Chinese (GBK)' }, { value: 'cp950', description: 'Traditional Chinese (Big5)' }, { value: 'big5hkscs', description: 'Traditional Chinese (Big5-HKSCS)' }, { value: 'cp866', description: 'Cyrillic (CP 866)' }, { value: 'iso88595', description: 'Cyrillic (ISO 8859-5)' }, { value: 'koi8r', description: 'Cyrillic (KOI8-R)' }, { value: 'koi8u', description: 'Cyrillic (KOI8-U)' }, { value: 'windows1251', description: 'Cyrillic (Windows 1251)' }, { value: 'cp437', description: 'DOS (CP 437)' }, { value: 'cp850', description: 'DOS (CP 850)' }, { value: 'iso885913', description: 'Estonian (ISO 8859-13)' }, { value: 'iso88597', description: 'Greek (ISO 8859-7)' }, { value: 'windows1253', description: 'Greek (Windows 1253)' }, { value: 'iso88598', description: 'Hebrew (ISO 8859-8)' }, { value: 'windows1255', description: 'Hebrew (Windows 1255)' }, { value: 'cp932', description: 'Japanese (CP 932)' }, { value: 'eucjp', description: 'Japanese (EUC-JP)' }, { value: 'shiftjis', description: 'Japanese (Shift JIS)' }, { value: 'euckr', description: 'Korean (EUC-KR)' }, { value: 'iso885910', description: 'Nordic (ISO 8859-10)' }, { value: 'iso885916', description: 'Romanian (ISO 8859-16)' }, { value: 'iso88599', description: 'Turkish (ISO 8859-9)' }, { value: 'windows1254', description: 'Turkish (Windows 1254)' }, { value: 'utf8', description: 'Unicode (UTF-8)' }, { value: 'utf16le', description: 'Unicode (UTF-16 LE)' }, { value: 'utf16be', description: 'Unicode (UTF-16 BE)' }, { value: 'windows1258', description: 'Vietnamese (Windows 1258)' }, { value: 'iso88591', description: 'Western (ISO 8859-1)' }, { value: 'iso88593', description: 'Western (ISO 8859-3)' }, { value: 'iso885915', description: 'Western (ISO 8859-15)' }, { value: 'macroman', description: 'Western (Mac Roman)' }, { value: 'windows1252', description: 'Western (Windows 1252)' } ] }, openEmptyEditorOnStart: { description: 'When checked opens an untitled editor when loading a blank environment (such as with _File > New Window_ or when "Restore Previous Windows On Start" is unchecked); otherwise no editor is opened when loading a blank environment. This setting has no effect when restoring a previous state.', type: 'boolean', default: true }, restorePreviousWindowsOnStart: { type: 'string', enum: ['no', 'yes', 'always'], default: 'yes', description: "When selected 'no', a blank environment is loaded. When selected 'yes' and Atom is started from the icon or `atom` by itself from the command line, restores the last state of all Atom windows; otherwise a blank environment is loaded. When selected 'always', restores the last state of all Atom windows always, no matter how Atom is started." }, reopenProjectMenuCount: { description: 'How many recent projects to show in the Reopen Project menu.', type: 'integer', default: 15 }, automaticallyUpdate: { description: 'Automatically update Atom when a new release is available.', type: 'boolean', default: true }, useProxySettingsWhenCallingApm: { title: 'Use Proxy Settings When Calling APM', description: 'Use detected proxy settings when calling the `apm` command-line tool.', type: 'boolean', default: true }, allowPendingPaneItems: { description: 'Allow items to be previewed without adding them to a pane permanently, such as when single clicking files in the tree view.', type: 'boolean', default: true }, telemetryConsent: { description: 'Allow usage statistics and exception reports to be sent to the Atom team to help improve the product.', title: 'Send Telemetry to the Atom Team', type: 'string', default: 'undecided', enum: [ { value: 'limited', description: 'Allow limited anonymous usage stats, exception and crash reporting' }, { value: 'no', description: 'Do not send any telemetry data' }, { value: 'undecided', description: 'Undecided (Atom will ask again next time it is launched)' } ] }, warnOnLargeFileLimit: { description: 'Warn before opening files larger than this number of megabytes.', type: 'number', default: 40 }, fileSystemWatcher: { description: 'Choose the underlying implementation used to watch for filesystem changes. Emulating changes will miss any events caused by applications other than Atom, but may help prevent crashes or freezes.', type: 'string', default: 'native', enum: [ { value: 'native', description: 'Native operating system APIs' }, { value: 'experimental', description: 'Experimental filesystem watching library' }, { value: 'poll', description: 'Polling' }, { value: 'atom', description: 'Emulated with Atom events' } ] }, useTreeSitterParsers: { type: 'boolean', default: true, description: 'Use Tree-sitter parsers for supported languages.' }, colorProfile: { description: "Specify whether Atom should use the operating system's color profile (recommended) or an alternative color profile.
          Changing this setting will require a relaunch of Atom to take effect.", type: 'string', default: 'default', enum: [ { value: 'default', description: 'Use color profile configured in the operating system' }, { value: 'srgb', description: 'Use sRGB color profile' } ] } } }, editor: { type: 'object', // These settings are used in scoped fashion only. No defaults. properties: { commentStart: { type: ['string', 'null'] }, commentEnd: { type: ['string', 'null'] }, increaseIndentPattern: { type: ['string', 'null'] }, decreaseIndentPattern: { type: ['string', 'null'] }, foldEndPattern: { type: ['string', 'null'] }, // These can be used as globals or scoped, thus defaults. fontFamily: { type: 'string', default: 'Menlo, Consolas, DejaVu Sans Mono, monospace', description: 'The name of the font family used for editor text.' }, fontSize: { type: 'integer', default: 14, minimum: 1, maximum: 100, description: 'Height in pixels of editor text.' }, defaultFontSize: { type: 'integer', default: 14, minimum: 1, maximum: 100, description: 'Default height in pixels of the editor text. Useful when resetting font size' }, lineHeight: { type: ['string', 'number'], default: 1.5, description: 'Height of editor lines, as a multiplier of font size.' }, showCursorOnSelection: { type: 'boolean', default: true, description: 'Show cursor while there is a selection.' }, showInvisibles: { type: 'boolean', default: false, description: 'Render placeholders for invisible characters, such as tabs, spaces and newlines.' }, showIndentGuide: { type: 'boolean', default: false, description: 'Show indentation indicators in the editor.' }, showLineNumbers: { type: 'boolean', default: true, description: "Show line numbers in the editor's gutter." }, atomicSoftTabs: { type: 'boolean', default: true, description: 'Skip over tab-length runs of leading whitespace when moving the cursor.' }, autoIndent: { type: 'boolean', default: true, description: 'Automatically indent the cursor when inserting a newline.' }, autoIndentOnPaste: { type: 'boolean', default: true, description: 'Automatically indent pasted text based on the indentation of the previous line.' }, nonWordCharacters: { type: 'string', default: '/\\()"\':,.;<>~!@#$%^&*|+=[]{}`?-…', description: 'A string of non-word characters to define word boundaries.' }, preferredLineLength: { type: 'integer', default: 80, 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, minimum: 1, description: 'Number of spaces used to represent a tab.' }, softWrap: { type: 'boolean', default: false, description: 'Wraps lines that exceed the width of the window. When `Soft Wrap At Preferred Line Length` is set, it will wrap to the number of characters defined by the `Preferred Line Length` setting.' }, softTabs: { type: 'boolean', default: true, description: 'If the `Tab Type` config setting is set to "auto" and autodetection of tab type from buffer content fails, then this config setting determines whether a soft tab or a hard tab will be inserted when the Tab key is pressed.' }, tabType: { type: 'string', default: 'auto', enum: ['auto', 'soft', 'hard'], description: 'Determine character inserted when Tab key is pressed. Possible values: "auto", "soft" and "hard". When set to "soft" or "hard", soft tabs (spaces) or hard tabs (tab characters) are used. When set to "auto", the editor auto-detects the tab type based on the contents of the buffer (it uses the first leading whitespace on a non-comment line), or uses the value of the Soft Tabs config setting if auto-detection fails.' }, softWrapAtPreferredLineLength: { type: 'boolean', default: false, description: "Instead of wrapping lines to the window's width, wrap lines to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when the soft wrap config setting is enabled globally or for the current language. **Note:** If you want to hide the wrap guide (the vertical line) you can disable the `wrap-guide` package." }, softWrapHangingIndent: { type: 'integer', default: 0, minimum: 0, description: 'When soft wrap is enabled, defines length of additional indentation applied to wrapped lines, in number of characters.' }, scrollSensitivity: { type: 'integer', default: 40, minimum: 10, maximum: 200, description: 'Determines how fast the editor scrolls when using a mouse or trackpad.' }, scrollPastEnd: { type: 'boolean', default: false, description: 'Allow the editor to be scrolled past the end of the last line.' }, undoGroupingInterval: { type: 'integer', default: 300, minimum: 0, description: 'Time interval in milliseconds within which text editing operations will be grouped together in the undo history.' }, confirmCheckoutHeadRevision: { type: 'boolean', default: true, title: 'Confirm Checkout HEAD Revision', description: 'Show confirmation dialog when checking out the HEAD revision and discarding changes to current file since last commit.' }, invisibles: { type: 'object', description: 'A hash of characters Atom will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false to turn off individual whitespace character types).', properties: { eol: { type: ['boolean', 'string'], default: '¬', maximumLength: 1, description: 'Character used to render newline characters (\\n) when the `Show Invisibles` setting is enabled. ' }, space: { type: ['boolean', 'string'], default: '·', maximumLength: 1, description: 'Character used to render leading and trailing space characters when the `Show Invisibles` setting is enabled.' }, tab: { type: ['boolean', 'string'], default: '»', maximumLength: 1, description: 'Character used to render hard tab characters (\\t) when the `Show Invisibles` setting is enabled.' }, cr: { type: ['boolean', 'string'], default: '¤', maximumLength: 1, description: 'Character used to render carriage return characters (for Microsoft-style line endings) when the `Show Invisibles` setting is enabled.' } } }, zoomFontWhenCtrlScrolling: { type: 'boolean', default: process.platform !== 'darwin', description: 'Change the editor font size when pressing the Ctrl key and scrolling the mouse up/down.' }, multiCursorOnClick: { type: 'boolean', default: true, description: 'Add multiple cursors when pressing the Ctrl key (Command key on MacOS) and clicking the editor.' } } } }; if (['win32', 'linux'].includes(process.platform)) { configSchema.core.properties.autoHideMenuBar = { type: 'boolean', default: false, description: 'Automatically hide the menu bar and toggle it by pressing Alt. This is only supported on Windows & Linux.' }; } if (process.platform === 'darwin') { configSchema.core.properties.titleBar = { type: 'string', default: 'native', enum: ['native', 'custom', 'custom-inset', 'hidden'], description: 'Experimental: A `custom` title bar adapts to theme colors. Choosing `custom-inset` adds a bit more padding. The title bar can also be completely `hidden`.
          Note: Switching to a custom or hidden title bar will compromise some functionality.
          This setting will require a relaunch of Atom to take effect.' }; configSchema.core.properties.simpleFullScreenWindows = { type: 'boolean', default: false, description: 'Use pre-Lion fullscreen on macOS. This does not create a new desktop space for the atom on fullscreen mode.' }; } if (process.platform === 'linux') { configSchema.editor.properties.selectionClipboard = { type: 'boolean', default: true, description: 'Enable pasting on middle mouse button click' }; } module.exports = configSchema; ================================================ FILE: src/config.js ================================================ const _ = require('underscore-plus'); const { Emitter } = require('event-kit'); const { getValueAtKeyPath, setValueAtKeyPath, deleteValueAtKeyPath, pushKeyPath, splitKeyPath } = require('key-path-helpers'); const Color = require('./color'); const ScopedPropertyStore = require('scoped-property-store'); const ScopeDescriptor = require('./scope-descriptor'); const schemaEnforcers = {}; // Essential: Used to access all of Atom's configuration details. // // An instance of this class is always available as the `atom.config` global. // // ## Getting and setting config settings. // // ```coffee // # Note that with no value set, ::get returns the setting's default value. // atom.config.get('my-package.myKey') # -> 'defaultValue' // // atom.config.set('my-package.myKey', 'value') // atom.config.get('my-package.myKey') # -> 'value' // ``` // // You may want to watch for changes. Use {::observe} to catch changes to the setting. // // ```coffee // atom.config.set('my-package.myKey', 'value') // atom.config.observe 'my-package.myKey', (newValue) -> // # `observe` calls immediately and every time the value is changed // console.log 'My configuration changed:', newValue // ``` // // If you want a notification only when the value changes, use {::onDidChange}. // // ```coffee // atom.config.onDidChange 'my-package.myKey', ({newValue, oldValue}) -> // console.log 'My configuration changed:', newValue, oldValue // ``` // // ### Value Coercion // // Config settings each have a type specified by way of a // [schema](json-schema.org). For example we might want an integer setting that only // allows integers greater than `0`: // // ```coffee // # When no value has been set, `::get` returns the setting's default value // atom.config.get('my-package.anInt') # -> 12 // // # The string will be coerced to the integer 123 // atom.config.set('my-package.anInt', '123') // atom.config.get('my-package.anInt') # -> 123 // // # The string will be coerced to an integer, but it must be greater than 0, so is set to 1 // atom.config.set('my-package.anInt', '-20') // atom.config.get('my-package.anInt') # -> 1 // ``` // // ## Defining settings for your package // // Define a schema under a `config` key in your package main. // // ```coffee // module.exports = // # Your config schema // config: // someInt: // type: 'integer' // default: 23 // minimum: 1 // // activate: (state) -> # ... // # ... // ``` // // See [package docs](http://flight-manual.atom.io/hacking-atom/sections/package-word-count/) for // more info. // // ## Config Schemas // // We use [json schema](http://json-schema.org) which allows you to define your value's // default, the type it should be, etc. A simple example: // // ```coffee // # We want to provide an `enableThing`, and a `thingVolume` // config: // enableThing: // type: 'boolean' // default: false // thingVolume: // type: 'integer' // default: 5 // minimum: 1 // maximum: 11 // ``` // // The type keyword allows for type coercion and validation. If a `thingVolume` is // set to a string `'10'`, it will be coerced into an integer. // // ```coffee // atom.config.set('my-package.thingVolume', '10') // atom.config.get('my-package.thingVolume') # -> 10 // // # It respects the min / max // atom.config.set('my-package.thingVolume', '400') // atom.config.get('my-package.thingVolume') # -> 11 // // # If it cannot be coerced, the value will not be set // atom.config.set('my-package.thingVolume', 'cats') // atom.config.get('my-package.thingVolume') # -> 11 // ``` // // ### Supported Types // // The `type` keyword can be a string with any one of the following. You can also // chain them by specifying multiple in an an array. For example // // ```coffee // config: // someSetting: // type: ['boolean', 'integer'] // default: 5 // // # Then // atom.config.set('my-package.someSetting', 'true') // atom.config.get('my-package.someSetting') # -> true // // atom.config.set('my-package.someSetting', '12') // atom.config.get('my-package.someSetting') # -> 12 // ``` // // #### string // // Values must be a string. // // ```coffee // config: // someSetting: // type: 'string' // default: 'hello' // ``` // // #### integer // // Values will be coerced into integer. Supports the (optional) `minimum` and // `maximum` keys. // // ```coffee // config: // someSetting: // type: 'integer' // default: 5 // minimum: 1 // maximum: 11 // ``` // // #### number // // Values will be coerced into a number, including real numbers. Supports the // (optional) `minimum` and `maximum` keys. // // ```coffee // config: // someSetting: // type: 'number' // default: 5.3 // minimum: 1.5 // maximum: 11.5 // ``` // // #### boolean // // Values will be coerced into a Boolean. `'true'` and `'false'` will be coerced into // a boolean. Numbers, arrays, objects, and anything else will not be coerced. // // ```coffee // config: // someSetting: // type: 'boolean' // default: false // ``` // // #### array // // Value must be an Array. The types of the values can be specified by a // subschema in the `items` key. // // ```coffee // config: // someSetting: // type: 'array' // default: [1, 2, 3] // items: // type: 'integer' // minimum: 1.5 // maximum: 11.5 // ``` // // #### color // // Values will be coerced into a {Color} with `red`, `green`, `blue`, and `alpha` // properties that all have numeric values. `red`, `green`, `blue` will be in // the range 0 to 255 and `value` will be in the range 0 to 1. Values can be any // valid CSS color format such as `#abc`, `#abcdef`, `white`, // `rgb(50, 100, 150)`, and `rgba(25, 75, 125, .75)`. // // ```coffee // config: // someSetting: // type: 'color' // default: 'white' // ``` // // #### object / Grouping other types // // A config setting with the type `object` allows grouping a set of config // settings. The group will be visually separated and has its own group headline. // The sub options must be listed under a `properties` key. // // ```coffee // config: // someSetting: // type: 'object' // properties: // myChildIntOption: // type: 'integer' // minimum: 1.5 // maximum: 11.5 // ``` // // ### Other Supported Keys // // #### enum // // All types support an `enum` key, which lets you specify all the values the // setting can take. `enum` may be an array of allowed values (of the specified // type), or an array of objects with `value` and `description` properties, where // the `value` is an allowed value, and the `description` is a descriptive string // used in the settings view. // // In this example, the setting must be one of the 4 integers: // // ```coffee // config: // someSetting: // type: 'integer' // default: 4 // enum: [2, 4, 6, 8] // ``` // // In this example, the setting must be either 'foo' or 'bar', which are // presented using the provided descriptions in the settings pane: // // ```coffee // config: // someSetting: // type: 'string' // default: 'foo' // enum: [ // {value: 'foo', description: 'Foo mode. You want this.'} // {value: 'bar', description: 'Bar mode. Nobody wants that!'} // ] // ``` // // If you only have a few elements, you can display your enum as a list of // radio buttons in the settings view rather than a select list. To do so, // specify `radio: true` as a sibling property to the `enum` array. // // ```coffee // config: // someSetting: // type: 'string' // default: 'foo' // enum: [ // {value: 'foo', description: 'Foo mode. You want this.'} // {value: 'bar', description: 'Bar mode. Nobody wants that!'} // ] // radio: true // ``` // // Usage: // // ```coffee // atom.config.set('my-package.someSetting', '2') // atom.config.get('my-package.someSetting') # -> 2 // // # will not set values outside of the enum values // atom.config.set('my-package.someSetting', '3') // atom.config.get('my-package.someSetting') # -> 2 // // # If it cannot be coerced, the value will not be set // atom.config.set('my-package.someSetting', '4') // atom.config.get('my-package.someSetting') # -> 4 // ``` // // #### title and description // // The settings view will use the `title` and `description` keys to display your // config setting in a readable way. By default the settings view humanizes your // config key, so `someSetting` becomes `Some Setting`. In some cases, this is // confusing for users, and a more descriptive title is useful. // // Descriptions will be displayed below the title in the settings view. // // For a group of config settings the humanized key or the title and the // description are used for the group headline. // // ```coffee // config: // someSetting: // title: 'Setting Magnitude' // description: 'This will affect the blah and the other blah' // type: 'integer' // default: 4 // ``` // // __Note__: You should strive to be so clear in your naming of the setting that // you do not need to specify a title or description! // // Descriptions allow a subset of // [Markdown formatting](https://help.github.com/articles/github-flavored-markdown/). // Specifically, you may use the following in configuration setting descriptions: // // * **bold** - `**bold**` // * *italics* - `*italics*` // * [links](https://atom.io) - `[links](https://atom.io)` // * `code spans` - `` `code spans` `` // * line breaks - `line breaks
          ` // * ~~strikethrough~~ - `~~strikethrough~~` // // #### order // // The settings view orders your settings alphabetically. You can override this // ordering with the order key. // // ```coffee // config: // zSetting: // type: 'integer' // default: 4 // order: 1 // aSetting: // type: 'integer' // default: 4 // order: 2 // ``` // // ## Manipulating values outside your configuration schema // // It is possible to manipulate(`get`, `set`, `observe` etc) values that do not // appear in your configuration schema. For example, if the config schema of the // package 'some-package' is // // ```coffee // config: // someSetting: // type: 'boolean' // default: false // ``` // // You can still do the following // // ```coffee // let otherSetting = atom.config.get('some-package.otherSetting') // atom.config.set('some-package.stillAnotherSetting', otherSetting * 5) // ``` // // In other words, if a function asks for a `key-path`, that path doesn't have to // be described in the config schema for the package or any package. However, as // highlighted in the best practices section, you are advised against doing the // above. // // ## Best practices // // * Don't depend on (or write to) configuration keys outside of your keypath. // class Config { static addSchemaEnforcer(typeName, enforcerFunction) { if (schemaEnforcers[typeName] == null) { schemaEnforcers[typeName] = []; } return schemaEnforcers[typeName].push(enforcerFunction); } static addSchemaEnforcers(filters) { for (let typeName in filters) { const functions = filters[typeName]; for (let name in functions) { const enforcerFunction = functions[name]; this.addSchemaEnforcer(typeName, enforcerFunction); } } } static executeSchemaEnforcers(keyPath, value, schema) { let error = null; let types = schema.type; if (!Array.isArray(types)) { types = [types]; } for (let type of types) { try { const enforcerFunctions = schemaEnforcers[type].concat( schemaEnforcers['*'] ); for (let enforcer of enforcerFunctions) { // At some point in one's life, one must call upon an enforcer. value = enforcer.call(this, keyPath, value, schema); } error = null; break; } catch (e) { error = e; } } if (error != null) { throw error; } return value; } // Created during initialization, available as `atom.config` constructor(params = {}) { this.clear(); this.initialize(params); } initialize({ saveCallback, mainSource, projectHomeSchema }) { if (saveCallback) { this.saveCallback = saveCallback; } if (mainSource) this.mainSource = mainSource; if (projectHomeSchema) { this.schema.properties.core.properties.projectHome = projectHomeSchema; this.defaultSettings.core.projectHome = projectHomeSchema.default; } } clear() { this.emitter = new Emitter(); this.schema = { type: 'object', properties: {} }; this.defaultSettings = {}; this.settings = {}; this.projectSettings = {}; this.projectFile = null; this.scopedSettingsStore = new ScopedPropertyStore(); this.settingsLoaded = false; this.transactDepth = 0; this.pendingOperations = []; this.legacyScopeAliases = new Map(); this.requestSave = _.debounce(() => this.save(), 1); } /* Section: Config Subscription */ // Essential: Add a listener for changes to a given key path. This is different // than {::onDidChange} in that it will immediately call your callback with the // current value of the config entry. // // ### Examples // // You might want to be notified when the themes change. We'll watch // `core.themes` for changes // // ```coffee // atom.config.observe 'core.themes', (value) -> // # do stuff with value // ``` // // * `keyPath` {String} name of the key to observe // * `options` (optional) {Object} // * `scope` (optional) {ScopeDescriptor} describing a path from // the root of the syntax tree to a token. Get one by calling // {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples. // See [the scopes docs](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/) // for more information. // * `callback` {Function} to call when the value of the key changes. // * `value` the new value of the key // // Returns a {Disposable} with the following keys on which you can call // `.dispose()` to unsubscribe. observe(...args) { let callback, keyPath, options, scopeDescriptor; if (args.length === 2) { [keyPath, callback] = args; } else if ( args.length === 3 && (_.isString(args[0]) && _.isObject(args[1])) ) { [keyPath, options, callback] = args; scopeDescriptor = options.scope; } else { console.error( 'An unsupported form of Config::observe is being used. See https://atom.io/docs/api/latest/Config for details' ); return; } if (scopeDescriptor != null) { return this.observeScopedKeyPath(scopeDescriptor, keyPath, callback); } else { return this.observeKeyPath( keyPath, options != null ? options : {}, callback ); } } // Essential: Add a listener for changes to a given key path. If `keyPath` is // not specified, your callback will be called on changes to any key. // // * `keyPath` (optional) {String} name of the key to observe. Must be // specified if `scopeDescriptor` is specified. // * `options` (optional) {Object} // * `scope` (optional) {ScopeDescriptor} describing a path from // the root of the syntax tree to a token. Get one by calling // {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples. // See [the scopes docs](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/) // for more information. // * `callback` {Function} to call when the value of the key changes. // * `event` {Object} // * `newValue` the new value of the key // * `oldValue` the prior value of the key. // // Returns a {Disposable} with the following keys on which you can call // `.dispose()` to unsubscribe. onDidChange(...args) { let callback, keyPath, scopeDescriptor; if (args.length === 1) { [callback] = args; } else if (args.length === 2) { [keyPath, callback] = args; } else { let options; [keyPath, options, callback] = args; scopeDescriptor = options.scope; } if (scopeDescriptor != null) { return this.onDidChangeScopedKeyPath(scopeDescriptor, keyPath, callback); } else { return this.onDidChangeKeyPath(keyPath, callback); } } /* Section: Managing Settings */ // Essential: Retrieves the setting for the given key. // // ### Examples // // You might want to know what themes are enabled, so check `core.themes` // // ```coffee // atom.config.get('core.themes') // ``` // // With scope descriptors you can get settings within a specific editor // scope. For example, you might want to know `editor.tabLength` for ruby // files. // // ```coffee // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2 // ``` // // This setting in ruby files might be different than the global tabLength setting // // ```coffee // atom.config.get('editor.tabLength') # => 4 // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2 // ``` // // You can get the language scope descriptor via // {TextEditor::getRootScopeDescriptor}. This will get the setting specifically // for the editor's language. // // ```coffee // atom.config.get('editor.tabLength', scope: @editor.getRootScopeDescriptor()) # => 2 // ``` // // Additionally, you can get the setting at the specific cursor position. // // ```coffee // scopeDescriptor = @editor.getLastCursor().getScopeDescriptor() // atom.config.get('editor.tabLength', scope: scopeDescriptor) # => 2 // ``` // // * `keyPath` The {String} name of the key to retrieve. // * `options` (optional) {Object} // * `sources` (optional) {Array} of {String} source names. If provided, only // values that were associated with these sources during {::set} will be used. // * `excludeSources` (optional) {Array} of {String} source names. If provided, // values that were associated with these sources during {::set} will not // be used. // * `scope` (optional) {ScopeDescriptor} describing a path from // the root of the syntax tree to a token. Get one by calling // {editor.getLastCursor().getScopeDescriptor()} // See [the scopes docs](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/) // for more information. // // Returns the value from Atom's default settings, the user's configuration // file in the type specified by the configuration schema. get(...args) { let keyPath, options, scope; if (args.length > 1) { if (typeof args[0] === 'string' || args[0] == null) { [keyPath, options] = args; ({ scope } = options); } } else { [keyPath] = args; } if (scope != null) { const value = this.getRawScopedValue(scope, keyPath, options); return value != null ? value : this.getRawValue(keyPath, options); } else { return this.getRawValue(keyPath, options); } } // Extended: Get all of the values for the given key-path, along with their // associated scope selector. // // * `keyPath` The {String} name of the key to retrieve // * `options` (optional) {Object} see the `options` argument to {::get} // // Returns an {Array} of {Object}s with the following keys: // * `scopeDescriptor` The {ScopeDescriptor} with which the value is associated // * `value` The value for the key-path getAll(keyPath, options) { let globalValue, result, scope; if (options != null) { ({ scope } = options); } if (scope != null) { let legacyScopeDescriptor; const scopeDescriptor = ScopeDescriptor.fromObject(scope); result = this.scopedSettingsStore.getAll( scopeDescriptor.getScopeChain(), keyPath, options ); legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor( scopeDescriptor ); if (legacyScopeDescriptor) { result.push( ...Array.from( this.scopedSettingsStore.getAll( legacyScopeDescriptor.getScopeChain(), keyPath, options ) || [] ) ); } } else { result = []; } globalValue = this.getRawValue(keyPath, options); if (globalValue) { result.push({ scopeSelector: '*', value: globalValue }); } return result; } // Essential: Sets the value for a configuration setting. // // This value is stored in Atom's internal configuration file. // // ### Examples // // You might want to change the themes programmatically: // // ```coffee // atom.config.set('core.themes', ['atom-light-ui', 'atom-light-syntax']) // ``` // // You can also set scoped settings. For example, you might want change the // `editor.tabLength` only for ruby files. // // ```coffee // atom.config.get('editor.tabLength') # => 4 // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 4 // atom.config.get('editor.tabLength', scope: ['source.js']) # => 4 // // # Set ruby to 2 // atom.config.set('editor.tabLength', 2, scopeSelector: '.source.ruby') # => true // // # Notice it's only set to 2 in the case of ruby // atom.config.get('editor.tabLength') # => 4 // atom.config.get('editor.tabLength', scope: ['source.ruby']) # => 2 // atom.config.get('editor.tabLength', scope: ['source.js']) # => 4 // ``` // // * `keyPath` The {String} name of the key. // * `value` The value of the setting. Passing `undefined` will revert the // setting to the default value. // * `options` (optional) {Object} // * `scopeSelector` (optional) {String}. eg. '.source.ruby' // See [the scopes docs](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/) // for more information. // * `source` (optional) {String} The name of a file with which the setting // is associated. Defaults to the user's config file. // // Returns a {Boolean} // * `true` if the value was set. // * `false` if the value was not able to be coerced to the type specified in the setting's schema. set(...args) { let [keyPath, value, options = {}] = args; if (!this.settingsLoaded) { this.pendingOperations.push(() => this.set(keyPath, value, options)); } // We should never use the scoped store to set global settings, since they are kept directly // in the config object. const scopeSelector = options.scopeSelector !== '*' ? options.scopeSelector : undefined; let source = options.source; const shouldSave = options.save != null ? options.save : true; if (source && !scopeSelector && source !== this.projectFile) { throw new Error( "::set with a 'source' and no 'sourceSelector' is not yet implemented!" ); } if (!source) source = this.mainSource; if (value !== undefined) { try { value = this.makeValueConformToSchema(keyPath, value); } catch (e) { return false; } } if (scopeSelector != null) { this.setRawScopedValue(keyPath, value, source, scopeSelector); } else { this.setRawValue(keyPath, value, { source }); } if (source === this.mainSource && shouldSave && this.settingsLoaded) { this.requestSave(); } return true; } // Essential: Restore the setting at `keyPath` to its default value. // // * `keyPath` The {String} name of the key. // * `options` (optional) {Object} // * `scopeSelector` (optional) {String}. See {::set} // * `source` (optional) {String}. See {::set} unset(keyPath, options) { if (!this.settingsLoaded) { this.pendingOperations.push(() => this.unset(keyPath, options)); } let { scopeSelector, source } = options != null ? options : {}; if (source == null) { source = this.mainSource; } if (scopeSelector != null) { if (keyPath != null) { let settings = this.scopedSettingsStore.propertiesForSourceAndSelector( source, scopeSelector ); if (getValueAtKeyPath(settings, keyPath) != null) { this.scopedSettingsStore.removePropertiesForSourceAndSelector( source, scopeSelector ); setValueAtKeyPath(settings, keyPath, undefined); settings = withoutEmptyObjects(settings); if (settings != null) { this.set(null, settings, { scopeSelector, source, priority: this.priorityForSource(source) }); } const configIsReady = source === this.mainSource && this.settingsLoaded; if (configIsReady) { return this.requestSave(); } } } else { this.scopedSettingsStore.removePropertiesForSourceAndSelector( source, scopeSelector ); return this.emitChangeEvent(); } } else { for (scopeSelector in this.scopedSettingsStore.propertiesForSource( source )) { this.unset(keyPath, { scopeSelector, source }); } if (keyPath != null && source === this.mainSource) { return this.set( keyPath, getValueAtKeyPath(this.defaultSettings, keyPath) ); } } } // Extended: Get an {Array} of all of the `source` {String}s with which // settings have been added via {::set}. getSources() { return _.uniq( _.pluck(this.scopedSettingsStore.propertySets, 'source') ).sort(); } // Extended: Retrieve the schema for a specific key path. The schema will tell // you what type the keyPath expects, and other metadata about the config // option. // // * `keyPath` The {String} name of the key. // // Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`. // Returns `null` when the keyPath has no schema specified, but is accessible // from the root schema. getSchema(keyPath) { const keys = splitKeyPath(keyPath); let { schema } = this; for (let key of keys) { let childSchema; if (schema.type === 'object') { childSchema = schema.properties != null ? schema.properties[key] : undefined; if (childSchema == null) { if (isPlainObject(schema.additionalProperties)) { childSchema = schema.additionalProperties; } else if (schema.additionalProperties === false) { return null; } else { return { type: 'any' }; } } } else { return null; } schema = childSchema; } return schema; } getUserConfigPath() { return this.mainSource; } // Extended: Suppress calls to handler functions registered with {::onDidChange} // and {::observe} for the duration of `callback`. After `callback` executes, // handlers will be called once if the value for their key-path has changed. // // * `callback` {Function} to execute while suppressing calls to handlers. transact(callback) { this.beginTransaction(); try { return callback(); } finally { this.endTransaction(); } } getLegacyScopeDescriptorForNewScopeDescriptor(scopeDescriptor) { return null; } /* Section: Internal methods used by core */ // Private: Suppress calls to handler functions registered with {::onDidChange} // and {::observe} for the duration of the {Promise} returned by `callback`. // After the {Promise} is either resolved or rejected, handlers will be called // once if the value for their key-path has changed. // // * `callback` {Function} that returns a {Promise}, which will be executed // while suppressing calls to handlers. // // Returns a {Promise} that is either resolved or rejected according to the // `{Promise}` returned by `callback`. If `callback` throws an error, a // rejected {Promise} will be returned instead. transactAsync(callback) { let endTransaction; this.beginTransaction(); try { endTransaction = fn => (...args) => { this.endTransaction(); return fn(...args); }; const result = callback(); return new Promise((resolve, reject) => { return result .then(endTransaction(resolve)) .catch(endTransaction(reject)); }); } catch (error) { this.endTransaction(); return Promise.reject(error); } } beginTransaction() { this.transactDepth++; } endTransaction() { this.transactDepth--; this.emitChangeEvent(); } pushAtKeyPath(keyPath, value) { const left = this.get(keyPath); const arrayValue = left == null ? [] : left; const result = arrayValue.push(value); this.set(keyPath, arrayValue); return result; } unshiftAtKeyPath(keyPath, value) { const left = this.get(keyPath); const arrayValue = left == null ? [] : left; const result = arrayValue.unshift(value); this.set(keyPath, arrayValue); return result; } removeAtKeyPath(keyPath, value) { const left = this.get(keyPath); const arrayValue = left == null ? [] : left; const result = _.remove(arrayValue, value); this.set(keyPath, arrayValue); return result; } setSchema(keyPath, schema) { if (!isPlainObject(schema)) { throw new Error( `Error loading schema for ${keyPath}: schemas can only be objects!` ); } if (schema.type == null) { throw new Error( `Error loading schema for ${keyPath}: schema objects must have a type attribute` ); } let rootSchema = this.schema; if (keyPath) { for (let key of splitKeyPath(keyPath)) { rootSchema.type = 'object'; if (rootSchema.properties == null) { rootSchema.properties = {}; } const { properties } = rootSchema; if (properties[key] == null) { properties[key] = {}; } rootSchema = properties[key]; } } Object.assign(rootSchema, schema); this.transact(() => { this.setDefaults(keyPath, this.extractDefaultsFromSchema(schema)); this.setScopedDefaultsFromSchema(keyPath, schema); this.resetSettingsForSchemaChange(); }); } save() { if (this.saveCallback) { let allSettings = { '*': this.settings }; allSettings = Object.assign( allSettings, this.scopedSettingsStore.propertiesForSource(this.mainSource) ); allSettings = sortObject(allSettings); this.saveCallback(allSettings); } } /* Section: Private methods managing global settings */ resetUserSettings(newSettings, options = {}) { this._resetSettings(newSettings, options); } _resetSettings(newSettings, options = {}) { const source = options.source; newSettings = Object.assign({}, newSettings); if (newSettings.global != null) { newSettings['*'] = newSettings.global; delete newSettings.global; } if (newSettings['*'] != null) { const scopedSettings = newSettings; newSettings = newSettings['*']; delete scopedSettings['*']; this.resetScopedSettings(scopedSettings, { source }); } return this.transact(() => { this._clearUnscopedSettingsForSource(source); this.settingsLoaded = true; for (let key in newSettings) { const value = newSettings[key]; this.set(key, value, { save: false, source }); } if (this.pendingOperations.length) { for (let op of this.pendingOperations) { op(); } this.pendingOperations = []; } }); } _clearUnscopedSettingsForSource(source) { if (source === this.projectFile) { this.projectSettings = {}; } else { this.settings = {}; } } resetProjectSettings(newSettings, projectFile) { // Sets the scope and source of all project settings to `path`. newSettings = Object.assign({}, newSettings); const oldProjectFile = this.projectFile; this.projectFile = projectFile; if (this.projectFile != null) { this._resetSettings(newSettings, { source: this.projectFile }); } else { this.scopedSettingsStore.removePropertiesForSource(oldProjectFile); this.projectSettings = {}; } } clearProjectSettings() { this.resetProjectSettings({}, null); } getRawValue(keyPath, options = {}) { let value; if ( !options.excludeSources || !options.excludeSources.includes(this.mainSource) ) { value = getValueAtKeyPath(this.settings, keyPath); if (this.projectFile != null) { const projectValue = getValueAtKeyPath(this.projectSettings, keyPath); value = projectValue === undefined ? value : projectValue; } } let defaultValue; if (!options.sources || options.sources.length === 0) { defaultValue = getValueAtKeyPath(this.defaultSettings, keyPath); } if (value != null) { value = this.deepClone(value); if (isPlainObject(value) && isPlainObject(defaultValue)) { this.deepDefaults(value, defaultValue); } return value; } else { return this.deepClone(defaultValue); } } setRawValue(keyPath, value, options = {}) { const source = options.source ? options.source : undefined; const settingsToChange = source === this.projectFile ? 'projectSettings' : 'settings'; const defaultValue = getValueAtKeyPath(this.defaultSettings, keyPath); if (_.isEqual(defaultValue, value)) { if (keyPath != null) { deleteValueAtKeyPath(this[settingsToChange], keyPath); } else { this[settingsToChange] = null; } } else { if (keyPath != null) { setValueAtKeyPath(this[settingsToChange], keyPath, value); } else { this[settingsToChange] = value; } } return this.emitChangeEvent(); } observeKeyPath(keyPath, options, callback) { callback(this.get(keyPath)); return this.onDidChangeKeyPath(keyPath, event => callback(event.newValue)); } onDidChangeKeyPath(keyPath, callback) { let oldValue = this.get(keyPath); return this.emitter.on('did-change', () => { const newValue = this.get(keyPath); if (!_.isEqual(oldValue, newValue)) { const event = { oldValue, newValue }; oldValue = newValue; return callback(event); } }); } isSubKeyPath(keyPath, subKeyPath) { if (keyPath == null || subKeyPath == null) { return false; } const pathSubTokens = splitKeyPath(subKeyPath); const pathTokens = splitKeyPath(keyPath).slice(0, pathSubTokens.length); return _.isEqual(pathTokens, pathSubTokens); } setRawDefault(keyPath, value) { setValueAtKeyPath(this.defaultSettings, keyPath, value); return this.emitChangeEvent(); } setDefaults(keyPath, defaults) { if (defaults != null && isPlainObject(defaults)) { const keys = splitKeyPath(keyPath); this.transact(() => { const result = []; for (let key in defaults) { const childValue = defaults[key]; if (!defaults.hasOwnProperty(key)) { continue; } result.push( this.setDefaults(keys.concat([key]).join('.'), childValue) ); } return result; }); } else { try { defaults = this.makeValueConformToSchema(keyPath, defaults); this.setRawDefault(keyPath, defaults); } catch (e) { console.warn( `'${keyPath}' could not set the default. Attempted default: ${JSON.stringify( defaults )}; Schema: ${JSON.stringify(this.getSchema(keyPath))}` ); } } } deepClone(object) { if (object instanceof Color) { return object.clone(); } else if (Array.isArray(object)) { return object.map(value => this.deepClone(value)); } else if (isPlainObject(object)) { return _.mapObject(object, (key, value) => [key, this.deepClone(value)]); } else { return object; } } deepDefaults(target) { let result = target; let i = 0; while (++i < arguments.length) { const object = arguments[i]; if (isPlainObject(result) && isPlainObject(object)) { for (let key of Object.keys(object)) { result[key] = this.deepDefaults(result[key], object[key]); } } else { if (result == null) { result = this.deepClone(object); } } } return result; } // `schema` will look something like this // // ```coffee // type: 'string' // default: 'ok' // scopes: // '.source.js': // default: 'omg' // ``` setScopedDefaultsFromSchema(keyPath, schema) { if (schema.scopes != null && isPlainObject(schema.scopes)) { const scopedDefaults = {}; for (let scope in schema.scopes) { const scopeSchema = schema.scopes[scope]; if (!scopeSchema.hasOwnProperty('default')) { continue; } scopedDefaults[scope] = {}; setValueAtKeyPath(scopedDefaults[scope], keyPath, scopeSchema.default); } this.scopedSettingsStore.addProperties('schema-default', scopedDefaults); } if ( schema.type === 'object' && schema.properties != null && isPlainObject(schema.properties) ) { const keys = splitKeyPath(keyPath); for (let key in schema.properties) { const childValue = schema.properties[key]; if (!schema.properties.hasOwnProperty(key)) { continue; } this.setScopedDefaultsFromSchema( keys.concat([key]).join('.'), childValue ); } } } extractDefaultsFromSchema(schema) { if (schema.default != null) { return schema.default; } else if ( schema.type === 'object' && schema.properties != null && isPlainObject(schema.properties) ) { const defaults = {}; const properties = schema.properties || {}; for (let key in properties) { const value = properties[key]; defaults[key] = this.extractDefaultsFromSchema(value); } return defaults; } } makeValueConformToSchema(keyPath, value, options) { if (options != null ? options.suppressException : undefined) { try { return this.makeValueConformToSchema(keyPath, value); } catch (e) { return undefined; } } else { let schema; if ((schema = this.getSchema(keyPath)) == null) { if (schema === false) { throw new Error(`Illegal key path ${keyPath}`); } } return this.constructor.executeSchemaEnforcers(keyPath, value, schema); } } // When the schema is changed / added, there may be values set in the config // that do not conform to the schema. This will reset make them conform. resetSettingsForSchemaChange(source) { if (source == null) { source = this.mainSource; } return this.transact(() => { this.settings = this.makeValueConformToSchema(null, this.settings, { suppressException: true }); const selectorsAndSettings = this.scopedSettingsStore.propertiesForSource( source ); this.scopedSettingsStore.removePropertiesForSource(source); for (let scopeSelector in selectorsAndSettings) { let settings = selectorsAndSettings[scopeSelector]; settings = this.makeValueConformToSchema(null, settings, { suppressException: true }); this.setRawScopedValue(null, settings, source, scopeSelector); } }); } /* Section: Private Scoped Settings */ priorityForSource(source) { switch (source) { case this.mainSource: return 1000; case this.projectFile: return 2000; default: return 0; } } emitChangeEvent() { if (this.transactDepth <= 0) { return this.emitter.emit('did-change'); } } resetScopedSettings(newScopedSettings, options = {}) { const source = options.source == null ? this.mainSource : options.source; const priority = this.priorityForSource(source); this.scopedSettingsStore.removePropertiesForSource(source); for (let scopeSelector in newScopedSettings) { let settings = newScopedSettings[scopeSelector]; settings = this.makeValueConformToSchema(null, settings, { suppressException: true }); const validatedSettings = {}; validatedSettings[scopeSelector] = withoutEmptyObjects(settings); if (validatedSettings[scopeSelector] != null) { this.scopedSettingsStore.addProperties(source, validatedSettings, { priority }); } } return this.emitChangeEvent(); } setRawScopedValue(keyPath, value, source, selector, options) { if (keyPath != null) { const newValue = {}; setValueAtKeyPath(newValue, keyPath, value); value = newValue; } const settingsBySelector = {}; settingsBySelector[selector] = value; this.scopedSettingsStore.addProperties(source, settingsBySelector, { priority: this.priorityForSource(source) }); return this.emitChangeEvent(); } getRawScopedValue(scopeDescriptor, keyPath, options) { scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor); const result = this.scopedSettingsStore.getPropertyValue( scopeDescriptor.getScopeChain(), keyPath, options ); const legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor( scopeDescriptor ); if (result != null) { return result; } else if (legacyScopeDescriptor) { return this.scopedSettingsStore.getPropertyValue( legacyScopeDescriptor.getScopeChain(), keyPath, options ); } } observeScopedKeyPath(scope, keyPath, callback) { callback(this.get(keyPath, { scope })); return this.onDidChangeScopedKeyPath(scope, keyPath, event => callback(event.newValue) ); } onDidChangeScopedKeyPath(scope, keyPath, callback) { let oldValue = this.get(keyPath, { scope }); return this.emitter.on('did-change', () => { const newValue = this.get(keyPath, { scope }); if (!_.isEqual(oldValue, newValue)) { const event = { oldValue, newValue }; oldValue = newValue; callback(event); } }); } } // Base schema enforcers. These will coerce raw input into the specified type, // and will throw an error when the value cannot be coerced. Throwing the error // will indicate that the value should not be set. // // Enforcers are run from most specific to least. For a schema with type // `integer`, all the enforcers for the `integer` type will be run first, in // order of specification. Then the `*` enforcers will be run, in order of // specification. Config.addSchemaEnforcers({ any: { coerce(keyPath, value, schema) { return value; } }, integer: { coerce(keyPath, value, schema) { value = parseInt(value); if (isNaN(value) || !isFinite(value)) { throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} cannot be coerced into an int` ); } return value; } }, number: { coerce(keyPath, value, schema) { value = parseFloat(value); if (isNaN(value) || !isFinite(value)) { throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} cannot be coerced into a number` ); } return value; } }, boolean: { coerce(keyPath, value, schema) { switch (typeof value) { case 'string': if (value.toLowerCase() === 'true') { return true; } else if (value.toLowerCase() === 'false') { return false; } else { throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} must be a boolean or the string 'true' or 'false'` ); } case 'boolean': return value; default: throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} must be a boolean or the string 'true' or 'false'` ); } } }, string: { validate(keyPath, value, schema) { if (typeof value !== 'string') { throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} must be a string` ); } return value; }, validateMaximumLength(keyPath, value, schema) { if ( typeof schema.maximumLength === 'number' && value.length > schema.maximumLength ) { return value.slice(0, schema.maximumLength); } else { return value; } } }, null: { // null sort of isnt supported. It will just unset in this case coerce(keyPath, value, schema) { if (![undefined, null].includes(value)) { throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} must be null` ); } return value; } }, object: { coerce(keyPath, value, schema) { if (!isPlainObject(value)) { throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} must be an object` ); } if (schema.properties == null) { return value; } let defaultChildSchema = null; let allowsAdditionalProperties = true; if (isPlainObject(schema.additionalProperties)) { defaultChildSchema = schema.additionalProperties; } if (schema.additionalProperties === false) { allowsAdditionalProperties = false; } const newValue = {}; for (let prop in value) { const propValue = value[prop]; const childSchema = schema.properties[prop] != null ? schema.properties[prop] : defaultChildSchema; if (childSchema != null) { try { newValue[prop] = this.executeSchemaEnforcers( pushKeyPath(keyPath, prop), propValue, childSchema ); } catch (error) { console.warn(`Error setting item in object: ${error.message}`); } } else if (allowsAdditionalProperties) { // Just pass through un-schema'd values newValue[prop] = propValue; } else { console.warn(`Illegal object key: ${keyPath}.${prop}`); } } return newValue; } }, array: { coerce(keyPath, value, schema) { if (!Array.isArray(value)) { throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} must be an array` ); } const itemSchema = schema.items; if (itemSchema != null) { const newValue = []; for (let item of value) { try { newValue.push( this.executeSchemaEnforcers(keyPath, item, itemSchema) ); } catch (error) { console.warn(`Error setting item in array: ${error.message}`); } } return newValue; } else { return value; } } }, color: { coerce(keyPath, value, schema) { const color = Color.parse(value); if (color == null) { throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} cannot be coerced into a color` ); } return color; } }, '*': { coerceMinimumAndMaximum(keyPath, value, schema) { if (typeof value !== 'number') { return value; } if (schema.minimum != null && typeof schema.minimum === 'number') { value = Math.max(value, schema.minimum); } if (schema.maximum != null && typeof schema.maximum === 'number') { value = Math.min(value, schema.maximum); } return value; }, validateEnum(keyPath, value, schema) { let possibleValues = schema.enum; if (Array.isArray(possibleValues)) { possibleValues = possibleValues.map(value => { if (value.hasOwnProperty('value')) { return value.value; } else { return value; } }); } if ( possibleValues == null || !Array.isArray(possibleValues) || !possibleValues.length ) { return value; } for (let possibleValue of possibleValues) { // Using `isEqual` for possibility of placing enums on array and object schemas if (_.isEqual(possibleValue, value)) { return value; } } throw new Error( `Validation failed at ${keyPath}, ${JSON.stringify( value )} is not one of ${JSON.stringify(possibleValues)}` ); } } }); let isPlainObject = value => _.isObject(value) && !Array.isArray(value) && !_.isFunction(value) && !_.isString(value) && !(value instanceof Color); let sortObject = value => { if (!isPlainObject(value)) { return value; } const result = {}; for (let key of Object.keys(value).sort()) { result[key] = sortObject(value[key]); } return result; }; const withoutEmptyObjects = object => { let resultObject; if (isPlainObject(object)) { for (let key in object) { const value = object[key]; const newValue = withoutEmptyObjects(value); if (newValue != null) { if (resultObject == null) { resultObject = {}; } resultObject[key] = newValue; } } } else { resultObject = object; } return resultObject; }; module.exports = Config; ================================================ FILE: src/context-menu-manager.coffee ================================================ path = require 'path' CSON = require 'season' fs = require 'fs-plus' {calculateSpecificity, validateSelector} = require 'clear-cut' {Disposable} = require 'event-kit' {remote} = require 'electron' MenuHelpers = require './menu-helpers' {sortMenuItems} = require './menu-sort-helpers' _ = require 'underscore-plus' platformContextMenu = require('../package.json')?._atomMenu?['context-menu'] # Extended: Provides a registry for commands that you'd like to appear in the # context menu. # # An instance of this class is always available as the `atom.contextMenu` # global. # # ## Context Menu CSON Format # # ```coffee # 'atom-workspace': [{label: 'Help', command: 'application:open-documentation'}] # 'atom-text-editor': [{ # label: 'History', # submenu: [ # {label: 'Undo', command:'core:undo'} # {label: 'Redo', command:'core:redo'} # ] # }] # ``` # # In your package's menu `.cson` file you need to specify it under a # `context-menu` key: # # ```coffee # 'context-menu': # 'atom-workspace': [{label: 'Help', command: 'application:open-documentation'}] # ... # ``` # # The format for use in {::add} is the same minus the `context-menu` key. See # {::add} for more information. module.exports = class ContextMenuManager constructor: ({@keymapManager}) -> @definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data @clear() @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems() initialize: ({@resourcePath, @devMode}) -> loadPlatformItems: -> if platformContextMenu? @add(platformContextMenu, @devMode ? false) else menusDirPath = path.join(@resourcePath, 'menus') platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) map = CSON.readFileSync(platformMenuPath) @add(map['context-menu']) # Public: Add context menu items scoped by CSS selectors. # # ## Examples # # To add a context menu, pass a selector matching the elements to which you # want the menu to apply as the top level key, followed by a menu descriptor. # The invocation below adds a global 'Help' context menu item and a 'History' # submenu on the editor supporting undo/redo. This is just for example # purposes and not the way the menu is actually configured in Atom by default. # # ```coffee # atom.contextMenu.add { # 'atom-workspace': [{label: 'Help', command: 'application:open-documentation'}] # 'atom-text-editor': [{ # label: 'History', # submenu: [ # {label: 'Undo', command:'core:undo'} # {label: 'Redo', command:'core:redo'} # ] # }] # } # ``` # # ## Arguments # # * `itemsBySelector` An {Object} whose keys are CSS selectors and whose # values are {Array}s of item {Object}s containing the following keys: # * `label` (optional) A {String} containing the menu item's label. # * `command` (optional) A {String} containing the command to invoke on the # target of the right click that invoked the context menu. # * `enabled` (optional) A {Boolean} indicating whether the menu item # should be clickable. Disabled menu items typically appear grayed out. # Defaults to `true`. # * `submenu` (optional) An {Array} of additional items. # * `type` (optional) If you want to create a separator, provide an item # with `type: 'separator'` and no other keys. # * `visible` (optional) A {Boolean} indicating whether the menu item # should appear in the menu. Defaults to `true`. # * `created` (optional) A {Function} that is called on the item each time a # context menu is created via a right click. You can assign properties to # `this` to dynamically compute the command, label, etc. This method is # actually called on a clone of the original item template to prevent state # from leaking across context menu deployments. Called with the following # argument: # * `event` The click event that deployed the context menu. # * `shouldDisplay` (optional) A {Function} that is called to determine # whether to display this item on a given context menu deployment. Called # with the following argument: # * `event` The click event that deployed the context menu. # # * `id` (internal) A {String} containing the menu item's id. # Returns a {Disposable} on which `.dispose()` can be called to remove the # added menu items. add: (itemsBySelector, throwOnInvalidSelector = true) -> addedItemSets = [] for selector, items of itemsBySelector validateSelector(selector) if throwOnInvalidSelector itemSet = new ContextMenuItemSet(selector, items) addedItemSets.push(itemSet) @itemSets.push(itemSet) new Disposable => for itemSet in addedItemSets @itemSets.splice(@itemSets.indexOf(itemSet), 1) return templateForElement: (target) -> @templateForEvent({target}) templateForEvent: (event) -> template = [] currentTarget = event.target while currentTarget? currentTargetItems = [] matchingItemSets = @itemSets.filter (itemSet) -> currentTarget.webkitMatchesSelector(itemSet.selector) for itemSet in matchingItemSets for item in itemSet.items itemForEvent = @cloneItemForEvent(item, event) if itemForEvent MenuHelpers.merge(currentTargetItems, itemForEvent, itemSet.specificity) for item in currentTargetItems MenuHelpers.merge(template, item, false) currentTarget = currentTarget.parentElement @pruneRedundantSeparators(template) @addAccelerators(template) return @sortTemplate(template) # Adds an `accelerator` property to items that have key bindings. Electron # uses this property to surface the relevant keymaps in the context menu. addAccelerators: (template) -> for id, item of template if item.command keymaps = @keymapManager.findKeyBindings({command: item.command, target: document.activeElement}) keystrokes = keymaps?[0]?.keystrokes if keystrokes # Electron does not support multi-keystroke accelerators. Therefore, # when the command maps to a multi-stroke key binding, show the # keystrokes next to the item's label. if keystrokes.includes(' ') item.label += " [#{_.humanizeKeystroke(keystrokes)}]" else item.accelerator = MenuHelpers.acceleratorForKeystroke(keystrokes) if Array.isArray(item.submenu) @addAccelerators(item.submenu) pruneRedundantSeparators: (menu) -> keepNextItemIfSeparator = false index = 0 while index < menu.length if menu[index].type is 'separator' if not keepNextItemIfSeparator or index is menu.length - 1 menu.splice(index, 1) else index++ else keepNextItemIfSeparator = true index++ sortTemplate: (template) -> template = sortMenuItems(template) for id, item of template if Array.isArray(item.submenu) item.submenu = @sortTemplate(item.submenu) return template # Returns an object compatible with `::add()` or `null`. cloneItemForEvent: (item, event) -> return null if item.devMode and not @devMode item = Object.create(item) if typeof item.shouldDisplay is 'function' return null unless item.shouldDisplay(event) item.created?(event) if Array.isArray(item.submenu) item.submenu = item.submenu .map((submenuItem) => @cloneItemForEvent(submenuItem, event)) .filter((submenuItem) -> submenuItem isnt null) return item showForEvent: (event) -> @activeElement = event.target menuTemplate = @templateForEvent(event) return unless menuTemplate?.length > 0 remote.getCurrentWindow().emit('context-menu', menuTemplate) return clear: -> @activeElement = null @itemSets = [] inspectElement = { 'atom-workspace': [{ label: 'Inspect Element' command: 'application:inspect' devMode: true created: (event) -> {pageX, pageY} = event @commandDetail = {x: pageX, y: pageY} }] } @add(inspectElement, false) class ContextMenuItemSet constructor: (@selector, @items) -> @specificity = calculateSpecificity(@selector) ================================================ FILE: src/core-uri-handlers.js ================================================ const fs = require('fs-plus'); // Converts a query string parameter for a line or column number // to a zero-based line or column number for the Atom API. function getLineColNumber(numStr) { const num = parseInt(numStr || 0, 10); return Math.max(num - 1, 0); } function openFile(atom, { query }) { const { filename, line, column } = query; atom.workspace.open(filename, { initialLine: getLineColNumber(line), initialColumn: getLineColNumber(column), searchAllPanes: true }); } function windowShouldOpenFile({ query }) { const { filename } = query; const stat = fs.statSyncNoException(filename); return win => win.containsLocation({ pathToOpen: filename, exists: Boolean(stat), isFile: stat.isFile(), isDirectory: stat.isDirectory() }); } const ROUTER = { '/open/file': { handler: openFile, getWindowPredicate: windowShouldOpenFile } }; module.exports = { create(atomEnv) { return function coreURIHandler(parsed) { const config = ROUTER[parsed.pathname]; if (config) { config.handler(atomEnv, parsed); } }; }, windowPredicate(parsed) { const config = ROUTER[parsed.pathname]; if (config && config.getWindowPredicate) { return config.getWindowPredicate(parsed); } else { return () => true; } } }; ================================================ FILE: src/crash-reporter-start.js ================================================ module.exports = function(params) { const { crashReporter } = require('electron'); const os = require('os'); const platformRelease = os.release(); const arch = os.arch(); const { uploadToServer, releaseChannel } = params; const parsedUploadToServer = uploadToServer !== null ? uploadToServer : false; crashReporter.start({ productName: 'Atom', companyName: 'GitHub', submitURL: 'https://atom.io/crash_reports', parsedUploadToServer, extra: { platformRelease, arch, releaseChannel } }); }; ================================================ FILE: src/cursor.js ================================================ 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: Retrieves the syntax tree scope descriptor for the cursor's current position. // // Returns a {ScopeDescriptor} getSyntaxTreeScopeDescriptor() { return this.editor.syntaxTreeScopeDescriptorForBufferPosition( 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() { const column = this.goalColumn; this.setBufferPosition(this.editor.getEofBufferPosition()); this.goalColumn = column; } // 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 = Range( Point(previousNonBlankRow || 0, 0), currentBufferPosition ); const ranges = this.editor.buffer.findAllInRangeSync( options.wordRegex || this.wordRegExp(), scanRange ); const range = ranges[ranges.length - 1]; if (range) { if ( range.start.row < currentBufferPosition.row && currentBufferPosition.column > 0 ) { return Point(currentBufferPosition.row, 0); } else if (currentBufferPosition.isGreaterThan(range.end)) { return Point.fromObject(range.end); } else { return Point.fromObject(range.start); } } else { return 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 = Range( currentBufferPosition, this.editor.getEofBufferPosition() ); const range = this.editor.buffer.findInRangeSync( options.wordRegex || this.wordRegExp(), scanRange ); if (range) { if (range.start.row > currentBufferPosition.row) { return Point(range.start.row, 0); } else if (currentBufferPosition.isLessThan(range.start)) { return Point.fromObject(range.start); } else { return Point.fromObject(range.end); } } else { return 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(options), 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(options), 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(options), 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 ]*$|[^\\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.getBufferPosition()); } changePosition(options, fn) { this.clearSelection({ autoscroll: false }); fn(); this.goalColumn = null; 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; } }; ================================================ FILE: src/decoration-manager.js ================================================ const { Emitter } = require('event-kit'); const Decoration = require('./decoration'); const LayerDecoration = require('./layer-decoration'); module.exports = class DecorationManager { constructor(editor) { this.editor = editor; this.displayLayer = this.editor.displayLayer; this.emitter = new Emitter(); this.decorationCountsByLayer = new Map(); this.markerDecorationCountsByLayer = new Map(); this.decorationsByMarker = new Map(); this.layerDecorationsByMarkerLayer = new Map(); this.overlayDecorations = new Set(); this.layerUpdateDisposablesByLayer = new WeakMap(); } observeDecorations(callback) { const decorations = this.getDecorations(); for (let i = 0; i < decorations.length; i++) { callback(decorations[i]); } return this.onDidAddDecoration(callback); } onDidAddDecoration(callback) { return this.emitter.on('did-add-decoration', callback); } onDidRemoveDecoration(callback) { return this.emitter.on('did-remove-decoration', callback); } onDidUpdateDecorations(callback) { return this.emitter.on('did-update-decorations', callback); } getDecorations(propertyFilter) { let allDecorations = []; this.decorationsByMarker.forEach(decorations => { decorations.forEach(decoration => allDecorations.push(decoration)); }); if (propertyFilter != null) { allDecorations = allDecorations.filter(function(decoration) { for (let key in propertyFilter) { const value = propertyFilter[key]; if (decoration.properties[key] !== value) return false; } return true; }); } return allDecorations; } getLineDecorations(propertyFilter) { return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line') ); } getLineNumberDecorations(propertyFilter) { return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line-number') ); } getHighlightDecorations(propertyFilter) { return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('highlight') ); } getOverlayDecorations(propertyFilter) { const result = []; result.push(...Array.from(this.overlayDecorations)); if (propertyFilter != null) { return result.filter(function(decoration) { for (let key in propertyFilter) { const value = propertyFilter[key]; if (decoration.properties[key] !== value) { return false; } } return true; }); } else { return result; } } decorationPropertiesByMarkerForScreenRowRange(startScreenRow, endScreenRow) { const decorationPropertiesByMarker = new Map(); this.decorationCountsByLayer.forEach((count, markerLayer) => { const markers = markerLayer.findMarkers({ intersectsScreenRowRange: [startScreenRow, endScreenRow - 1] }); const layerDecorations = this.layerDecorationsByMarkerLayer.get( markerLayer ); const hasMarkerDecorations = this.markerDecorationCountsByLayer.get(markerLayer) > 0; for (let i = 0; i < markers.length; i++) { const marker = markers[i]; if (!marker.isValid()) continue; let decorationPropertiesForMarker = decorationPropertiesByMarker.get( marker ); if (decorationPropertiesForMarker == null) { decorationPropertiesForMarker = []; decorationPropertiesByMarker.set( marker, decorationPropertiesForMarker ); } if (layerDecorations) { layerDecorations.forEach(layerDecoration => { const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties(); decorationPropertiesForMarker.push(properties); }); } if (hasMarkerDecorations) { const decorationsForMarker = this.decorationsByMarker.get(marker); if (decorationsForMarker) { decorationsForMarker.forEach(decoration => { decorationPropertiesForMarker.push(decoration.getProperties()); }); } } } }); return decorationPropertiesByMarker; } decorationsForScreenRowRange(startScreenRow, endScreenRow) { const decorationsByMarkerId = {}; for (const layer of this.decorationCountsByLayer.keys()) { for (const marker of layer.findMarkers({ intersectsScreenRowRange: [startScreenRow, endScreenRow] })) { const decorations = this.decorationsByMarker.get(marker); if (decorations) { decorationsByMarkerId[marker.id] = Array.from(decorations); } } } return decorationsByMarkerId; } decorationsStateForScreenRowRange(startScreenRow, endScreenRow) { const decorationsState = {}; for (const layer of this.decorationCountsByLayer.keys()) { for (const marker of layer.findMarkers({ intersectsScreenRowRange: [startScreenRow, endScreenRow] })) { if (marker.isValid()) { const screenRange = marker.getScreenRange(); const bufferRange = marker.getBufferRange(); const rangeIsReversed = marker.isReversed(); const decorations = this.decorationsByMarker.get(marker); if (decorations) { decorations.forEach(decoration => { decorationsState[decoration.id] = { properties: decoration.properties, screenRange, bufferRange, rangeIsReversed }; }); } const layerDecorations = this.layerDecorationsByMarkerLayer.get( layer ); if (layerDecorations) { layerDecorations.forEach(layerDecoration => { const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties(); decorationsState[`${layerDecoration.id}-${marker.id}`] = { properties, screenRange, bufferRange, rangeIsReversed }; }); } } } } return decorationsState; } decorateMarker(marker, decorationParams) { if (marker.isDestroyed()) { const error = new Error('Cannot decorate a destroyed marker'); error.metadata = { markerLayerIsDestroyed: marker.layer.isDestroyed() }; if (marker.destroyStackTrace != null) { error.metadata.destroyStackTrace = marker.destroyStackTrace; } if ( marker.bufferMarker != null && marker.bufferMarker.destroyStackTrace != null ) { error.metadata.destroyStackTrace = marker.bufferMarker.destroyStackTrace; } throw error; } marker = this.displayLayer .getMarkerLayer(marker.layer.id) .getMarker(marker.id); const decoration = new Decoration(marker, this, decorationParams); let decorationsForMarker = this.decorationsByMarker.get(marker); if (!decorationsForMarker) { decorationsForMarker = new Set(); this.decorationsByMarker.set(marker, decorationsForMarker); } decorationsForMarker.add(decoration); if (decoration.isType('overlay')) this.overlayDecorations.add(decoration); this.observeDecoratedLayer(marker.layer, true); this.editor.didAddDecoration(decoration); this.emitDidUpdateDecorations(); this.emitter.emit('did-add-decoration', decoration); return decoration; } decorateMarkerLayer(markerLayer, decorationParams) { if (markerLayer.isDestroyed()) { throw new Error('Cannot decorate a destroyed marker layer'); } markerLayer = this.displayLayer.getMarkerLayer(markerLayer.id); const decoration = new LayerDecoration(markerLayer, this, decorationParams); let layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer); if (layerDecorations == null) { layerDecorations = new Set(); this.layerDecorationsByMarkerLayer.set(markerLayer, layerDecorations); } layerDecorations.add(decoration); this.observeDecoratedLayer(markerLayer, false); this.emitDidUpdateDecorations(); return decoration; } emitDidUpdateDecorations() { this.editor.scheduleComponentUpdate(); this.emitter.emit('did-update-decorations'); } decorationDidChangeType(decoration) { if (decoration.isType('overlay')) { this.overlayDecorations.add(decoration); } else { this.overlayDecorations.delete(decoration); } } didDestroyMarkerDecoration(decoration) { const { marker } = decoration; const decorations = this.decorationsByMarker.get(marker); if (decorations && decorations.has(decoration)) { decorations.delete(decoration); if (decorations.size === 0) this.decorationsByMarker.delete(marker); this.overlayDecorations.delete(decoration); this.unobserveDecoratedLayer(marker.layer, true); this.emitter.emit('did-remove-decoration', decoration); this.emitDidUpdateDecorations(); } } didDestroyLayerDecoration(decoration) { const { markerLayer } = decoration; const decorations = this.layerDecorationsByMarkerLayer.get(markerLayer); if (decorations && decorations.has(decoration)) { decorations.delete(decoration); if (decorations.size === 0) { this.layerDecorationsByMarkerLayer.delete(markerLayer); } this.unobserveDecoratedLayer(markerLayer, true); this.emitDidUpdateDecorations(); } } observeDecoratedLayer(layer, isMarkerDecoration) { const newCount = (this.decorationCountsByLayer.get(layer) || 0) + 1; this.decorationCountsByLayer.set(layer, newCount); if (newCount === 1) { this.layerUpdateDisposablesByLayer.set( layer, layer.onDidUpdate(this.emitDidUpdateDecorations.bind(this)) ); } if (isMarkerDecoration) { this.markerDecorationCountsByLayer.set( layer, (this.markerDecorationCountsByLayer.get(layer) || 0) + 1 ); } } unobserveDecoratedLayer(layer, isMarkerDecoration) { const newCount = this.decorationCountsByLayer.get(layer) - 1; if (newCount === 0) { this.layerUpdateDisposablesByLayer.get(layer).dispose(); this.decorationCountsByLayer.delete(layer); } else { this.decorationCountsByLayer.set(layer, newCount); } if (isMarkerDecoration) { this.markerDecorationCountsByLayer.set( this.markerDecorationCountsByLayer.get(layer) - 1 ); } } }; ================================================ FILE: src/decoration.js ================================================ const { Emitter } = require('event-kit'); let idCounter = 0; const nextId = () => idCounter++; const normalizeDecorationProperties = function(decoration, decorationParams) { decorationParams.id = decoration.id; if ( decorationParams.type === 'line-number' && decorationParams.gutterName == null ) { decorationParams.gutterName = 'line-number'; } if (decorationParams.order == null) { decorationParams.order = Infinity; } 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 (Array.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.setProperties({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 = normalizeDecorationProperties(this, 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'); } }; ================================================ FILE: src/default-directory-provider.coffee ================================================ {Directory} = require 'pathwatcher' fs = require 'fs-plus' path = require 'path' url = require 'url' module.exports = class DefaultDirectoryProvider # Public: Create a Directory that corresponds to the specified URI. # # * `uri` {String} The path to the directory to add. This is guaranteed not to # be contained by a {Directory} in `atom.project`. # # Returns: # * {Directory} if the given URI is compatible with this provider. # * `null` if the given URI is not compatible with this provider. directoryForURISync: (uri) -> normalizedPath = @normalizePath(uri) {host} = url.parse(uri) directoryPath = if host uri else if not fs.isDirectorySync(normalizedPath) and fs.isDirectorySync(path.dirname(normalizedPath)) path.dirname(normalizedPath) else normalizedPath # TODO: Stop normalizing the path in pathwatcher's Directory. directory = new Directory(directoryPath) if host directory.path = directoryPath if fs.isCaseInsensitive() directory.lowerCasePath = directoryPath.toLowerCase() directory # Public: Create a Directory that corresponds to the specified URI. # # * `uri` {String} The path to the directory to add. This is guaranteed not to # be contained by a {Directory} in `atom.project`. # # Returns a {Promise} that resolves to: # * {Directory} if the given URI is compatible with this provider. # * `null` if the given URI is not compatible with this provider. directoryForURI: (uri) -> Promise.resolve(@directoryForURISync(uri)) # Public: Normalizes path. # # * `uri` {String} The path that should be normalized. # # Returns a {String} with normalized path. normalizePath: (uri) -> # Normalize disk drive letter on Windows to avoid opening two buffers for the same file pathWithNormalizedDiskDriveLetter = if process.platform is 'win32' and matchData = uri.match(/^([a-z]):/) "#{matchData[1].toUpperCase()}#{uri.slice(1)}" else uri path.normalize(pathWithNormalizedDiskDriveLetter) ================================================ FILE: src/default-directory-searcher.js ================================================ const Task = require('./task'); // Searches local files for lines matching a specified regex. Implements `.then()` // so that it can be used with `Promise.all()`. class DirectorySearch { constructor(rootPaths, regex, options) { const scanHandlerOptions = { ignoreCase: regex.ignoreCase, inclusions: options.inclusions, includeHidden: options.includeHidden, excludeVcsIgnores: options.excludeVcsIgnores, globalExclusions: options.exclusions, follow: options.follow }; const searchOptions = { leadingContextLineCount: options.leadingContextLineCount, trailingContextLineCount: options.trailingContextLineCount }; this.task = new Task(require.resolve('./scan-handler')); this.task.on('scan:result-found', options.didMatch); this.task.on('scan:file-error', options.didError); this.task.on('scan:paths-searched', options.didSearchPaths); this.promise = new Promise((resolve, reject) => { this.task.on('task:cancelled', reject); this.task.start( rootPaths, regex.source, scanHandlerOptions, searchOptions, () => { this.task.terminate(); resolve(); } ); }); } then(...args) { return this.promise.then.apply(this.promise, args); } cancel() { // This will cause @promise to reject. this.task.cancel(); } } // Default provider for the `atom.directory-searcher` service. module.exports = class DefaultDirectorySearcher { // Determines whether this object supports search for a `Directory`. // // * `directory` {Directory} whose search needs might be supported by this object. // // Returns a `boolean` indicating whether this object can search this `Directory`. canSearchDirectory(directory) { return true; } // Performs a text search for files in the specified `Directory`, subject to the // specified parameters. // // Results are streamed back to the caller by invoking methods on the specified `options`, // such as `didMatch` and `didError`. // // * `directories` {Array} of {Directory} objects to search, all of which have been accepted by // this searcher's `canSearchDirectory()` predicate. // * `regex` {RegExp} to search with. // * `options` {Object} with the following properties: // * `didMatch` {Function} call with a search result structured as follows: // * `searchResult` {Object} with the following keys: // * `filePath` {String} absolute path to the matching file. // * `matches` {Array} with object elements with the following keys: // * `lineText` {String} The full text of the matching line (without a line terminator character). // * `lineTextOffset` {Number} If > 0, the provided line text is truncated and starts at this offset // * `matchText` {String} The text that matched the `regex` used for the search. // * `range` {Range} Identifies the matching region in the file. (Likely as an array of numeric arrays.) // * `didError` {Function} call with an Error if there is a problem during the search. // * `didSearchPaths` {Function} periodically call with the number of paths searched thus far. // * `inclusions` {Array} of glob patterns (as strings) to search within. Note that this // array may be empty, indicating that all files should be searched. // // Each item in the array is a file/directory pattern, e.g., `src` to search in the "src" // directory or `*.js` to search all JavaScript files. In practice, this often comes from the // comma-delimited list of patterns in the bottom text input of the ProjectFindView dialog. // * `includeHidden` {boolean} whether to ignore hidden files. // * `excludeVcsIgnores` {boolean} whether to exclude VCS ignored paths. // * `exclusions` {Array} similar to inclusions // * `follow` {boolean} whether symlinks should be followed. // // Returns a *thenable* `DirectorySearch` that includes a `cancel()` method. If `cancel()` is // invoked before the `DirectorySearch` is determined, it will resolve the `DirectorySearch`. search(directories, regex, options) { const rootPaths = directories.map(directory => directory.getPath()); let isCancelled = false; const directorySearch = new DirectorySearch(rootPaths, regex, options); const promise = new Promise(function(resolve, reject) { directorySearch.then(resolve, function() { if (isCancelled) { resolve(); } else { reject(); // eslint-disable-line prefer-promise-reject-errors } }); }); return { then: promise.then.bind(promise), catch: promise.catch.bind(promise), cancel() { isCancelled = true; directorySearch.cancel(); } }; } }; ================================================ FILE: src/delegated-listener.js ================================================ const EventKit = require('event-kit'); module.exports = function listen(element, eventName, selector, handler) { const innerHandler = function(event) { if (selector) { var currentTarget = event.target; while (currentTarget) { if (currentTarget.matches && currentTarget.matches(selector)) { handler({ type: event.type, currentTarget: currentTarget, target: event.target, preventDefault: function() { event.preventDefault(); }, originalEvent: event }); } if (currentTarget === element) break; currentTarget = currentTarget.parentNode; } } else { handler({ type: event.type, currentTarget: event.currentTarget, target: event.target, preventDefault: function() { event.preventDefault(); }, originalEvent: event }); } }; element.addEventListener(eventName, innerHandler); return new EventKit.Disposable(function() { element.removeEventListener(eventName, innerHandler); }); }; ================================================ FILE: src/deprecated-syntax-selectors.js ================================================ module.exports = new Set([ 'AFDKO', 'AFKDO', 'ASS', 'AVX', 'AVX2', 'AVX512', 'AVX512BW', 'AVX512DQ', 'Alignment', 'Alpha', 'AlphaLevel', 'Angle', 'Animation', 'AnimationGroup', 'ArchaeologyDigSiteFrame', 'Arrow__', 'AtLilyPond', 'AttrBaseType', 'AttrSetVal__', 'BackColour', 'Banner', 'Bold', 'Bonlang', 'BorderStyle', 'Browser', 'Button', 'C99', 'CALCULATE', 'CharacterSet', 'ChatScript', 'Chatscript', 'CheckButton', 'ClipboardFormat', 'ClipboardType', 'Clipboard__', 'CodePage', 'Codepages__', 'Collisions', 'ColorSelect', 'ColourActual', 'ColourLogical', 'ColourReal', 'ColourScheme', 'ColourSize', 'Column', 'Comment', 'ConfCachePolicy', 'ControlPoint', 'Cooldown', 'DBE', 'DDL', 'DML', 'DSC', 'Database__', 'DdcMode', 'Dialogue', 'DiscussionFilterType', 'DiscussionStatus', 'DisplaySchemes', 'Document-Structuring-Comment', 'DressUpModel', 'Edit', 'EditBox', 'Effect', 'Encoding', 'End', 'ExternalLinkBehaviour', 'ExternalLinkDirection', 'F16c', 'FMA', 'FilterType', 'Font', 'FontInstance', 'FontString', 'Fontname', 'Fonts__', 'Fontsize', 'Format', 'Frame', 'GameTooltip', 'GroupList', 'HLE', 'HeaderEvent', 'HistoryType', 'HttpVerb', 'II', 'IO', 'Icon', 'IconID', 'InPlaceBox__', 'InPlaceEditEvent', 'Info', 'Italic', 'JSXEndTagStart', 'JSXStartTagEnd', 'KNC', 'KeyModifier', 'Kotlin', 'LUW', 'Language', 'Layer', 'LayeredRegion', 'LdapItemList', 'LineSpacing', 'LinkFilter', 'LinkLimit', 'ListView', 'Locales__', 'Lock', 'LoginPolicy', 'MA_End__', 'MA_StdCombo__', 'MA_StdItem__', 'MA_StdMenu__', 'MISSING', 'Mapping', 'MarginL', 'MarginR', 'MarginV', 'Marked', 'MessageFrame', 'Minimap', 'MovieFrame', 'Name', 'Outline', 'OutlineColour', 'ParentedObject', 'Path', 'Permission', 'PlayRes', 'PlayerModel', 'PrimaryColour', 'Proof', 'QuestPOIFrame', 'RTM', 'RecentModule__', 'Regexp', 'Region', 'Rotation', 'SCADABasic', 'SSA', 'Scale', 'ScaleX', 'ScaleY', 'ScaledBorderAndShadow', 'ScenarioPOIFrame', 'ScriptObject', 'Script__', 'Scroll', 'ScrollEvent', 'ScrollFrame', 'ScrollSide', 'ScrollingMessageFrame', 'SecondaryColour', 'Sensitivity', 'Shadow', 'SimpleHTML', 'Slider', 'Spacing', 'Start', 'StatusBar', 'Stream', 'StrikeOut', 'Style', 'TIS', 'TODO', 'TabardModel', 'Text', 'Texture', 'Timer', 'ToolType', 'Translation', 'TreeView', 'TriggerStatus', 'UIObject', 'Underline', 'UserClass', 'UserList', 'UserNotifyList', 'VisibleRegion', 'Vplus', 'WrapStyle', 'XHPEndTagStart', 'XHPStartTagEnd', 'ZipType', '__package-name__', '_c', '_function', 'a', 'a10networks', 'aaa', 'abaqus', 'abbrev', 'abbreviated', 'abbreviation', 'abcnotation', 'abl', 'abnf', 'abp', 'absolute', 'abstract', 'academic', 'access', 'access-control', 'access-qualifiers', 'accessed', 'accessor', 'account', 'accumulator', 'ace', 'ace3', 'acl', 'acos', 'act', 'action', 'action-map', 'actionhandler', 'actionpack', 'actions', 'actionscript', 'activerecord', 'activesupport', 'actual', 'acute-accent', 'ada', 'add', 'adddon', 'added', 'addition', 'additional-character', 'additive', 'addon', 'address', 'address-of', 'address-space', 'addrfam', 'adjustment', 'admonition', 'adr', 'adverb', 'adx', 'ael', 'aem', 'aerospace', 'aes', 'aes_functions', 'aesni', 'aexLightGreen', 'af', 'afii', 'aflex', 'after', 'after-expression', 'agc', 'agda', 'agentspeak', 'aggregate', 'aggregation', 'ahk', 'ai-connection', 'ai-player', 'ai-wheeled-vehicle', 'aif', 'alabel', 'alarms', 'alda', 'alert', 'algebraic-type', 'alias', 'aliases', 'align', 'align-attribute', 'alignment', 'alignment-cue-setting', 'alignment-mode', 'all', 'all-once', 'all-solutions', 'allocate', 'alloy', 'alloyglobals', 'alloyxml', 'alog', 'alpha', 'alphabeticalllt', 'alphabeticallyge', 'alphabeticallygt', 'alphabeticallyle', 'alt', 'alter', 'alternate-wysiwyg-string', 'alternates', 'alternation', 'alternatives', 'am', 'ambient-audio-manager', 'ambient-reflectivity', 'amd', 'amd3DNow', 'amdnops', 'ameter', 'amount', 'amp', 'ampersand', 'ampl', 'ampscript', 'an', 'analysis', 'analytics', 'anb', 'anchor', 'and', 'andop', 'angelscript', 'angle', 'angle-brackets', 'angular', 'animation', 'annot', 'annotated', 'annotation', 'annotation-arguments', 'anon', 'anonymous', 'another', 'ansi', 'ansi-c', 'ansi-colored', 'ansi-escape-code', 'ansi-formatted', 'ansi2', 'ansible', 'answer', 'antialiasing', 'antl', 'antlr', 'antlr4', 'anubis', 'any', 'any-method', 'anyclass', 'aolserver', 'apa', 'apache', 'apache-config', 'apc', 'apdl', 'apex', 'api', 'api-notation', 'apiary', 'apib', 'apl', 'apostrophe', 'appcache', 'applescript', 'application', 'application-name', 'application-process', 'approx-equal', 'aql', 'aqua', 'ar', 'arbitrary-radix', 'arbitrary-repetition', 'arbitrary-repitition', 'arch', 'arch_specification', 'architecture', 'archive', 'archives', 'arduino', 'area-code', 'arendelle', 'argcount', 'args', 'argument', 'argument-label', 'argument-separator', 'argument-seperator', 'argument-type', 'arguments', 'arith', 'arithmetic', 'arithmetical', 'arithmeticcql', 'ark', 'arm', 'arma', 'armaConfig', 'arnoldc', 'arp', 'arpop', 'arr', 'array', 'array-expression', 'array-literal', 'arrays', 'arrow', 'articulation', 'artihmetic', 'arvo', 'aryop', 'as', 'as4', 'ascii', 'asciidoc', 'asdoc', 'ash', 'ashx', 'asl', 'asm', 'asm-instruction', 'asm-type-prefix', 'asn', 'asp', 'asp-core-2', 'aspx', 'ass', 'assembly', 'assert', 'assertion', 'assigment', 'assign', 'assign-class', 'assigned', 'assigned-class', 'assigned-value', 'assignee', 'assignement', 'assignment', 'assignmentforge-config', 'associate', 'association', 'associativity', 'assocs', 'asterisk', 'async', 'at-marker', 'at-root', 'at-rule', 'at-sign', 'atmark', 'atml3', 'atoemp', 'atom', 'atom-term-processing', 'atomic', 'atomscript', 'att', 'attachment', 'attr', 'attribute', 'attribute-entry', 'attribute-expression', 'attribute-key-value', 'attribute-list', 'attribute-lookup', 'attribute-name', 'attribute-reference', 'attribute-selector', 'attribute-value', 'attribute-values', 'attribute-with-value', 'attribute_list', 'attribute_value', 'attribute_value2', 'attributelist', 'attributes', 'attrset', 'attrset-or-function', 'audio', 'audio-file', 'auditor', 'augmented', 'auth', 'auth_basic', 'author', 'author-names', 'authorization', 'auto', 'auto-event', 'autoconf', 'autoindex', 'autoit', 'automake', 'automatic', 'autotools', 'autovar', 'aux', 'auxiliary', 'avdl', 'avra', 'avrasm', 'avrdisasm', 'avs', 'avx', 'avx2', 'avx512', 'awk', 'axes_group', 'axis', 'axl', 'b', 'b-spline-patch', 'babel', 'back', 'back-from', 'back-reference', 'back-slash', 'backend', 'background', 'backreference', 'backslash', 'backslash-bar', 'backslash-g', 'backspace', 'backtick', 'bad-ampersand', 'bad-angle-bracket', 'bad-assignment', 'bad-comments-or-CDATA', 'bad-escape', 'bad-octal', 'bad-var', 'bang', 'banner', 'bar', 'bareword', 'barline', 'base', 'base-11', 'base-12', 'base-13', 'base-14', 'base-15', 'base-16', 'base-17', 'base-18', 'base-19', 'base-20', 'base-21', 'base-22', 'base-23', 'base-24', 'base-25', 'base-26', 'base-27', 'base-28', 'base-29', 'base-3', 'base-30', 'base-31', 'base-32', 'base-33', 'base-34', 'base-35', 'base-36', 'base-4', 'base-5', 'base-6', 'base-7', 'base-9', 'base-call', 'base-integer', 'base64', 'base85', 'base_pound_number_pound', 'basetype', 'basic', 'basic-arithmetic', 'basic-type', 'basic_functions', 'basicblock', 'basis-matrix', 'bat', 'batch', 'batchfile', 'battlesim', 'bb', 'bbcode', 'bcmath', 'be', 'beam', 'beamer', 'beancount', 'before', 'begin', 'begin-document', 'begin-emphasis', 'begin-end', 'begin-end-group', 'begin-literal', 'begin-symbolic', 'begintimeblock', 'behaviour', 'bem', 'between-tag-pair', 'bevel', 'bezier-patch', 'bfeac', 'bff', 'bg', 'bg-black', 'bg-blue', 'bg-cyan', 'bg-green', 'bg-normal', 'bg-purple', 'bg-red', 'bg-white', 'bg-yellow', 'bhtml', 'bhv', 'bibitem', 'bibliography-anchor', 'biblioref', 'bibpaper', 'bibtex', 'bif', 'big-arrow', 'big-arrow-left', 'bigdecimal', 'bigint', 'biicode', 'biiconf', 'bin', 'binOp', 'binary', 'binary-arithmetic', 'bind', 'binder', 'binding', 'binding-prefix', 'bindings', 'binop', 'bioinformatics', 'biosphere', 'bird-track', 'bis', 'bison', 'bit', 'bit-and-byte', 'bit-range', 'bit-wise', 'bitarray', 'bitop', 'bits-mov', 'bitvector', 'bitwise', 'black', 'blade', 'blanks', 'blaze', 'blenc', 'blend', 'blending', 'blendtype', 'blendu', 'blendv', 'blip', 'block', 'block-attribute', 'block-dartdoc', 'block-data', 'block-level', 'blockid', 'blockname', 'blockquote', 'blocktitle', 'blue', 'blueprint', 'bluespec', 'blur', 'bm', 'bmi', 'bmi1', 'bmi2', 'bnd', 'bnf', 'body', 'body-statement', 'bold', 'bold-italic-text', 'bold-text', 'bolt', 'bond', 'bonlang', 'boo', 'boogie', 'bool', 'boolean', 'boolean-test', 'boost', 'boot', 'bord', 'border', 'botml', 'bottom', 'boundary', 'bounded', 'bounds', 'bow', 'box', 'bpl', 'bpr', 'bqparam', 'brace', 'braced', 'braces', 'bracket', 'bracketed', 'brackets', 'brainfuck', 'branch', 'branch-point', 'break', 'breakpoint', 'breakpoints', 'breaks', 'bridle', 'brightscript', 'bro', 'broken', 'browser', 'browsers', 'bs', 'bsl', 'btw', 'buffered', 'buffers', 'bugzilla-number', 'build', 'buildin', 'buildout', 'built-in', 'built-in-variable', 'built-ins', 'builtin', 'builtin-comparison', 'builtins', 'bullet', 'bullet-point', 'bump', 'bump-multiplier', 'bundle', 'but', 'button', 'buttons', 'by', 'by-name', 'by-number', 'byref', 'byte', 'bytearray', 'bz2', 'bzl', 'c', 'c-style', 'c0', 'c1', 'c2hs', 'ca', 'cabal', 'cabal-keyword', 'cache', 'cache-management', 'cacheability-control', 'cake', 'calc', 'calca', 'calendar', 'call', 'callable', 'callback', 'caller', 'calling', 'callmethod', 'callout', 'callparent', 'camera', 'camlp4', 'camlp4-stream', 'canonicalized-program-name', 'canopen', 'capability', 'capnp', 'cappuccino', 'caps', 'caption', 'capture', 'capturename', 'cardinal-curve', 'cardinal-patch', 'cascade', 'case', 'case-block', 'case-body', 'case-class', 'case-clause', 'case-clause-body', 'case-expression', 'case-modifier', 'case-pattern', 'case-statement', 'case-terminator', 'case-value', 'cassius', 'cast', 'catch', 'catch-exception', 'catcode', 'categories', 'categort', 'category', 'cba', 'cbmbasic', 'cbot', 'cbs', 'cc', 'cc65', 'ccml', 'cdata', 'cdef', 'cdtor', 'ceiling', 'cell', 'cellcontents', 'cellwall', 'ceq', 'ces', 'cet', 'cexpr', 'cextern', 'ceylon', 'ceylondoc', 'cf', 'cfdg', 'cfengine', 'cfg', 'cfml', 'cfscript', 'cfunction', 'cg', 'cgi', 'cgx', 'chain', 'chained', 'chaining', 'chainname', 'changed', 'changelogs', 'changes', 'channel', 'chapel', 'chapter', 'char', 'characater', 'character', 'character-class', 'character-data-not-allowed-here', 'character-literal', 'character-literal-too-long', 'character-not-allowed-here', 'character-range', 'character-reference', 'character-token', 'character_not_allowed', 'character_not_allowed_here', 'characters', 'chars', 'chars-and-bytes-io', 'charset', 'check', 'check-identifier', 'checkboxes', 'checker', 'chef', 'chem', 'chemical', 'children', 'choice', 'choicescript', 'chord', 'chorus', 'chuck', 'chunk', 'ciexyz', 'circle', 'circle-jot', 'cirru', 'cisco', 'cisco-ios-config', 'citation', 'cite', 'citrine', 'cjam', 'cjson', 'clamp', 'clamping', 'class', 'class-constraint', 'class-constraints', 'class-declaration', 'class-definition', 'class-fns', 'class-instance', 'class-list', 'class-struct-block', 'class-type', 'class-type-definition', 'classcode', 'classes', 'classic', 'classicalb', 'classmethods', 'classobj', 'classtree', 'clause', 'clause-head-body', 'clauses', 'clear', 'clear-argument', 'cleared', 'clflushopt', 'click', 'client', 'client-server', 'clip', 'clipboard', 'clips', 'clmul', 'clock', 'clojure', 'cloned', 'close', 'closed', 'closing', 'closing-text', 'closure', 'clothes-body', 'cm', 'cmake', 'cmb', 'cmd', 'cnet', 'cns', 'cobject', 'cocoa', 'cocor', 'cod4mp', 'code', 'code-example', 'codeblock', 'codepoint', 'codimension', 'codstr', 'coffee', 'coffeescript', 'coffeescript-preview', 'coil', 'collection', 'collision', 'colon', 'colons', 'color', 'color-adjustment', 'coloring', 'colour', 'colour-correction', 'colour-interpolation', 'colour-name', 'colour-scheme', 'colspan', 'column', 'column-divider', 'column-specials', 'com', 'combinators', 'comboboxes', 'comma', 'comma-bar', 'comma-parenthesis', 'command', 'command-name', 'command-synopsis', 'commandline', 'commands', 'comment', 'comment-ish', 'comment-italic', 'commented-out', 'commit-command', 'commit-message', 'commodity', 'common', 'commonform', 'communications', 'community', 'commute', 'comnd', 'compare', 'compareOp', 'comparison', 'compile', 'compile-only', 'compiled', 'compiled-papyrus', 'compiler', 'compiler-directive', 'compiletime', 'compiling-and-loading', 'complement', 'complete', 'completed', 'complex', 'component', 'component-separator', 'component_instantiation', 'compositor', 'compound', 'compound-assignment', 'compress', 'computer', 'computercraft', 'concat', 'concatenated-arguments', 'concatenation', 'concatenator', 'concatination', 'concealed', 'concise', 'concrete', 'condition', 'conditional', 'conditional-directive', 'conditional-short', 'conditionals', 'conditions', 'conf', 'config', 'configuration', 'configure', 'confluence', 'conftype', 'conjunction', 'conky', 'connect', 'connection-state', 'connectivity', 'connstate', 'cons', 'consecutive-tags', 'considering', 'console', 'const', 'const-data', 'constant', 'constants', 'constrained', 'constraint', 'constraints', 'construct', 'constructor', 'constructor-list', 'constructs', 'consult', 'contacts', 'container', 'containers-raycast', 'contains', 'content', 'content-detective', 'contentSupplying', 'contentitem', 'context', 'context-free', 'context-signature', 'continuation', 'continuations', 'continue', 'continued', 'continuum', 'contol', 'contract', 'contracts', 'contrl', 'control', 'control-char', 'control-handlers', 'control-management', 'control-systems', 'control-transfer', 'controller', 'controlline', 'controls', 'contstant', 'conventional', 'conversion', 'convert-type', 'cookie', 'cool', 'coord1', 'coord2', 'coord3', 'coordinates', 'copy', 'copying', 'coq', 'core', 'core-parse', 'coreutils', 'correct', 'cos', 'counter', 'counters', 'cover', 'cplkg', 'cplusplus', 'cpm', 'cpp', 'cpp-include', 'cpp-type', 'cpp_type', 'cpu12', 'cql', 'cram', 'crc32', 'create', 'creation', 'critic', 'crl', 'crontab', 'crypto', 'crystal', 'cs', 'csharp', 'cshtml', 'csi', 'csjs', 'csound', 'csound-document', 'csound-score', 'cspm', 'css', 'csv', 'csx', 'ct', 'ctkey', 'ctor', 'ctxvar', 'ctxvarbracket', 'ctype', 'cubic-bezier', 'cucumber', 'cuda', 'cue-identifier', 'cue-timings', 'cuesheet', 'cup', 'cupsym', 'curl', 'curley', 'curly', 'currency', 'current', 'current-escape-char', 'curve', 'curve-2d', 'curve-fitting', 'curve-reference', 'curve-technique', 'custom', 'customevent', 'cut', 'cve-number', 'cvs', 'cw', 'cxx', 'cy-GB', 'cyan', 'cyc', 'cycle', 'cypher', 'cyrix', 'cython', 'd', 'da', 'daml', 'dana', 'danger', 'danmakufu', 'dark_aqua', 'dark_blue', 'dark_gray', 'dark_green', 'dark_purple', 'dark_red', 'dart', 'dartdoc', 'dash', 'dasm', 'data', 'data-acquisition', 'data-extension', 'data-integrity', 'data-item', 'data-step', 'data-transfer', 'database', 'database-name', 'datablock', 'datablocks', 'datafeed', 'datatype', 'datatypes', 'date', 'date-time', 'datetime', 'dav', 'day', 'dayofmonth', 'dayofweek', 'db', 'dba', 'dbx', 'dc', 'dcon', 'dd', 'ddp', 'de', 'dealii', 'deallocate', 'deb-control', 'debian', 'debris', 'debug', 'debug-specification', 'debugger', 'debugging', 'debugging-comment', 'dec', 'decal', 'decimal', 'decimal-arithmetic', 'decision', 'decl', 'declaration', 'declaration-expr', 'declaration-prod', 'declarations', 'declarator', 'declaratyion', 'declare', 'decode', 'decoration', 'decorator', 'decreasing', 'decrement', 'def', 'default', 'define', 'define-colour', 'defined', 'definedness', 'definingobj', 'definition', 'definitions', 'defintions', 'deflate', 'delay', 'delegated', 'delete', 'deleted', 'deletion', 'delimeter', 'delimited', 'delimiter', 'delimiter-too-long', 'delimiters', 'dense', 'deprecated', 'depricated', 'dereference', 'derived-type', 'deriving', 'desc', 'describe', 'description', 'descriptors', 'design', 'desktop', 'destination', 'destructor', 'destructured', 'determ', 'developer', 'device', 'device-io', 'dformat', 'dg', 'dhcp', 'diagnostic', 'dialogue', 'diamond', 'dict', 'dictionary', 'dictionaryname', 'diff', 'difference', 'different', 'diffuse-reflectivity', 'digdag', 'digit-width', 'dim', 'dimension', 'dip', 'dir', 'dir-target', 'dircolors', 'direct', 'direction', 'directive', 'directive-option', 'directives', 'directory', 'dirjs', 'dirtyblue', 'dirtygreen', 'disable', 'disable-markdown', 'disable-todo', 'discarded', 'discusson', 'disjunction', 'disk', 'disk-folder-file', 'dism', 'displacement', 'display', 'dissolve', 'dissolve-interpolation', 'distribution', 'diverging-function', 'divert', 'divide', 'divider', 'django', 'dl', 'dlv', 'dm', 'dmf', 'dml', 'do', 'dobody', 'doc', 'doc-comment', 'docRoot', 'dockerfile', 'dockerignore', 'doconce', 'docstring', 'doctest', 'doctree-option', 'doctype', 'document', 'documentation', 'documentroot', 'does', 'dogescript', 'doki', 'dollar', 'dollar-quote', 'dollar_variable', 'dom', 'domain', 'dontcollect', 'doors', 'dop', 'dot', 'dot-access', 'dotenv', 'dotfiles', 'dothandout', 'dotnet', 'dotnote', 'dots', 'dotted', 'dotted-circle', 'dotted-del', 'dotted-greater', 'dotted-tack-up', 'double', 'double-arrow', 'double-colon', 'double-dash', 'double-dash-not-allowed', 'double-dot', 'double-number-sign', 'double-percentage', 'double-qoute', 'double-quote', 'double-quoted', 'double-quoted-string', 'double-semicolon', 'double-slash', 'doublequote', 'doubleslash', 'dougle', 'down', 'download', 'downwards', 'doxyfile', 'doxygen', 'dragdrop', 'drawing', 'drive', 'droiuby', 'drop', 'drop-shadow', 'droplevel', 'drummode', 'drupal', 'dsl', 'dsv', 'dt', 'dtl', 'due', 'dummy', 'dummy-variable', 'dump', 'duration', 'dust', 'dust_Conditional', 'dust_end_section_tag', 'dust_filter', 'dust_partial', 'dust_partial_not_self_closing', 'dust_ref', 'dust_ref_name', 'dust_section_context', 'dust_section_name', 'dust_section_params', 'dust_self_closing_section_tag', 'dust_special', 'dust_start_section_tag', 'dustjs', 'dut', 'dwscript', 'dxl', 'dylan', 'dynamic', 'dyndoc', 'dyon', 'e', 'e3globals', 'each', 'eachin', 'earl-grey', 'ebnf', 'ebuild', 'echo', 'eclass', 'ecmascript', 'eco', 'ecr', 'ect', 'ect2', 'ect3', 'ect4', 'edasm', 'edge', 'edit-manager', 'editfields', 'editors', 'ee', 'eex', 'effect', 'effectgroup', 'effective_routine_body', 'effects', 'eiffel', 'eight', 'eio', 'eiz', 'ejectors', 'el', 'elasticsearch', 'elasticsearch2', 'element', 'elements', 'elemnt', 'elif', 'elipse', 'elision', 'elixir', 'ellipsis', 'elm', 'elmx', 'else', 'else-condition', 'else-if', 'elseif', 'elseif-condition', 'elsewhere', 'eltype', 'elvis', 'em', 'email', 'embed', 'embed-diversion', 'embedded', 'embedded-c', 'embedded-ruby', 'embedded2', 'embeded', 'ember', 'emberscript', 'emblem', 'embperl', 'emissive-colour', 'eml', 'emlist', 'emoji', 'emojicode', 'emp', 'emph', 'emphasis', 'empty', 'empty-dictionary', 'empty-list', 'empty-parenthesis', 'empty-start', 'empty-string', 'empty-tag', 'empty-tuple', 'empty-typing-pair', 'empty_gif', 'emptyelement', 'en', 'en-Scouse', 'en-au', 'en-lol', 'en-old', 'en-pirate', 'enable', 'enc', 'enchant', 'enclose', 'encode', 'encoding', 'encryption', 'end', 'end-block-data', 'end-definition', 'end-document', 'end-enum', 'end-footnote', 'end-of-line', 'end-statement', 'end-value', 'endassociate', 'endcode', 'enddo', 'endfile', 'endforall', 'endfunction', 'endian', 'endianness', 'endif', 'endinfo', 'ending', 'ending-space', 'endinterface', 'endlocaltable', 'endmodule', 'endobject', 'endobjecttable', 'endparamtable', 'endprogram', 'endproperty', 'endpropertygroup', 'endpropertygrouptable', 'endpropertytable', 'endselect', 'endstate', 'endstatetable', 'endstruct', 'endstructtable', 'endsubmodule', 'endsubroutine', 'endtimeblock', 'endtype', 'enduserflagsref', 'endvariable', 'endvariabletable', 'endwhere', 'engine', 'enterprise', 'entity', 'entity-creation-and-abolishing', 'entity_instantiation', 'entry', 'entry-definition', 'entry-key', 'entry-type', 'entrypoint', 'enum', 'enum-block', 'enum-declaration', 'enumeration', 'enumerator', 'enumerator-specification', 'env', 'environment', 'environment-variable', 'eo', 'eof', 'epatch', 'eq', 'eqn', 'eqnarray', 'equal', 'equal-or-greater', 'equal-or-less', 'equalexpr', 'equality', 'equals', 'equals-sign', 'equation', 'equation-label', 'erb', 'ereg', 'erlang', 'error', 'error-control', 'errorfunc', 'errorstop', 'es', 'es6', 'es6import', 'esc', 'escape', 'escape-char', 'escape-code', 'escape-sequence', 'escape-unicode', 'escaped', 'escapes', 'escript', 'eso-lua', 'eso-txt', 'essence', 'et', 'eth', 'ethaddr', 'etml', 'etpl', 'eudoc', 'euler', 'euphoria', 'european', 'evaled', 'evaluable', 'evaluation', 'even-tab', 'event', 'event-call', 'event-handler', 'event-handling', 'event-schedulling', 'eventType', 'eventb', 'eventend', 'events', 'evnd', 'exactly', 'example', 'exampleText', 'examples', 'exceeding-sections', 'excel-link', 'exception', 'exceptions', 'exclaimation-point', 'exclamation', 'exec', 'exec-command', 'execution-context', 'exif', 'existential', 'exit', 'exp', 'expand-register', 'expanded', 'expansion', 'expected-array-separator', 'expected-dictionary-separator', 'expected-extends', 'expected-implements', 'expected-range-separator', 'experimental', 'expires', 'expl3', 'explosion', 'exponent', 'exponential', 'export', 'exports', 'expr', 'expression', 'expression-separator', 'expression-seperator', 'expressions', 'expressions-and-types', 'exprwrap', 'ext', 'extempore', 'extend', 'extended', 'extends', 'extension', 'extension-specification', 'extensions', 'extern', 'extern-block', 'external', 'external-call', 'external-signature', 'extersk', 'extglob', 'extra', 'extra-characters', 'extra-equals-sign', 'extracted', 'extras', 'extrassk', 'exxample', 'eztpl', 'f', 'f5networks', 'fa', 'face', 'fact', 'factor', 'factorial', 'fadeawayheight', 'fadeawaywidth', 'fail', 'fakeroot', 'fallback', 'fallout4', 'false', 'fandoc', 'fann', 'fantom', 'fastcgi', 'fbaccidental', 'fbfigure', 'fbgroupclose', 'fbgroupopen', 'fbp', 'fctn', 'fe', 'feature', 'features', 'feedrate', 'fenced', 'fftwfn', 'fhem', 'fi', 'field', 'field-assignment', 'field-completions', 'field-id', 'field-level-comment', 'field-name', 'field-tag', 'fields', 'figbassmode', 'figure', 'figuregroup', 'filder-design-hdl-coder', 'file', 'file-i-o', 'file-io', 'file-name', 'file-object', 'file-path', 'fileinfo', 'filename', 'filepath', 'filetest', 'filter', 'filter-pipe', 'filteredtranscludeblock', 'filters', 'final', 'final-procedure', 'finally', 'financial', 'financial-derivatives', 'find', 'find-in-files', 'find-m', 'finder', 'finish', 'finn', 'fire', 'firebug', 'first', 'first-class', 'first-line', 'fish', 'fitnesse', 'five', 'fix_this_later', 'fixed', 'fixed-income', 'fixed-point', 'fixme', 'fl', 'flag', 'flag-control', 'flags', 'flash', 'flatbuffers', 'flex-config', 'fload', 'float', 'float-exponent', 'float_exp', 'floating-point', 'floating_point', 'floor', 'flow', 'flow-control', 'flowcontrol', 'flows', 'flowtype', 'flush', 'fma', 'fma4', 'fmod', 'fn', 'fold', 'folder', 'folder-actions', 'following', 'font', 'font-cache', 'font-face', 'font-name', 'font-size', 'fontface', 'fontforge', 'foobar', 'footer', 'footnote', 'for', 'for-in-loop', 'for-loop', 'for-quantity', 'forall', 'force', 'foreach', 'foreign', 'forever', 'forge-config', 'forin', 'form', 'form-feed', 'formal', 'format', 'format-register', 'format-verb', 'formatted', 'formatter', 'formatting', 'forth', 'fortran', 'forward', 'foundation', 'fountain', 'four', 'fourd-command', 'fourd-constant', 'fourd-constant-hex', 'fourd-constant-number', 'fourd-constant-string', 'fourd-control-begin', 'fourd-control-end', 'fourd-declaration', 'fourd-declaration-array', 'fourd-local-variable', 'fourd-parameter', 'fourd-table', 'fourd-tag', 'fourd-variable', 'fpm', 'fpu', 'fpu_x87', 'fr', 'fragment', 'frame', 'frames', 'frametitle', 'framexml', 'free', 'free-form', 'freebasic', 'freefem', 'freespace2', 'from', 'from-file', 'front-matter', 'fs', 'fs2', 'fsc', 'fsgsbase', 'fsharp', 'fsi', 'fsl', 'fsm', 'fsp', 'fsx', 'fth', 'ftl', 'ftl20n', 'full-line', 'full-stop', 'fun', 'funarg', 'func-tag', 'func_call', 'funchand', 'function', 'function-arity', 'function-attribute', 'function-call', 'function-definition', 'function-literal', 'function-parameter', 'function-recursive', 'function-return', 'function-type', 'functionDeclaration', 'functionDefinition', 'function_definition', 'function_prototype', 'functional_test', 'functionend', 'functions', 'functionstart', 'fundimental', 'funk', 'funtion-definition', 'fus', 'future', 'futures', 'fuzzy-logic', 'fx', 'fx-foliage-replicator', 'fx-light', 'fx-shape-replicator', 'fx-sun-light', 'g', 'g-code', 'ga', 'gain', 'galaxy', 'gallery', 'game-base', 'game-connection', 'game-server', 'gamebusk', 'gamescript', 'gams', 'gams-lst', 'gap', 'garch', 'gather', 'gcode', 'gdb', 'gdscript', 'gdx', 'ge', 'geant4-macro', 'geck', 'geck-keyword', 'general', 'general-purpose', 'generate', 'generator', 'generic', 'generic-config', 'generic-spec', 'generic-type', 'generic_list', 'genericcall', 'generics', 'genetic-algorithms', 'geo', 'geometric', 'geometry', 'geometry-adjustment', 'get', 'getproperty', 'getsec', 'getset', 'getter', 'gettext', 'getword', 'gfm', 'gfm-todotxt', 'gfx', 'gh-number', 'gherkin', 'gisdk', 'git', 'git-attributes', 'git-commit', 'git-config', 'git-rebase', 'gitignore', 'given', 'gj', 'gl', 'glob', 'global', 'global-functions', 'globals', 'globalsection', 'glsl', 'glue', 'glyph_class_name', 'glyphname-value', 'gml', 'gmp', 'gmsh', 'gmx', 'gn', 'gnu', 'gnuplot', 'go', 'goal', 'goatee', 'godmode', 'gohtml', 'gold', 'golo', 'google', 'gosub', 'gotemplate', 'goto', 'goto-label', 'gpd', 'gpd_note', 'gpp', 'grace', 'grade-down', 'grade-up', 'gradient', 'gradle', 'grails', 'grammar', 'grammar-rule', 'grammar_production', 'grap', 'grapahql', 'graph', 'graphics', 'graphql', 'grave-accent', 'gray', 'greater', 'greater-equal', 'greater-or-equal', 'greek', 'greek-letter', 'green', 'gremlin', 'grey', 'grg', 'grid-table', 'gridlists', 'grog', 'groovy', 'groovy-properties', 'group', 'group-level-comment', 'group-name', 'group-number', 'group-reference', 'group-title', 'group1', 'group10', 'group11', 'group2', 'group3', 'group4', 'group5', 'group6', 'group7', 'group8', 'group9', 'groupend', 'groupflag', 'grouping-statement', 'groupname', 'groupstart', 'growl', 'grr', 'gs', 'gsc', 'gsp', 'gt', 'guard', 'guards', 'gui', 'gui-bitmap-ctrl', 'gui-button-base-ctrl', 'gui-canvas', 'gui-control', 'gui-filter-ctrl', 'gui-frameset-ctrl', 'gui-menu-bar', 'gui-message-vector-ctrl', 'gui-ml-text-ctrl', 'gui-popup-menu-ctrl', 'gui-scroll-ctrl', 'gui-slider-ctrl', 'gui-text-ctrl', 'gui-text-edit-ctrl', 'gui-text-list-ctrl', 'guid', 'guillemot', 'guis', 'gzip', 'gzip_static', 'h', 'h1', 'hack', 'hackfragment', 'haddock', 'hairpin', 'ham', 'haml', 'hamlbars', 'hamlc', 'hamlet', 'hamlpy', 'handlebar', 'handlebars', 'handler', 'hanging-paragraph', 'haproxy-config', 'harbou', 'harbour', 'hard-break', 'hardlinebreaks', 'hash', 'hash-tick', 'hashbang', 'hashicorp', 'hashkey', 'haskell', 'haxe', 'hbs', 'hcl', 'hdl', 'hdr', 'he', 'header', 'header-continuation', 'header-value', 'headername', 'headers', 'heading', 'heading-0', 'heading-1', 'heading-2', 'heading-3', 'heading-4', 'heading-5', 'heading-6', 'height', 'helen', 'help', 'helper', 'helpers', 'heredoc', 'heredoc-token', 'herestring', 'heritage', 'hex', 'hex-ascii', 'hex-byte', 'hex-literal', 'hex-old', 'hex-string', 'hex-value', 'hex8', 'hexadecimal', 'hexidecimal', 'hexprefix', 'hg-commit', 'hgignore', 'hi', 'hidden', 'hide', 'high-minus', 'highlight-end', 'highlight-group', 'highlight-start', 'hint', 'history', 'hive', 'hive-name', 'hjson', 'hl7', 'hlsl', 'hn', 'hoa', 'hoc', 'hocharacter', 'hocomment', 'hocon', 'hoconstant', 'hocontinuation', 'hocontrol', 'hombrew-formula', 'homebrew', 'homematic', 'hook', 'hoon', 'horizontal-blending', 'horizontal-packed-arithmetic', 'horizontal-rule', 'hostname', 'hosts', 'hour', 'hours', 'hps', 'hql', 'hr', 'hrm', 'hs', 'hsc2hs', 'ht', 'htaccess', 'htl', 'html', 'html_entity', 'htmlbars', 'http', 'hu', 'hungary', 'hxml', 'hy', 'hydrant', 'hydrogen', 'hyperbolic', 'hyperlink', 'hyphen', 'hyphenation', 'hyphenation-char', 'i', 'i-beam', 'i18n', 'iRev', 'ice', 'icinga2', 'icmc', 'icmptype', 'icmpv6type', 'icmpxtype', 'iconv', 'id', 'id-type', 'id-with-protocol', 'idd', 'ideal', 'identical', 'identifer', 'identified', 'identifier', 'identifier-type', 'identifiers-and-DTDs', 'identity', 'idf', 'idl', 'idris', 'ieee', 'if', 'if-block', 'if-branch', 'if-condition', 'if-else', 'if-then', 'ifacespec', 'ifdef', 'ifname', 'ifndef', 'ignore', 'ignore-eol', 'ignore-errors', 'ignorebii', 'ignored', 'ignored-binding', 'ignoring', 'iisfunc', 'ijk', 'ilasm', 'illagal', 'illeagal', 'illegal', 'illumination-model', 'image', 'image-acquisition', 'image-alignment', 'image-option', 'image-processing', 'images', 'imap', 'imba', 'imfchan', 'img', 'immediate', 'immediately-evaluated', 'immutable', 'impex', 'implementation', 'implementation-defined-hooks', 'implemented', 'implements', 'implicit', 'import', 'import-all', 'importall', 'important', 'in', 'in-block', 'in-module', 'in-out', 'inappropriate', 'include', 'include-statement', 'includefile', 'incomplete', 'incomplete-variable-assignment', 'inconsistent', 'increment', 'increment-decrement', 'indent', 'indented', 'indented-paragraph', 'indepimage', 'index', 'index-seperator', 'indexed', 'indexer', 'indexes', 'indicator', 'indices', 'indirect', 'indirection', 'individual-enum-definition', 'individual-rpc-call', 'inet', 'inetprototype', 'inferred', 'infes', 'infinity', 'infix', 'info', 'inform', 'inform6', 'inform7', 'infotype', 'ingore-eol', 'inherit', 'inheritDoc', 'inheritance', 'inherited', 'inherited-class', 'inherited-struct', 'inherits', 'ini', 'init', 'initial-lowercase', 'initial-uppercase', 'initial-value', 'initialization', 'initialize', 'initializer-list', 'ink', 'inline', 'inline-data', 'inlineConditionalBranchSeparator', 'inlineConditionalClause', 'inlineConditionalEnd', 'inlineConditionalStart', 'inlineLogicEnd', 'inlineLogicStart', 'inlineSequenceEnd', 'inlineSequenceSeparator', 'inlineSequenceStart', 'inlineSequenceTypeChar', 'inlineblock', 'inlinecode', 'inlinecomment', 'inlinetag', 'inner', 'inner-class', 'inno', 'ino', 'inout', 'input', 'inquire', 'inserted', 'insertion', 'insertion-and-extraction', 'inside', 'install', 'instance', 'instancemethods', 'instanceof', 'instances', 'instantiation', 'instruction', 'instruction-pointer', 'instructions', 'instrument', 'instrument-block', 'instrument-control', 'instrument-declaration', 'int', 'int32', 'int64', 'integer', 'integer-float', 'intel', 'intel-hex', 'intent', 'intepreted', 'interaction', 'interbase', 'interface', 'interface-block', 'interface-or-protocol', 'interfaces', 'interior-instance', 'interiors', 'interlink', 'internal', 'internet', 'interpolate-argument', 'interpolate-string', 'interpolate-variable', 'interpolated', 'interpolation', 'interrupt', 'intersection', 'interval', 'intervalOrList', 'intl', 'intrinsic', 'intuicio4', 'invalid', 'invalid-character', 'invalid-character-escape', 'invalid-inequality', 'invalid-quote', 'invalid-variable-name', 'invariant', 'invocation', 'invoke', 'invokee', 'io', 'ior', 'iota', 'ip', 'ip-port', 'ip6', 'ipkg', 'ipsec', 'ipv4', 'ipv6', 'ipynb', 'irct', 'irule', 'is', 'isa', 'isc', 'iscexport', 'isclass', 'isml', 'issue', 'it', 'italic', 'italic-text', 'item', 'item-access', 'itemlevel', 'items', 'iteration', 'itunes', 'ivar', 'ja', 'jack', 'jade', 'jakefile', 'jasmin', 'java', 'java-properties', 'java-props', 'javadoc', 'javascript', 'jbeam', 'jekyll', 'jflex', 'jibo-rule', 'jinja', 'jison', 'jisonlex', 'jmp', 'joint', 'joker', 'jolie', 'jot', 'journaling', 'jpl', 'jq', 'jquery', 'js', 'js-label', 'jsdoc', 'jsduck', 'jsim', 'json', 'json5', 'jsoniq', 'jsonnet', 'jsont', 'jsp', 'jsx', 'julia', 'julius', 'jump', 'juniper', 'juniper-junos-config', 'junit-test-report', 'junos', 'juttle', 'jv', 'jxa', 'k', 'kag', 'kagex', 'kb', 'kbd', 'kconfig', 'kerboscript', 'kernel', 'kevs', 'kevscript', 'kewyword', 'key', 'key-assignment', 'key-letter', 'key-pair', 'key-path', 'key-value', 'keyboard', 'keyframe', 'keyframes', 'keygroup', 'keyname', 'keyspace', 'keyspace-name', 'keyvalue', 'keyword', 'keyword-parameter', 'keyword1', 'keyword2', 'keyword3', 'keyword4', 'keyword5', 'keyword6', 'keyword7', 'keyword8', 'keyword_arrays', 'keyword_objects', 'keyword_roots', 'keyword_string', 'keywords', 'keywork', 'kickstart', 'kind', 'kmd', 'kn', 'knitr', 'knockout', 'knot', 'ko', 'ko-virtual', 'kos', 'kotlin', 'krl', 'ksp-cfg', 'kspcfg', 'kurumin', 'kv', 'kxi', 'kxigauge', 'l', 'l20n', 'l4proto', 'label', 'label-expression', 'labeled', 'labeled-parameter', 'labelled-thing', 'lagda', 'lambda', 'lambda-function', 'lammps', 'langref', 'language', 'language-range', 'languagebabel', 'langversion', 'largesk', 'lasso', 'last', 'last-paren-match', 'latex', 'latex2', 'latino', 'latte', 'launch', 'layout', 'layoutbii', 'lbsearch', 'lc', 'lc-3', 'lcb', 'ldap', 'ldif', 'le', 'leader-char', 'leading', 'leading-space', 'leading-tabs', 'leaf', 'lean', 'ledger', 'left', 'left-margin', 'leftshift', 'lefttoright', 'legacy', 'legacy-setting', 'lemon', 'len', 'length', 'leopard', 'less', 'less-equal', 'less-or-equal', 'let', 'letter', 'level', 'level-of-detail', 'level1', 'level2', 'level3', 'level4', 'level5', 'level6', 'levels', 'lex', 'lexc', 'lexical', 'lf-in-string', 'lhs', 'li', 'lib', 'libfile', 'library', 'libs', 'libxml', 'lid', 'lifetime', 'ligature', 'light', 'light_purple', 'lighting', 'lightning', 'lilypond', 'lilypond-drummode', 'lilypond-figbassmode', 'lilypond-figuregroup', 'lilypond-internals', 'lilypond-lyricsmode', 'lilypond-markupmode', 'lilypond-notedrum', 'lilypond-notemode', 'lilypond-notemode-explicit', 'lilypond-notenames', 'lilypond-schememode', 'limit_zone', 'line-block', 'line-break', 'line-continuation', 'line-cue-setting', 'line-statement', 'line-too-long', 'linebreak', 'linenumber', 'link', 'link-label', 'link-text', 'link-url', 'linkage', 'linkage-type', 'linkedin', 'linkedsockets', 'linkplain', 'linkplain-label', 'linq', 'linuxcncgcode', 'liquid', 'liquidhaskell', 'liquidsoap', 'lisp', 'lisp-repl', 'list', 'list-done', 'list-separator', 'list-style-type', 'list-today', 'list_item', 'listing', 'listnum', 'listvalues', 'litaco', 'litcoffee', 'literal', 'literal-string', 'literate', 'litword', 'livecodescript', 'livescript', 'livescriptscript', 'll', 'llvm', 'load-constants', 'load-hint', 'loader', 'local', 'local-variables', 'localhost', 'localizable', 'localized', 'localname', 'locals', 'localtable', 'location', 'lock', 'log', 'log-debug', 'log-error', 'log-failed', 'log-info', 'log-patch', 'log-success', 'log-verbose', 'log-warning', 'logarithm', 'logging', 'logic', 'logicBegin', 'logical', 'logical-expression', 'logicblox', 'logicode', 'logo', 'logstash', 'logtalk', 'lol', 'long', 'look-ahead', 'look-behind', 'lookahead', 'lookaround', 'lookbehind', 'loop', 'loop-control', 'low-high', 'lowercase', 'lowercase_character_not_allowed_here', 'lozenge', 'lparen', 'lsg', 'lsl', 'lst', 'lst-cpu12', 'lstdo', 'lt', 'lt-gt', 'lterat', 'lu', 'lua', 'lucee', 'lucius', 'lury', 'lv', 'lyricsmode', 'm', 'm4', 'm4sh', 'm65816', 'm68k', 'mac-classic', 'mac-fsaa', 'machine', 'machineclause', 'macro', 'macro-usage', 'macro11', 'macrocallblock', 'macrocallinline', 'madoko', 'magenta', 'magic', 'magik', 'mail', 'mailer', 'mailto', 'main', 'makefile', 'makefile2', 'mako', 'mamba', 'man', 'mantissa', 'manualmelisma', 'map', 'map-library', 'map-name', 'mapfile', 'mapkey', 'mapping', 'mapping-type', 'maprange', 'marasm', 'margin', 'marginpar', 'mark', 'mark-input', 'markdown', 'marker', 'marko', 'marko-attribute', 'marko-tag', 'markup', 'markupmode', 'mas2j', 'mask', 'mason', 'mat', 'mata', 'match', 'match-bind', 'match-branch', 'match-condition', 'match-definition', 'match-exception', 'match-option', 'match-pattern', 'material', 'material-library', 'material-name', 'math', 'math-symbol', 'math_complex', 'math_real', 'mathematic', 'mathematica', 'mathematical', 'mathematical-symbols', 'mathematics', 'mathjax', 'mathml', 'matlab', 'matrix', 'maude', 'maven', 'max', 'max-angle', 'max-distance', 'max-length', 'maxscript', 'maybe', 'mb', 'mbstring', 'mc', 'mcc', 'mccolor', 'mch', 'mcn', 'mcode', 'mcq', 'mcr', 'mcrypt', 'mcs', 'md', 'mdash', 'mdoc', 'mdx', 'me', 'measure', 'media', 'media-feature', 'media-property', 'media-type', 'mediawiki', 'mei', 'mel', 'memaddress', 'member', 'member-function-attribute', 'member-of', 'membership', 'memcache', 'memcached', 'memoir', 'memoir-alltt', 'memoir-fbox', 'memoir-verbatim', 'memory', 'memory-management', 'memory-protection', 'memos', 'menhir', 'mention', 'menu', 'mercury', 'merge-group', 'merge-key', 'merlin', 'mesgTrigger', 'mesgType', 'message', 'message-declaration', 'message-forwarding-handler', 'message-sending', 'message-vector', 'messages', 'meta', 'meta-conditional', 'meta-data', 'meta-file', 'meta-info', 'metaclass', 'metacommand', 'metadata', 'metakey', 'metamodel', 'metapost', 'metascript', 'meteor', 'method', 'method-call', 'method-definition', 'method-modification', 'method-mofification', 'method-parameter', 'method-parameters', 'method-restriction', 'methodcalls', 'methods', 'metrics', 'mhash', 'microsites', 'microsoft-dynamics', 'middle', 'midi_processing', 'migration', 'mime', 'min', 'minelua', 'minetweaker', 'minitemplate', 'minitest', 'minus', 'minute', 'mips', 'mirah', 'misc', 'miscellaneous', 'mismatched', 'missing', 'missing-asterisk', 'missing-inheritance', 'missing-parameters', 'missing-section-begin', 'missingend', 'mission-area', 'mixin', 'mixin-name', 'mjml', 'ml', 'mlab', 'mls', 'mm', 'mml', 'mmx', 'mmx_instructions', 'mn', 'mnemonic', 'mobile-messaging', 'mochi', 'mod', 'mod-r', 'mod_perl', 'mod_perl_1', 'modblock', 'modbus', 'mode', 'model', 'model-based-calibration', 'model-predictive-control', 'modelica', 'modelicascript', 'modeline', 'models', 'modern', 'modified', 'modifier', 'modifiers', 'modify', 'modify-range', 'modifytime', 'modl', 'modr', 'modula-2', 'module', 'module-alias', 'module-binding', 'module-definition', 'module-expression', 'module-function', 'module-reference', 'module-rename', 'module-sum', 'module-type', 'module-type-definition', 'modules', 'modulo', 'modx', 'mojolicious', 'mojom', 'moment', 'mond', 'money', 'mongo', 'mongodb', 'monicelli', 'monitor', 'monkberry', 'monkey', 'monospace', 'monospaced', 'monte', 'month', 'moon', 'moonscript', 'moos', 'moose', 'moosecpp', 'motion', 'mouse', 'mov', 'movement', 'movie', 'movie-file', 'mozu', 'mpw', 'mpx', 'mqsc', 'ms', 'mscgen', 'mscript', 'msg', 'msgctxt', 'msgenny', 'msgid', 'msgstr', 'mson', 'mson-block', 'mss', 'mta', 'mtl', 'mucow', 'mult', 'multi', 'multi-line', 'multi-symbol', 'multi-threading', 'multiclet', 'multids-file', 'multiline', 'multiline-cell', 'multiline-text-reference', 'multiline-tiddler-title', 'multimethod', 'multipart', 'multiplication', 'multiplicative', 'multiply', 'multiverse', 'mumps', 'mundosk', 'music', 'must_be', 'mustache', 'mut', 'mutable', 'mutator', 'mx', 'mxml', 'mydsl1', 'mylanguage', 'mysql', 'mysqli', 'mysqlnd-memcache', 'mysqlnd-ms', 'mysqlnd-qc', 'mysqlnd-uh', 'mzn', 'nabla', 'nagios', 'name', 'name-list', 'name-of-parameter', 'named', 'named-char', 'named-key', 'named-tuple', 'nameless-typed', 'namelist', 'names', 'namespace', 'namespace-block', 'namespace-definition', 'namespace-language', 'namespace-prefix', 'namespace-reference', 'namespace-statement', 'namespaces', 'nan', 'nand', 'nant', 'nant-build', 'narration', 'nas', 'nasal', 'nasl', 'nasm', 'nastran', 'nat', 'native', 'nativeint', 'natural', 'navigation', 'nbtkey', 'ncf', 'ncl', 'ndash', 'ne', 'nearley', 'neg-ratio', 'negatable', 'negate', 'negated', 'negation', 'negative', 'negative-look-ahead', 'negative-look-behind', 'negativity', 'nesc', 'nessuskb', 'nested', 'nested_braces', 'nested_brackets', 'nested_ltgt', 'nested_parens', 'nesty', 'net', 'net-object', 'netbios', 'network', 'network-value', 'networking', 'neural-network', 'new', 'new-line', 'new-object', 'newline', 'newline-spacing', 'newlinetext', 'newlisp', 'newobject', 'nez', 'nft', 'ngdoc', 'nginx', 'nickname', 'nil', 'nim', 'nine', 'ninja', 'ninjaforce', 'nit', 'nitro', 'nix', 'nl', 'nlf', 'nm', 'nm7', 'no', 'no-capture', 'no-completions', 'no-content', 'no-default', 'no-indent', 'no-leading-digits', 'no-trailing-digits', 'no-validate-params', 'node', 'nogc', 'noindent', 'nokia-sros-config', 'non', 'non-capturing', 'non-immediate', 'non-null-typehinted', 'non-standard', 'non-terminal', 'nondir-target', 'none', 'none-parameter', 'nonlocal', 'nonterminal', 'noon', 'noop', 'nop', 'noparams', 'nor', 'normal', 'normal_numeric', 'normal_objects', 'normal_text', 'normalised', 'not', 'not-a-number', 'not-equal', 'not-identical', 'notation', 'note', 'notechord', 'notemode', 'notequal', 'notequalexpr', 'notes', 'notidentical', 'notification', 'nowdoc', 'noweb', 'nrtdrv', 'nsapi', 'nscript', 'nse', 'nsis', 'nsl', 'ntriples', 'nul', 'null', 'nullify', 'nullological', 'nulltype', 'num', 'number', 'number-sign', 'number-sign-equals', 'numbered', 'numberic', 'numbers', 'numbersign', 'numeric', 'numeric_std', 'numerical', 'nunjucks', 'nut', 'nvatom', 'nxc', 'o', 'obj', 'objaggregation', 'objc', 'objcpp', 'objdump', 'object', 'object-comments', 'object-definition', 'object-level-comment', 'object-name', 'objects', 'objectset', 'objecttable', 'objectvalues', 'objj', 'obsolete', 'ocaml', 'ocamllex', 'occam', 'oci8', 'ocmal', 'oct', 'octal', 'octave', 'octave-change', 'octave-shift', 'octet', 'octo', 'octobercms', 'octothorpe', 'odd-tab', 'odedsl', 'ods', 'of', 'off', 'offset', 'ofx', 'ogre', 'ok', 'ol', 'old', 'old-style', 'omap', 'omitted', 'on-background', 'on-error', 'once', 'one', 'one-sixth-em', 'one-twelfth-em', 'oniguruma', 'oniguruma-comment', 'only', 'only-in', 'onoff', 'ooc', 'oot', 'op-domain', 'op-range', 'opa', 'opaque', 'opc', 'opcache', 'opcode', 'opcode-argument-types', 'opcode-declaration', 'opcode-definition', 'opcode-details', 'open', 'open-gl', 'openal', 'openbinding', 'opencl', 'opendss', 'opening', 'opening-text', 'openmp', 'openssl', 'opentype', 'operand', 'operands', 'operation', 'operator', 'operator2', 'operator3', 'operators', 'opmask', 'opmaskregs', 'optical-density', 'optimization', 'option', 'option-description', 'option-toggle', 'optional', 'optional-parameter', 'optional-parameter-assignment', 'optionals', 'optionname', 'options', 'optiontype', 'or', 'oracle', 'orbbasic', 'orcam', 'orchestra', 'order', 'ordered', 'ordered-block', 'ordinal', 'organized', 'orgtype', 'origin', 'osiris', 'other', 'other-inherited-class', 'other_buildins', 'other_keywords', 'others', 'otherwise', 'otherwise-expression', 'out', 'outer', 'output', 'overload', 'override', 'owner', 'ownership', 'oz', 'p', 'p4', 'p5', 'p8', 'pa', 'package', 'package-definition', 'package_body', 'packages', 'packed', 'packed-arithmetic', 'packed-blending', 'packed-comparison', 'packed-conversion', 'packed-floating-point', 'packed-integer', 'packed-math', 'packed-mov', 'packed-other', 'packed-shift', 'packed-shuffle', 'packed-test', 'padlock', 'page', 'page-props', 'pagebreak', 'pair', 'pair-programming', 'paket', 'pandoc', 'papyrus', 'papyrus-assembly', 'paragraph', 'parallel', 'param', 'param-list', 'paramater', 'paramerised-type', 'parameter', 'parameter-entity', 'parameter-space', 'parameterless', 'parameters', 'paramless', 'params', 'paramtable', 'paramter', 'paren', 'paren-group', 'parens', 'parent', 'parent-reference', 'parent-selector', 'parent-selector-suffix', 'parenthases', 'parentheses', 'parenthesis', 'parenthetical', 'parenthetical_list', 'parenthetical_pair', 'parfor', 'parfor-quantity', 'parse', 'parsed', 'parser', 'parser-function', 'parser-token', 'parser3', 'part', 'partial', 'particle', 'pascal', 'pass', 'pass-through', 'passive', 'passthrough', 'password', 'password-hash', 'patch', 'path', 'path-camera', 'path-pattern', 'pathoperation', 'paths', 'pathspec', 'patientId', 'pattern', 'pattern-argument', 'pattern-binding', 'pattern-definition', 'pattern-match', 'pattern-offset', 'patterns', 'pause', 'payee', 'payload', 'pbo', 'pbtxt', 'pcdata', 'pcntl', 'pdd', 'pddl', 'ped', 'pegcoffee', 'pegjs', 'pending', 'percentage', 'percentage-sign', 'percussionnote', 'period', 'perl', 'perl-section', 'perl6', 'perl6fe', 'perlfe', 'perlt6e', 'perm', 'permutations', 'personalization', 'pervasive', 'pf', 'pflotran', 'pfm', 'pfx', 'pgn', 'pgsql', 'phone', 'phone-number', 'phonix', 'php', 'php-code-in-comment', 'php_apache', 'php_dom', 'php_ftp', 'php_imap', 'php_mssql', 'php_odbc', 'php_pcre', 'php_spl', 'php_zip', 'phpdoc', 'phrasemodifiers', 'phraslur', 'physical-zone', 'physics', 'pi', 'pic', 'pick', 'pickup', 'picture', 'pig', 'pillar', 'pipe', 'pipe-sign', 'pipeline', 'piratesk', 'pitch', 'pixie', 'pkgbuild', 'pl', 'placeholder', 'placeholder-parts', 'plain', 'plainsimple-emphasize', 'plainsimple-heading', 'plainsimple-number', 'plantuml', 'player', 'playerversion', 'pld_modeling', 'please-build', 'please-build-defs', 'plist', 'plsql', 'plugin', 'plus', 'plztarget', 'pmc', 'pml', 'pmlPhysics-arrangecharacter', 'pmlPhysics-emphasisequote', 'pmlPhysics-graphic', 'pmlPhysics-header', 'pmlPhysics-htmlencoded', 'pmlPhysics-links', 'pmlPhysics-listtable', 'pmlPhysics-physicalquantity', 'pmlPhysics-relationships', 'pmlPhysics-slides', 'pmlPhysics-slidestacks', 'pmlPhysics-speech', 'pmlPhysics-structure', 'pnt', 'po', 'pod', 'poe', 'pogoscript', 'point', 'point-size', 'pointer', 'pointer-arith', 'pointer-following', 'points', 'polarcoord', 'policiesbii', 'policy', 'polydelim', 'polygonal', 'polymer', 'polymorphic', 'polymorphic-variant', 'polynomial-degree', 'polysep', 'pony', 'port', 'port_list', 'pos-ratio', 'position-cue-setting', 'positional', 'positive', 'posix', 'posix-reserved', 'post-match', 'postblit', 'postcss', 'postfix', 'postpone', 'postscript', 'potigol', 'potion', 'pound', 'pound-sign', 'povray', 'power', 'power_set', 'powershell', 'pp', 'ppc', 'ppcasm', 'ppd', 'praat', 'pragma', 'pragma-all-once', 'pragma-mark', 'pragma-message', 'pragma-newline-spacing', 'pragma-newline-spacing-value', 'pragma-once', 'pragma-stg', 'pragma-stg-value', 'pre', 'pre-defined', 'pre-match', 'preamble', 'prec', 'precedence', 'precipitation', 'precision', 'precision-point', 'pred', 'predefined', 'predicate', 'prefetch', 'prefetchwt', 'prefix', 'prefixed-uri', 'prefixes', 'preinst', 'prelude', 'prepare', 'prepocessor', 'preposition', 'prepositional', 'preprocessor', 'prerequisites', 'preset', 'preview', 'previous', 'prg', 'primary', 'primitive', 'primitive-datatypes', 'primitive-field', 'print', 'print-argument', 'priority', 'prism', 'private', 'privileged', 'pro', 'probe', 'proc', 'procedure', 'procedure_definition', 'procedure_prototype', 'process', 'process-id', 'process-substitution', 'processes', 'processing', 'proctitle', 'production', 'profile', 'profiling', 'program', 'program-block', 'program-name', 'progressbars', 'proguard', 'project', 'projectile', 'prolog', 'prolog-flags', 'prologue', 'promoted', 'prompt', 'prompt-prefix', 'prop', 'properties', 'properties_literal', 'property', 'property-flag', 'property-list', 'property-name', 'property-value', 'property-with-attributes', 'propertydef', 'propertyend', 'propertygroup', 'propertygrouptable', 'propertyset', 'propertytable', 'proposition', 'protection', 'protections', 'proto', 'protobuf', 'protobufs', 'protocol', 'protocol-specification', 'prototype', 'provision', 'proxy', 'psci', 'pseudo', 'pseudo-class', 'pseudo-element', 'pseudo-method', 'pseudo-mnemonic', 'pseudo-variable', 'pshdl', 'pspell', 'psql', 'pt', 'ptc-config', 'ptc-config-modelcheck', 'pthread', 'ptr', 'ptx', 'public', 'pug', 'punchcard', 'punctual', 'punctuation', 'punctutation', 'puncuation', 'puncutation', 'puntuation', 'puppet', 'purebasic', 'purescript', 'pweave', 'pwisa', 'pwn', 'py2pml', 'pyj', 'pyjade', 'pymol', 'pyresttest', 'python', 'python-function', 'q', 'q-brace', 'q-bracket', 'q-ltgt', 'q-paren', 'qa', 'qm', 'qml', 'qos', 'qoute', 'qq', 'qq-brace', 'qq-bracket', 'qq-ltgt', 'qq-paren', 'qry', 'qtpro', 'quad', 'quad-arrow-down', 'quad-arrow-left', 'quad-arrow-right', 'quad-arrow-up', 'quad-backslash', 'quad-caret-down', 'quad-caret-up', 'quad-circle', 'quad-colon', 'quad-del-down', 'quad-del-up', 'quad-diamond', 'quad-divide', 'quad-equal', 'quad-jot', 'quad-less', 'quad-not-equal', 'quad-question', 'quad-quote', 'quad-slash', 'quadrigraph', 'qual', 'qualified', 'qualifier', 'quality', 'quant', 'quantifier', 'quantifiers', 'quartz', 'quasi', 'quasiquote', 'quasiquotes', 'query', 'query-dsl', 'question', 'questionmark', 'quicel', 'quicktemplate', 'quicktime-file', 'quotation', 'quote', 'quoted', 'quoted-identifier', 'quoted-object', 'quoted-or-unquoted', 'quotes', 'qx', 'qx-brace', 'qx-bracket', 'qx-ltgt', 'qx-paren', 'r', 'r3', 'rabl', 'racket', 'radar', 'radar-area', 'radiobuttons', 'radix', 'rails', 'rainmeter', 'raml', 'random', 'random_number', 'randomsk', 'range', 'range-2', 'rank', 'rant', 'rapid', 'rarity', 'ratio', 'rational-form', 'raw', 'raw-regex', 'raxe', 'rb', 'rd', 'rdfs-type', 'rdrand', 'rdseed', 'react', 'read', 'readline', 'readonly', 'readwrite', 'real', 'realip', 'rebeca', 'rebol', 'rec', 'receive', 'receive-channel', 'recipe', 'recipient-subscriber-list', 'recode', 'record', 'record-field', 'record-usage', 'recordfield', 'recutils', 'red', 'redbook-audio', 'redirect', 'redirection', 'redprl', 'redundancy', 'ref', 'refer', 'reference', 'referer', 'refinement', 'reflection', 'reg', 'regex', 'regexname', 'regexp', 'regexp-option', 'region-anchor-setting', 'region-cue-setting', 'region-identifier-setting', 'region-lines-setting', 'region-scroll-setting', 'region-viewport-anchor-setting', 'region-width-setting', 'register', 'register-64', 'registers', 'regular', 'reiny', 'reject', 'rejecttype', 'rel', 'related', 'relation', 'relational', 'relations', 'relationship', 'relationship-name', 'relationship-pattern', 'relationship-pattern-end', 'relationship-pattern-start', 'relationship-type', 'relationship-type-or', 'relationship-type-ored', 'relationship-type-start', 'relative', 'rem', 'reminder', 'remote', 'removed', 'rename', 'renamed-from', 'renamed-to', 'renaming', 'render', 'renpy', 'reocrd', 'reparator', 'repeat', 'repl-prompt', 'replace', 'replaceXXX', 'replaced', 'replacement', 'reply', 'repo', 'reporter', 'reporting', 'repository', 'request', 'request-type', 'require', 'required', 'requiredness', 'requirement', 'requirements', 'rescue', 'reserved', 'reset', 'resolution', 'resource', 'resource-manager', 'response', 'response-type', 'rest', 'rest-args', 'rester', 'restriced', 'restructuredtext', 'result', 'result-separator', 'results', 'retro', 'return', 'return-type', 'return-value', 'returns', 'rev', 'reverse', 'reversed', 'review', 'rewrite', 'rewrite-condition', 'rewrite-operator', 'rewrite-pattern', 'rewrite-substitution', 'rewrite-test', 'rewritecond', 'rewriterule', 'rf', 'rfc', 'rgb', 'rgb-percentage', 'rgb-value', 'rhap', 'rho', 'rhs', 'rhtml', 'richtext', 'rid', 'right', 'ring', 'riot', 'rivescript', 'rjs', 'rl', 'rmarkdown', 'rnc', 'rng', 'ro', 'roboconf', 'robot', 'robotc', 'robust-control', 'rockerfile', 'roff', 'role', 'rollout-control', 'root', 'rotate', 'rotate-first', 'rotate-last', 'round', 'round-brackets', 'router', 'routeros', 'routes', 'routine', 'row', 'row2', 'rowspan', 'roxygen', 'rparent', 'rpc', 'rpc-definition', 'rpe', 'rpm-spec', 'rpmspec', 'rpt', 'rq', 'rrd', 'rsl', 'rspec', 'rtemplate', 'ru', 'ruby', 'rubymotion', 'rule', 'rule-identifier', 'rule-name', 'rule-pattern', 'rule-tag', 'ruleDefinition', 'rules', 'run', 'rune', 'runoff', 'runtime', 'rust', 'rviz', 'rx', 's', 'safe-call', 'safe-navigation', 'safe-trap', 'safer', 'safety', 'sage', 'salesforce', 'salt', 'sampler', 'sampler-comparison', 'samplerarg', 'sampling', 'sas', 'sass', 'sass-script-maps', 'satcom', 'satisfies', 'sblock', 'scad', 'scala', 'scaladoc', 'scalar', 'scale', 'scam', 'scan', 'scenario', 'scenario_outline', 'scene', 'scene-object', 'scheduled', 'schelp', 'schem', 'schema', 'scheme', 'schememode', 'scientific', 'scilab', 'sck', 'scl', 'scope', 'scope-name', 'scope-resolution', 'scoping', 'score', 'screen', 'scribble', 'script', 'script-flag', 'script-metadata', 'script-object', 'script-tag', 'scripting', 'scriptlet', 'scriptlocal', 'scriptname', 'scriptname-declaration', 'scripts', 'scroll', 'scrollbars', 'scrollpanes', 'scss', 'scumm', 'sdbl', 'sdl', 'sdo', 'sealed', 'search', 'seawolf', 'second', 'secondary', 'section', 'section-attribute', 'sectionname', 'sections', 'see', 'segment', 'segment-registers', 'segment-resolution', 'select', 'select-block', 'selector', 'self', 'self-binding', 'self-close', 'sem', 'semantic', 'semanticmodel', 'semi-colon', 'semicolon', 'semicoron', 'semireserved', 'send-channel', 'sender', 'senum', 'sep', 'separator', 'separatory', 'sepatator', 'seperator', 'sequence', 'sequences', 'serial', 'serpent', 'server', 'service', 'service-declaration', 'service-rpc', 'services', 'session', 'set', 'set-colour', 'set-size', 'set-variable', 'setbagmix', 'setname', 'setproperty', 'sets', 'setter', 'setting', 'settings', 'settype', 'setword', 'seven', 'severity', 'sexpr', 'sfd', 'sfst', 'sgml', 'sgx1', 'sgx2', 'sha', 'sha256', 'sha512', 'sha_functions', 'shad', 'shade', 'shaderlab', 'shadow-object', 'shape', 'shape-base', 'shape-base-data', 'shared', 'shared-static', 'sharp', 'sharpequal', 'sharpge', 'sharpgt', 'sharple', 'sharplt', 'sharpness', 'shebang', 'shell', 'shell-function', 'shell-session', 'shift', 'shift-and-rotate', 'shift-left', 'shift-right', 'shine', 'shinescript', 'shipflow', 'shmop', 'short', 'shortcut', 'shortcuts', 'shorthand', 'shorthandpropertyname', 'show', 'show-argument', 'shuffle-and-unpack', 'shutdown', 'shy', 'sidebar', 'sifu', 'sigdec', 'sigil', 'sign-line', 'signal', 'signal-processing', 'signature', 'signed', 'signed-int', 'signedness', 'signifier', 'silent', 'sim-group', 'sim-object', 'sim-set', 'simd', 'simd-horizontal', 'simd-integer', 'simple', 'simple-delimiter', 'simple-divider', 'simple-element', 'simple_delimiter', 'simplexml', 'simplez', 'simulate', 'since', 'singe', 'single', 'single-line', 'single-quote', 'single-quoted', 'single_quote', 'singlequote', 'singleton', 'singleword', 'sites', 'six', 'size', 'size-cue-setting', 'sized_integer', 'sizeof', 'sjs', 'sjson', 'sk', 'skaction', 'skdragon', 'skeeland', 'skellett', 'sketchplugin', 'skevolved', 'skew', 'skill', 'skipped', 'skmorkaz', 'skquery', 'skrambled', 'skrayfall', 'skript', 'skrpg', 'sksharp', 'skstuff', 'skutilities', 'skvoice', 'sky', 'skyrim', 'sl', 'slash', 'slash-bar', 'slash-option', 'slash-sign', 'slashes', 'sleet', 'slice', 'slim', 'slm', 'sln', 'slot', 'slugignore', 'sma', 'smali', 'smalltalk', 'smarty', 'smb', 'smbinternal', 'smilebasic', 'sml', 'smoothing-group', 'smpte', 'smtlib', 'smx', 'snakeskin', 'snapshot', 'snlog', 'snmp', 'so', 'soap', 'social', 'socketgroup', 'sockets', 'soft', 'solidity', 'solve', 'soma', 'somearg', 'something', 'soql', 'sort', 'sorting', 'souce', 'sound', 'sound_processing', 'sound_synthesys', 'source', 'source-constant', 'soy', 'sp', 'space', 'space-after-command', 'spacebars', 'spaces', 'sparql', 'spath', 'spec', 'special', 'special-attributes', 'special-character', 'special-curve', 'special-functions', 'special-hook', 'special-keyword', 'special-method', 'special-point', 'special-token-sequence', 'special-tokens', 'special-type', 'specification', 'specifier', 'spectral-curve', 'specular-exponent', 'specular-reflectivity', 'sphinx', 'sphinx-domain', 'spice', 'spider', 'spindlespeed', 'splat', 'spline', 'splunk', 'splunk-conf', 'splus', 'spn', 'spread', 'spread-line', 'spreadmap', 'sprite', 'sproto', 'sproutcore', 'sqf', 'sql', 'sqlbuiltin', 'sqlite', 'sqlsrv', 'sqr', 'sqsp', 'squad', 'square', 'squart', 'squirrel', 'sr-Cyrl', 'sr-Latn', 'src', 'srltext', 'sros', 'srt', 'srv', 'ss', 'ssa', 'sse', 'sse2', 'sse2_simd', 'sse3', 'sse4', 'sse4_simd', 'sse5', 'sse_avx', 'sse_simd', 'ssh-config', 'ssi', 'ssl', 'ssn', 'sstemplate', 'st', 'stable', 'stack', 'stack-effect', 'stackframe', 'stage', 'stan', 'standard', 'standard-key', 'standard-links', 'standard-suite', 'standardadditions', 'standoc', 'star', 'starline', 'start', 'start-block', 'start-condition', 'start-symbol', 'start-value', 'starting-function-params', 'starting-functions', 'starting-functions-point', 'startshape', 'stata', 'statamic', 'state', 'state-flag', 'state-management', 'stateend', 'stategrouparg', 'stategroupval', 'statement', 'statement-separator', 'states', 'statestart', 'statetable', 'static', 'static-assert', 'static-classes', 'static-if', 'static-shape', 'staticimages', 'statistics', 'stats', 'std', 'stdWrap', 'std_logic', 'std_logic_1164', 'stderr-write-file', 'stdint', 'stdlib', 'stdlibcall', 'stdplugin', 'stem', 'step', 'step-size', 'steps', 'stg', 'stile-shoe-left', 'stile-shoe-up', 'stile-tilde', 'stitch', 'stk', 'stmt', 'stochastic', 'stop', 'stopping', 'storage', 'story', 'stp', 'straight-quote', 'stray', 'stray-comment-end', 'stream', 'stream-selection-and-control', 'streamsfuncs', 'streem', 'strict', 'strictness', 'strike', 'strikethrough', 'string', 'string-constant', 'string-format', 'string-interpolation', 'string-long-quote', 'string-long-single-quote', 'string-single-quote', 'stringchar', 'stringize', 'strings', 'strong', 'struc', 'struct', 'struct-union-block', 'structdef', 'structend', 'structs', 'structstart', 'structtable', 'structure', 'stuff', 'stupid-goddamn-hack', 'style', 'styleblock', 'styles', 'stylus', 'sub', 'sub-pattern', 'subchord', 'subckt', 'subcmd', 'subexp', 'subexpression', 'subkey', 'subkeys', 'subl', 'submodule', 'subnet', 'subnet6', 'subpattern', 'subprogram', 'subroutine', 'subscript', 'subsection', 'subsections', 'subset', 'subshell', 'subsort', 'substituted', 'substitution', 'substitution-definition', 'subtitle', 'subtlegradient', 'subtlegray', 'subtract', 'subtraction', 'subtype', 'suffix', 'sugarml', 'sugarss', 'sugly', 'sugly-comparison-operators', 'sugly-control-keywords', 'sugly-declare-function', 'sugly-delcare-operator', 'sugly-delcare-variable', 'sugly-else-in-invalid-position', 'sugly-encode-clause', 'sugly-function-groups', 'sugly-function-recursion', 'sugly-function-variables', 'sugly-general-functions', 'sugly-general-operators', 'sugly-generic-classes', 'sugly-generic-types', 'sugly-global-function', 'sugly-int-constants', 'sugly-invoke-function', 'sugly-json-clause', 'sugly-language-constants', 'sugly-math-clause', 'sugly-math-constants', 'sugly-multiple-parameter-function', 'sugly-number-constants', 'sugly-operator-operands', 'sugly-print-clause', 'sugly-single-parameter-function', 'sugly-subject-or-predicate', 'sugly-type-function', 'sugly-uri-clause', 'summary', 'super', 'superclass', 'supercollider', 'superscript', 'superset', 'supervisor', 'supervisord', 'supplemental', 'supplimental', 'support', 'suppress-image-or-category', 'suppressed', 'surface', 'surface-technique', 'sv', 'svg', 'svm', 'svn', 'swift', 'swig', 'switch', 'switch-block', 'switch-expression', 'switch-statement', 'switchEnd', 'switchStart', 'swizzle', 'sybase', 'syllableseparator', 'symbol', 'symbol-definition', 'symbol-type', 'symbolic', 'symbolic-math', 'symbols', 'symmetry', 'sync-match', 'sync-mode', 'sync-mode-location', 'synchronization', 'synchronize', 'synchronized', 'synergy', 'synopsis', 'syntax', 'syntax-case', 'syntax-cluster', 'syntax-conceal', 'syntax-error', 'syntax-include', 'syntax-item', 'syntax-keywords', 'syntax-match', 'syntax-option', 'syntax-region', 'syntax-rule', 'syntax-spellcheck', 'syntax-sync', 'sys-types', 'sysj', 'syslink', 'syslog-ng', 'system', 'system-events', 'system-identification', 'system-table-pointer', 'systemreference', 'sytem-events', 't', 't3datastructure', 't4', 't5', 't7', 'ta', 'tab', 'table', 'table-name', 'tablename', 'tabpanels', 'tabs', 'tabular', 'tacacs', 'tack-down', 'tack-up', 'taco', 'tads3', 'tag', 'tag-string', 'tag-value', 'tagbraces', 'tagdef', 'tagged', 'tagger_script', 'taglib', 'tagname', 'tagnamedjango', 'tags', 'taint', 'take', 'target', 'targetobj', 'targetprop', 'task', 'tasks', 'tbdfile', 'tbl', 'tbody', 'tcl', 'tcoffee', 'tcp-object', 'td', 'tdl', 'tea', 'team', 'telegram', 'tell', 'telnet', 'temp', 'template', 'template-call', 'template-parameter', 'templatetag', 'tempo', 'temporal', 'term', 'term-comparison', 'term-creation-and-decomposition', 'term-io', 'term-testing', 'term-unification', 'terminal', 'terminate', 'termination', 'terminator', 'terms', 'ternary', 'ternary-if', 'terra', 'terraform', 'terrain-block', 'test', 'testcase', 'testing', 'tests', 'testsuite', 'testx', 'tex', 'texres', 'texshop', 'text', 'text-reference', 'text-suite', 'textbf', 'textcolor', 'textile', 'textio', 'textit', 'textlabels', 'textmate', 'texttt', 'textual', 'texture', 'texture-map', 'texture-option', 'tfoot', 'th', 'thead', 'then', 'therefore', 'thin', 'thing1', 'third', 'this', 'thorn', 'thread', 'three', 'thrift', 'throughput', 'throw', 'throwables', 'throws', 'tick', 'ticket-num', 'ticket-psa', 'tid-file', 'tidal', 'tidalcycles', 'tiddler', 'tiddler-field', 'tiddler-fields', 'tidy', 'tier', 'tieslur', 'tikz', 'tilde', 'time', 'timeblock', 'timehrap', 'timeout', 'timer', 'times', 'timesig', 'timespan', 'timespec', 'timestamp', 'timing', 'titanium', 'title', 'title-page', 'title-text', 'titled-paragraph', 'tjs', 'tl', 'tla', 'tlh', 'tmpl', 'tmsim', 'tmux', 'tnote', 'tnsaudit', 'to', 'to-file', 'to-type', 'toc', 'toc-list', 'todo', 'todo_extra', 'todotxt', 'token', 'token-def', 'token-paste', 'token-type', 'tokenised', 'tokenizer', 'toml', 'too-many-tildes', 'tool', 'toolbox', 'tooltip', 'top', 'top-level', 'top_level', 'topas', 'topic', 'topic-decoration', 'topic-title', 'tornado', 'torque', 'torquescript', 'tosca', 'total-config', 'totaljs', 'tpye', 'tr', 'trace', 'trace-argument', 'trace-object', 'traceback', 'tracing', 'track_processing', 'trader', 'tradersk', 'trail', 'trailing', 'trailing-array-separator', 'trailing-dictionary-separator', 'trailing-match', 'trait', 'traits', 'traits-keyword', 'transaction', 'transcendental', 'transcludeblock', 'transcludeinline', 'transclusion', 'transform', 'transformation', 'transient', 'transition', 'transitionable-property-value', 'translation', 'transmission-filter', 'transparency', 'transparent-line', 'transpose', 'transposed-func', 'transposed-matrix', 'transposed-parens', 'transposed-variable', 'trap', 'tree', 'treetop', 'trenni', 'trigEvent_', 'trigLevelMod_', 'trigLevel_', 'trigger', 'trigger-words', 'triggermodifier', 'trigonometry', 'trimming-loop', 'triple', 'triple-dash', 'triple-slash', 'triple-star', 'true', 'truncate', 'truncation', 'truthgreen', 'try', 'try-catch', 'trycatch', 'ts', 'tsql', 'tss', 'tst', 'tsv', 'tsx', 'tt', 'ttcn3', 'ttlextension', 'ttpmacro', 'tts', 'tubaina', 'tubaina2', 'tul', 'tup', 'tuple', 'turbulence', 'turing', 'turquoise', 'turtle', 'tutch', 'tvml', 'tw5', 'twig', 'twigil', 'twiki', 'two', 'txl', 'txt', 'txt2tags', 'type', 'type-annotation', 'type-cast', 'type-cheat', 'type-checking', 'type-constrained', 'type-constraint', 'type-declaration', 'type-def', 'type-definition', 'type-definition-group', 'type-definitions', 'type-descriptor', 'type-of', 'type-or', 'type-parameter', 'type-parameters', 'type-signature', 'type-spec', 'type-specialization', 'type-specifiers', 'type_2', 'type_trait', 'typeabbrev', 'typeclass', 'typed', 'typed-hole', 'typedblock', 'typedcoffeescript', 'typedecl', 'typedef', 'typeexp', 'typehint', 'typehinted', 'typeid', 'typename', 'types', 'typesbii', 'typescriptish', 'typographic-quotes', 'typoscript', 'typoscript2', 'u', 'u-degree', 'u-end', 'u-offset', 'u-resolution', 'u-scale', 'u-segments', 'u-size', 'u-start', 'u-value', 'uc', 'ucicfg', 'ucicmd', 'udaf', 'udf', 'udl', 'udp', 'udtf', 'ui', 'ui-block', 'ui-group', 'ui-state', 'ui-subgroup', 'uintptr', 'ujm', 'uk', 'ul', 'umbaska', 'unOp', 'unary', 'unbuffered', 'unchecked', 'uncleared', 'unclosed', 'unclosed-string', 'unconstrained', 'undef', 'undefined', 'underbar-circle', 'underbar-diamond', 'underbar-iota', 'underbar-jot', 'underbar-quote', 'underbar-semicolon', 'underline', 'underline-text', 'underlined', 'underscore', 'undocumented', 'unescaped-quote', 'unexpected', 'unexpected-characters', 'unexpected-extends', 'unexpected-extends-character', 'unfiled', 'unformatted', 'unicode', 'unicode-16-bit', 'unicode-32-bit', 'unicode-escape', 'unicode-raw', 'unicode-raw-regex', 'unified', 'unify', 'unimplemented', 'unimportant', 'union', 'union-declaration', 'unique-id', 'unit', 'unit-checking', 'unit-test', 'unit_test', 'unittest', 'unity', 'unityscript', 'universal-match', 'unix', 'unknown', 'unknown-escape', 'unknown-method', 'unknown-property-name', 'unknown-rune', 'unlabeled', 'unless', 'unnecessary', 'unnumbered', 'uno', 'unoconfig', 'unop', 'unoproj', 'unordered', 'unordered-block', 'unosln', 'unpack', 'unpacking', 'unparsed', 'unqualified', 'unquoted', 'unrecognized', 'unrecognized-character', 'unrecognized-character-escape', 'unrecognized-string-escape', 'unsafe', 'unsigned', 'unsigned-int', 'unsized_integer', 'unsupplied', 'until', 'untitled', 'untyped', 'unused', 'uopz', 'update', 'uppercase', 'upstream', 'upwards', 'ur', 'uri', 'url', 'usable', 'usage', 'use', 'use-as', 'use-map', 'use-material', 'usebean', 'usecase', 'usecase-block', 'user', 'user-defined', 'user-defined-property', 'user-defined-type', 'user-interaction', 'userflagsref', 'userid', 'username', 'users', 'using', 'using-namespace-declaration', 'using_animtree', 'util', 'utilities', 'utility', 'utxt', 'uv-resolution', 'uvu', 'uvw', 'ux', 'uxc', 'uxl', 'uz', 'v', 'v-degree', 'v-end', 'v-offset', 'v-resolution', 'v-scale', 'v-segments', 'v-size', 'v-start', 'v-value', 'val', 'vala', 'valgrind', 'valid', 'valid-ampersand', 'valid-bracket', 'valign', 'value', 'value-pair', 'value-signature', 'value-size', 'value-type', 'valuepair', 'vamos', 'vamp', 'vane-down', 'vane-left', 'vane-right', 'vane-up', 'var', 'var-single-variable', 'var1', 'var2', 'variable', 'variable-access', 'variable-assignment', 'variable-declaration', 'variable-definition', 'variable-modifier', 'variable-parameter', 'variable-reference', 'variable-usage', 'variables', 'variabletable', 'variant', 'variant-definition', 'varname', 'varnish', 'vars', 'vb', 'vbnet', 'vbs', 'vc', 'vcard', 'vcd', 'vcl', 'vcs', 'vector', 'vector-load', 'vectors', 'vehicle', 'velocity', 'vendor-prefix', 'verb', 'verbatim', 'verdict', 'verilog', 'version', 'version-number', 'version-specification', 'vertex', 'vertex-reference', 'vertical-blending', 'vertical-span', 'vertical-text-cue-setting', 'vex', 'vhdl', 'vhost', 'vi', 'via', 'video-texturing', 'video_processing', 'view', 'viewhelpers', 'vimAugroupKey', 'vimBehaveModel', 'vimFTCmd', 'vimFTOption', 'vimFuncKey', 'vimGroupSpecial', 'vimHiAttrib', 'vimHiClear', 'vimMapModKey', 'vimPattern', 'vimSynCase', 'vimSynType', 'vimSyncC', 'vimSyncLinecont', 'vimSyncMatch', 'vimSyncNone', 'vimSyncRegion', 'vimUserAttrbCmplt', 'vimUserAttrbKey', 'vimUserCommand', 'viml', 'virtual', 'virtual-host', 'virtual-reality', 'visibility', 'visualforce', 'visualization', 'vlanhdr', 'vle', 'vmap', 'vmx', 'voice', 'void', 'volatile', 'volt', 'volume', 'vpath', 'vplus', 'vrf', 'vtt', 'vue', 'vue-jade', 'vue-stylus', 'w-offset', 'w-scale', 'w-value', 'w3c-extended-color-name', 'w3c-non-standard-color-name', 'w3c-standard-color-name', 'wait', 'waitress', 'waitress-config', 'waitress-rb', 'warn', 'warning', 'warnings', 'wast', 'water', 'watson-todo', 'wavefront', 'wavelet', 'wddx', 'wdiff', 'weapon', 'weave', 'weaveBracket', 'weaveBullet', 'webidl', 'webspeed', 'webvtt', 'weekday', 'weirdland', 'wf', 'wh', 'whatever', 'wheeled-vehicle', 'when', 'where', 'while', 'while-condition', 'while-loop', 'whiskey', 'white', 'whitespace', 'widget', 'width', 'wiki', 'wiki-link', 'wildcard', 'wildsk', 'win', 'window', 'window-classes', 'windows', 'winered', 'with', 'with-arg', 'with-args', 'with-arguments', 'with-params', 'with-prefix', 'with-side-effects', 'with-suffix', 'with-terminator', 'with-value', 'with_colon', 'without-args', 'without-arguments', 'wla-dx', 'word', 'word-op', 'wordnet', 'wordpress', 'words', 'workitem', 'world', 'wow', 'wp', 'write', 'wrong', 'wrong-access-type', 'wrong-division', 'wrong-division-assignment', 'ws', 'www', 'wxml', 'wysiwyg-string', 'x10', 'x86', 'x86_64', 'x86asm', 'xacro', 'xbase', 'xchg', 'xhp', 'xhprof', 'xikij', 'xml', 'xml-attr', 'xmlrpc', 'xmlwriter', 'xop', 'xor', 'xparse', 'xq', 'xquery', 'xref', 'xsave', 'xsd-all', 'xsd_nillable', 'xsd_optional', 'xsl', 'xslt', 'xsse3_simd', 'xst', 'xtend', 'xtoy', 'xtpl', 'xu', 'xvc', 'xve', 'xyzw', 'y', 'y1', 'y2', 'yabb', 'yaml', 'yaml-ext', 'yang', 'yara', 'yate', 'yaws', 'year', 'yellow', 'yield', 'ykk', 'yorick', 'you-forgot-semicolon', 'z', 'z80', 'zap', 'zapper', 'zep', 'zepon', 'zepto', 'zero', 'zero-width-marker', 'zero-width-print', 'zeroop', 'zh-CN', 'zh-TW', 'zig', 'zilde', 'zlib', 'zoomfilter', 'zzz' ]); ================================================ FILE: src/deserializer-manager.js ================================================ const { Disposable } = require('event-kit'); // Extended: Manages the deserializers used for serialized state // // An instance of this class is always available as the `atom.deserializers` // global. // // ## Examples // // ```coffee // class MyPackageView extends View // atom.deserializers.add(this) // // @deserialize: (state) -> // new MyPackageView(state) // // constructor: (@state) -> // // serialize: -> // @state // ``` module.exports = class DeserializerManager { constructor(atomEnvironment) { this.atomEnvironment = atomEnvironment; this.deserializers = {}; } // Public: Register the given class(es) as deserializers. // // * `deserializers` One or more deserializers to register. A deserializer can // be any object with a `.name` property and a `.deserialize()` method. A // common approach is to register a *constructor* as the deserializer for its // instances by adding a `.deserialize()` class method. When your method is // called, it will be passed serialized state as the first argument and the // {AtomEnvironment} object as the second argument, which is useful if you // wish to avoid referencing the `atom` global. add(...deserializers) { for (let i = 0; i < deserializers.length; i++) { let deserializer = deserializers[i]; this.deserializers[deserializer.name] = deserializer; } return new Disposable(() => { for (let j = 0; j < deserializers.length; j++) { let deserializer = deserializers[j]; delete this.deserializers[deserializer.name]; } }); } getDeserializerCount() { return Object.keys(this.deserializers).length; } // Public: Deserialize the state and params. // // * `state` The state {Object} to deserialize. deserialize(state) { if (state == null) { return; } const deserializer = this.get(state); if (deserializer) { let stateVersion = (typeof state.get === 'function' && state.get('version')) || state.version; if ( deserializer.version != null && deserializer.version !== stateVersion ) { return; } return deserializer.deserialize(state, this.atomEnvironment); } else { return console.warn('No deserializer found for', state); } } // Get the deserializer for the state. // // * `state` The state {Object} being deserialized. get(state) { if (state == null) { return; } let stateDeserializer = (typeof state.get === 'function' && state.get('deserializer')) || state.deserializer; return this.deserializers[stateDeserializer]; } clear() { this.deserializers = {}; } }; ================================================ FILE: src/dock.js ================================================ const etch = require('etch'); const _ = require('underscore-plus'); const { CompositeDisposable, Emitter } = require('event-kit'); const PaneContainer = require('./pane-container'); const TextEditor = require('./text-editor'); const Grim = require('grim'); const $ = etch.dom; const MINIMUM_SIZE = 100; const DEFAULT_INITIAL_SIZE = 300; const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate'; const VISIBLE_CLASS = 'atom-dock-open'; const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable'; const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible'; const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible'; // Extended: A container at the edges of the editor window capable of holding items. // You should not create a Dock directly. Instead, access one of the three docks of the workspace // via {Workspace::getLeftDock}, {Workspace::getRightDock}, and {Workspace::getBottomDock} // or add an item to a dock via {Workspace::open}. module.exports = class Dock { constructor(params) { this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind( this ); this.handleResizeToFit = this.handleResizeToFit.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseUp = this.handleMouseUp.bind(this); this.handleDrag = _.throttle(this.handleDrag.bind(this), 30); this.handleDragEnd = this.handleDragEnd.bind(this); this.handleToggleButtonDragEnter = this.handleToggleButtonDragEnter.bind( this ); this.toggle = this.toggle.bind(this); this.location = params.location; this.widthOrHeight = getWidthOrHeight(this.location); this.config = params.config; this.applicationDelegate = params.applicationDelegate; this.deserializerManager = params.deserializerManager; this.notificationManager = params.notificationManager; this.viewRegistry = params.viewRegistry; this.didActivate = params.didActivate; this.emitter = new Emitter(); this.paneContainer = new PaneContainer({ location: this.location, config: this.config, applicationDelegate: this.applicationDelegate, deserializerManager: this.deserializerManager, notificationManager: this.notificationManager, viewRegistry: this.viewRegistry }); this.state = { size: null, visible: false, shouldAnimate: false }; this.subscriptions = new CompositeDisposable( this.emitter, this.paneContainer.onDidActivatePane(() => { this.show(); this.didActivate(this); }), this.paneContainer.observePanes(pane => { pane.onDidAddItem(this.handleDidAddPaneItem.bind(this)); pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this)); }), this.paneContainer.onDidChangeActivePane(item => params.didChangeActivePane(this, item) ), this.paneContainer.onDidChangeActivePaneItem(item => params.didChangeActivePaneItem(this, item) ), this.paneContainer.onDidDestroyPaneItem(item => params.didDestroyPaneItem(item) ) ); } // This method is called explicitly by the object which adds the Dock to the document. elementAttached() { // Re-render when the dock is attached to make sure we remeasure sizes defined in CSS. etch.updateSync(this); } getElement() { // Because this code is included in the snapshot, we have to make sure we don't touch the DOM // during initialization. Therefore, we defer initialization of the component (which creates a // DOM element) until somebody asks for the element. if (this.element == null) { etch.initialize(this); } return this.element; } getLocation() { return this.location; } destroy() { this.subscriptions.dispose(); this.paneContainer.destroy(); window.removeEventListener('mousemove', this.handleMouseMove); window.removeEventListener('mouseup', this.handleMouseUp); window.removeEventListener('drag', this.handleDrag); window.removeEventListener('dragend', this.handleDragEnd); } setHovered(hovered) { if (hovered === this.state.hovered) return; this.setState({ hovered }); } setDraggingItem(draggingItem) { if (draggingItem === this.state.draggingItem) return; this.setState({ draggingItem }); } // Extended: Show the dock and focus its active {Pane}. activate() { this.getActivePane().activate(); } // Extended: Show the dock without focusing it. show() { this.setState({ visible: true }); } // Extended: Hide the dock and activate the {WorkspaceCenter} if the dock was // was previously focused. hide() { this.setState({ visible: false }); } // Extended: Toggle the dock's visibility without changing the {Workspace}'s // active pane container. toggle() { const state = { visible: !this.state.visible }; if (!state.visible) state.hovered = false; this.setState(state); } // Extended: Check if the dock is visible. // // Returns a {Boolean}. isVisible() { return this.state.visible; } setState(newState) { const prevState = this.state; const nextState = Object.assign({}, prevState, newState); // Update the `shouldAnimate` state. This needs to be written to the DOM before updating the // class that changes the animated property. Normally we'd have to defer the class change a // 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 visibility... nextState.shouldAnimate = false; } else if ( !nextState.visible && nextState.draggingItem && !prevState.draggingItem ) { // ...but do animate if you start dragging while the panel is hidden. nextState.shouldAnimate = true; } this.state = nextState; const { hovered, visible } = this.state; // Render immediately if the dock becomes visible or the size changes in case people are // measuring after opening, for example. if (this.element != null) { if ((visible && !prevState.visible) || this.state.size !== prevState.size) etch.updateSync(this); else etch.update(this); } if (hovered !== prevState.hovered) { this.emitter.emit('did-change-hovered', hovered); } if (visible !== prevState.visible) { this.emitter.emit('did-change-visible', visible); } } render() { const innerElementClassList = ['atom-dock-inner', this.location]; if (this.state.visible) innerElementClassList.push(VISIBLE_CLASS); const maskElementClassList = ['atom-dock-mask']; if (this.state.shouldAnimate) maskElementClassList.push(SHOULD_ANIMATE_CLASS); const cursorOverlayElementClassList = [ 'atom-dock-cursor-overlay', this.location ]; if (this.state.resizing) cursorOverlayElementClassList.push(CURSOR_OVERLAY_VISIBLE_CLASS); const shouldBeVisible = this.state.visible || this.state.showDropTarget; const size = Math.max( MINIMUM_SIZE, this.state.size || (this.state.draggingItem && getPreferredSize(this.state.draggingItem, this.location)) || DEFAULT_INITIAL_SIZE ); // We need to change the size of the mask... const maskStyle = { [this.widthOrHeight]: `${shouldBeVisible ? size : 0}px` }; // ...but the content needs to maintain a constant size. const wrapperStyle = { [this.widthOrHeight]: `${size}px` }; return $( 'atom-dock', { className: this.location }, $.div( { ref: 'innerElement', className: innerElementClassList.join(' ') }, $.div( { className: maskElementClassList.join(' '), style: maskStyle }, $.div( { ref: 'wrapperElement', className: `atom-dock-content-wrapper ${this.location}`, style: wrapperStyle }, $(DockResizeHandle, { location: this.location, onResizeStart: this.handleResizeHandleDragStart, onResizeToFit: this.handleResizeToFit, dockIsVisible: this.state.visible }), $(ElementComponent, { element: this.paneContainer.getElement() }), $.div({ className: cursorOverlayElementClassList.join(' ') }) ) ), $(DockToggleButton, { ref: 'toggleButton', onDragEnter: this.state.draggingItem ? this.handleToggleButtonDragEnter : null, location: this.location, toggle: this.toggle, dockIsVisible: shouldBeVisible, visible: // Don't show the toggle button if the dock is closed and empty... (this.state.hovered && (this.state.visible || this.getPaneItems().length > 0)) || // ...or if the item can't be dropped in that dock. (!shouldBeVisible && this.state.draggingItem && isItemAllowed(this.state.draggingItem, this.location)) }) ) ); } update(props) { // Since we're interopping with non-etch stuff, this method's actually never called. return etch.update(this); } handleDidAddPaneItem() { if (this.state.size == null) { this.setState({ size: this.getInitialSize() }); } } handleDidRemovePaneItem() { // Hide the dock if you remove the last item. if (this.paneContainer.getPaneItems().length === 0) { this.setState({ visible: false, hovered: false, size: null }); } } handleResizeHandleDragStart() { window.addEventListener('mousemove', this.handleMouseMove); window.addEventListener('mouseup', this.handleMouseUp); this.setState({ resizing: true }); } handleResizeToFit() { const item = this.getActivePaneItem(); if (item) { const size = getPreferredSize(item, this.getLocation()); if (size != null) this.setState({ size }); } } handleMouseMove(event) { if (event.buttons === 0) { // We missed the mouseup event. For some reason it happens on Windows this.handleMouseUp(event); return; } let size = 0; switch (this.location) { case 'left': size = event.pageX - this.element.getBoundingClientRect().left; break; case 'bottom': size = this.element.getBoundingClientRect().bottom - event.pageY; break; case 'right': size = this.element.getBoundingClientRect().right - event.pageX; break; } this.setState({ size }); } handleMouseUp(event) { window.removeEventListener('mousemove', this.handleMouseMove); window.removeEventListener('mouseup', this.handleMouseUp); this.setState({ resizing: false }); } handleToggleButtonDragEnter() { this.setState({ showDropTarget: true }); window.addEventListener('drag', this.handleDrag); window.addEventListener('dragend', this.handleDragEnd); } handleDrag(event) { if (!this.pointWithinHoverArea({ x: event.pageX, y: event.pageY }, true)) { this.draggedOut(); } } handleDragEnd() { this.draggedOut(); } draggedOut() { this.setState({ showDropTarget: false }); window.removeEventListener('drag', this.handleDrag); window.removeEventListener('dragend', this.handleDragEnd); } // Determine whether the cursor is within the dock hover area. This isn't as simple as just using // mouseenter/leave because we want to be a little more forgiving. For example, if the cursor is // over the footer, we want to show the bottom dock's toggle button. Also note that our criteria // for detecting entry are different than detecting exit but, in order for us to avoid jitter, the // area considered when detecting exit MUST fully encompass the area considered when detecting // entry. pointWithinHoverArea(point, detectingExit) { const dockBounds = this.refs.innerElement.getBoundingClientRect(); // Copy the bounds object since we can't mutate it. const bounds = { top: dockBounds.top, right: dockBounds.right, bottom: dockBounds.bottom, left: dockBounds.left }; // To provide a minimum target, expand the area toward the center a bit. switch (this.location) { case 'right': bounds.left = Math.min(bounds.left, bounds.right - 2); break; case 'bottom': bounds.top = Math.min(bounds.top, bounds.bottom - 1); break; case 'left': bounds.right = Math.max(bounds.right, bounds.left + 2); break; } // Further expand the area to include all panels that are closer to the edge than the dock. switch (this.location) { case 'right': bounds.right = Number.POSITIVE_INFINITY; break; case 'bottom': bounds.bottom = Number.POSITIVE_INFINITY; break; case 'left': bounds.left = Number.NEGATIVE_INFINITY; break; } // If we're in this area, we know we're within the hover area without having to take further // measurements. if (rectContainsPoint(bounds, point)) return true; // If we're within the toggle button, we're definitely in the hover area. Unfortunately, we // can't do this measurement conditionally (e.g. only if the toggle button is visible) because // our knowledge of the toggle's button is incomplete due to CSS animations. (We may think the // toggle button isn't visible when in actuality it is, but is animating to its hidden state.) // // Since `point` is always the current mouse position, one possible optimization would be to // remove it as an argument and determine whether we're inside the toggle button using // mouseenter/leave events on it. This class would still need to keep track of the mouse // position (via a mousemove listener) for the other measurements, though. const toggleButtonBounds = this.refs.toggleButton.getBounds(); if (rectContainsPoint(toggleButtonBounds, point)) return true; // The area used when detecting exit is actually larger than when detecting entrances. Expand // our bounds and recheck them. if (detectingExit) { const hoverMargin = 20; switch (this.location) { case 'right': bounds.left = Math.min(bounds.left, toggleButtonBounds.left) - hoverMargin; break; case 'bottom': bounds.top = Math.min(bounds.top, toggleButtonBounds.top) - hoverMargin; break; case 'left': bounds.right = Math.max(bounds.right, toggleButtonBounds.right) + hoverMargin; break; } if (rectContainsPoint(bounds, point)) return true; } return false; } getInitialSize() { // The item may not have been activated yet. If that's the case, just use the first item. const activePaneItem = this.paneContainer.getActivePaneItem() || this.paneContainer.getPaneItems()[0]; // If there are items, we should have an explicit width; if not, we shouldn't. return activePaneItem ? getPreferredSize(activePaneItem, this.location) || DEFAULT_INITIAL_SIZE : null; } serialize() { return { deserializer: 'Dock', size: this.state.size, paneContainer: this.paneContainer.serialize(), visible: this.state.visible }; } deserialize(serialized, deserializerManager) { this.paneContainer.deserialize( serialized.paneContainer, deserializerManager ); this.setState({ size: serialized.size || this.getInitialSize(), // If no items could be deserialized, we don't want to show the dock (even if it was visible last time) visible: serialized.visible && this.paneContainer.getPaneItems().length > 0 }); } /* Section: Event Subscription */ // Essential: Invoke the given callback when the visibility of the dock changes. // // * `callback` {Function} to be called when the visibility changes. // * `visible` {Boolean} Is the dock now visible? // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeVisible(callback) { return this.emitter.on('did-change-visible', callback); } // Essential: Invoke the given callback with the current and all future visibilities of the dock. // // * `callback` {Function} to be called when the visibility changes. // * `visible` {Boolean} Is the dock now visible? // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeVisible(callback) { callback(this.isVisible()); return this.onDidChangeVisible(callback); } // Essential: Invoke the given callback with all current and future panes items // in the dock. // // * `callback` {Function} to be called with current and future pane items. // * `item` An item that is present in {::getPaneItems} at the time of // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePaneItems(callback) { return this.paneContainer.observePaneItems(callback); } // Essential: Invoke the given callback when the active pane item changes. // // Because observers are invoked synchronously, it's important not to perform // any expensive operations via this method. Consider // {::onDidStopChangingActivePaneItem} to delay operations until after changes // stop occurring. // // * `callback` {Function} to be called when the active pane item changes. // * `item` The active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActivePaneItem(callback) { return this.paneContainer.onDidChangeActivePaneItem(callback); } // Essential: Invoke the given callback when the active pane item stops // changing. // // Observers are called asynchronously 100ms after the last active pane item // change. Handling changes here rather than in the synchronous // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly // changing or closing tabs and ensures critical UI feedback, like changing the // highlighted tab, gets priority over work that can be done asynchronously. // // * `callback` {Function} to be called when the active pane item stopts // changing. // * `item` The active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidStopChangingActivePaneItem(callback) { return this.paneContainer.onDidStopChangingActivePaneItem(callback); } // Essential: Invoke the given callback with the current active pane item and // with all future active pane items in the dock. // // * `callback` {Function} to be called when the active pane item changes. // * `item` The current active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeActivePaneItem(callback) { return this.paneContainer.observeActivePaneItem(callback); } // Extended: Invoke the given callback when a pane is added to the dock. // // * `callback` {Function} to be called panes are added. // * `event` {Object} with the following keys: // * `pane` The added pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPane(callback) { return this.paneContainer.onDidAddPane(callback); } // Extended: Invoke the given callback before a pane is destroyed in the // dock. // // * `callback` {Function} to be called before panes are destroyed. // * `event` {Object} with the following keys: // * `pane` The pane to be destroyed. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onWillDestroyPane(callback) { return this.paneContainer.onWillDestroyPane(callback); } // Extended: Invoke the given callback when a pane is destroyed in the dock. // // * `callback` {Function} to be called panes are destroyed. // * `event` {Object} with the following keys: // * `pane` The destroyed pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroyPane(callback) { return this.paneContainer.onDidDestroyPane(callback); } // Extended: Invoke the given callback with all current and future panes in the // dock. // // * `callback` {Function} to be called with current and future panes. // * `pane` A {Pane} that is present in {::getPanes} at the time of // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePanes(callback) { return this.paneContainer.observePanes(callback); } // Extended: Invoke the given callback when the active pane changes. // // * `callback` {Function} to be called when the active pane changes. // * `pane` A {Pane} that is the current return value of {::getActivePane}. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActivePane(callback) { return this.paneContainer.onDidChangeActivePane(callback); } // Extended: Invoke the given callback with the current active pane and when // the active pane changes. // // * `callback` {Function} to be called with the current and future active# // panes. // * `pane` A {Pane} that is the current return value of {::getActivePane}. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeActivePane(callback) { return this.paneContainer.observeActivePane(callback); } // Extended: Invoke the given callback when a pane item is added to the dock. // // * `callback` {Function} to be called when pane items are added. // * `event` {Object} with the following keys: // * `item` The added pane item. // * `pane` {Pane} containing the added item. // * `index` {Number} indicating the index of the added item in its pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPaneItem(callback) { return this.paneContainer.onDidAddPaneItem(callback); } // 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. // * `event` {Object} with the following keys: // * `item` The item to be destroyed. // * `pane` {Pane} containing the item to be destroyed. // * `index` {Number} indicating the index of the item to be destroyed in // its pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onWillDestroyPaneItem(callback) { return this.paneContainer.onWillDestroyPaneItem(callback); } // Extended: Invoke the given callback when a pane item is destroyed. // // * `callback` {Function} to be called when pane items are destroyed. // * `event` {Object} with the following keys: // * `item` The destroyed item. // * `pane` {Pane} containing the destroyed item. // * `index` {Number} indicating the index of the destroyed item in its // pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onDidDestroyPaneItem(callback) { return this.paneContainer.onDidDestroyPaneItem(callback); } // Extended: Invoke the given callback when the hovered state of the dock changes. // // * `callback` {Function} to be called when the hovered state changes. // * `hovered` {Boolean} Is the dock now hovered? // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeHovered(callback) { return this.emitter.on('did-change-hovered', callback); } /* Section: Pane Items */ // Essential: Get all pane items in the dock. // // Returns an {Array} of items. getPaneItems() { return this.paneContainer.getPaneItems(); } // Essential: Get the active {Pane}'s active item. // // Returns an pane item {Object}. getActivePaneItem() { return this.paneContainer.getActivePaneItem(); } // Deprecated: Get the active item if it is a {TextEditor}. // // Returns a {TextEditor} or `undefined` if the current active item is not a // {TextEditor}. getActiveTextEditor() { Grim.deprecate( 'Text editors are not allowed in docks. Use atom.workspace.getActiveTextEditor() instead.' ); const activeItem = this.getActivePaneItem(); if (activeItem instanceof TextEditor) { return activeItem; } } // Save all pane items. saveAll() { this.paneContainer.saveAll(); } confirmClose(options) { return this.paneContainer.confirmClose(options); } /* Section: Panes */ // Extended: Get all panes in the dock. // // Returns an {Array} of {Pane}s. getPanes() { return this.paneContainer.getPanes(); } // Extended: Get the active {Pane}. // // Returns a {Pane}. getActivePane() { return this.paneContainer.getActivePane(); } // Extended: Make the next pane active. activateNextPane() { return this.paneContainer.activateNextPane(); } // Extended: Make the previous pane active. activatePreviousPane() { return this.paneContainer.activatePreviousPane(); } paneForURI(uri) { return this.paneContainer.paneForURI(uri); } paneForItem(item) { return this.paneContainer.paneForItem(item); } // Destroy (close) the active pane. destroyActivePane() { const activePane = this.getActivePane(); if (activePane != null) { activePane.destroy(); } } }; class DockResizeHandle { constructor(props) { this.props = props; etch.initialize(this); } render() { const classList = ['atom-dock-resize-handle', this.props.location]; if (this.props.dockIsVisible) classList.push(RESIZE_HANDLE_RESIZABLE_CLASS); return $.div({ className: classList.join(' '), on: { mousedown: this.handleMouseDown } }); } getElement() { return this.element; } getSize() { if (!this.size) { this.size = this.element.getBoundingClientRect()[ getWidthOrHeight(this.props.location) ]; } return this.size; } update(newProps) { this.props = Object.assign({}, this.props, newProps); return etch.update(this); } handleMouseDown(event) { if (event.detail === 2) { this.props.onResizeToFit(); } else if (this.props.dockIsVisible) { this.props.onResizeStart(); } } } class DockToggleButton { constructor(props) { this.props = props; etch.initialize(this); } render() { const classList = ['atom-dock-toggle-button', this.props.location]; if (this.props.visible) classList.push(TOGGLE_BUTTON_VISIBLE_CLASS); return $.div( { className: classList.join(' ') }, $.div( { ref: 'innerElement', className: `atom-dock-toggle-button-inner ${this.props.location}`, on: { click: this.handleClick, dragenter: this.props.onDragEnter } }, $.span({ ref: 'iconElement', className: `icon ${getIconName( this.props.location, this.props.dockIsVisible )}` }) ) ); } getElement() { return this.element; } getBounds() { return this.refs.innerElement.getBoundingClientRect(); } update(newProps) { this.props = Object.assign({}, this.props, newProps); return etch.update(this); } handleClick() { this.props.toggle(); } } // An etch component that doesn't use etch, this component provides a gateway from JSX back into // the mutable DOM world. class ElementComponent { constructor(props) { this.element = props.element; } update(props) { this.element = props.element; } } function getWidthOrHeight(location) { return location === 'left' || location === 'right' ? 'width' : 'height'; } function getPreferredSize(item, location) { switch (location) { case 'left': case 'right': return typeof item.getPreferredWidth === 'function' ? item.getPreferredWidth() : null; default: return typeof item.getPreferredHeight === 'function' ? item.getPreferredHeight() : null; } } function getIconName(location, visible) { switch (location) { case 'right': return visible ? 'icon-chevron-right' : 'icon-chevron-left'; case 'bottom': return visible ? 'icon-chevron-down' : 'icon-chevron-up'; case 'left': return visible ? 'icon-chevron-left' : 'icon-chevron-right'; default: throw new Error(`Invalid location: ${location}`); } } function rectContainsPoint(rect, point) { return ( point.x >= rect.left && point.y >= rect.top && point.x <= rect.right && point.y <= rect.bottom ); } // Is the item allowed in the given location? function isItemAllowed(item, location) { if (typeof item.getAllowedLocations !== 'function') return true; return item.getAllowedLocations().includes(location); } ================================================ FILE: src/electron-shims.js ================================================ const path = require('path'); const electron = require('electron'); const dirname = path.dirname; path.dirname = function(path) { if (typeof path !== 'string') { path = '' + path; const Grim = require('grim'); Grim.deprecate('Argument to `path.dirname` must be a string'); } return dirname(path); }; const extname = path.extname; path.extname = function(path) { if (typeof path !== 'string') { path = '' + path; const Grim = require('grim'); Grim.deprecate('Argument to `path.extname` must be a string'); } return extname(path); }; const basename = path.basename; path.basename = function(path, ext) { if ( typeof path !== 'string' || (ext !== undefined && typeof ext !== 'string') ) { path = '' + path; const Grim = require('grim'); Grim.deprecate('Arguments to `path.basename` must be strings'); } return basename(path, ext); }; electron.ipcRenderer.sendChannel = function() { const Grim = require('grim'); Grim.deprecate('Use `ipcRenderer.send` instead of `ipcRenderer.sendChannel`'); return this.send.apply(this, arguments); }; const remoteRequire = electron.remote.require; electron.remote.require = function(moduleName) { const Grim = require('grim'); switch (moduleName) { case 'menu': Grim.deprecate('Use `remote.Menu` instead of `remote.require("menu")`'); return this.Menu; case 'menu-item': Grim.deprecate( 'Use `remote.MenuItem` instead of `remote.require("menu-item")`' ); return this.MenuItem; case 'browser-window': Grim.deprecate( 'Use `remote.BrowserWindow` instead of `remote.require("browser-window")`' ); return this.BrowserWindow; case 'dialog': Grim.deprecate( 'Use `remote.Dialog` instead of `remote.require("dialog")`' ); return this.Dialog; case 'app': Grim.deprecate('Use `remote.app` instead of `remote.require("app")`'); return this.app; case 'crash-reporter': Grim.deprecate( 'Use `remote.crashReporter` instead of `remote.require("crashReporter")`' ); return this.crashReporter; case 'global-shortcut': Grim.deprecate( 'Use `remote.globalShortcut` instead of `remote.require("global-shortcut")`' ); return this.globalShortcut; case 'clipboard': Grim.deprecate( 'Use `remote.clipboard` instead of `remote.require("clipboard")`' ); return this.clipboard; case 'native-image': Grim.deprecate( 'Use `remote.nativeImage` instead of `remote.require("native-image")`' ); return this.nativeImage; case 'tray': Grim.deprecate('Use `remote.Tray` instead of `remote.require("tray")`'); return this.Tray; default: return remoteRequire.call(this, moduleName); } }; ================================================ FILE: src/file-system-blob-store.js ================================================ 'use strict'; const fs = require('fs-plus'); const path = require('path'); module.exports = class FileSystemBlobStore { static load(directory) { let instance = new FileSystemBlobStore(directory); instance.load(); return instance; } constructor(directory) { this.blobFilename = path.join(directory, 'BLOB'); this.blobMapFilename = path.join(directory, 'MAP'); this.lockFilename = path.join(directory, 'LOCK'); this.reset(); } reset() { this.inMemoryBlobs = new Map(); this.storedBlob = Buffer.alloc(0); this.storedBlobMap = {}; this.usedKeys = new Set(); } load() { if (!fs.existsSync(this.blobMapFilename)) { return; } if (!fs.existsSync(this.blobFilename)) { return; } try { this.storedBlob = fs.readFileSync(this.blobFilename); this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename)); } catch (e) { this.reset(); } } save() { let dump = this.getDump(); let blobToStore = Buffer.concat(dump[0]); let mapToStore = JSON.stringify(dump[1]); let acquiredLock = false; try { fs.writeFileSync(this.lockFilename, 'LOCK', { flag: 'wx' }); acquiredLock = true; fs.writeFileSync(this.blobFilename, blobToStore); fs.writeFileSync(this.blobMapFilename, mapToStore); } catch (error) { // Swallow the exception silently only if we fail to acquire the lock. if (error.code !== 'EEXIST') { throw error; } } finally { if (acquiredLock) { fs.unlinkSync(this.lockFilename); } } } has(key) { return ( this.inMemoryBlobs.has(key) || this.storedBlobMap.hasOwnProperty(key) ); } get(key) { if (this.has(key)) { this.usedKeys.add(key); return this.getFromMemory(key) || this.getFromStorage(key); } } set(key, buffer) { this.usedKeys.add(key); return this.inMemoryBlobs.set(key, buffer); } delete(key) { this.inMemoryBlobs.delete(key); delete this.storedBlobMap[key]; } getFromMemory(key) { return this.inMemoryBlobs.get(key); } getFromStorage(key) { if (!this.storedBlobMap[key]) { return; } return this.storedBlob.slice.apply( this.storedBlob, this.storedBlobMap[key] ); } getDump() { let buffers = []; let blobMap = {}; let currentBufferStart = 0; function dump(key, getBufferByKey) { let buffer = getBufferByKey(key); buffers.push(buffer); blobMap[key] = [currentBufferStart, currentBufferStart + buffer.length]; currentBufferStart += buffer.length; } for (let key of this.inMemoryBlobs.keys()) { if (this.usedKeys.has(key)) { dump(key, this.getFromMemory.bind(this)); } } for (let key of Object.keys(this.storedBlobMap)) { if (!blobMap[key] && this.usedKeys.has(key)) { dump(key, this.getFromStorage.bind(this)); } } return [buffers, blobMap]; } }; ================================================ FILE: src/first-mate-helpers.js ================================================ module.exports = { fromFirstMateScopeId(firstMateScopeId) { let atomScopeId = -firstMateScopeId; if ((atomScopeId & 1) === 0) atomScopeId--; return atomScopeId + 256; }, toFirstMateScopeId(atomScopeId) { return -(atomScopeId - 256); } }; ================================================ FILE: src/get-app-name.js ================================================ const { app } = require('electron'); const getReleaseChannel = require('./get-release-channel'); module.exports = function getAppName() { if (process.type === 'renderer') { return atom.getAppName(); } const releaseChannel = getReleaseChannel(app.getVersion()); const appNameParts = [app.getName()]; if (releaseChannel !== 'stable') { appNameParts.push( releaseChannel.charAt(0).toUpperCase() + releaseChannel.slice(1) ); } return appNameParts.join(' '); }; ================================================ FILE: src/get-release-channel.js ================================================ module.exports = function(version) { // This matches stable, dev (with or without commit hash) and any other // release channel following the pattern '1.00.0-channel0' const match = version.match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/); if (!match) { return 'unrecognized'; } else if (match[2]) { return match[2]; } return 'stable'; }; ================================================ FILE: src/get-window-load-settings.js ================================================ const { remote } = require('electron'); let windowLoadSettings = null; module.exports = () => { if (!windowLoadSettings) { windowLoadSettings = JSON.parse(remote.getCurrentWindow().loadSettingsJSON); } return windowLoadSettings; }; ================================================ FILE: src/git-repository-provider.js ================================================ const fs = require('fs'); const { Directory } = require('pathwatcher'); const GitRepository = require('./git-repository'); const GIT_FILE_REGEX = RegExp('^gitdir: (.+)'); // Returns the .gitdir path in the agnostic Git symlink .git file given, or // null if the path is not a valid gitfile. // // * `gitFile` {String} path of gitfile to parse function pathFromGitFileSync(gitFile) { try { const gitFileBuff = fs.readFileSync(gitFile, 'utf8'); return gitFileBuff != null ? gitFileBuff.match(GIT_FILE_REGEX)[1] : null; } catch (error) {} } // Returns a {Promise} that resolves to the .gitdir path in the agnostic // Git symlink .git file given, or null if the path is not a valid gitfile. // // * `gitFile` {String} path of gitfile to parse function pathFromGitFile(gitFile) { return new Promise(resolve => { fs.readFile(gitFile, 'utf8', (err, gitFileBuff) => { if (err == null && gitFileBuff != null) { const result = gitFileBuff.toString().match(GIT_FILE_REGEX); resolve(result != null ? result[1] : null); } else { resolve(null); } }); }); } // Checks whether a valid `.git` directory is contained within the given // directory or one of its ancestors. If so, a Directory that corresponds to the // `.git` folder will be returned. Otherwise, returns `null`. // // * `directory` {Directory} to explore whether it is part of a Git repository. function findGitDirectorySync(directory) { // TODO: Fix node-pathwatcher/src/directory.coffee so the following methods // can return cached values rather than always returning new objects: // getParent(), getFile(), getSubdirectory(). let gitDir = directory.getSubdirectory('.git'); if (typeof gitDir.getPath === 'function') { const gitDirPath = pathFromGitFileSync(gitDir.getPath()); if (gitDirPath) { gitDir = new Directory(directory.resolve(gitDirPath)); } } if ( typeof gitDir.existsSync === 'function' && gitDir.existsSync() && isValidGitDirectorySync(gitDir) ) { return gitDir; } else if (directory.isRoot()) { return null; } else { return findGitDirectorySync(directory.getParent()); } } // Checks whether a valid `.git` directory is contained within the given // directory or one of its ancestors. If so, a Directory that corresponds to the // `.git` folder will be returned. Otherwise, returns `null`. // // Returns a {Promise} that resolves to // * `directory` {Directory} to explore whether it is part of a Git repository. async function findGitDirectory(directory) { // TODO: Fix node-pathwatcher/src/directory.coffee so the following methods // can return cached values rather than always returning new objects: // getParent(), getFile(), getSubdirectory(). let gitDir = directory.getSubdirectory('.git'); if (typeof gitDir.getPath === 'function') { const gitDirPath = await pathFromGitFile(gitDir.getPath()); if (gitDirPath) { gitDir = new Directory(directory.resolve(gitDirPath)); } } if ( typeof gitDir.exists === 'function' && (await gitDir.exists()) && (await isValidGitDirectory(gitDir)) ) { return gitDir; } else if (directory.isRoot()) { return null; } else { return findGitDirectory(directory.getParent()); } } // Returns a boolean indicating whether the specified directory represents a Git // repository. // // * `directory` {Directory} whose base name is `.git`. function isValidGitDirectorySync(directory) { // To decide whether a directory has a valid .git folder, we use // the heuristic adopted by the valid_repository_path() function defined in // node_modules/git-utils/deps/libgit2/src/repository.c. const commonDirFile = directory.getSubdirectory('commondir'); let commonDir; if (commonDirFile.existsSync()) { const commonDirPathBuff = fs.readFileSync(commonDirFile.getPath()); const commonDirPathString = commonDirPathBuff.toString().trim(); commonDir = new Directory(directory.resolve(commonDirPathString)); if (!commonDir.existsSync()) { return false; } } else { commonDir = directory; } return ( directory.getFile('HEAD').existsSync() && commonDir.getSubdirectory('objects').existsSync() && commonDir.getSubdirectory('refs').existsSync() ); } // Returns a {Promise} that resolves to a {Boolean} indicating whether the // specified directory represents a Git repository. // // * `directory` {Directory} whose base name is `.git`. async function isValidGitDirectory(directory) { // To decide whether a directory has a valid .git folder, we use // the heuristic adopted by the valid_repository_path() function defined in // node_modules/git-utils/deps/libgit2/src/repository.c. const commonDirFile = directory.getSubdirectory('commondir'); let commonDir; if (await commonDirFile.exists()) { const commonDirPathBuff = await fs.readFile(commonDirFile.getPath()); const commonDirPathString = commonDirPathBuff.toString().trim(); commonDir = new Directory(directory.resolve(commonDirPathString)); if (!(await commonDir.exists())) { return false; } } else { commonDir = directory; } return ( (await directory.getFile('HEAD').exists()) && (await commonDir.getSubdirectory('objects').exists()) && commonDir.getSubdirectory('refs').exists() ); } // Provider that conforms to the atom.repository-provider@0.1.0 service. class GitRepositoryProvider { constructor(project, config) { // Keys are real paths that end in `.git`. // Values are the corresponding GitRepository objects. this.project = project; this.config = config; this.pathToRepository = {}; } // Returns a {Promise} that resolves with either: // * {GitRepository} if the given directory has a Git repository. // * `null` if the given directory does not have a Git repository. async repositoryForDirectory(directory) { // Only one GitRepository should be created for each .git folder. Therefore, // we must check directory and its parent directories to find the nearest // .git folder. const gitDir = await findGitDirectory(directory); return this.repositoryForGitDirectory(gitDir); } // Returns either: // * {GitRepository} if the given directory has a Git repository. // * `null` if the given directory does not have a Git repository. repositoryForDirectorySync(directory) { // Only one GitRepository should be created for each .git folder. Therefore, // we must check directory and its parent directories to find the nearest // .git folder. const gitDir = findGitDirectorySync(directory); return this.repositoryForGitDirectory(gitDir); } // Returns either: // * {GitRepository} if the given Git directory has a Git repository. // * `null` if the given directory does not have a Git repository. repositoryForGitDirectory(gitDir) { if (!gitDir) { return null; } const gitDirPath = gitDir.getPath(); let repo = this.pathToRepository[gitDirPath]; if (!repo) { repo = GitRepository.open(gitDirPath, { project: this.project, config: this.config }); if (!repo) { return null; } repo.onDidDestroy(() => delete this.pathToRepository[gitDirPath]); this.pathToRepository[gitDirPath] = repo; repo.refreshIndex(); repo.refreshStatus(); } return repo; } } module.exports = GitRepositoryProvider; ================================================ FILE: src/git-repository.js ================================================ const path = require('path'); const fs = require('fs-plus'); const _ = require('underscore-plus'); const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); 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} 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(filePath) { if (!filePath) return false; const repo = this.getRepo(filePath); if (repo.isSubmodule(repo.relativize(filePath))) { return true; } else { // Check if the filePath is a working directory in a repo that isn't the root. return ( repo !== this.getRepo() && repo.relativize(path.join(filePath, 'dir')) === 'dir' ); } } // 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'); } }; ================================================ FILE: src/grammar-registry.js ================================================ const _ = require('underscore-plus'); const Grim = require('grim'); const CSON = require('season'); const FirstMate = require('first-mate'); const { Disposable, CompositeDisposable } = require('event-kit'); const TextMateLanguageMode = require('./text-mate-language-mode'); const TreeSitterLanguageMode = require('./tree-sitter-language-mode'); const TreeSitterGrammar = require('./tree-sitter-grammar'); const ScopeDescriptor = require('./scope-descriptor'); const Token = require('./token'); const fs = require('fs-plus'); const { Point, Range } = require('text-buffer'); const PATH_SPLIT_REGEX = new RegExp('[/.]'); // Extended: This class holds the grammars used for tokenizing. // // An instance of this class is always available as the `atom.grammars` global. module.exports = class GrammarRegistry { constructor({ config } = {}) { this.config = config; this.subscriptions = new CompositeDisposable(); this.textmateRegistry = new FirstMate.GrammarRegistry({ maxTokensPerLine: 100, maxLineLength: 1000 }); this.clear(); } clear() { this.textmateRegistry.clear(); this.treeSitterGrammarsById = {}; if (this.subscriptions) this.subscriptions.dispose(); this.subscriptions = new CompositeDisposable(); this.languageOverridesByBufferId = new Map(); this.grammarScoresByBuffer = new Map(); this.textMateScopeNamesByTreeSitterLanguageId = new Map(); this.treeSitterLanguageIdsByTextMateScopeName = new Map(); const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this); this.textmateRegistry.onDidAddGrammar(grammarAddedOrUpdated); this.textmateRegistry.onDidUpdateGrammar(grammarAddedOrUpdated); this.subscriptions.add( this.config.onDidChange('core.useTreeSitterParsers', () => { this.grammarScoresByBuffer.forEach((score, buffer) => { if (!this.languageOverridesByBufferId.has(buffer.id)) { this.autoAssignLanguageMode(buffer); } }); }) ); } serialize() { const languageOverridesByBufferId = {}; this.languageOverridesByBufferId.forEach((languageId, bufferId) => { languageOverridesByBufferId[bufferId] = languageId; }); return { languageOverridesByBufferId }; } deserialize(params) { for (const bufferId in params.languageOverridesByBufferId || {}) { this.languageOverridesByBufferId.set( bufferId, params.languageOverridesByBufferId[bufferId] ); } } createToken(value, scopes) { return new Token({ value, scopes }); } // Extended: set a {TextBuffer}'s language mode based on its path and content, // and continue to update its language mode as grammars are added or updated, or // the buffer's file path changes. // // * `buffer` The {TextBuffer} whose language mode will be maintained. // // Returns a {Disposable} that can be used to stop updating the buffer's // language mode. maintainLanguageMode(buffer) { this.grammarScoresByBuffer.set(buffer, null); const languageOverride = this.languageOverridesByBufferId.get(buffer.id); if (languageOverride) { this.assignLanguageMode(buffer, languageOverride); } else { this.autoAssignLanguageMode(buffer); } const pathChangeSubscription = buffer.onDidChangePath(() => { this.grammarScoresByBuffer.delete(buffer); if (!this.languageOverridesByBufferId.has(buffer.id)) { this.autoAssignLanguageMode(buffer); } }); const destroySubscription = buffer.onDidDestroy(() => { this.grammarScoresByBuffer.delete(buffer); this.languageOverridesByBufferId.delete(buffer.id); this.subscriptions.remove(destroySubscription); this.subscriptions.remove(pathChangeSubscription); }); this.subscriptions.add(pathChangeSubscription, destroySubscription); return new Disposable(() => { destroySubscription.dispose(); pathChangeSubscription.dispose(); this.subscriptions.remove(pathChangeSubscription); this.subscriptions.remove(destroySubscription); this.grammarScoresByBuffer.delete(buffer); this.languageOverridesByBufferId.delete(buffer.id); }); } // Extended: Force a {TextBuffer} to use a different grammar than the // one that would otherwise be selected for it. // // * `buffer` The {TextBuffer} whose grammar will be set. // * `languageId` The {String} id of the desired language. // // Returns a {Boolean} that indicates whether the language was successfully // found. assignLanguageMode(buffer, languageId) { if (buffer.getBuffer) buffer = buffer.getBuffer(); let grammar = null; if (languageId != null) { grammar = this.grammarForId(languageId); if (!grammar) return false; this.languageOverridesByBufferId.set(buffer.id, languageId); } else { this.languageOverridesByBufferId.set(buffer.id, null); grammar = this.textmateRegistry.nullGrammar; } this.grammarScoresByBuffer.set(buffer, null); if (grammar !== buffer.getLanguageMode().grammar) { buffer.setLanguageMode( this.languageModeForGrammarAndBuffer(grammar, buffer) ); } return true; } // Extended: Force a {TextBuffer} to use a different grammar than the // one that would otherwise be selected for it. // // * `buffer` The {TextBuffer} whose grammar will be set. // * `grammar` The desired {Grammar}. // // Returns a {Boolean} that indicates whether the assignment was successful assignGrammar(buffer, grammar) { if (!grammar) return false; if (buffer.getBuffer) buffer = buffer.getBuffer(); this.languageOverridesByBufferId.set(buffer.id, grammar.scopeName || null); this.grammarScoresByBuffer.set(buffer, null); if (grammar !== buffer.getLanguageMode().grammar) { buffer.setLanguageMode( this.languageModeForGrammarAndBuffer(grammar, buffer) ); } return true; } // Extended: Get the `languageId` that has been explicitly assigned to // the given buffer, if any. // // Returns a {String} id of the language getAssignedLanguageId(buffer) { return this.languageOverridesByBufferId.get(buffer.id); } // Extended: Remove any language mode override that has been set for the // given {TextBuffer}. This will assign to the buffer the best language // mode available. // // * `buffer` The {TextBuffer}. autoAssignLanguageMode(buffer) { const result = this.selectGrammarWithScore( buffer.getPath(), getGrammarSelectionContent(buffer) ); this.languageOverridesByBufferId.delete(buffer.id); this.grammarScoresByBuffer.set(buffer, result.score); if (result.grammar !== buffer.getLanguageMode().grammar) { buffer.setLanguageMode( this.languageModeForGrammarAndBuffer(result.grammar, buffer) ); } } languageModeForGrammarAndBuffer(grammar, buffer) { if (grammar instanceof TreeSitterGrammar) { return new TreeSitterLanguageMode({ grammar, buffer, config: this.config, grammars: this }); } else { return new TextMateLanguageMode({ grammar, buffer, config: this.config }); } } // 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; this.forEachGrammar(grammar => { 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'); } // Initially identify matching grammars based on the filename and the first // line of the file. let score = this.getGrammarPathScore(grammar, filePath); if (this.grammarMatchesPrefix(grammar, contents)) score += 0.5; // If multiple grammars match by one of the above criteria, break ties. if (score > 0) { const isTreeSitter = grammar instanceof TreeSitterGrammar; // Prefer either TextMate or Tree-sitter grammars based on the user's settings. if (isTreeSitter) { if (this.shouldUseTreeSitterParser(grammar.scopeName)) { score += 0.1; } else { return -Infinity; } } // Prefer grammars with matching content regexes. Prefer a grammar with no content regex // over one with a non-matching content regex. if (grammar.contentRegex) { const contentMatch = isTreeSitter ? grammar.contentRegex.test(contents) : grammar.contentRegex.testSync(contents); if (contentMatch) { score += 0.05; } else { score -= 0.05; } } // Prefer grammars that the user has manually installed over bundled grammars. if (!grammar.bundledPackage) score += 0.01; } return score; } getGrammarPathScore(grammar, filePath) { if (!filePath) return -1; if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/'); } const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX); let pathScore = 0; 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(PATH_SPLIT_REGEX); 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; } grammarMatchesPrefix(grammar, contents) { if (contents && grammar.firstLineRegex) { 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 prefix = contents .split('\n') .slice(0, numberOfNewlinesInRegex + 1) .join('\n'); if (grammar.firstLineRegex.testSync) { return grammar.firstLineRegex.testSync(prefix); } else { return grammar.firstLineRegex.test(prefix); } } else { return false; } } forEachGrammar(callback) { this.getGrammars({ includeTreeSitter: true }).forEach(callback); } grammarForId(languageId) { if (!languageId) return null; if (this.shouldUseTreeSitterParser(languageId)) { return ( this.treeSitterGrammarsById[languageId] || this.textmateRegistry.grammarForScopeName(languageId) ); } else { return ( this.textmateRegistry.grammarForScopeName(languageId) || this.treeSitterGrammarsById[languageId] ); } } // 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 buffer.getLanguageMode().getLanguageId() instead'); const buffer = atom.project.findBufferForPath(filePath); if (buffer) return this.getAssignedLanguageId(buffer); } // Deprecated: Set the grammar override for the given file path. // // * `filePath` A non-empty {String} file path. // * `languageId` A {String} such as `"source.js"`. // // Returns undefined. setGrammarOverrideForPath(filePath, languageId) { Grim.deprecate( 'Use atom.grammars.assignLanguageMode(buffer, languageId) instead' ); const buffer = atom.project.findBufferForPath(filePath); if (buffer) { const grammar = this.grammarForScopeName(languageId); if (grammar) this.languageOverridesByBufferId.set(buffer.id, grammar.name); } } // Remove the grammar override for the given file path. // // * `filePath` A {String} file path. // // Returns undefined. clearGrammarOverrideForPath(filePath) { Grim.deprecate('Use atom.grammars.autoAssignLanguageMode(buffer) instead'); const buffer = atom.project.findBufferForPath(filePath); if (buffer) this.languageOverridesByBufferId.delete(buffer.id); } grammarAddedOrUpdated(grammar) { if (grammar.scopeName && !grammar.id) grammar.id = grammar.scopeName; this.grammarScoresByBuffer.forEach((score, buffer) => { const languageMode = buffer.getLanguageMode(); const languageOverride = this.languageOverridesByBufferId.get(buffer.id); if ( grammar === buffer.getLanguageMode().grammar || grammar === this.grammarForId(languageOverride) ) { buffer.setLanguageMode( this.languageModeForGrammarAndBuffer(grammar, buffer) ); return; } else if (!languageOverride) { const score = this.getGrammarScore( grammar, buffer.getPath(), getGrammarSelectionContent(buffer) ); const currentScore = this.grammarScoresByBuffer.get(buffer); if (currentScore == null || score > currentScore) { buffer.setLanguageMode( this.languageModeForGrammarAndBuffer(grammar, buffer) ); this.grammarScoresByBuffer.set(buffer, score); return; } } languageMode.updateForInjection(grammar); }); } // Extended: Invoke the given callback when a grammar is added to the registry. // // * `callback` {Function} to call when a grammar is added. // * `grammar` {Grammar} that was added. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddGrammar(callback) { return this.textmateRegistry.onDidAddGrammar(callback); } // Extended: Invoke the given callback when a grammar is updated due to a grammar // it depends on being added or removed from the registry. // // * `callback` {Function} to call when a grammar is updated. // * `grammar` {Grammar} that was updated. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidUpdateGrammar(callback) { return this.textmateRegistry.onDidUpdateGrammar(callback); } // Experimental: Specify a type of syntax node that may embed other languages. // // * `grammarId` The {String} id of the parent language // * `injectionPoint` An {Object} with the following keys: // * `type` The {String} type of syntax node that may embed other languages // * `language` A {Function} that is called with syntax nodes of the specified `type` and // returns a {String} that will be tested against other grammars' `injectionRegex` in // order to determine what language should be embedded. // * `content` A {Function} that is called with syntax nodes of the specified `type` and // returns another syntax node or array of syntax nodes that contain the embedded source code. addInjectionPoint(grammarId, injectionPoint) { const grammar = this.treeSitterGrammarsById[grammarId]; if (grammar) { if (grammar.addInjectionPoint) { grammar.addInjectionPoint(injectionPoint); } else { grammar.injectionPoints.push(injectionPoint); } } else { this.treeSitterGrammarsById[grammarId] = { injectionPoints: [injectionPoint] }; } return new Disposable(() => { const grammar = this.treeSitterGrammarsById[grammarId]; grammar.removeInjectionPoint(injectionPoint); }); } get nullGrammar() { return this.textmateRegistry.nullGrammar; } get grammars() { return this.getGrammars(); } decodeTokens() { return this.textmateRegistry.decodeTokens.apply( this.textmateRegistry, arguments ); } grammarForScopeName(scopeName) { return this.grammarForId(scopeName); } addGrammar(grammar) { if (grammar instanceof TreeSitterGrammar) { const existingParams = this.treeSitterGrammarsById[grammar.scopeName] || {}; if (grammar.scopeName) this.treeSitterGrammarsById[grammar.scopeName] = grammar; if (existingParams.injectionPoints) { for (const injectionPoint of existingParams.injectionPoints) { grammar.addInjectionPoint(injectionPoint); } } this.grammarAddedOrUpdated(grammar); return new Disposable(() => this.removeGrammar(grammar)); } else { return this.textmateRegistry.addGrammar(grammar); } } removeGrammar(grammar) { if (grammar instanceof TreeSitterGrammar) { delete this.treeSitterGrammarsById[grammar.scopeName]; } else { return this.textmateRegistry.removeGrammar(grammar); } } removeGrammarForScopeName(scopeName) { return this.textmateRegistry.removeGrammarForScopeName(scopeName); } // Extended: Read a grammar asynchronously and add it to the registry. // // * `grammarPath` A {String} absolute file path to a grammar file. // * `callback` A {Function} to call when loaded with the following arguments: // * `error` An {Error}, may be null. // * `grammar` A {Grammar} or null if an error occurred. loadGrammar(grammarPath, callback) { this.readGrammar(grammarPath, (error, grammar) => { if (error) return callback(error); this.addGrammar(grammar); callback(null, grammar); }); } // Extended: Read a grammar synchronously and add it to this registry. // // * `grammarPath` A {String} absolute file path to a grammar file. // // Returns a {Grammar}. loadGrammarSync(grammarPath) { const grammar = this.readGrammarSync(grammarPath); this.addGrammar(grammar); return grammar; } // Extended: Read a grammar asynchronously but don't add it to the registry. // // * `grammarPath` A {String} absolute file path to a grammar file. // * `callback` A {Function} to call when read with the following arguments: // * `error` An {Error}, may be null. // * `grammar` A {Grammar} or null if an error occurred. // // Returns undefined. readGrammar(grammarPath, callback) { if (!callback) callback = () => {}; CSON.readFile(grammarPath, (error, params = {}) => { if (error) return callback(error); try { callback(null, this.createGrammar(grammarPath, params)); } catch (error) { callback(error); } }); } // Extended: Read a grammar synchronously but don't add it to the registry. // // * `grammarPath` A {String} absolute file path to a grammar file. // // Returns a {Grammar}. readGrammarSync(grammarPath) { return this.createGrammar( grammarPath, CSON.readFileSync(grammarPath) || {} ); } createGrammar(grammarPath, params) { if (params.type === 'tree-sitter') { return new TreeSitterGrammar(this, grammarPath, params); } else { if ( typeof params.scopeName !== 'string' || params.scopeName.length === 0 ) { throw new Error( `Grammar missing required scopeName property: ${grammarPath}` ); } return this.textmateRegistry.createGrammar(grammarPath, params); } } // Extended: Get all the grammars in this registry. // // * `options` (optional) {Object} // * `includeTreeSitter` (optional) {Boolean} Set to include // [Tree-sitter](https://github.blog/2018-10-31-atoms-new-parsing-system/) grammars // // Returns a non-empty {Array} of {Grammar} instances. getGrammars(params) { let tmGrammars = this.textmateRegistry.getGrammars(); if (!(params && params.includeTreeSitter)) return tmGrammars; const tsGrammars = Object.values(this.treeSitterGrammarsById).filter( g => g.scopeName ); return tmGrammars.concat(tsGrammars); // NullGrammar is expected to be first } scopeForId(id) { return this.textmateRegistry.scopeForId(id); } treeSitterGrammarForLanguageString(languageString) { let longestMatchLength = 0; let grammarWithLongestMatch = null; for (const id in this.treeSitterGrammarsById) { const grammar = this.treeSitterGrammarsById[id]; if (grammar.injectionRegex) { const match = languageString.match(grammar.injectionRegex); if (match) { const { length } = match[0]; if (length > longestMatchLength) { grammarWithLongestMatch = grammar; longestMatchLength = length; } } } } return grammarWithLongestMatch; } shouldUseTreeSitterParser(languageId) { return this.config.get('core.useTreeSitterParsers', { scope: new ScopeDescriptor({ scopes: [languageId] }) }); } }; function getGrammarSelectionContent(buffer) { return buffer.getTextInRange( Range(Point(0, 0), buffer.positionForCharacterIndex(1024)) ); } ================================================ FILE: src/gutter-container.js ================================================ 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.type === 'line-number') { options.type = 'line-number'; } else { options.type = 'gutter'; } options.gutterName = gutter.name; return this.textEditor.decorateMarker(marker, options); } }; ================================================ FILE: src/gutter.js ================================================ 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.type = options && options.type != null ? options.type : 'decorated'; this.labelFn = options && options.labelFn; this.className = options && options.class; this.onMouseDown = options && options.onMouseDown; this.onMouseMove = options && options.onMouseMove; 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; } }; ================================================ FILE: src/history-manager.js ================================================ const { Emitter, CompositeDisposable } = require('event-kit'); // Extended: History manager for remembering which projects have been opened. // // An instance of this class is always available as the `atom.history` global. // // The project history is used to enable the 'Reopen Project' menu. class HistoryManager { constructor({ project, commands, stateStore }) { this.stateStore = stateStore; this.emitter = new Emitter(); this.projects = []; this.disposables = new CompositeDisposable(); this.disposables.add( commands.add( 'atom-workspace', { 'application:clear-project-history': this.clearProjects.bind(this) }, false ) ); this.disposables.add( project.onDidChangePaths(projectPaths => this.addProject(projectPaths)) ); } destroy() { this.disposables.dispose(); } // Public: Obtain a list of previously opened projects. // // Returns an {Array} of {HistoryProject} objects, most recent first. getProjects() { return this.projects.map(p => new HistoryProject(p.paths, p.lastOpened)); } // Public: Clear all projects from the history. // // Note: This is not a privacy function - other traces will still exist, // e.g. window state. // // Return a {Promise} that resolves when the history has been successfully // cleared. async clearProjects() { this.projects = []; await this.saveState(); this.didChangeProjects(); } // Public: Invoke the given callback when the list of projects changes. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeProjects(callback) { return this.emitter.on('did-change-projects', callback); } didChangeProjects(args = { reloaded: false }) { this.emitter.emit('did-change-projects', args); } async addProject(paths, lastOpened) { if (paths.length === 0) return; let project = this.getProject(paths); if (!project) { project = new HistoryProject(paths); this.projects.push(project); } project.lastOpened = lastOpened || new Date(); this.projects.sort((a, b) => b.lastOpened - a.lastOpened); await this.saveState(); this.didChangeProjects(); } async removeProject(paths) { if (paths.length === 0) return; let project = this.getProject(paths); if (!project) return; let index = this.projects.indexOf(project); this.projects.splice(index, 1); await this.saveState(); this.didChangeProjects(); } getProject(paths) { for (let i = 0; i < this.projects.length; i++) { if (arrayEquivalent(paths, this.projects[i].paths)) { return this.projects[i]; } } return null; } async loadState() { const history = await this.stateStore.load('history-manager'); if (history && history.projects) { this.projects = history.projects .filter(p => Array.isArray(p.paths) && p.paths.length > 0) .map(p => new HistoryProject(p.paths, new Date(p.lastOpened))); this.didChangeProjects({ reloaded: true }); } else { this.projects = []; } } async saveState() { const projects = this.projects.map(p => ({ paths: p.paths, lastOpened: p.lastOpened })); await this.stateStore.save('history-manager', { projects }); } } function arrayEquivalent(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } class HistoryProject { constructor(paths, lastOpened) { this.paths = paths; this.lastOpened = lastOpened || new Date(); } set paths(paths) { this._paths = paths; } get paths() { return this._paths; } set lastOpened(lastOpened) { this._lastOpened = lastOpened; } get lastOpened() { return this._lastOpened; } } module.exports = { HistoryManager, HistoryProject }; ================================================ FILE: src/initialize-application-window.js ================================================ const AtomEnvironment = require('./atom-environment'); const ApplicationDelegate = require('./application-delegate'); const Clipboard = require('./clipboard'); const TextEditor = require('./text-editor'); require('./text-editor-component'); require('./file-system-blob-store'); require('./native-compile-cache'); require('./compile-cache'); require('./module-cache'); if (global.isGeneratingSnapshot) { require('about'); require('archive-view'); require('autocomplete-atom-api'); require('autocomplete-css'); require('autocomplete-html'); require('autocomplete-plus'); require('autocomplete-snippets'); require('autoflow'); require('autosave'); require('background-tips'); require('bookmarks'); require('bracket-matcher'); require('command-palette'); require('deprecation-cop'); require('dev-live-reload'); require('encoding-selector'); require('exception-reporting'); require('dalek'); require('find-and-replace'); require('fuzzy-finder'); require('github'); require('git-diff'); require('go-to-line'); require('grammar-selector'); require('image-view'); require('incompatible-packages'); require('keybinding-resolver'); require('language-c'); require('language-html'); require('language-javascript'); require('language-ruby'); require('language-rust-bundled'); require('language-typescript'); require('line-ending-selector'); require('link'); require('markdown-preview'); require('metrics'); require('notifications'); require('open-on-github'); require('package-generator'); require('settings-view'); require('snippets'); require('spell-check'); require('status-bar'); require('styleguide'); require('symbols-view'); require('tabs'); require('timecop'); require('tree-view'); require('update-package-dependencies'); require('welcome'); require('whitespace'); require('wrap-guide'); } const clipboard = new Clipboard(); TextEditor.setClipboard(clipboard); TextEditor.viewForItem = item => atom.views.getView(item); global.atom = new AtomEnvironment({ clipboard, applicationDelegate: new ApplicationDelegate(), enablePersistence: true }); TextEditor.setScheduler(global.atom.views); global.atom.preloadPackages(); // Like sands through the hourglass, so are the days of our lives. module.exports = function({ blobStore }) { const { updateProcessEnv } = require('./update-process-env'); const path = require('path'); require('./window'); const getWindowLoadSettings = require('./get-window-load-settings'); const { ipcRenderer } = require('electron'); const { resourcePath, devMode } = getWindowLoadSettings(); require('./electron-shims'); // Add application-specific exports to module search path. const exportsPath = path.join(resourcePath, 'exports'); require('module').globalPaths.push(exportsPath); process.env.NODE_PATH = exportsPath; // Make React faster if (!devMode && process.env.NODE_ENV == null) { process.env.NODE_ENV = 'production'; } global.atom.initialize({ window, document, blobStore, configDirPath: process.env.ATOM_HOME, env: process.env }); return global.atom.startEditorWindow().then(function() { // Workaround for focus getting cleared upon window creation const windowFocused = function() { window.removeEventListener('focus', windowFocused); setTimeout(() => document.querySelector('atom-workspace').focus(), 0); }; window.addEventListener('focus', windowFocused); ipcRenderer.on('environment', (event, env) => updateProcessEnv(env)); }); }; ================================================ FILE: src/initialize-benchmark-window.js ================================================ const { remote } = require('electron'); const path = require('path'); const ipcHelpers = require('./ipc-helpers'); const util = require('util'); module.exports = async function() { const getWindowLoadSettings = require('./get-window-load-settings'); const { test, headless, resourcePath, benchmarkPaths } = getWindowLoadSettings(); try { const Clipboard = require('../src/clipboard'); const ApplicationDelegate = require('../src/application-delegate'); const AtomEnvironment = require('../src/atom-environment'); const TextEditor = require('../src/text-editor'); require('./electron-shims'); const exportsPath = path.join(resourcePath, 'exports'); require('module').globalPaths.push(exportsPath); // Add 'exports' to module search path. process.env.NODE_PATH = exportsPath; // Set NODE_PATH env variable since tasks may need it. document.title = 'Benchmarks'; // Allow `document.title` to be assigned in benchmarks without actually changing the window title. let documentTitle = null; Object.defineProperty(document, 'title', { get() { return documentTitle; }, set(title) { documentTitle = title; } }); window.addEventListener( 'keydown', event => { // Reload: cmd-r / ctrl-r if ((event.metaKey || event.ctrlKey) && event.keyCode === 82) { ipcHelpers.call('window-method', 'reload'); } // Toggle Dev Tools: cmd-alt-i (Mac) / ctrl-shift-i (Linux/Windows) if (event.keyCode === 73) { const isDarwin = process.platform === 'darwin'; if ( (isDarwin && event.metaKey && event.altKey) || (!isDarwin && event.ctrlKey && event.shiftKey) ) { ipcHelpers.call('window-method', 'toggleDevTools'); } } // Close: cmd-w / ctrl-w if ((event.metaKey || event.ctrlKey) && event.keyCode === 87) { ipcHelpers.call('window-method', 'close'); } // Copy: cmd-c / ctrl-c if ((event.metaKey || event.ctrlKey) && event.keyCode === 67) { ipcHelpers.call('window-method', 'copy'); } }, { capture: true } ); const clipboard = new Clipboard(); TextEditor.setClipboard(clipboard); TextEditor.viewForItem = item => atom.views.getView(item); const applicationDelegate = new ApplicationDelegate(); const environmentParams = { applicationDelegate, window, document, clipboard, configDirPath: process.env.ATOM_HOME, enablePersistence: false }; global.atom = new AtomEnvironment(environmentParams); global.atom.initialize(environmentParams); // Prevent benchmarks from modifying application menus global.atom.menu.sendToBrowserProcess = function() {}; if (headless) { Object.defineProperties(process, { stdout: { value: remote.process.stdout }, stderr: { value: remote.process.stderr } }); console.log = function(...args) { const formatted = util.format(...args); process.stdout.write(formatted + '\n'); }; console.warn = function(...args) { const formatted = util.format(...args); process.stderr.write(formatted + '\n'); }; console.error = function(...args) { const formatted = util.format(...args); process.stderr.write(formatted + '\n'); }; } else { remote.getCurrentWindow().show(); } const benchmarkRunner = require('../benchmarks/benchmark-runner'); const statusCode = await benchmarkRunner({ test, benchmarkPaths }); if (headless) { exitWithStatusCode(statusCode); } } catch (error) { if (headless) { console.error(error.stack || error); exitWithStatusCode(1); } else { ipcHelpers.call('window-method', 'openDevTools'); throw error; } } }; function exitWithStatusCode(statusCode) { remote.app.emit('will-quit'); remote.process.exit(statusCode); } ================================================ FILE: src/initialize-test-window.js ================================================ const ipcHelpers = require('./ipc-helpers'); const { requireModule } = require('./module-utils'); function cloneObject(object) { const clone = {}; for (const key in object) { clone[key] = object[key]; } return clone; } module.exports = async function({ blobStore }) { const { remote } = require('electron'); const getWindowLoadSettings = require('./get-window-load-settings'); const exitWithStatusCode = function(status) { remote.app.emit('will-quit'); remote.process.exit(status); }; try { const path = require('path'); const { ipcRenderer } = require('electron'); const CompileCache = require('./compile-cache'); const AtomEnvironment = require('../src/atom-environment'); const ApplicationDelegate = require('../src/application-delegate'); const Clipboard = require('../src/clipboard'); const TextEditor = require('../src/text-editor'); const { updateProcessEnv } = require('./update-process-env'); require('./electron-shims'); ipcRenderer.on('environment', (event, env) => updateProcessEnv(env)); const { testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths, env } = getWindowLoadSettings(); if (headless) { // Install console functions that output to stdout and stderr. const util = require('util'); Object.defineProperties(process, { stdout: { value: remote.process.stdout }, stderr: { value: remote.process.stderr } }); console.log = (...args) => process.stdout.write(`${util.format(...args)}\n`); console.error = (...args) => process.stderr.write(`${util.format(...args)}\n`); } else { // Show window synchronously so a focusout doesn't fire on input elements // that are focused in the very first spec run. remote.getCurrentWindow().show(); } const handleKeydown = function(event) { // Reload: cmd-r / ctrl-r if ((event.metaKey || event.ctrlKey) && event.keyCode === 82) { ipcHelpers.call('window-method', 'reload'); } // Toggle Dev Tools: cmd-alt-i (Mac) / ctrl-shift-i (Linux/Windows) if ( event.keyCode === 73 && ((process.platform === 'darwin' && event.metaKey && event.altKey) || (process.platform !== 'darwin' && event.ctrlKey && event.shiftKey)) ) { ipcHelpers.call('window-method', 'toggleDevTools'); } // Close: cmd-w / ctrl-w if ((event.metaKey || event.ctrlKey) && event.keyCode === 87) { ipcHelpers.call('window-method', 'close'); } // Copy: cmd-c / ctrl-c if ((event.metaKey || event.ctrlKey) && event.keyCode === 67) { atom.clipboard.write(window.getSelection().toString()); } }; window.addEventListener('keydown', handleKeydown, { capture: true }); // Add 'exports' to module search path. const exportsPath = path.join( getWindowLoadSettings().resourcePath, 'exports' ); require('module').globalPaths.push(exportsPath); process.env.NODE_PATH = exportsPath; // Set NODE_PATH env variable since tasks may need it. updateProcessEnv(env); // Set up optional transpilation for packages under test if any const FindParentDir = require('find-parent-dir'); const packageRoot = FindParentDir.sync(testPaths[0], 'package.json'); if (packageRoot) { const packageMetadata = require(path.join(packageRoot, 'package.json')); if (packageMetadata.atomTranspilers) { CompileCache.addTranspilerConfigForPath( packageRoot, packageMetadata.name, packageMetadata, packageMetadata.atomTranspilers ); } } document.title = 'Spec Suite'; const clipboard = new Clipboard(); TextEditor.setClipboard(clipboard); TextEditor.viewForItem = item => atom.views.getView(item); const testRunner = requireModule(testRunnerPath); const legacyTestRunner = require(legacyTestRunnerPath); const buildDefaultApplicationDelegate = () => new ApplicationDelegate(); const buildAtomEnvironment = function(params) { params = cloneObject(params); if (!params.hasOwnProperty('clipboard')) { params.clipboard = clipboard; } if (!params.hasOwnProperty('blobStore')) { params.blobStore = blobStore; } if (!params.hasOwnProperty('onlyLoadBaseStyleSheets')) { params.onlyLoadBaseStyleSheets = true; } const atomEnvironment = new AtomEnvironment(params); atomEnvironment.initialize(params); TextEditor.setScheduler(atomEnvironment.views); return atomEnvironment; }; const statusCode = await testRunner({ logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner }); if (getWindowLoadSettings().headless) { exitWithStatusCode(statusCode); } } catch (error) { if (getWindowLoadSettings().headless) { console.error(error.stack || error); exitWithStatusCode(1); } else { throw error; } } }; ================================================ FILE: src/ipc-helpers.js ================================================ const Disposable = require('event-kit').Disposable; let ipcRenderer = null; let ipcMain = null; let BrowserWindow = null; let nextResponseChannelId = 0; exports.on = function(emitter, eventName, callback) { emitter.on(eventName, callback); return new Disposable(() => emitter.removeListener(eventName, callback)); }; exports.call = function(channel, ...args) { if (!ipcRenderer) { ipcRenderer = require('electron').ipcRenderer; ipcRenderer.setMaxListeners(20); } const responseChannel = `ipc-helpers-response-${nextResponseChannelId++}`; return new Promise(resolve => { ipcRenderer.on(responseChannel, (event, result) => { ipcRenderer.removeAllListeners(responseChannel); resolve(result); }); ipcRenderer.send(channel, responseChannel, ...args); }); }; exports.respondTo = function(channel, callback) { if (!ipcMain) { const electron = require('electron'); ipcMain = electron.ipcMain; BrowserWindow = electron.BrowserWindow; } return exports.on( ipcMain, channel, async (event, responseChannel, ...args) => { const browserWindow = BrowserWindow.fromWebContents(event.sender); const result = await callback(browserWindow, ...args); if (!event.sender.isDestroyed()) { event.sender.send(responseChannel, result); } } ); }; ================================================ FILE: src/item-registry.js ================================================ module.exports = class ItemRegistry { constructor() { this.items = new WeakSet(); } addItem(item) { if (this.hasItem(item)) { throw new Error( `The workspace can only contain one instance of item ${item}` ); } return this.items.add(item); } removeItem(item) { return this.items.delete(item); } hasItem(item) { return this.items.has(item); } }; ================================================ FILE: src/keymap-extensions.coffee ================================================ fs = require 'fs-plus' path = require 'path' KeymapManager = require 'atom-keymap' CSON = require 'season' bundledKeymaps = require('../package.json')?._atomKeymaps KeymapManager::onDidLoadBundledKeymaps = (callback) -> @emitter.on 'did-load-bundled-keymaps', callback KeymapManager::onDidLoadUserKeymap = (callback) -> @emitter.on 'did-load-user-keymap', callback KeymapManager::canLoadBundledKeymapsFromMemory = -> bundledKeymaps? KeymapManager::loadBundledKeymaps = -> if bundledKeymaps? for keymapName, keymap of bundledKeymaps keymapPath = "core:#{keymapName}" @add(keymapPath, keymap, 0, @devMode ? false) else keymapsPath = path.join(@resourcePath, 'keymaps') @loadKeymap(keymapsPath) @emitter.emit 'did-load-bundled-keymaps' KeymapManager::getUserKeymapPath = -> return "" unless @configDirPath? if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap')) userKeymapPath else path.join(@configDirPath, 'keymap.cson') KeymapManager::loadUserKeymap = -> userKeymapPath = @getUserKeymapPath() return unless fs.isFileSync(userKeymapPath) try @loadKeymap(userKeymapPath, watch: true, suppressErrors: true, priority: 100) catch error if error.message.indexOf('Unable to watch path') > -1 message = """ Unable to watch path: `#{path.basename(userKeymapPath)}`. Make sure you have permission to read `#{userKeymapPath}`. On linux there are currently problems with watch sizes. See [this document][watches] for more info. [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path """ @notificationManager.addError(message, {dismissable: true}) else detail = error.path stack = error.stack @notificationManager.addFatalError(error.message, {detail, stack, dismissable: true}) @emitter.emit 'did-load-user-keymap' KeymapManager::subscribeToFileReadFailure = -> @onDidFailToReadFile (error) => userKeymapPath = @getUserKeymapPath() message = "Failed to load `#{userKeymapPath}`" detail = if error.location? error.stack else error.message @notificationManager.addError(message, {detail, dismissable: true}) module.exports = KeymapManager ================================================ FILE: src/layer-decoration.coffee ================================================ idCounter = 0 nextId = -> idCounter++ # Essential: Represents a decoration that applies to every marker on a given # layer. Created via {TextEditor::decorateMarkerLayer}. module.exports = class LayerDecoration constructor: (@markerLayer, @decorationManager, @properties) -> @id = nextId() @destroyed = false @markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy() @overridePropertiesByMarker = null # Essential: Destroys the decoration. destroy: -> return if @destroyed @markerLayerDestroyedDisposable.dispose() @markerLayerDestroyedDisposable = null @destroyed = true @decorationManager.didDestroyLayerDecoration(this) # Essential: Determine whether this decoration is destroyed. # # Returns a {Boolean}. isDestroyed: -> @destroyed getId: -> @id getMarkerLayer: -> @markerLayer # Essential: Get this decoration's properties. # # Returns an {Object}. getProperties: -> @properties # Essential: Set this decoration's properties. # # * `newProperties` See {TextEditor::decorateMarker} for more information on # the properties. The `type` of `gutter` and `overlay` are not supported on # layer decorations. setProperties: (newProperties) -> return if @destroyed @properties = newProperties @decorationManager.emitDidUpdateDecorations() # Essential: Override the decoration properties for a specific marker. # # * `marker` The {DisplayMarker} or {Marker} for which to override # properties. # * `properties` An {Object} containing properties to apply to this marker. # Pass `null` to clear the override. setPropertiesForMarker: (marker, properties) -> return if @destroyed @overridePropertiesByMarker ?= new Map() marker = @markerLayer.getMarker(marker.id) if properties? @overridePropertiesByMarker.set(marker, properties) else @overridePropertiesByMarker.delete(marker) @decorationManager.emitDidUpdateDecorations() getPropertiesForMarker: (marker) -> @overridePropertiesByMarker?.get(marker) ================================================ FILE: src/less-compile-cache.coffee ================================================ path = require 'path' LessCache = require 'less-cache' # {LessCache} wrapper used by {ThemeManager} to read stylesheets. module.exports = class LessCompileCache constructor: ({resourcePath, importPaths, lessSourcesByRelativeFilePath, importedFilePathsByRelativeImportPath}) -> cacheDir = path.join(process.env.ATOM_HOME, 'compile-cache', 'less') @lessSearchPaths = [ path.join(resourcePath, 'static', 'variables') path.join(resourcePath, 'static') ] if importPaths? importPaths = importPaths.concat(@lessSearchPaths) else importPaths = @lessSearchPaths @cache = new LessCache({ importPaths, resourcePath, lessSourcesByRelativeFilePath, importedFilePathsByRelativeImportPath, cacheDir, fallbackDir: path.join(resourcePath, 'less-compile-cache') }) setImportPaths: (importPaths=[]) -> @cache.setImportPaths(importPaths.concat(@lessSearchPaths)) read: (stylesheetPath) -> @cache.readFileSync(stylesheetPath) cssForFile: (stylesheetPath, lessContent, digest) -> @cache.cssForFile(stylesheetPath, lessContent, digest) ================================================ FILE: src/main-process/application-menu.js ================================================ const { app, Menu } = require('electron'); const _ = require('underscore-plus'); const MenuHelpers = require('../menu-helpers'); // Used to manage the global application menu. // // It's created by {AtomApplication} upon instantiation and used to add, remove // and maintain the state of all menu items. module.exports = class ApplicationMenu { constructor(version, autoUpdateManager) { this.version = version; this.autoUpdateManager = autoUpdateManager; this.windowTemplates = new WeakMap(); this.setActiveTemplate(this.getDefaultTemplate()); this.autoUpdateManager.on('state-changed', state => this.showUpdateMenuItem(state) ); } // Public: Updates the entire menu with the given keybindings. // // window - The BrowserWindow this menu template is associated with. // template - The Object which describes the menu to display. // keystrokesByCommand - An Object where the keys are commands and the values // are Arrays containing the keystroke. update(window, template, keystrokesByCommand) { this.translateTemplate(template, keystrokesByCommand); this.substituteVersion(template); this.windowTemplates.set(window, template); if (window === this.lastFocusedWindow) return this.setActiveTemplate(template); } setActiveTemplate(template) { if (!_.isEqual(template, this.activeTemplate)) { this.activeTemplate = template; this.menu = Menu.buildFromTemplate(_.deepClone(template)); Menu.setApplicationMenu(this.menu); } return this.showUpdateMenuItem(this.autoUpdateManager.getState()); } // Register a BrowserWindow with this application menu. addWindow(window) { if (this.lastFocusedWindow == null) this.lastFocusedWindow = window; const focusHandler = () => { this.lastFocusedWindow = window; const template = this.windowTemplates.get(window); if (template) this.setActiveTemplate(template); }; window.on('focus', focusHandler); window.once('closed', () => { if (window === this.lastFocusedWindow) this.lastFocusedWindow = null; this.windowTemplates.delete(window); window.removeListener('focus', focusHandler); }); this.enableWindowSpecificItems(true); } // Flattens the given menu and submenu items into an single Array. // // menu - A complete menu configuration object for atom-shell's menu API. // // Returns an Array of native menu items. flattenMenuItems(menu) { const object = menu.items || {}; let items = []; for (let index in object) { const item = object[index]; items.push(item); if (item.submenu) items = items.concat(this.flattenMenuItems(item.submenu)); } return items; } // Flattens the given menu template into an single Array. // // template - An object describing the menu item. // // Returns an Array of native menu items. flattenMenuTemplate(template) { let items = []; for (let item of template) { items.push(item); if (item.submenu) items = items.concat(this.flattenMenuTemplate(item.submenu)); } return items; } // Public: Used to make all window related menu items are active. // // enable - If true enables all window specific items, if false disables all // window specific items. enableWindowSpecificItems(enable) { for (let item of this.flattenMenuItems(this.menu)) { if (item.metadata && item.metadata.windowSpecific) item.enabled = enable; } } // Replaces VERSION with the current version. substituteVersion(template) { let item = this.flattenMenuTemplate(template).find( ({ label }) => label === 'VERSION' ); if (item) item.label = `Version ${this.version}`; } // Sets the proper visible state the update menu items showUpdateMenuItem(state) { const items = this.flattenMenuItems(this.menu); const checkForUpdateItem = items.find( ({ id }) => id === 'Check for Update' ); const checkingForUpdateItem = items.find( ({ id }) => id === 'Checking for Update' ); const downloadingUpdateItem = items.find( ({ id }) => id === 'Downloading Update' ); const installUpdateItem = items.find( ({ id }) => id === 'Restart and Install Update' ); if ( !checkForUpdateItem || !checkingForUpdateItem || !downloadingUpdateItem || !installUpdateItem ) return; checkForUpdateItem.visible = false; checkingForUpdateItem.visible = false; downloadingUpdateItem.visible = false; installUpdateItem.visible = false; switch (state) { case 'idle': case 'error': case 'no-update-available': checkForUpdateItem.visible = true; break; case 'checking': checkingForUpdateItem.visible = true; break; case 'downloading': downloadingUpdateItem.visible = true; break; case 'update-available': installUpdateItem.visible = true; break; } } // Default list of menu items. // // Returns an Array of menu item Objects. getDefaultTemplate() { return [ { label: 'Atom', id: 'Atom', submenu: [ { label: 'Check for Update', id: 'Check for Update', metadata: { autoUpdate: true } }, { label: 'Reload', id: 'Reload', accelerator: 'Command+R', click: () => { const window = this.focusedWindow(); if (window) window.reload(); } }, { label: 'Close Window', id: 'Close Window', accelerator: 'Command+Shift+W', click: () => { const window = this.focusedWindow(); if (window) window.close(); } }, { label: 'Toggle Dev Tools', id: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: () => { const window = this.focusedWindow(); if (window) window.toggleDevTools(); } }, { label: 'Quit', id: 'Quit', accelerator: 'Command+Q', click: () => app.quit() } ] } ]; } focusedWindow() { return global.atomApplication .getAllWindows() .find(window => window.isFocused()); } // Combines a menu template with the appropriate keystroke. // // template - An Object conforming to atom-shell's menu api but lacking // accelerator and click properties. // keystrokesByCommand - An Object where the keys are commands and the values // are Arrays containing the keystroke. // // Returns a complete menu configuration object for atom-shell's menu API. translateTemplate(template, keystrokesByCommand) { template.forEach(item => { if (item.metadata == null) item.metadata = {}; if (item.command) { const keystrokes = keystrokesByCommand[item.command]; if (keystrokes && keystrokes.length > 0) { const keystroke = keystrokes[0]; // Electron does not support multi-keystroke accelerators. Therefore, // when the command maps to a multi-stroke key binding, show the // keystrokes next to the item's label. if (keystroke.includes(' ')) { item.label += ` [${_.humanizeKeystroke(keystroke)}]`; } else { item.accelerator = MenuHelpers.acceleratorForKeystroke(keystroke); } } item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail); if (!/^application:/.test(item.command)) { item.metadata.windowSpecific = true; } } if (item.submenu) this.translateTemplate(item.submenu, keystrokesByCommand); }); return template; } }; ================================================ FILE: src/main-process/atom-application.js ================================================ const AtomWindow = require('./atom-window'); const ApplicationMenu = require('./application-menu'); const AtomProtocolHandler = require('./atom-protocol-handler'); const AutoUpdateManager = require('./auto-update-manager'); const StorageFolder = require('../storage-folder'); const Config = require('../config'); const ConfigFile = require('../config-file'); const FileRecoveryService = require('./file-recovery-service'); const StartupTime = require('../startup-time'); const ipcHelpers = require('../ipc-helpers'); const { BrowserWindow, Menu, app, clipboard, dialog, ipcMain, shell, screen } = require('electron'); const { CompositeDisposable, Disposable } = require('event-kit'); const crypto = require('crypto'); const fs = require('fs-plus'); const path = require('path'); const os = require('os'); const net = require('net'); const url = require('url'); const { promisify } = require('util'); const { EventEmitter } = require('events'); const _ = require('underscore-plus'); let FindParentDir = null; let Resolve = null; const ConfigSchema = require('../config-schema'); const LocationSuffixRegExp = /(:\d+)(:\d+)?$/; // Increment this when changing the serialization format of `${ATOM_HOME}/storage/application.json` used by // AtomApplication::saveCurrentWindowOptions() and AtomApplication::loadPreviousWindowOptions() in a backward- // incompatible way. const APPLICATION_STATE_VERSION = '1'; const getDefaultPath = () => { const editor = atom.workspace.getActiveTextEditor(); if (!editor || !editor.getPath()) { return; } const paths = atom.project.getPaths(); if (paths) { return paths[0]; } }; const getSocketSecretPath = atomVersion => { const { username } = os.userInfo(); const atomHome = path.resolve(process.env.ATOM_HOME); return path.join(atomHome, `.atom-socket-secret-${username}-${atomVersion}`); }; const getSocketPath = socketSecret => { if (!socketSecret) { return null; } // Hash the secret to create the socket name to not expose it. const socketName = crypto .createHmac('sha256', socketSecret) .update('socketName') .digest('hex') .substr(0, 12); if (process.platform === 'win32') { return `\\\\.\\pipe\\atom-${socketName}-sock`; } else { return path.join(os.tmpdir(), `atom-${socketName}.sock`); } }; const getExistingSocketSecret = atomVersion => { const socketSecretPath = getSocketSecretPath(atomVersion); if (!fs.existsSync(socketSecretPath)) { return null; } return fs.readFileSync(socketSecretPath, 'utf8'); }; const getRandomBytes = promisify(crypto.randomBytes); const writeFile = promisify(fs.writeFile); const createSocketSecret = async atomVersion => { const socketSecret = (await getRandomBytes(16)).toString('hex'); await writeFile(getSocketSecretPath(atomVersion), socketSecret, { encoding: 'utf8', mode: 0o600 }); return socketSecret; }; const encryptOptions = (options, secret) => { const message = JSON.stringify(options); const initVector = crypto.randomBytes(16); // AES uses 16 bytes for iV const cipher = crypto.createCipheriv('aes-256-gcm', secret, initVector); let content = cipher.update(message, 'utf8', 'hex'); content += cipher.final('hex'); const authTag = cipher.getAuthTag().toString('hex'); return JSON.stringify({ authTag, content, initVector: initVector.toString('hex') }); }; const decryptOptions = (optionsMessage, secret) => { const { authTag, content, initVector } = JSON.parse(optionsMessage); const decipher = crypto.createDecipheriv( 'aes-256-gcm', secret, Buffer.from(initVector, 'hex') ); decipher.setAuthTag(Buffer.from(authTag, 'hex')); let message = decipher.update(content, 'hex', 'utf8'); message += decipher.final('utf8'); return JSON.parse(message); }; ipcMain.handle('isDefaultProtocolClient', (_, { protocol, path, args }) => { return app.isDefaultProtocolClient(protocol, path, args); }); ipcMain.handle('setAsDefaultProtocolClient', (_, { protocol, path, args }) => { return app.setAsDefaultProtocolClient(protocol, path, args); }); // The application's singleton class. // // It's the entry point into the Atom application and maintains the global state // of the application. // module.exports = class AtomApplication extends EventEmitter { // Public: The entry point into the Atom application. static open(options) { StartupTime.addMarker('main-process:atom-application:open'); const socketSecret = getExistingSocketSecret(options.version); const socketPath = getSocketPath(socketSecret); const createApplication = options.createApplication || (async () => { const app = new AtomApplication(options); await app.initialize(options); return app; }); // FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely // take a few seconds to trigger 'error' event, it could be a bug of node // or electron, before it's fixed we check the existence of socketPath to // speedup startup. if ( !socketPath || options.test || options.benchmark || options.benchmarkTest || (process.platform !== 'win32' && !fs.existsSync(socketPath)) ) { return createApplication(options); } return new Promise(resolve => { const client = net.connect({ path: socketPath }, () => { client.write(encryptOptions(options, socketSecret), () => { client.end(); app.quit(); resolve(null); }); }); client.on('error', () => resolve(createApplication(options))); }); } exit(status) { app.exit(status); } constructor(options) { StartupTime.addMarker('main-process:atom-application:constructor:start'); super(); this.quitting = false; this.quittingForUpdate = false; this.getAllWindows = this.getAllWindows.bind(this); this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this); this.resourcePath = options.resourcePath; this.devResourcePath = options.devResourcePath; this.version = options.version; this.devMode = options.devMode; this.safeMode = options.safeMode; this.logFile = options.logFile; this.userDataDir = options.userDataDir; this._killProcess = options.killProcess || process.kill.bind(process); this.waitSessionsByWindow = new Map(); this.windowStack = new WindowStack(); this.initializeAtomHome(process.env.ATOM_HOME); const configFilePath = fs.existsSync( path.join(process.env.ATOM_HOME, 'config.json') ) ? path.join(process.env.ATOM_HOME, 'config.json') : path.join(process.env.ATOM_HOME, 'config.cson'); this.configFile = ConfigFile.at(configFilePath); this.config = new Config({ saveCallback: settings => { if (!this.quitting) { return this.configFile.update(settings); } } }); this.config.setSchema(null, { type: 'object', properties: _.clone(ConfigSchema) }); this.fileRecoveryService = new FileRecoveryService( path.join(process.env.ATOM_HOME, 'recovery') ); this.storageFolder = new StorageFolder(process.env.ATOM_HOME); this.autoUpdateManager = new AutoUpdateManager( this.version, options.test || options.benchmark || options.benchmarkTest, this.config ); this.disposable = new CompositeDisposable(); this.handleEvents(); StartupTime.addMarker('main-process:atom-application:constructor:end'); } // This stuff was previously done in the constructor, but we want to be able to construct this object // for testing purposes without booting up the world. As you add tests, feel free to move instantiation // of these various sub-objects into the constructor, but you'll need to remove the side-effects they // perform during their construction, adding an initialize method that you call here. async initialize(options) { StartupTime.addMarker('main-process:atom-application:initialize:start'); global.atomApplication = this; this.applicationMenu = new ApplicationMenu( this.version, this.autoUpdateManager ); this.atomProtocolHandler = new AtomProtocolHandler( this.resourcePath, this.safeMode ); let socketServerPromise; if (options.test || options.benchmark || options.benchmarkTest) { socketServerPromise = Promise.resolve(); } else { socketServerPromise = this.listenForArgumentsFromNewProcess(); } await socketServerPromise; this.setupDockMenu(); const result = await this.launch(options); this.autoUpdateManager.initialize(); StartupTime.addMarker('main-process:atom-application:initialize:end'); return result; } async destroy() { const windowsClosePromises = this.getAllWindows().map(window => { window.close(); return window.closedPromise; }); await Promise.all(windowsClosePromises); this.disposable.dispose(); } async launch(options) { if (!this.configFilePromise) { this.configFilePromise = this.configFile.watch().then(disposable => { this.disposable.add(disposable); this.config.onDidChange('core.titleBar', () => this.promptForRestart()); this.config.onDidChange('core.colorProfile', () => this.promptForRestart() ); }); await this.configFilePromise; } let optionsForWindowsToOpen = []; let shouldReopenPreviousWindows = false; if (options.test || options.benchmark || options.benchmarkTest) { optionsForWindowsToOpen.push(options); } else if (options.newWindow) { shouldReopenPreviousWindows = false; } else if ( (options.pathsToOpen && options.pathsToOpen.length > 0) || (options.urlsToOpen && options.urlsToOpen.length > 0) ) { optionsForWindowsToOpen.push(options); shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') === 'always'; } else { shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') !== 'no'; } if (shouldReopenPreviousWindows) { optionsForWindowsToOpen = [ ...(await this.loadPreviousWindowOptions()), ...optionsForWindowsToOpen ]; } if (optionsForWindowsToOpen.length === 0) { optionsForWindowsToOpen.push(options); } // Preserve window opening order const windows = []; for (const options of optionsForWindowsToOpen) { windows.push(await this.openWithOptions(options)); } return windows; } openWithOptions(options) { const { pathsToOpen, executedFrom, foldersToOpen, urlsToOpen, benchmark, benchmarkTest, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow, preserveFocus, env } = options; if (!preserveFocus) { app.focus(); } if (test) { return this.runTests({ headless: true, devMode, resourcePath: this.resourcePath, executedFrom, pathsToOpen, logFile, timeout, env }); } else if (benchmark || benchmarkTest) { return this.runBenchmarks({ headless: true, test: benchmarkTest, resourcePath: this.resourcePath, executedFrom, pathsToOpen, timeout, env }); } else if ( (pathsToOpen && pathsToOpen.length > 0) || (foldersToOpen && foldersToOpen.length > 0) ) { return this.openPaths({ pathsToOpen, foldersToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env }); } else if (urlsToOpen && urlsToOpen.length > 0) { return Promise.all( urlsToOpen.map(urlToOpen => this.openUrl({ urlToOpen, devMode, safeMode, env }) ) ); } else { // Always open an editor window if this is the first instance of Atom. return this.openPath({ pathToOpen: null, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env }); } } // Public: Create a new {AtomWindow} bound to this application. createWindow(settings) { return new AtomWindow(this, this.fileRecoveryService, settings); } // Public: Removes the {AtomWindow} from the global window list. removeWindow(window) { this.windowStack.removeWindow(window); if (this.getAllWindows().length === 0 && process.platform !== 'darwin') { app.quit(); return; } if (!window.isSpec) this.saveCurrentWindowOptions(true); } // Public: Adds the {AtomWindow} to the global window list. addWindow(window) { this.windowStack.addWindow(window); if (this.applicationMenu) this.applicationMenu.addWindow(window.browserWindow); window.once('window:loaded', () => { this.autoUpdateManager && this.autoUpdateManager.emitUpdateAvailableEvent(window); }); if (!window.isSpec) { const focusHandler = () => this.windowStack.touch(window); const blurHandler = () => this.saveCurrentWindowOptions(false); window.browserWindow.on('focus', focusHandler); window.browserWindow.on('blur', blurHandler); window.browserWindow.once('closed', () => { this.windowStack.removeWindow(window); window.browserWindow.removeListener('focus', focusHandler); window.browserWindow.removeListener('blur', blurHandler); }); window.browserWindow.webContents.once('did-finish-load', blurHandler); this.saveCurrentWindowOptions(false); } } getAllWindows() { return this.windowStack.all().slice(); } getLastFocusedWindow(predicate) { return this.windowStack.getLastFocusedWindow(predicate); } // Creates server to listen for additional atom application launches. // // You can run the atom command multiple times, but after the first launch // the other launches will just pass their information to this server and then // close immediately. async listenForArgumentsFromNewProcess() { this.socketSecretPromise = createSocketSecret(this.version); this.socketSecret = await this.socketSecretPromise; this.socketPath = getSocketPath(this.socketSecret); await this.deleteSocketFile(); const server = net.createServer(connection => { let data = ''; connection.on('data', chunk => { data += chunk; }); connection.on('end', () => { try { const options = decryptOptions(data, this.socketSecret); this.openWithOptions(options); } catch (e) { // Error while parsing/decrypting the options passed by the client. // We cannot trust the client, aborting. } }); }); return new Promise(resolve => { server.listen(this.socketPath, resolve); server.on('error', error => console.error('Application server failed', error) ); }); } async deleteSocketFile() { if (process.platform === 'win32') return; if (!this.socketSecretPromise) { return; } await this.socketSecretPromise; if (fs.existsSync(this.socketPath)) { try { fs.unlinkSync(this.socketPath); } catch (error) { // Ignore ENOENT errors in case the file was deleted between the exists // check and the call to unlink sync. This occurred occasionally on CI // which is why this check is here. if (error.code !== 'ENOENT') throw error; } } } async deleteSocketSecretFile() { if (!this.socketSecretPromise) { return; } await this.socketSecretPromise; const socketSecretPath = getSocketSecretPath(this.version); if (fs.existsSync(socketSecretPath)) { try { fs.unlinkSync(socketSecretPath); } catch (error) { // Ignore ENOENT errors in case the file was deleted between the exists // check and the call to unlink sync. if (error.code !== 'ENOENT') throw error; } } } // Registers basic application commands, non-idempotent. handleEvents() { const createOpenSettings = ({ event, sameWindow }) => { const targetWindow = event ? this.atomWindowForEvent(event) : this.focusedWindow(); return { devMode: targetWindow ? targetWindow.devMode : false, safeMode: targetWindow ? targetWindow.safeMode : false, window: sameWindow && targetWindow ? targetWindow : null }; }; this.on('application:quit', () => app.quit()); this.on('application:new-window', () => this.openPath(createOpenSettings({})) ); this.on('application:new-file', () => (this.focusedWindow() || this).openPath() ); this.on('application:open-dev', () => this.promptForPathToOpen('all', { devMode: true }) ); this.on('application:open-safe', () => this.promptForPathToOpen('all', { safeMode: true }) ); this.on('application:inspect', ({ x, y, atomWindow }) => { if (!atomWindow) atomWindow = this.focusedWindow(); if (atomWindow) atomWindow.browserWindow.inspectElement(x, y); }); this.on('application:open-documentation', () => shell.openExternal('http://flight-manual.atom.io') ); this.on('application:open-discussions', () => shell.openExternal('https://github.com/atom/atom/discussions') ); this.on('application:open-faq', () => shell.openExternal('https://atom.io/faq') ); this.on('application:open-terms-of-use', () => shell.openExternal('https://atom.io/terms') ); this.on('application:report-issue', () => shell.openExternal( 'https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs' ) ); this.on('application:search-issues', () => shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom') ); this.on('application:install-update', () => { this.quitting = true; this.quittingForUpdate = true; this.autoUpdateManager.install(); }); this.on('application:check-for-update', () => this.autoUpdateManager.check() ); if (process.platform === 'darwin') { this.on('application:reopen-project', ({ paths }) => { const focusedWindow = this.focusedWindow(); if (focusedWindow) { const { safeMode, devMode } = focusedWindow; this.openPaths({ pathsToOpen: paths, safeMode, devMode }); return; } this.openPaths({ pathsToOpen: paths }); }); this.on('application:open', () => { this.promptForPathToOpen( 'all', createOpenSettings({ sameWindow: true }), getDefaultPath() ); }); this.on('application:open-file', () => { this.promptForPathToOpen( 'file', createOpenSettings({ sameWindow: true }), getDefaultPath() ); }); this.on('application:open-folder', () => { this.promptForPathToOpen( 'folder', createOpenSettings({ sameWindow: true }), getDefaultPath() ); }); this.on('application:bring-all-windows-to-front', () => Menu.sendActionToFirstResponder('arrangeInFront:') ); this.on('application:hide', () => Menu.sendActionToFirstResponder('hide:') ); this.on('application:hide-other-applications', () => Menu.sendActionToFirstResponder('hideOtherApplications:') ); this.on('application:minimize', () => Menu.sendActionToFirstResponder('performMiniaturize:') ); this.on('application:unhide-all-applications', () => Menu.sendActionToFirstResponder('unhideAllApplications:') ); this.on('application:zoom', () => Menu.sendActionToFirstResponder('zoom:') ); } else { this.on('application:minimize', () => { const window = this.focusedWindow(); if (window) window.minimize(); }); this.on('application:zoom', function() { const window = this.focusedWindow(); if (window) window.maximize(); }); } this.openPathOnEvent('application:about', 'atom://about'); this.openPathOnEvent('application:show-settings', 'atom://config'); this.openPathOnEvent('application:open-your-config', 'atom://.atom/config'); this.openPathOnEvent( 'application:open-your-init-script', 'atom://.atom/init-script' ); this.openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap'); this.openPathOnEvent( 'application:open-your-snippets', 'atom://.atom/snippets' ); this.openPathOnEvent( 'application:open-your-stylesheet', 'atom://.atom/stylesheet' ); this.openPathOnEvent( 'application:open-license', path.join(process.resourcesPath, 'LICENSE.md') ); this.configFile.onDidChange(settings => { for (let window of this.getAllWindows()) { window.didChangeUserSettings(settings); } this.config.resetUserSettings(settings); }); this.configFile.onDidError(message => { const window = this.focusedWindow() || this.getLastFocusedWindow(); if (window) { window.didFailToReadUserSettings(message); } else { console.error(message); } }); this.disposable.add( ipcHelpers.on(app, 'before-quit', async event => { let resolveBeforeQuitPromise; this.lastBeforeQuitPromise = new Promise(resolve => { resolveBeforeQuitPromise = resolve; }); if (!this.quitting) { this.quitting = true; event.preventDefault(); const windowUnloadPromises = this.getAllWindows().map( async window => { const unloaded = await window.prepareToUnload(); if (unloaded) { window.close(); await window.closedPromise; } return unloaded; } ); const windowUnloadedResults = await Promise.all(windowUnloadPromises); if (windowUnloadedResults.every(Boolean)) { app.quit(); } else { this.quitting = false; } } resolveBeforeQuitPromise(); }) ); this.disposable.add( ipcHelpers.on(app, 'will-quit', () => { this.killAllProcesses(); return Promise.all([ this.deleteSocketFile(), this.deleteSocketSecretFile() ]); }) ); // See: https://www.electronjs.org/docs/api/app#event-window-all-closed this.disposable.add( ipcHelpers.on(app, 'window-all-closed', () => { if (this.applicationMenu != null) { this.applicationMenu.enableWindowSpecificItems(false); } // Don't quit when the last window is closed on macOS. if (process.platform !== 'darwin') { app.quit(); } }) ); // Triggered by the 'open-file' event from Electron: // https://electronjs.org/docs/api/app#event-open-file-macos // For example, this is fired when a file is dragged and dropped onto the Atom application icon in the dock. this.disposable.add( ipcHelpers.on(app, 'open-file', (event, pathToOpen) => { event.preventDefault(); this.openPath({ pathToOpen }); }) ); this.disposable.add( ipcHelpers.on(app, 'open-url', (event, urlToOpen) => { event.preventDefault(); this.openUrl({ urlToOpen, devMode: this.devMode, safeMode: this.safeMode }); }) ); this.disposable.add( ipcHelpers.on(app, 'activate', (event, hasVisibleWindows) => { if (hasVisibleWindows) return; if (event) event.preventDefault(); this.emit('application:new-window'); }) ); this.disposable.add( ipcHelpers.on(ipcMain, 'restart-application', () => { this.restart(); }) ); this.disposable.add( ipcHelpers.on(ipcMain, 'resolve-proxy', async (event, requestId, url) => { const proxy = await event.sender.session.resolveProxy(url); if (!event.sender.isDestroyed()) event.sender.send('did-resolve-proxy', requestId, proxy); }) ); this.disposable.add( ipcHelpers.on(ipcMain, 'did-change-history-manager', event => { for (let atomWindow of this.getAllWindows()) { const { webContents } = atomWindow.browserWindow; if (webContents !== event.sender) webContents.send('did-change-history-manager'); } }) ); // A request from the associated render process to open a set of paths using the standard window location logic. // Used for application:reopen-project. this.disposable.add( ipcHelpers.on(ipcMain, 'open', (event, options) => { if (options) { if (typeof options.pathsToOpen === 'string') { options.pathsToOpen = [options.pathsToOpen]; } if (options.here) { options.window = this.atomWindowForEvent(event); } if (options.pathsToOpen && options.pathsToOpen.length > 0) { this.openPaths(options); } else { this.addWindow(this.createWindow(options)); } } else { this.promptForPathToOpen('all', {}); } }) ); // Prompt for a file, folder, or either, then open the chosen paths. Files will be opened in the originating // window; folders will be opened in a new window unless an existing window exactly contains all of them. this.disposable.add( ipcHelpers.on(ipcMain, 'open-chosen-any', (event, defaultPath) => { this.promptForPathToOpen( 'all', createOpenSettings({ event, sameWindow: true }), defaultPath ); }) ); this.disposable.add( ipcHelpers.on(ipcMain, 'open-chosen-file', (event, defaultPath) => { this.promptForPathToOpen( 'file', createOpenSettings({ event, sameWindow: true }), defaultPath ); }) ); this.disposable.add( ipcHelpers.on(ipcMain, 'open-chosen-folder', (event, defaultPath) => { this.promptForPathToOpen( 'folder', createOpenSettings({ event }), defaultPath ); }) ); this.disposable.add( ipcHelpers.on( ipcMain, 'update-application-menu', (event, template, menu) => { const window = BrowserWindow.fromWebContents(event.sender); if (this.applicationMenu) this.applicationMenu.update(window, template, menu); } ) ); this.disposable.add( ipcHelpers.on( ipcMain, 'run-package-specs', (event, packageSpecPath, options = {}) => { this.runTests( Object.assign( { resourcePath: this.devResourcePath, pathsToOpen: [packageSpecPath], headless: false }, options ) ); } ) ); this.disposable.add( ipcHelpers.on(ipcMain, 'run-benchmarks', (event, benchmarksPath) => { this.runBenchmarks({ resourcePath: this.devResourcePath, pathsToOpen: [benchmarksPath], headless: false, test: false }); }) ); this.disposable.add( ipcHelpers.on(ipcMain, 'command', (event, command) => { this.emit(command); }) ); this.disposable.add( ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => { const window = BrowserWindow.fromWebContents(event.sender); return window && window.emit(command, ...args); }) ); this.disposable.add( ipcHelpers.respondTo( 'window-method', (browserWindow, method, ...args) => { const window = this.atomWindowForBrowserWindow(browserWindow); if (window) window[method](...args); } ) ); this.disposable.add( ipcHelpers.on(ipcMain, 'pick-folder', (event, responseChannel) => { this.promptForPath('folder', paths => event.sender.send(responseChannel, paths) ); }) ); this.disposable.add( ipcHelpers.respondTo('set-window-size', (window, width, height) => { window.setSize(width, height); }) ); this.disposable.add( ipcHelpers.respondTo('set-window-position', (window, x, y) => { window.setPosition(x, y); }) ); this.disposable.add( ipcHelpers.respondTo( 'set-user-settings', (window, settings, filePath) => { if (!this.quitting) { return ConfigFile.at(filePath || this.configFilePath).update( JSON.parse(settings) ); } } ) ); this.disposable.add( ipcHelpers.respondTo('center-window', window => window.center()) ); this.disposable.add( ipcHelpers.respondTo('focus-window', window => window.focus()) ); this.disposable.add( ipcHelpers.respondTo('show-window', window => window.show()) ); this.disposable.add( ipcHelpers.respondTo('hide-window', window => window.hide()) ); this.disposable.add( ipcHelpers.respondTo( 'get-temporary-window-state', window => window.temporaryState ) ); this.disposable.add( ipcHelpers.respondTo('set-temporary-window-state', (win, state) => { win.temporaryState = state; }) ); this.disposable.add( ipcHelpers.on( ipcMain, 'write-text-to-selection-clipboard', (event, text) => clipboard.writeText(text, 'selection') ) ); this.disposable.add( ipcHelpers.on(ipcMain, 'write-to-stdout', (event, output) => process.stdout.write(output) ) ); this.disposable.add( ipcHelpers.on(ipcMain, 'write-to-stderr', (event, output) => process.stderr.write(output) ) ); this.disposable.add( ipcHelpers.on(ipcMain, 'add-recent-document', (event, filename) => app.addRecentDocument(filename) ) ); this.disposable.add( ipcHelpers.on( ipcMain, 'execute-javascript-in-dev-tools', (event, code) => event.sender.devToolsWebContents && event.sender.devToolsWebContents.executeJavaScript(code) ) ); this.disposable.add( ipcHelpers.on(ipcMain, 'get-auto-update-manager-state', event => { event.returnValue = this.autoUpdateManager.getState(); }) ); this.disposable.add( ipcHelpers.on(ipcMain, 'get-auto-update-manager-error', event => { event.returnValue = this.autoUpdateManager.getErrorMessage(); }) ); this.disposable.add( ipcHelpers.respondTo('will-save-path', (window, path) => this.fileRecoveryService.willSavePath(window, path) ) ); this.disposable.add( ipcHelpers.respondTo('did-save-path', (window, path) => this.fileRecoveryService.didSavePath(window, path) ) ); this.disposable.add(this.disableZoomOnDisplayChange()); } setupDockMenu() { if (process.platform === 'darwin') { return app.dock.setMenu( Menu.buildFromTemplate([ { label: 'New Window', click: () => this.emit('application:new-window') } ]) ); } } initializeAtomHome(configDirPath) { if (!fs.existsSync(configDirPath)) { const templateConfigDirPath = fs.resolve(this.resourcePath, 'dot-atom'); fs.copySync(templateConfigDirPath, configDirPath); } } // Public: Executes the given command. // // If it isn't handled globally, delegate to the currently focused window. // // command - The string representing the command. // args - The optional arguments to pass along. sendCommand(command, ...args) { if (!this.emit(command, ...args)) { const focusedWindow = this.focusedWindow(); if (focusedWindow) { return focusedWindow.sendCommand(command, ...args); } else { return this.sendCommandToFirstResponder(command); } } } // Public: Executes the given command on the given window. // // command - The string representing the command. // atomWindow - The {AtomWindow} to send the command to. // args - The optional arguments to pass along. sendCommandToWindow(command, atomWindow, ...args) { if (!this.emit(command, ...args)) { if (atomWindow) { return atomWindow.sendCommand(command, ...args); } else { return this.sendCommandToFirstResponder(command); } } } // Translates the command into macOS action and sends it to application's first // responder. sendCommandToFirstResponder(command) { if (process.platform !== 'darwin') return false; switch (command) { case 'core:undo': Menu.sendActionToFirstResponder('undo:'); break; case 'core:redo': Menu.sendActionToFirstResponder('redo:'); break; case 'core:copy': Menu.sendActionToFirstResponder('copy:'); break; case 'core:cut': Menu.sendActionToFirstResponder('cut:'); break; case 'core:paste': Menu.sendActionToFirstResponder('paste:'); break; case 'core:select-all': Menu.sendActionToFirstResponder('selectAll:'); break; default: return false; } return true; } // Public: Open the given path in the focused window when the event is // triggered. // // A new window will be created if there is no currently focused window. // // eventName - The event to listen for. // pathToOpen - The path to open when the event is triggered. openPathOnEvent(eventName, pathToOpen) { this.on(eventName, () => { const window = this.focusedWindow(); if (window) { return window.openPath(pathToOpen); } else { return this.openPath({ pathToOpen }); } }); } // Returns the {AtomWindow} for the given locations. windowForLocations(locationsToOpen, devMode, safeMode) { return this.getLastFocusedWindow( window => !window.isSpec && window.devMode === devMode && window.safeMode === safeMode && window.containsLocations(locationsToOpen) ); } // Returns the {AtomWindow} for the given ipcMain event. atomWindowForEvent({ sender }) { return this.atomWindowForBrowserWindow( BrowserWindow.fromWebContents(sender) ); } atomWindowForBrowserWindow(browserWindow) { return this.getAllWindows().find( atomWindow => atomWindow.browserWindow === browserWindow ); } // Public: Returns the currently focused {AtomWindow} or undefined if none. focusedWindow() { return this.getAllWindows().find(window => window.isFocused()); } // Get the platform-specific window offset for new windows. getWindowOffsetForCurrentPlatform() { const offsetByPlatform = { darwin: 22, win32: 26 }; return offsetByPlatform[process.platform] || 0; } // Get the dimensions for opening a new window by cascading as appropriate to // the platform. getDimensionsForNewWindow() { const window = this.focusedWindow() || this.getLastFocusedWindow(); if (!window || window.isMaximized()) return; const dimensions = window.getDimensions(); if (dimensions) { const offset = this.getWindowOffsetForCurrentPlatform(); dimensions.x += offset; dimensions.y += offset; return dimensions; } } // Public: Opens a single path, in an existing window if possible. // // options - // :pathToOpen - The file path to open // :pidToKillWhenClosed - The integer of the pid to kill // :newWindow - Boolean of whether this should be opened in a new window. // :devMode - Boolean to control the opened window's dev mode. // :safeMode - Boolean to control the opened window's safe mode. // :profileStartup - Boolean to control creating a profile of the startup time. // :window - {AtomWindow} to open file paths in. // :addToLastWindow - Boolean of whether this should be opened in last focused window. openPath({ pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env } = {}) { return this.openPaths({ pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env }); } // Public: Opens multiple paths, in existing windows if possible. // // options - // :pathsToOpen - The array of file paths to open // :foldersToOpen - An array of additional paths to open that must be existing directories // :pidToKillWhenClosed - The integer of the pid to kill // :newWindow - Boolean of whether this should be opened in a new window. // :devMode - Boolean to control the opened window's dev mode. // :safeMode - Boolean to control the opened window's safe mode. // :windowDimensions - Object with height and width keys. // :window - {AtomWindow} to open file paths in. // :addToLastWindow - Boolean of whether this should be opened in last focused window. async openPaths({ pathsToOpen, foldersToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env } = {}) { if (!env) env = process.env; if (!pathsToOpen) pathsToOpen = []; if (!foldersToOpen) foldersToOpen = []; devMode = Boolean(devMode); safeMode = Boolean(safeMode); clearWindowState = Boolean(clearWindowState); const locationsToOpen = await Promise.all( pathsToOpen.map(pathToOpen => this.parsePathToOpen(pathToOpen, executedFrom, { hasWaitSession: pidToKillWhenClosed != null }) ) ); for (const folderToOpen of foldersToOpen) { locationsToOpen.push({ pathToOpen: folderToOpen, initialLine: null, initialColumn: null, exists: true, isDirectory: true, hasWaitSession: pidToKillWhenClosed != null }); } if (locationsToOpen.length === 0) { return; } const hasNonEmptyPath = locationsToOpen.some( location => location.pathToOpen ); const createNewWindow = newWindow || !hasNonEmptyPath; let existingWindow; if (!createNewWindow) { // An explicitly provided AtomWindow has precedence. existingWindow = window; // If no window is specified and at least one path is provided, locate an existing window that contains all // provided paths. if (!existingWindow && hasNonEmptyPath) { existingWindow = this.windowForLocations( locationsToOpen, devMode, safeMode ); } // No window specified, no existing window found, and addition to the last window requested. Find the last // focused window that matches the requested dev and safe modes. if (!existingWindow && addToLastWindow) { existingWindow = this.getLastFocusedWindow(win => { return ( !win.isSpec && win.devMode === devMode && win.safeMode === safeMode ); }); } // Fall back to the last focused window that has no project roots. if (!existingWindow) { existingWindow = this.getLastFocusedWindow( win => !win.isSpec && !win.hasProjectPaths() ); } // One last case: if *no* paths are directories, add to the last focused window. if (!existingWindow) { const noDirectories = locationsToOpen.every( location => !location.isDirectory ); if (noDirectories) { existingWindow = this.getLastFocusedWindow(win => { return ( !win.isSpec && win.devMode === devMode && win.safeMode === safeMode ); }); } } } let openedWindow; if (existingWindow) { openedWindow = existingWindow; StartupTime.addMarker('main-process:atom-application:open-in-existing'); openedWindow.openLocations(locationsToOpen); if (openedWindow.isMinimized()) { openedWindow.restore(); } else { openedWindow.focus(); } openedWindow.replaceEnvironment(env); } else { let resourcePath, windowInitializationScript; if (devMode) { try { windowInitializationScript = require.resolve( path.join( this.devResourcePath, 'src', 'initialize-application-window' ) ); resourcePath = this.devResourcePath; } catch (error) {} } if (!windowInitializationScript) { windowInitializationScript = require.resolve( '../initialize-application-window' ); } if (!resourcePath) resourcePath = this.resourcePath; if (!windowDimensions) windowDimensions = this.getDimensionsForNewWindow(); StartupTime.addMarker('main-process:atom-application:create-window'); openedWindow = this.createWindow({ locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env }); this.addWindow(openedWindow); openedWindow.focus(); } if (pidToKillWhenClosed != null) { if (!this.waitSessionsByWindow.has(openedWindow)) { this.waitSessionsByWindow.set(openedWindow, []); } this.waitSessionsByWindow.get(openedWindow).push({ pid: pidToKillWhenClosed, remainingPaths: new Set( locationsToOpen.map(location => location.pathToOpen).filter(Boolean) ) }); } openedWindow.browserWindow.once('closed', () => this.killProcessesForWindow(openedWindow) ); return openedWindow; } // Kill all processes associated with opened windows. killAllProcesses() { for (let window of this.waitSessionsByWindow.keys()) { this.killProcessesForWindow(window); } } killProcessesForWindow(window) { const sessions = this.waitSessionsByWindow.get(window); if (!sessions) return; for (const session of sessions) { this.killProcess(session.pid); } this.waitSessionsByWindow.delete(window); } windowDidClosePathWithWaitSession(window, initialPath) { const waitSessions = this.waitSessionsByWindow.get(window); if (!waitSessions) return; for (let i = waitSessions.length - 1; i >= 0; i--) { const session = waitSessions[i]; session.remainingPaths.delete(initialPath); if (session.remainingPaths.size === 0) { this.killProcess(session.pid); waitSessions.splice(i, 1); } } } // Kill the process with the given pid. killProcess(pid) { try { const parsedPid = parseInt(pid); if (isFinite(parsedPid)) this._killProcess(parsedPid); } catch (error) { if (error.code !== 'ESRCH') { console.log( `Killing process ${pid} failed: ${ error.code != null ? error.code : error.message }` ); } } } async saveCurrentWindowOptions(allowEmpty = false) { if (this.quitting) return; const windows = this.getAllWindows(); const hasASpecWindow = windows.some(window => window.isSpec); if (windows.length === 1 && hasASpecWindow) return; const state = { version: APPLICATION_STATE_VERSION, windows: windows .filter(window => !window.isSpec) .map(window => ({ projectRoots: window.projectRoots })) }; state.windows.reverse(); if (state.windows.length > 0 || allowEmpty) { await this.storageFolder.store('application.json', state); this.emit('application:did-save-state'); } } async loadPreviousWindowOptions() { const state = await this.storageFolder.load('application.json'); if (!state) { return []; } if (state.version === APPLICATION_STATE_VERSION) { // Atom >=1.36.1 // Schema: {version: '1', windows: [{projectRoots: ['', ...]}, ...]} return state.windows.map(each => ({ foldersToOpen: each.projectRoots, devMode: this.devMode, safeMode: this.safeMode, newWindow: true })); } else if (state.version === undefined) { // Atom <= 1.36.0 // Schema: [{initialPaths: ['', ...]}, ...] return Promise.all( state.map(async windowState => { // Classify each window's initialPaths as directories or non-directories const classifiedPaths = await Promise.all( windowState.initialPaths.map( initialPath => new Promise(resolve => { fs.isDirectory(initialPath, isDir => resolve({ initialPath, isDir }) ); }) ) ); // Only accept initialPaths that are existing directories return { foldersToOpen: classifiedPaths .filter(({ isDir }) => isDir) .map(({ initialPath }) => initialPath), devMode: this.devMode, safeMode: this.safeMode, newWindow: true }; }) ); } else { // Unrecognized version (from a newer Atom?) return []; } } // Open an atom:// url. // // The host of the URL being opened is assumed to be the package name // responsible for opening the URL. A new window will be created with // that package's `urlMain` as the bootstrap script. // // options - // :urlToOpen - The atom:// url to open. // :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 }) { const parsedUrl = url.parse(urlToOpen, true); if (parsedUrl.protocol !== 'atom:') return; const pack = this.findPackageWithName(parsedUrl.host, devMode); if (pack && pack.urlMain) { return this.openPackageUrlMain( parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env ); } else { return this.openPackageUriHandler( urlToOpen, parsedUrl, devMode, safeMode, env ); } } openPackageUriHandler(url, parsedUrl, devMode, safeMode, env) { let bestWindow; if (parsedUrl.host === 'core') { const predicate = require('../core-uri-handlers').windowPredicate( parsedUrl ); bestWindow = this.getLastFocusedWindow( win => !win.isSpecWindow() && predicate(win) ); } if (!bestWindow) bestWindow = this.getLastFocusedWindow(win => !win.isSpecWindow()); if (bestWindow) { bestWindow.sendURIMessage(url); bestWindow.focus(); return bestWindow; } else { let windowInitializationScript; let { resourcePath } = this; if (devMode) { try { windowInitializationScript = require.resolve( path.join( this.devResourcePath, 'src', 'initialize-application-window' ) ); resourcePath = this.devResourcePath; } catch (error) {} } if (!windowInitializationScript) { windowInitializationScript = require.resolve( '../initialize-application-window' ); } const windowDimensions = this.getDimensionsForNewWindow(); const window = this.createWindow({ resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env }); this.addWindow(window); window.on('window:loaded', () => window.sendURIMessage(url)); return window; } } findPackageWithName(packageName, devMode) { return this.getPackageManager(devMode) .getAvailablePackageMetadata() .find(({ name }) => name === packageName); } openPackageUrlMain( packageName, packageUrlMain, urlToOpen, devMode, safeMode, env ) { const packagePath = this.getPackageManager(devMode).resolvePackagePath( packageName ); const windowInitializationScript = path.resolve( packagePath, packageUrlMain ); const windowDimensions = this.getDimensionsForNewWindow(); const window = this.createWindow({ windowInitializationScript, resourcePath: this.resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env }); this.addWindow(window); return window; } getPackageManager(devMode) { if (this.packages == null) { const PackageManager = require('../package-manager'); this.packages = new PackageManager({}); this.packages.initialize({ configDirPath: process.env.ATOM_HOME, devMode, resourcePath: this.resourcePath }); } return this.packages; } // Opens up a new {AtomWindow} to run specs within. // // options - // :headless - A Boolean that, if true, will close the window upon // completion. // :resourcePath - The path to include specs from. // :specPath - The directory to load specs from. // :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages // and ~/.atom/dev/packages, defaults to false. runTests({ headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env }) { let windowInitializationScript; if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) { ({ resourcePath } = this); } const timeoutInSeconds = Number.parseFloat(timeout); if (!Number.isNaN(timeoutInSeconds)) { const timeoutHandler = function() { console.log( `The test suite has timed out because it has been running for more than ${timeoutInSeconds} seconds.` ); return process.exit(124); // Use the same exit code as the UNIX timeout util. }; setTimeout(timeoutHandler, timeoutInSeconds * 1000); } try { windowInitializationScript = require.resolve( path.resolve(this.devResourcePath, 'src', 'initialize-test-window') ); } catch (error) { windowInitializationScript = require.resolve( path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window') ); } const testPaths = []; if (pathsToOpen != null) { for (let pathToOpen of pathsToOpen) { testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))); } } if (testPaths.length === 0) { process.stderr.write('Error: Specify at least one test path\n\n'); process.exit(1); } const legacyTestRunnerPath = this.resolveLegacyTestRunnerPath(); const testRunnerPath = this.resolveTestRunnerPath(testPaths[0]); const devMode = true; const isSpec = true; if (safeMode == null) { safeMode = false; } const window = this.createWindow({ windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env }); this.addWindow(window); if (env) window.replaceEnvironment(env); return window; } runBenchmarks({ headless, test, resourcePath, executedFrom, pathsToOpen, env }) { let windowInitializationScript; if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) { ({ resourcePath } = this); } try { windowInitializationScript = require.resolve( path.resolve(this.devResourcePath, 'src', 'initialize-benchmark-window') ); } catch (error) { windowInitializationScript = require.resolve( path.resolve( __dirname, '..', '..', 'src', 'initialize-benchmark-window' ) ); } const benchmarkPaths = []; if (pathsToOpen != null) { for (let pathToOpen of pathsToOpen) { benchmarkPaths.push( path.resolve(executedFrom, fs.normalize(pathToOpen)) ); } } if (benchmarkPaths.length === 0) { process.stderr.write('Error: Specify at least one benchmark path.\n\n'); process.exit(1); } const devMode = true; const isSpec = true; const safeMode = false; const window = this.createWindow({ windowInitializationScript, resourcePath, headless, test, isSpec, devMode, benchmarkPaths, safeMode, env }); this.addWindow(window); return window; } resolveTestRunnerPath(testPath) { let packageRoot; if (FindParentDir == null) { FindParentDir = require('find-parent-dir'); } if ((packageRoot = FindParentDir.sync(testPath, 'package.json'))) { const packageMetadata = require(path.join(packageRoot, 'package.json')); if (packageMetadata.atomTestRunner) { let testRunnerPath; if (Resolve == null) { Resolve = require('resolve'); } if ( (testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, { basedir: packageRoot, extensions: Object.keys(require.extensions) })) ) { return testRunnerPath; } else { process.stderr.write( `Error: Could not resolve test runner path '${ packageMetadata.atomTestRunner }'` ); process.exit(1); } } } return this.resolveLegacyTestRunnerPath(); } resolveLegacyTestRunnerPath() { try { return require.resolve( path.resolve(this.devResourcePath, 'spec', 'jasmine-test-runner') ); } catch (error) { return require.resolve( path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner') ); } } async parsePathToOpen(pathToOpen, executedFrom, extra) { const result = Object.assign( { pathToOpen, initialColumn: null, initialLine: null, exists: false, isDirectory: false, isFile: false }, extra ); if (!pathToOpen) { return result; } result.pathToOpen = result.pathToOpen.replace(/[:\s]+$/, ''); const match = result.pathToOpen.match(LocationSuffixRegExp); if (match != null) { result.pathToOpen = result.pathToOpen.slice(0, -match[0].length); if (match[1]) { result.initialLine = Math.max(0, parseInt(match[1].slice(1), 10) - 1); } if (match[2]) { result.initialColumn = Math.max(0, parseInt(match[2].slice(1), 10) - 1); } } const normalizedPath = path.normalize( path.resolve(executedFrom, fs.normalize(result.pathToOpen)) ); if (!url.parse(pathToOpen).protocol) { result.pathToOpen = normalizedPath; } await new Promise((resolve, reject) => { fs.stat(result.pathToOpen, (err, st) => { if (err) { if (err.code === 'ENOENT' || err.code === 'EACCES') { result.exists = false; resolve(); } else { reject(err); } return; } result.exists = true; result.isFile = st.isFile(); result.isDirectory = st.isDirectory(); resolve(); }); }); return result; } // Opens a native dialog to prompt the user for a path. // // Once paths are selected, they're opened in a new or existing {AtomWindow}s. // // options - // :type - A String which specifies the type of the dialog, could be 'file', // 'folder' or 'all'. The 'all' is only available on macOS. // :devMode - A Boolean which controls whether any newly opened windows // should be in dev mode or not. // :safeMode - A Boolean which controls whether any newly opened windows // should be in safe mode or not. // :window - An {AtomWindow} to use for opening selected file paths as long as // all are files. // :path - An optional String which controls the default path to which the // file dialog opens. promptForPathToOpen(type, { devMode, safeMode, window }, path = null) { return this.promptForPath( type, async pathsToOpen => { let targetWindow; // Open in :window as long as no chosen paths are folders. If any chosen path is a folder, open in a // new window instead. if (type === 'folder') { targetWindow = null; } else if (type === 'file') { targetWindow = window; } else if (type === 'all') { const areDirectories = await Promise.all( pathsToOpen.map( pathToOpen => new Promise(resolve => fs.isDirectory(pathToOpen, resolve)) ) ); if (!areDirectories.some(Boolean)) { targetWindow = window; } } return this.openPaths({ pathsToOpen, devMode, safeMode, window: targetWindow }); }, path ); } promptForPath(type, callback, path) { const properties = (() => { switch (type) { case 'file': return ['openFile']; case 'folder': return ['openDirectory']; case 'all': return ['openFile', 'openDirectory']; default: throw new Error(`${type} is an invalid type for promptForPath`); } })(); // Show the open dialog as child window on Windows and Linux, and as an independent dialog on macOS. This matches // most native apps. const parentWindow = process.platform === 'darwin' ? null : BrowserWindow.getFocusedWindow(); const openOptions = { properties: properties.concat(['multiSelections', 'createDirectory']), title: (() => { switch (type) { case 'file': return 'Open File'; case 'folder': return 'Open Folder'; default: return 'Open'; } })() }; // File dialog defaults to project directory of currently active editor if (path) openOptions.defaultPath = path; dialog .showOpenDialog(parentWindow, openOptions) .then(({ filePaths, bookmarks }) => { if (typeof callback === 'function') { callback(filePaths, bookmarks); } }); } async promptForRestart() { const result = await dialog.showMessageBox( BrowserWindow.getFocusedWindow(), { type: 'warning', title: 'Restart required', message: 'You will need to restart Atom for this change to take effect.', buttons: ['Restart Atom', 'Cancel'] } ); if (result.response === 0) this.restart(); } restart() { const args = []; if (this.safeMode) args.push('--safe'); if (this.logFile != null) args.push(`--log-file=${this.logFile}`); if (this.userDataDir != null) args.push(`--user-data-dir=${this.userDataDir}`); if (this.devMode) { args.push('--dev'); args.push(`--resource-path=${this.resourcePath}`); } app.relaunch({ args }); app.quit(); } disableZoomOnDisplayChange() { const callback = () => { this.getAllWindows().map(window => window.disableZoom()); }; // Set the limits every time a display is added or removed, otherwise the // configuration gets reset to the default, which allows zooming the // webframe. screen.on('display-added', callback); screen.on('display-removed', callback); return new Disposable(() => { screen.removeListener('display-added', callback); screen.removeListener('display-removed', callback); }); } }; class WindowStack { constructor(windows = []) { this.addWindow = this.addWindow.bind(this); this.touch = this.touch.bind(this); this.removeWindow = this.removeWindow.bind(this); this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this); this.all = this.all.bind(this); this.windows = windows; } addWindow(window) { this.removeWindow(window); return this.windows.unshift(window); } touch(window) { return this.addWindow(window); } removeWindow(window) { const currentIndex = this.windows.indexOf(window); if (currentIndex > -1) { return this.windows.splice(currentIndex, 1); } } getLastFocusedWindow(predicate) { if (predicate == null) { predicate = win => true; } return this.windows.find(predicate); } all() { return this.windows; } } ================================================ FILE: src/main-process/atom-protocol-handler.js ================================================ const { protocol } = require('electron'); const fs = require('fs-plus'); const path = require('path'); // Handles requests with 'atom' protocol. // // It's created by {AtomApplication} upon instantiation and is used to create a // custom resource loader for 'atom://' URLs. // // The following directories are searched in order: // * ~/.atom/assets // * ~/.atom/dev/packages (unless in safe mode) // * ~/.atom/packages // * RESOURCE_PATH/node_modules // module.exports = class AtomProtocolHandler { constructor(resourcePath, safeMode) { this.loadPaths = []; if (!safeMode) { this.loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages')); this.loadPaths.push(path.join(resourcePath, 'packages')); } this.loadPaths.push(path.join(process.env.ATOM_HOME, 'packages')); this.loadPaths.push(path.join(resourcePath, 'node_modules')); this.registerAtomProtocol(); } // Creates the 'atom' custom protocol handler. registerAtomProtocol() { protocol.registerFileProtocol('atom', (request, callback) => { const relativePath = path.normalize(request.url.substr(7)); let filePath; if (relativePath.indexOf('assets/') === 0) { const assetsPath = path.join(process.env.ATOM_HOME, relativePath); const stat = fs.statSyncNoException(assetsPath); if (stat && stat.isFile()) filePath = assetsPath; } if (!filePath) { for (let loadPath of this.loadPaths) { filePath = path.join(loadPath, relativePath); const stat = fs.statSyncNoException(filePath); if (stat && stat.isFile()) break; } } callback(filePath); }); } }; ================================================ FILE: src/main-process/atom-window.js ================================================ const { BrowserWindow, app, dialog, ipcMain, nativeImage } = require('electron'); const getAppName = require('../get-app-name'); const path = require('path'); const url = require('url'); const { EventEmitter } = require('events'); const StartupTime = require('../startup-time'); const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png'); let includeShellLoadTime = true; let nextId = 0; module.exports = class AtomWindow extends EventEmitter { constructor(atomApplication, fileRecoveryService, settings = {}) { StartupTime.addMarker('main-process:atom-window:start'); super(); this.id = nextId++; this.atomApplication = atomApplication; this.fileRecoveryService = fileRecoveryService; this.isSpec = settings.isSpec; this.headless = settings.headless; this.safeMode = settings.safeMode; this.devMode = settings.devMode; this.resourcePath = settings.resourcePath; const locationsToOpen = settings.locationsToOpen || []; this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve; }); this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve; }); const options = { show: false, title: getAppName(), tabbingIdentifier: 'atom', webPreferences: { // Prevent specs from throttling when the window is in the background: // this should result in faster CI builds, and an improvement in the // local development experience when running specs through the UI (which // now won't pause when e.g. minimizing the window). backgroundThrottling: !this.isSpec, // Disable the `auxclick` feature so that `click` events are triggered in // response to a middle-click. // (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960) disableBlinkFeatures: 'Auxclick', nodeIntegration: true, webviewTag: true, // TodoElectronIssue: remote module is deprecated https://www.electronjs.org/docs/breaking-changes#default-changed-enableremotemodule-defaults-to-false enableRemoteModule: true, // node support in threads nodeIntegrationInWorker: true }, simpleFullscreen: this.getSimpleFullscreen() }; // Don't set icon on Windows so the exe's ico will be used as window and // taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. if (process.platform === 'linux') options.icon = nativeImage.createFromPath(ICON_PATH); if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'; if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hiddenInset'; if (this.shouldHideTitleBar()) options.frame = false; const BrowserWindowConstructor = settings.browserWindowConstructor || BrowserWindow; this.browserWindow = new BrowserWindowConstructor(options); Object.defineProperty(this.browserWindow, 'loadSettingsJSON', { get: () => JSON.stringify( Object.assign( { userSettings: !this.isSpec ? this.atomApplication.configFile.get() : null }, this.loadSettings ) ) }); this.handleEvents(); this.loadSettings = Object.assign({}, settings); this.loadSettings.appVersion = app.getVersion(); this.loadSettings.appName = getAppName(); this.loadSettings.resourcePath = this.resourcePath; this.loadSettings.atomHome = process.env.ATOM_HOME; if (this.loadSettings.devMode == null) this.loadSettings.devMode = false; if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false; if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false; this.addLocationsToOpen(locationsToOpen); this.loadSettings.hasOpenFiles = locationsToOpen.some( location => location.pathToOpen && !location.isDirectory ); this.loadSettings.initialProjectRoots = this.projectRoots; StartupTime.addMarker('main-process:atom-window:end'); // Expose the startup markers to the renderer process, so we can have unified // measures about startup time between the main process and the renderer process. Object.defineProperty(this.browserWindow, 'startupMarkers', { get: () => { // We only want to make the main process startup data available once, // so if the window is refreshed or a new window is opened, the // renderer process won't use it again. const timingData = StartupTime.exportData(); StartupTime.deleteData(); return timingData; } }); // Only send to the first non-spec window created if (includeShellLoadTime && !this.isSpec) { includeShellLoadTime = false; if (!this.loadSettings.shellLoadTime) { this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime; } } if (!this.loadSettings.env) this.env = this.loadSettings.env; this.browserWindow.on('window:loaded', () => { this.disableZoom(); this.emit('window:loaded'); this.resolveLoadedPromise(); }); this.browserWindow.on('window:locations-opened', () => { this.emit('window:locations-opened'); }); this.browserWindow.on('enter-full-screen', () => { this.browserWindow.webContents.send('did-enter-full-screen'); }); this.browserWindow.on('leave-full-screen', () => { this.browserWindow.webContents.send('did-leave-full-screen'); }); this.browserWindow.loadURL( url.format({ protocol: 'file', pathname: `${this.resourcePath}/static/index.html`, slashes: true }) ); this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this); if (this.isSpec) this.browserWindow.focusOnWebView(); const hasPathToOpen = !( locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null ); if (hasPathToOpen && !this.isSpecWindow()) this.openLocations(locationsToOpen); } hasProjectPaths() { return this.projectRoots.length > 0; } setupContextMenu() { const ContextMenu = require('./context-menu'); this.browserWindow.on('context-menu', menuTemplate => { return new ContextMenu(menuTemplate, this); }); } containsLocations(locations) { return locations.every(location => this.containsLocation(location)); } containsLocation(location) { if (!location.pathToOpen) return false; return this.projectRoots.some(projectPath => { if (location.pathToOpen === projectPath) return true; if (location.pathToOpen.startsWith(path.join(projectPath, path.sep))) { if (!location.exists) return true; if (!location.isDirectory) return true; } return false; }); } handleEvents() { this.browserWindow.on('close', async event => { if ( (!this.atomApplication.quitting || this.atomApplication.quittingForUpdate) && !this.unloading ) { event.preventDefault(); this.unloading = true; this.atomApplication.saveCurrentWindowOptions(false); if (await this.prepareToUnload()) this.close(); } }); this.browserWindow.on('closed', () => { this.fileRecoveryService.didCloseWindow(this); this.atomApplication.removeWindow(this); this.resolveClosedPromise(); }); this.browserWindow.on('unresponsive', async () => { if (this.isSpec) return; const result = await dialog.showMessageBox(this.browserWindow, { type: 'warning', buttons: ['Force Close', 'Keep Waiting'], cancelId: 1, // Canceling should be the least destructive action message: 'Editor is not responding', detail: 'The editor is not responding. Would you like to force close it or just keep waiting?' }); if (result.response === 0) this.browserWindow.destroy(); }); this.browserWindow.webContents.on('render-process-gone', async () => { if (this.headless) { console.log('Renderer process crashed, exiting'); this.atomApplication.exit(100); return; } await this.fileRecoveryService.didCrashWindow(this); const result = await dialog.showMessageBox(this.browserWindow, { type: 'warning', buttons: ['Close Window', 'Reload', 'Keep It Open'], cancelId: 2, // Canceling should be the least destructive action message: 'The editor has crashed', detail: 'Please report this issue to https://github.com/atom/atom' }); switch (result.response) { case 0: this.browserWindow.destroy(); break; case 1: this.browserWindow.reload(); break; } }); this.browserWindow.webContents.on('will-navigate', (event, url) => { if (url !== this.browserWindow.webContents.getURL()) event.preventDefault(); }); this.setupContextMenu(); // Spec window's web view should always have focus if (this.isSpec) this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView()); } async prepareToUnload() { if (this.isSpecWindow()) return true; this.lastPrepareToUnloadPromise = new Promise(resolve => { const callback = (event, result) => { if ( BrowserWindow.fromWebContents(event.sender) === this.browserWindow ) { ipcMain.removeListener('did-prepare-to-unload', callback); if (!result) { this.unloading = false; this.atomApplication.quitting = false; } resolve(result); } }; ipcMain.on('did-prepare-to-unload', callback); this.browserWindow.webContents.send('prepare-to-unload'); }); return this.lastPrepareToUnloadPromise; } openPath(pathToOpen, initialLine, initialColumn) { return this.openLocations([{ pathToOpen, initialLine, initialColumn }]); } async openLocations(locationsToOpen) { this.addLocationsToOpen(locationsToOpen); await this.loadedPromise; this.sendMessage('open-locations', locationsToOpen); } didChangeUserSettings(settings) { this.sendMessage('did-change-user-settings', settings); } didFailToReadUserSettings(message) { this.sendMessage('did-fail-to-read-user-settings', message); } addLocationsToOpen(locationsToOpen) { const roots = new Set(this.projectRoots || []); for (const { pathToOpen, isDirectory } of locationsToOpen) { if (isDirectory) { roots.add(pathToOpen); } } this.projectRoots = Array.from(roots); this.projectRoots.sort(); } replaceEnvironment(env) { const { NODE_ENV, NODE_PATH, ATOM_HOME, ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT } = env; this.browserWindow.webContents.send('environment', { NODE_ENV, NODE_PATH, ATOM_HOME, ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT }); } sendMessage(message, detail) { this.browserWindow.webContents.send('message', message, detail); } sendCommand(command, ...args) { if (this.isSpecWindow()) { if (!this.atomApplication.sendCommandToFirstResponder(command)) { switch (command) { case 'window:reload': return this.reload(); case 'window:toggle-dev-tools': return this.toggleDevTools(); case 'window:close': return this.close(); } } } else if (this.isWebViewFocused()) { this.sendCommandToBrowserWindow(command, ...args); } else if (!this.atomApplication.sendCommandToFirstResponder(command)) { this.sendCommandToBrowserWindow(command, ...args); } } sendURIMessage(uri) { this.browserWindow.webContents.send('uri-message', uri); } sendCommandToBrowserWindow(command, ...args) { const action = args[0] && args[0].contextCommand ? 'context-command' : 'command'; this.browserWindow.webContents.send(action, command, ...args); } getDimensions() { const [x, y] = Array.from(this.browserWindow.getPosition()); const [width, height] = Array.from(this.browserWindow.getSize()); return { x, y, width, height }; } getSimpleFullscreen() { return this.atomApplication.config.get('core.simpleFullScreenWindows'); } shouldAddCustomTitleBar() { return ( !this.isSpec && process.platform === 'darwin' && this.atomApplication.config.get('core.titleBar') === 'custom' ); } shouldAddCustomInsetTitleBar() { return ( !this.isSpec && process.platform === 'darwin' && this.atomApplication.config.get('core.titleBar') === 'custom-inset' ); } shouldHideTitleBar() { return ( !this.isSpec && this.atomApplication.config.get('core.titleBar') === 'hidden' ); } close() { return this.browserWindow.close(); } focus() { return this.browserWindow.focus(); } minimize() { return this.browserWindow.minimize(); } maximize() { return this.browserWindow.maximize(); } unmaximize() { return this.browserWindow.unmaximize(); } restore() { return this.browserWindow.restore(); } setFullScreen(fullScreen) { return this.browserWindow.setFullScreen(fullScreen); } setAutoHideMenuBar(autoHideMenuBar) { return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar); } handlesAtomCommands() { return !this.isSpecWindow() && this.isWebViewFocused(); } isFocused() { return this.browserWindow.isFocused(); } isMaximized() { return this.browserWindow.isMaximized(); } isMinimized() { return this.browserWindow.isMinimized(); } isWebViewFocused() { return this.browserWindow.isWebViewFocused(); } isSpecWindow() { return this.isSpec; } reload() { this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve; }); this.prepareToUnload().then(canUnload => { if (canUnload) this.browserWindow.reload(); }); return this.loadedPromise; } showSaveDialog(options, callback) { options = Object.assign( { title: 'Save File', defaultPath: this.projectRoots[0] }, options ); let promise = dialog.showSaveDialog(this.browserWindow, options); if (typeof callback === 'function') { promise = promise.then(({ filePath, bookmark }) => { callback(filePath, bookmark); }); } return promise; } toggleDevTools() { return this.browserWindow.toggleDevTools(); } openDevTools() { return this.browserWindow.openDevTools(); } closeDevTools() { return this.browserWindow.closeDevTools(); } setDocumentEdited(documentEdited) { return this.browserWindow.setDocumentEdited(documentEdited); } setRepresentedFilename(representedFilename) { return this.browserWindow.setRepresentedFilename(representedFilename); } setProjectRoots(projectRootPaths) { this.projectRoots = projectRootPaths; this.projectRoots.sort(); this.loadSettings.initialProjectRoots = this.projectRoots; return this.atomApplication.saveCurrentWindowOptions(); } didClosePathWithWaitSession(path) { this.atomApplication.windowDidClosePathWithWaitSession(this, path); } copy() { return this.browserWindow.copy(); } disableZoom() { return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1); } getLoadedPromise() { return this.loadedPromise; } }; ================================================ FILE: src/main-process/auto-update-manager.js ================================================ const { EventEmitter } = require('events'); const os = require('os'); const path = require('path'); const IdleState = 'idle'; const CheckingState = 'checking'; const DownloadingState = 'downloading'; const UpdateAvailableState = 'update-available'; const NoUpdateAvailableState = 'no-update-available'; const UnsupportedState = 'unsupported'; const ErrorState = 'error'; let autoUpdater = null; module.exports = class AutoUpdateManager extends EventEmitter { constructor(version, testMode, config) { super(); this.onUpdateNotAvailable = this.onUpdateNotAvailable.bind(this); this.onUpdateError = this.onUpdateError.bind(this); this.version = version; this.testMode = testMode; this.config = config; this.state = IdleState; this.iconPath = path.resolve( __dirname, '..', '..', 'resources', 'atom.png' ); this.updateUrlPrefix = process.env.ATOM_UPDATE_URL_PREFIX || 'https://atom.io'; } initialize() { if (process.platform === 'win32') { const archSuffix = process.arch === 'ia32' ? '' : `-${process.arch}`; this.feedUrl = this.updateUrlPrefix + `/api/updates${archSuffix}?version=${this.version}&os_version=${ os.release }`; autoUpdater = require('./auto-updater-win32'); } else { this.feedUrl = this.updateUrlPrefix + `/api/updates?version=${this.version}&os_version=${os.release}`; ({ autoUpdater } = require('electron')); } autoUpdater.on('error', (event, message) => { this.setState(ErrorState, message); this.emitWindowEvent('update-error'); console.error(`Error Downloading Update: ${message}`); }); autoUpdater.setFeedURL(this.feedUrl); autoUpdater.on('checking-for-update', () => { this.setState(CheckingState); this.emitWindowEvent('checking-for-update'); }); autoUpdater.on('update-not-available', () => { this.setState(NoUpdateAvailableState); this.emitWindowEvent('update-not-available'); }); autoUpdater.on('update-available', () => { this.setState(DownloadingState); // We use sendMessage to send an event called 'update-available' in 'update-downloaded' // once the update download is complete. This mismatch between the electron // autoUpdater events is unfortunate but in the interest of not changing the // one existing event handled by applicationDelegate this.emitWindowEvent('did-begin-downloading-update'); this.emit('did-begin-download'); }); autoUpdater.on( 'update-downloaded', (event, releaseNotes, releaseVersion) => { this.releaseVersion = releaseVersion; this.setState(UpdateAvailableState); this.emitUpdateAvailableEvent(); } ); this.config.onDidChange('core.automaticallyUpdate', ({ newValue }) => { if (newValue) { this.scheduleUpdateCheck(); } else { this.cancelScheduledUpdateCheck(); } }); if (this.config.get('core.automaticallyUpdate')) this.scheduleUpdateCheck(); switch (process.platform) { case 'win32': if (!autoUpdater.supportsUpdates()) { this.setState(UnsupportedState); } break; case 'linux': this.setState(UnsupportedState); } } emitUpdateAvailableEvent() { if (this.releaseVersion == null) return; this.emitWindowEvent('update-available', { releaseVersion: this.releaseVersion }); } emitWindowEvent(eventName, payload) { for (let atomWindow of this.getWindows()) { atomWindow.sendMessage(eventName, payload); } } setState(state, errorMessage) { if (this.state === state) return; this.state = state; this.errorMessage = errorMessage; this.emit('state-changed', this.state); } getState() { return this.state; } getErrorMessage() { return this.errorMessage; } scheduleUpdateCheck() { // Only schedule update check periodically if running in release version and // and there is no existing scheduled update check. if (!/-dev/.test(this.version) && !this.checkForUpdatesIntervalID) { const checkForUpdates = () => this.check({ hidePopups: true }); const fourHours = 1000 * 60 * 60 * 4; this.checkForUpdatesIntervalID = setInterval(checkForUpdates, fourHours); checkForUpdates(); } } cancelScheduledUpdateCheck() { if (this.checkForUpdatesIntervalID) { clearInterval(this.checkForUpdatesIntervalID); this.checkForUpdatesIntervalID = null; } } check({ hidePopups } = {}) { if (!hidePopups) { autoUpdater.once('update-not-available', this.onUpdateNotAvailable); autoUpdater.once('error', this.onUpdateError); } if (!this.testMode) autoUpdater.checkForUpdates(); } install() { if (!this.testMode) autoUpdater.quitAndInstall(); } onUpdateNotAvailable() { autoUpdater.removeListener('error', this.onUpdateError); const { dialog } = require('electron'); dialog.showMessageBox({ type: 'info', buttons: ['OK'], icon: this.iconPath, message: 'No update available.', title: 'No Update Available', detail: `Version ${this.version} is the latest version.` }); } onUpdateError(event, message) { autoUpdater.removeListener( 'update-not-available', this.onUpdateNotAvailable ); const { dialog } = require('electron'); dialog.showMessageBox({ type: 'warning', buttons: ['OK'], icon: this.iconPath, message: 'There was an error checking for updates.', title: 'Update Error', detail: message }); } getWindows() { return global.atomApplication.getAllWindows(); } }; ================================================ FILE: src/main-process/auto-updater-win32.js ================================================ const { EventEmitter } = require('events'); const SquirrelUpdate = require('./squirrel-update'); class AutoUpdater extends EventEmitter { setFeedURL(updateUrl) { this.updateUrl = updateUrl; } quitAndInstall() { if (SquirrelUpdate.existsSync()) { SquirrelUpdate.restartAtom(); } else { require('electron').autoUpdater.quitAndInstall(); } } downloadUpdate(callback) { SquirrelUpdate.spawn(['--download', this.updateUrl], function( error, stdout ) { let update; if (error != null) return callback(error); try { // Last line of output is the JSON details about the releases const json = stdout .trim() .split('\n') .pop(); const data = JSON.parse(json); const releasesToApply = data && data.releasesToApply; if (releasesToApply.pop) update = releasesToApply.pop(); } catch (error) { error.stdout = stdout; return callback(error); } callback(null, update); }); } installUpdate(callback) { SquirrelUpdate.spawn(['--update', this.updateUrl], callback); } supportsUpdates() { return SquirrelUpdate.existsSync(); } checkForUpdates() { if (!this.updateUrl) throw new Error('Update URL is not set'); this.emit('checking-for-update'); if (!SquirrelUpdate.existsSync()) { this.emit('update-not-available'); return; } this.downloadUpdate((error, update) => { if (error != null) { this.emit('update-not-available'); return; } if (update == null) { this.emit('update-not-available'); return; } this.emit('update-available'); this.installUpdate(error => { if (error != null) { this.emit('update-not-available'); return; } this.emit( 'update-downloaded', {}, update.releaseNotes, update.version, new Date(), 'https://atom.io', () => this.quitAndInstall() ); }); }); } } module.exports = new AutoUpdater(); ================================================ FILE: src/main-process/context-menu.js ================================================ const { Menu } = require('electron'); module.exports = class ContextMenu { constructor(template, atomWindow) { this.atomWindow = atomWindow; this.createClickHandlers(template); const menu = Menu.buildFromTemplate(template); menu.popup(this.atomWindow.browserWindow, { async: true }); } // It's necessary to build the event handlers in this process, otherwise // closures are dragged across processes and failed to be garbage collected // appropriately. createClickHandlers(template) { template.forEach(item => { if (item.command) { if (!item.commandDetail) item.commandDetail = {}; item.commandDetail.contextCommand = true; item.click = () => { global.atomApplication.sendCommandToWindow( item.command, this.atomWindow, item.commandDetail ); }; } else if (item.submenu) { this.createClickHandlers(item.submenu); } }); } }; ================================================ FILE: src/main-process/file-recovery-service.js ================================================ const { dialog } = require('electron'); const crypto = require('crypto'); const Path = require('path'); const fs = require('fs-plus'); const mkdirp = require('mkdirp'); module.exports = class FileRecoveryService { constructor(recoveryDirectory) { this.recoveryDirectory = recoveryDirectory; this.recoveryFilesByFilePath = new Map(); this.recoveryFilesByWindow = new WeakMap(); this.windowsByRecoveryFile = new Map(); } async willSavePath(window, path) { const stats = await tryStatFile(path); if (!stats) return; const recoveryPath = Path.join( this.recoveryDirectory, RecoveryFile.fileNameForPath(path) ); const recoveryFile = this.recoveryFilesByFilePath.get(path) || new RecoveryFile(path, stats.mode, recoveryPath); try { await recoveryFile.retain(); } catch (err) { console.log( `Couldn't retain ${recoveryFile.recoveryPath}. Code: ${ err.code }. Message: ${err.message}` ); return; } if (!this.recoveryFilesByWindow.has(window)) { this.recoveryFilesByWindow.set(window, new Set()); } if (!this.windowsByRecoveryFile.has(recoveryFile)) { this.windowsByRecoveryFile.set(recoveryFile, new Set()); } this.recoveryFilesByWindow.get(window).add(recoveryFile); this.windowsByRecoveryFile.get(recoveryFile).add(window); this.recoveryFilesByFilePath.set(path, recoveryFile); } async didSavePath(window, path) { const recoveryFile = this.recoveryFilesByFilePath.get(path); if (recoveryFile != null) { try { await recoveryFile.release(); } catch (err) { console.log( `Couldn't release ${recoveryFile.recoveryPath}. Code: ${ err.code }. Message: ${err.message}` ); } if (recoveryFile.isReleased()) this.recoveryFilesByFilePath.delete(path); this.recoveryFilesByWindow.get(window).delete(recoveryFile); this.windowsByRecoveryFile.get(recoveryFile).delete(window); } } async didCrashWindow(window) { if (!this.recoveryFilesByWindow.has(window)) return; const promises = []; for (const recoveryFile of this.recoveryFilesByWindow.get(window)) { promises.push( recoveryFile .recover() .catch(error => { const message = 'A file that Atom was saving could be corrupted'; const detail = `Error ${error.code}. There was a crash while saving "${ recoveryFile.originalPath }", so this file might be blank or corrupted.\n` + `Atom couldn't recover it automatically, but a recovery file has been saved at: "${ recoveryFile.recoveryPath }".`; console.log(detail); dialog.showMessageBox(window, { type: 'info', buttons: ['OK'], message, detail }); }) .then(() => { for (let window of this.windowsByRecoveryFile.get(recoveryFile)) { this.recoveryFilesByWindow.get(window).delete(recoveryFile); } this.windowsByRecoveryFile.delete(recoveryFile); this.recoveryFilesByFilePath.delete(recoveryFile.originalPath); }) ); } await Promise.all(promises); } didCloseWindow(window) { if (!this.recoveryFilesByWindow.has(window)) return; for (let recoveryFile of this.recoveryFilesByWindow.get(window)) { this.windowsByRecoveryFile.get(recoveryFile).delete(window); } this.recoveryFilesByWindow.delete(window); } }; class RecoveryFile { static fileNameForPath(path) { const extension = Path.extname(path); const basename = Path.basename(path, extension).substring(0, 34); const randomSuffix = crypto.randomBytes(3).toString('hex'); return `${basename}-${randomSuffix}${extension}`; } constructor(originalPath, fileMode, recoveryPath) { this.originalPath = originalPath; this.fileMode = fileMode; this.recoveryPath = recoveryPath; this.refCount = 0; } async store() { await copyFile(this.originalPath, this.recoveryPath, this.fileMode); } async recover() { await copyFile(this.recoveryPath, this.originalPath, this.fileMode); await this.remove(); } async remove() { return new Promise((resolve, reject) => fs.unlink(this.recoveryPath, error => error && error.code !== 'ENOENT' ? reject(error) : resolve() ) ); } async retain() { if (this.isReleased()) await this.store(); this.refCount++; } async release() { this.refCount--; if (this.isReleased()) await this.remove(); } isReleased() { return this.refCount === 0; } } async function tryStatFile(path) { return new Promise((resolve, reject) => fs.stat(path, (error, result) => resolve(error == null && result)) ); } async function copyFile(source, destination, mode) { return new Promise((resolve, reject) => { mkdirp(Path.dirname(destination), error => { if (error) return reject(error); const readStream = fs.createReadStream(source); readStream.on('error', reject).once('open', () => { const writeStream = fs.createWriteStream(destination, { mode }); writeStream .on('error', reject) .on('open', () => readStream.pipe(writeStream)) .once('close', () => resolve()); }); }); }); } ================================================ FILE: src/main-process/main.js ================================================ if (typeof snapshotResult !== 'undefined') { snapshotResult.setGlobals(global, process, global, {}, console, require); } const startTime = Date.now(); const StartupTime = require('../startup-time'); StartupTime.setStartTime(); const path = require('path'); const fs = require('fs-plus'); const CSON = require('season'); const yargs = require('yargs'); const { app } = require('electron'); const version = `Atom : ${app.getVersion()} Electron: ${process.versions.electron} Chrome : ${process.versions.chrome} Node : ${process.versions.node}`; const args = yargs(process.argv) .alias('v', 'version') .version(version) .alias('d', 'dev') .alias('t', 'test') .alias('r', 'resource-path').argv; function isAtomRepoPath(repoPath) { let packageJsonPath = path.join(repoPath, 'package.json'); if (fs.statSyncNoException(packageJsonPath)) { try { let packageJson = CSON.readFileSync(packageJsonPath); return packageJson.name === 'atom'; } catch (e) { return false; } } return false; } let resourcePath; let devResourcePath; if (args.resourcePath) { resourcePath = args.resourcePath; devResourcePath = resourcePath; } else { const stableResourcePath = path.dirname(path.dirname(__dirname)); const defaultRepositoryPath = path.join( app.getPath('home'), 'github', 'atom' ); if (process.env.ATOM_DEV_RESOURCE_PATH) { devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH; } else if (isAtomRepoPath(process.cwd())) { devResourcePath = process.cwd(); } else if (fs.statSyncNoException(defaultRepositoryPath)) { devResourcePath = defaultRepositoryPath; } else { devResourcePath = stableResourcePath; } if (args.dev || args.test || args.benchmark || args.benchmarkTest) { resourcePath = devResourcePath; } else { resourcePath = stableResourcePath; } } const start = require(path.join(resourcePath, 'src', 'main-process', 'start')); start(resourcePath, devResourcePath, startTime); ================================================ FILE: src/main-process/parse-command-line.js ================================================ 'use strict'; const dedent = require('dedent'); const yargs = require('yargs'); const { app } = require('electron'); module.exports = function parseCommandLine(processArgs) { // macOS Gatekeeper adds a flag ("-psn_0_[six or seven digits here]") when it intercepts Atom launches. // (This happens for fresh downloads, new installs, or first launches after upgrading). // We don't need this flag, and yargs interprets it as many short flags. So, we filter it out. const filteredArgs = processArgs.filter(arg => !arg.startsWith('-psn_')); const options = yargs(filteredArgs).wrap(yargs.terminalWidth()); const version = app.getVersion(); options.usage( dedent`Atom Editor v${version} Usage: atom atom [options] [path ...] atom file[:line[:column]] One or more paths to files or folders may be specified. If there is an existing Atom window that contains all of the given folders, the paths will be opened in that window. Otherwise, they will be opened in a new window. A file may be opened at the desired line (and optionally column) by appending the numbers right after the file name, e.g. \`atom file:5:8\`. 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. Defaults to \`~/github/atom\`. ATOM_HOME The root path for all configuration files and folders. Defaults to \`~/.atom\`.` ); // Deprecated 1.0 API preview flag options .alias('1', 'one') .boolean('1') .describe('1', 'This option is no longer supported.'); options .boolean('include-deprecated-apis') .describe( 'include-deprecated-apis', 'This option is not currently supported.' ); options .alias('d', 'dev') .boolean('d') .describe('d', 'Run in development mode.'); options .alias('f', 'foreground') .boolean('f') .describe('f', 'Keep the main process in the foreground.'); options .alias('h', 'help') .boolean('h') .describe('h', 'Print this usage message.'); options .alias('l', 'log-file') .string('l') .describe('l', 'Log all output to file when running tests.'); options .alias('n', 'new-window') .boolean('n') .describe('n', 'Open a new window.'); options .boolean('profile-startup') .describe( 'profile-startup', 'Create a profile of the startup execution time.' ); options .alias('r', 'resource-path') .string('r') .describe( 'r', 'Set the path to the Atom source directory and enable dev-mode.' ); options .boolean('safe') .describe( 'safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.' ); options .boolean('benchmark') .describe( 'benchmark', 'Open a new window that runs the specified benchmarks.' ); options .boolean('benchmark-test') .describe( 'benchmark-test', 'Run a faster version of the benchmarks in headless mode.' ); options .alias('t', 'test') .boolean('t') .describe( 't', 'Run the specified specs and exit with error code on failures.' ); options .alias('m', 'main-process') .boolean('m') .describe('m', 'Run the specified specs in the main process.'); options .string('timeout') .describe( 'timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).' ); options .alias('w', 'wait') .boolean('w') .describe('w', 'Wait for window to be closed before returning.'); options .alias('a', 'add') .boolean('a') .describe('add', 'Open path as a new project in last used window.'); 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'); 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()); process.exit(0); } const addToLastWindow = args['add']; const safeMode = args['safe']; const benchmark = args['benchmark']; const benchmarkTest = args['benchmark-test']; const test = args['test']; const mainProcess = args['main-process']; const timeout = args['timeout']; const newWindow = args['new-window']; let executedFrom = null; if (args['executed-from'] && args['executed-from'].toString()) { executedFrom = args['executed-from'].toString(); } else { executedFrom = process.cwd(); } if (newWindow && addToLastWindow) { process.stderr.write( `Only one of the --add and --new-window options may be specified at the same time.\n\n${options.help()}` ); // Exiting the main process with a nonzero exit code on MacOS causes the app open to fail with the mysterious // message "LSOpenURLsWithRole() failed for the application /Applications/Atom Dev.app with error -10810." process.exit(0); } let pidToKillWhenClosed = null; if (args['wait']) { pidToKillWhenClosed = args['pid']; } const logFile = args['log-file']; const userDataDir = args['user-data-dir']; const profileStartup = args['profile-startup']; const clearWindowState = args['clear-window-state']; let pathsToOpen = []; let urlsToOpen = []; let devMode = args['dev']; for (const path of args._) { if (typeof path !== 'string') { // Sometimes non-strings (such as numbers or boolean true) get into args._ // In the next block, .startsWith() only works on strings. So, skip non-string arguments. continue; } if (path.startsWith('atom://')) { urlsToOpen.push(path); } else { pathsToOpen.push(path); } } if (args.resourcePath || test) { devMode = true; } if (args['path-environment']) { // On Yosemite the $PATH is not inherited by the "open" command, so we have to // explicitly pass it by command line, see http://git.io/YC8_Ew. process.env.PATH = args['path-environment']; } return { pathsToOpen, urlsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, userDataDir, profileStartup, timeout, clearWindowState, addToLastWindow, mainProcess, benchmark, benchmarkTest, env: process.env }; }; ================================================ FILE: src/main-process/spawner.js ================================================ const ChildProcess = require('child_process'); // Spawn a command and invoke the callback when it completes with an error // and the output from standard out. // // * `command` The underlying OS command {String} to execute. // * `args` (optional) The {Array} with arguments to be passed to command. // * `callback` (optional) The {Function} to call after the command has run. It will be invoked with arguments: // * `error` (optional) An {Error} object returned by the command, `null` if no error was thrown. // * `code` Error code returned by the command. // * `stdout` The {String} output text generated by the command. // * `stdout` The {String} output text generated by the command. exports.spawn = function(command, args, callback) { let error; let spawnedProcess; let stdout = ''; try { spawnedProcess = ChildProcess.spawn(command, args); } catch (error) { process.nextTick(() => callback && callback(error, stdout)); return; } spawnedProcess.stdout.on('data', data => { stdout += data; }); spawnedProcess.on('error', processError => { error = processError; }); spawnedProcess.on('close', (code, signal) => { if (!error && code !== 0) { error = new Error(`Command failed: ${signal != null ? signal : code}`); } if (error) { if (error.code == null) error.code = code; if (error.stdout == null) error.stdout = stdout; } callback && callback(error, stdout); }); // This is necessary if using Powershell 2 on Windows 7 to get the events to raise // http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs return spawnedProcess.stdin.end(); }; ================================================ FILE: src/main-process/squirrel-update.js ================================================ let setxPath; const { app } = require('electron'); const fs = require('fs-plus'); const getAppName = require('../get-app-name'); const path = require('path'); const Spawner = require('./spawner'); const WinShell = require('./win-shell'); const WinPowerShell = require('./win-powershell'); const appFolder = path.resolve(process.execPath, '..'); const rootAtomFolder = path.resolve(appFolder, '..'); const binFolder = path.join(rootAtomFolder, 'bin'); const updateDotExe = path.join(rootAtomFolder, 'Update.exe'); const execName = path.basename(app.getPath('exe')); if (process.env.SystemRoot) { const system32Path = path.join(process.env.SystemRoot, 'System32'); setxPath = path.join(system32Path, 'setx.exe'); } else { setxPath = 'setx.exe'; } // Spawn setx.exe and callback when it completes const spawnSetx = (args, callback) => Spawner.spawn(setxPath, args, callback); // Spawn the Update.exe with the given arguments and invoke the callback when // the command completes. const spawnUpdate = (args, callback) => Spawner.spawn(updateDotExe, args, callback); // Add atom and apm to the PATH // // This is done by adding .cmd shims to the root bin folder in the Atom // install directory that point to the newly installed versions inside // the versioned app directories. const addCommandsToPath = callback => { const atomCmdName = execName.replace('.exe', '.cmd'); const apmCmdName = atomCmdName.replace('atom', 'apm'); const atomShName = execName.replace('.exe', ''); const apmShName = atomShName.replace('atom', 'apm'); const installCommands = callback => { const atomCommandPath = path.join(binFolder, atomCmdName); const relativeAtomPath = path.relative( binFolder, path.join(appFolder, 'resources', 'cli', 'atom.cmd') ); const atomCommand = `@echo off\r\n"%~dp0\\${relativeAtomPath}" %*`; const atomShCommandPath = path.join(binFolder, atomShName); const relativeAtomShPath = path.relative( binFolder, path.join(appFolder, 'resources', 'cli', 'atom.sh') ); const atomShCommand = `#!/bin/sh\r\n"$(dirname "$0")/${relativeAtomShPath.replace( /\\/g, '/' )}" "$@"\r\necho`; const apmCommandPath = path.join(binFolder, apmCmdName); const relativeApmPath = path.relative( binFolder, path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm.cmd') ); const apmCommand = `@echo off\r\n"%~dp0\\${relativeApmPath}" %*`; const apmShCommandPath = path.join(binFolder, apmShName); const relativeApmShPath = path.relative( binFolder, path.join(appFolder, 'resources', 'cli', 'apm.sh') ); const apmShCommand = `#!/bin/sh\r\n"$(dirname "$0")/${relativeApmShPath.replace( /\\/g, '/' )}" "$@"`; fs.writeFile(atomCommandPath, atomCommand, () => fs.writeFile(atomShCommandPath, atomShCommand, () => fs.writeFile(apmCommandPath, apmCommand, () => fs.writeFile(apmShCommandPath, apmShCommand, () => callback()) ) ) ); }; const addBinToPath = (pathSegments, callback) => { pathSegments.push(binFolder); const newPathEnv = pathSegments.join(';'); spawnSetx(['Path', newPathEnv], callback); }; installCommands(error => { if (error) return callback(error); WinPowerShell.getPath((error, pathEnv) => { if (error) return callback(error); const pathSegments = pathEnv .split(/;+/) .filter(pathSegment => pathSegment); if (pathSegments.indexOf(binFolder) === -1) { addBinToPath(pathSegments, callback); } else { callback(); } }); }); }; // Remove atom and apm from the PATH const removeCommandsFromPath = callback => WinPowerShell.getPath((error, pathEnv) => { if (error != null) { return callback(error); } const pathSegments = pathEnv .split(/;+/) .filter(pathSegment => pathSegment && pathSegment !== binFolder); const newPathEnv = pathSegments.join(';'); if (pathEnv !== newPathEnv) { return spawnSetx(['Path', newPathEnv], callback); } else { return callback(); } }); // Create a desktop and start menu shortcut by using the command line API // provided by Squirrel's Update.exe const createShortcuts = (locations, callback) => spawnUpdate( ['--createShortcut', execName, '-l', locations.join(',')], callback ); // Update the desktop and start menu shortcuts by using the command line API // provided by Squirrel's Update.exe const updateShortcuts = callback => { const homeDirectory = fs.getHomeDirectory(); if (homeDirectory) { const desktopShortcutPath = path.join( homeDirectory, 'Desktop', `${getAppName()}.lnk` ); // Check if the desktop shortcut has been previously deleted and // and keep it deleted if it was fs.exists(desktopShortcutPath, desktopShortcutExists => { const locations = ['StartMenu']; if (desktopShortcutExists) { locations.push('Desktop'); } createShortcuts(locations, callback); }); } else { createShortcuts(['Desktop', 'StartMenu'], callback); } }; // Remove the desktop and start menu shortcuts by using the command line API // provided by Squirrel's Update.exe const removeShortcuts = callback => spawnUpdate(['--removeShortcut', execName], callback); exports.spawn = spawnUpdate; // Is the Update.exe installed with Atom? exports.existsSync = () => fs.existsSync(updateDotExe); // Restart Atom using the version pointed to by the atom.cmd shim exports.restartAtom = () => { let args; const atomCmdName = execName.replace('.exe', '.cmd'); if (global.atomApplication && global.atomApplication.lastFocusedWindow) { const { projectPath } = global.atomApplication.lastFocusedWindow; if (projectPath) args = [projectPath]; } Spawner.spawn(path.join(binFolder, atomCmdName), args); app.quit(); }; const updateContextMenus = callback => WinShell.fileContextMenu.update(() => WinShell.folderContextMenu.update(() => WinShell.folderBackgroundContextMenu.update(() => callback()) ) ); // Handle squirrel events denoted by --squirrel-* command line arguments. exports.handleStartupEvent = squirrelCommand => { switch (squirrelCommand) { case '--squirrel-install': createShortcuts(['Desktop', 'StartMenu'], () => addCommandsToPath(() => WinShell.fileHandler.register(() => updateContextMenus(() => app.quit()) ) ) ); return true; case '--squirrel-updated': updateShortcuts(() => addCommandsToPath(() => WinShell.fileHandler.update(() => updateContextMenus(() => app.quit()) ) ) ); return true; case '--squirrel-uninstall': removeShortcuts(() => removeCommandsFromPath(() => WinShell.fileHandler.deregister(() => WinShell.fileContextMenu.deregister(() => WinShell.folderContextMenu.deregister(() => WinShell.folderBackgroundContextMenu.deregister(() => app.quit() ) ) ) ) ) ); return true; case '--squirrel-obsolete': app.quit(); return true; default: return false; } }; ================================================ FILE: src/main-process/start.js ================================================ const { app } = require('electron'); const nslog = require('nslog'); const path = require('path'); const temp = require('temp'); const parseCommandLine = require('./parse-command-line'); const startCrashReporter = require('../crash-reporter-start'); const getReleaseChannel = require('../get-release-channel'); const atomPaths = require('../atom-paths'); const fs = require('fs'); const CSON = require('season'); const Config = require('../config'); const StartupTime = require('../startup-time'); StartupTime.setStartTime(); module.exports = function start(resourcePath, devResourcePath, startTime) { global.shellStartTime = startTime; StartupTime.addMarker('main-process:start'); process.on('uncaughtException', function(error = {}) { if (error.message != null) { console.log(error.message); } if (error.stack != null) { console.log(error.stack); } }); process.on('unhandledRejection', function(error = {}) { if (error.message != null) { console.log(error.message); } if (error.stack != null) { console.log(error.stack); } }); const previousConsoleLog = console.log; console.log = nslog; // TodoElectronIssue this should be set to true before Electron 12 - https://github.com/electron/electron/issues/18397 app.allowRendererProcessReuse = false; app.commandLine.appendSwitch('enable-experimental-web-platform-features'); const args = parseCommandLine(process.argv.slice(1)); args.resourcePath = normalizeDriveLetterName(resourcePath); args.devResourcePath = normalizeDriveLetterName(devResourcePath); atomPaths.setAtomHome(app.getPath('home')); atomPaths.setUserData(app); const config = getConfig(); const colorProfile = config.get('core.colorProfile'); if (colorProfile && colorProfile !== 'default') { app.commandLine.appendSwitch('force-color-profile', colorProfile); } if (handleStartupEventWithSquirrel()) { return; } else if (args.test && args.mainProcess) { app.setPath( 'userData', temp.mkdirSync('atom-user-data-dir-for-main-process-tests') ); console.log = previousConsoleLog; app.on('ready', function() { const testRunner = require(path.join( args.resourcePath, 'spec/main-process/mocha-test-runner' )); testRunner(args.pathsToOpen); }); return; } const releaseChannel = getReleaseChannel(app.getVersion()); let appUserModelId = 'com.squirrel.atom.' + process.arch; // If the release channel is not stable, we append it to the app user model id. // This allows having the different release channels as separate items in the taskbar. if (releaseChannel !== 'stable') { appUserModelId += `-${releaseChannel}`; } // NB: This prevents Win10 from showing dupe items in the taskbar. app.setAppUserModelId(appUserModelId); function addPathToOpen(event, pathToOpen) { event.preventDefault(); args.pathsToOpen.push(pathToOpen); } function addUrlToOpen(event, urlToOpen) { event.preventDefault(); args.urlsToOpen.push(urlToOpen); } app.on('open-file', addPathToOpen); app.on('open-url', addUrlToOpen); app.on('will-finish-launching', () => startCrashReporter({ uploadToServer: config.get('core.telemetryConsent') === 'limited', releaseChannel }) ); if (args.userDataDir != null) { app.setPath('userData', args.userDataDir); } else if (args.test || args.benchmark || args.benchmarkTest) { app.setPath('userData', temp.mkdirSync('atom-test-data')); } StartupTime.addMarker('main-process:electron-onready:start'); app.on('ready', function() { StartupTime.addMarker('main-process:electron-onready:end'); app.removeListener('open-file', addPathToOpen); app.removeListener('open-url', addUrlToOpen); const AtomApplication = require(path.join( args.resourcePath, 'src', 'main-process', 'atom-application' )); AtomApplication.open(args); }); }; function handleStartupEventWithSquirrel() { if (process.platform !== 'win32') { return false; } const SquirrelUpdate = require('./squirrel-update'); const squirrelCommand = process.argv[1]; return SquirrelUpdate.handleStartupEvent(squirrelCommand); } function getConfig() { const config = new Config(); let configFilePath; if (fs.existsSync(path.join(process.env.ATOM_HOME, 'config.json'))) { configFilePath = path.join(process.env.ATOM_HOME, 'config.json'); } else if (fs.existsSync(path.join(process.env.ATOM_HOME, 'config.cson'))) { configFilePath = path.join(process.env.ATOM_HOME, 'config.cson'); } if (configFilePath) { const configFileData = CSON.readFileSync(configFilePath); config.resetUserSettings(configFileData); } return config; } function normalizeDriveLetterName(filePath) { if (process.platform === 'win32' && filePath) { return filePath.replace( /^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':' ); } else { return filePath; } } ================================================ FILE: src/main-process/win-powershell.js ================================================ let powershellPath; const path = require('path'); const Spawner = require('./spawner'); if (process.env.SystemRoot) { const system32Path = path.join(process.env.SystemRoot, 'System32'); powershellPath = path.join( system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe' ); } else { powershellPath = 'powershell.exe'; } // Spawn powershell.exe and callback when it completes const spawnPowershell = function(args, callback) { // Set encoding and execute the command, capture the output, and return it // via .NET's console in order to have consistent UTF-8 encoding. // See http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell // to address https://github.com/atom/atom/issues/5063 args[0] = `\ [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 $output=${args[0]} [Console]::WriteLine($output)\ `; args.unshift('-command'); args.unshift('RemoteSigned'); args.unshift('-ExecutionPolicy'); args.unshift('-noprofile'); Spawner.spawn(powershellPath, args, callback); }; // Get the user's PATH environment variable registry value. // // * `callback` The {Function} to call after registry operation is done. // It will be invoked with the same arguments provided by {Spawner.spawn}. // // Returns the user's path {String}. exports.getPath = callback => spawnPowershell( ["[environment]::GetEnvironmentVariable('Path','User')"], function(error, stdout) { if (error != null) { return callback(error); } const pathOutput = stdout.replace(/^\s+|\s+$/g, ''); return callback(null, pathOutput); } ); ================================================ FILE: src/main-process/win-shell.js ================================================ const Registry = require('winreg'); const Path = require('path'); const getAppName = require('../get-app-name'); const appName = getAppName(); const exeName = Path.basename(process.execPath); const appPath = `"${process.execPath}"`; const fileIconPath = `"${Path.join( process.execPath, '..', 'resources', 'cli', 'file.ico' )}"`; class ShellOption { constructor(key, parts) { this.isRegistered = this.isRegistered.bind(this); this.register = this.register.bind(this); this.deregister = this.deregister.bind(this); this.update = this.update.bind(this); this.key = key; this.parts = parts; } isRegistered(callback) { new Registry({ hive: 'HKCU', key: `${this.key}\\${this.parts[0].key}` }).get(this.parts[0].name, (err, val) => callback(err == null && val != null && val.value === this.parts[0].value) ); } register(callback) { let doneCount = this.parts.length; this.parts.forEach(part => { let reg = new Registry({ hive: 'HKCU', key: part.key != null ? `${this.key}\\${part.key}` : this.key }); return reg.create(() => reg.set(part.name, Registry.REG_SZ, part.value, () => { if (--doneCount === 0) return callback(); }) ); }); } deregister(callback) { this.isRegistered(isRegistered => { if (isRegistered) { new Registry({ hive: 'HKCU', key: this.key }).destroy(() => callback(null, true) ); } else { callback(null, false); } }); } update(callback) { new Registry({ hive: 'HKCU', key: `${this.key}\\${this.parts[0].key}` }).get(this.parts[0].name, (err, val) => { if (err != null || val == null) { callback(err); } else { this.register(callback); } }); } } exports.appName = appName; exports.fileHandler = new ShellOption( `\\Software\\Classes\\Applications\\${exeName}`, [ { key: 'shell\\open\\command', name: '', value: `${appPath} "%1"` }, { key: 'shell\\open', name: 'FriendlyAppName', value: `${appName}` }, { key: 'DefaultIcon', name: '', value: `${fileIconPath}` } ] ); let contextParts = [ { key: 'command', name: '', value: `${appPath} "%1"` }, { name: '', value: `Open with ${appName}` }, { name: 'Icon', value: `${appPath}` } ]; exports.fileContextMenu = new ShellOption( `\\Software\\Classes\\*\\shell\\${appName}`, contextParts ); exports.folderContextMenu = new ShellOption( `\\Software\\Classes\\Directory\\shell\\${appName}`, contextParts ); exports.folderBackgroundContextMenu = new ShellOption( `\\Software\\Classes\\Directory\\background\\shell\\${appName}`, JSON.parse(JSON.stringify(contextParts).replace('%1', '%V')) ); ================================================ FILE: src/menu-helpers.js ================================================ const _ = require('underscore-plus'); const ItemSpecificities = new WeakMap(); // Add an item to a menu, ensuring separators are not duplicated. function addItemToMenu(item, menu) { const lastMenuItem = _.last(menu); const lastMenuItemIsSpearator = lastMenuItem && lastMenuItem.type === 'separator'; if (!(item.type === 'separator' && lastMenuItemIsSpearator)) { menu.push(item); } } function merge(menu, item, itemSpecificity = Infinity) { item = cloneMenuItem(item); ItemSpecificities.set(item, itemSpecificity); const matchingItemIndex = findMatchingItemIndex(menu, item); if (matchingItemIndex === -1) { addItemToMenu(item, menu); return; } const matchingItem = menu[matchingItemIndex]; if (item.submenu != null) { for (let submenuItem of item.submenu) { merge(matchingItem.submenu, submenuItem, itemSpecificity); } } else if ( itemSpecificity && itemSpecificity >= ItemSpecificities.get(matchingItem) ) { menu[matchingItemIndex] = item; } } function unmerge(menu, item) { item = cloneMenuItem(item); const matchingItemIndex = findMatchingItemIndex(menu, item); if (matchingItemIndex === -1) { return; } const matchingItem = menu[matchingItemIndex]; if (item.submenu != null) { for (let submenuItem of item.submenu) { unmerge(matchingItem.submenu, submenuItem); } } if (matchingItem.submenu == null || matchingItem.submenu.length === 0) { menu.splice(matchingItemIndex, 1); } } function findMatchingItemIndex(menu, { type, id, submenu }) { if (type === 'separator') { return -1; } for (let index = 0; index < menu.length; index++) { const item = menu[index]; if (item.id === id && (item.submenu != null) === (submenu != null)) { return index; } } return -1; } function normalizeLabel(label) { if (label == null) { return; } return process.platform === 'darwin' ? label : label.replace(/&/g, ''); } function cloneMenuItem(item) { item = _.pick( item, 'type', 'label', 'id', 'enabled', 'visible', 'command', 'submenu', 'commandDetail', 'role', 'accelerator', 'before', 'after', 'beforeGroupContaining', 'afterGroupContaining' ); if (item.id === null || item.id === undefined) { item.id = normalizeLabel(item.label); } if (item.submenu != null) { item.submenu = item.submenu.map(submenuItem => cloneMenuItem(submenuItem)); } return item; } // Determine the Electron accelerator for a given Atom keystroke. // // keystroke - The keystroke. // // Returns a String containing the keystroke in a format that can be interpreted // by Electron to provide nice icons where available. function acceleratorForKeystroke(keystroke) { if (!keystroke) { return null; } let modifiers = keystroke.split(/-(?=.)/); const key = modifiers .pop() .toUpperCase() .replace('+', 'Plus'); modifiers = modifiers.map(modifier => modifier .replace(/shift/gi, 'Shift') .replace(/cmd/gi, 'Command') .replace(/ctrl/gi, 'Ctrl') .replace(/alt/gi, 'Alt') ); const keys = [...modifiers, key]; return keys.join('+'); } module.exports = { merge, unmerge, normalizeLabel, cloneMenuItem, acceleratorForKeystroke }; ================================================ FILE: src/menu-manager.coffee ================================================ path = require 'path' _ = require 'underscore-plus' {ipcRenderer} = require 'electron' CSON = require 'season' fs = require 'fs-plus' {Disposable} = require 'event-kit' MenuHelpers = require './menu-helpers' platformMenu = require('../package.json')?._atomMenu?.menu # Extended: Provides a registry for menu items that you'd like to appear in the # application menu. # # An instance of this class is always available as the `atom.menu` global. # # ## Menu CSON Format # # Here is an example from the [tree-view](https://github.com/atom/tree-view/blob/master/menus/tree-view.cson): # # ```coffee # [ # { # 'label': 'View' # 'submenu': [ # { 'label': 'Toggle Tree View', 'command': 'tree-view:toggle' } # ] # } # { # 'label': 'Packages' # 'submenu': [ # 'label': 'Tree View' # 'submenu': [ # { 'label': 'Focus', 'command': 'tree-view:toggle-focus' } # { 'label': 'Toggle', 'command': 'tree-view:toggle' } # { 'label': 'Reveal Active File', 'command': 'tree-view:reveal-active-file' } # { 'label': 'Toggle Tree Side', 'command': 'tree-view:toggle-side' } # ] # ] # } # ] # ``` # # Use in your package's menu `.cson` file requires that you place your menu # structure under a `menu` key. # # ```coffee # 'menu': [ # { # 'label': 'View' # 'submenu': [ # { 'label': 'Toggle Tree View', 'command': 'tree-view:toggle' } # ] # } # ] # ``` # # See {::add} for more info about adding menu's directly. module.exports = class MenuManager constructor: ({@resourcePath, @keymapManager, @packageManager}) -> @initialized = false @pendingUpdateOperation = null @template = [] @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems() @packageManager.onDidActivateInitialPackages => @sortPackagesMenu() initialize: ({@resourcePath}) -> @keymapManager.onDidReloadKeymap => @update() @update() @initialized = true # Public: Adds the given items to the application menu. # # ## Examples # ```coffee # atom.menu.add [ # { # label: 'Hello' # submenu : [{label: 'World!', id: 'World!', command: 'hello:world'}] # } # ] # ``` # # * `items` An {Array} of menu item {Object}s containing the keys: # * `label` The {String} menu label. # * `submenu` An optional {Array} of sub menu items. # * `command` An optional {String} command to trigger when the item is # clicked. # # * `id` (internal) A {String} containing the menu item's id. # Returns a {Disposable} on which `.dispose()` can be called to remove the # added menu items. add: (items) -> items = _.deepClone(items) for item in items continue unless item.label? # TODO: Should we emit a warning here? @merge(@template, item) @update() new Disposable => @remove(items) remove: (items) -> @unmerge(@template, item) for item in items @update() clear: -> @template = [] @update() # Should the binding for the given selector be included in the menu # commands. # # * `selector` A {String} selector to check. # # Returns a {Boolean}, true to include the selector, false otherwise. includeSelector: (selector) -> try return true if document.body.webkitMatchesSelector(selector) catch error # Selector isn't valid return false # Simulate an atom-text-editor element attached to a atom-workspace element attached # to a body element that has the same classes as the current body element. unless @testEditor? # Use new document so that custom elements don't actually get created testDocument = document.implementation.createDocument(document.namespaceURI, 'html') testBody = testDocument.createElement('body') testBody.classList.add(@classesForElement(document.body)...) testWorkspace = testDocument.createElement('atom-workspace') workspaceClasses = @classesForElement(document.body.querySelector('atom-workspace')) workspaceClasses = ['workspace'] if workspaceClasses.length is 0 testWorkspace.classList.add(workspaceClasses...) testBody.appendChild(testWorkspace) @testEditor = testDocument.createElement('atom-text-editor') @testEditor.classList.add('editor') testWorkspace.appendChild(@testEditor) element = @testEditor while element return true if element.webkitMatchesSelector(selector) element = element.parentElement false # Public: Refreshes the currently visible menu. update: -> return unless @initialized clearTimeout(@pendingUpdateOperation) if @pendingUpdateOperation? @pendingUpdateOperation = setTimeout(=> unsetKeystrokes = new Set for binding in @keymapManager.getKeyBindings() if binding.command is 'unset!' unsetKeystrokes.add(binding.keystrokes) keystrokesByCommand = {} for binding in @keymapManager.getKeyBindings() continue unless @includeSelector(binding.selector) continue if unsetKeystrokes.has(binding.keystrokes) continue if process.platform is 'darwin' and /^alt-(shift-)?.$/.test(binding.keystrokes) continue if process.platform is 'win32' and /^ctrl-alt-(shift-)?.$/.test(binding.keystrokes) keystrokesByCommand[binding.command] ?= [] keystrokesByCommand[binding.command].unshift binding.keystrokes @sendToBrowserProcess(@template, keystrokesByCommand) , 1) loadPlatformItems: -> if platformMenu? @add(platformMenu) else menusDirPath = path.join(@resourcePath, 'menus') platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json']) {menu} = CSON.readFileSync(platformMenuPath) @add(menu) # Merges an item in a submenu aware way such that new items are always # appended to the bottom of existing menus where possible. merge: (menu, item) -> MenuHelpers.merge(menu, item) unmerge: (menu, item) -> MenuHelpers.unmerge(menu, item) sendToBrowserProcess: (template, keystrokesByCommand) -> ipcRenderer.send 'update-application-menu', template, keystrokesByCommand # Get an {Array} of {String} classes for the given element. classesForElement: (element) -> if classList = element?.classList Array::slice.apply(classList) else [] sortPackagesMenu: -> packagesMenu = _.find @template, ({id}) -> MenuHelpers.normalizeLabel(id) is 'Packages' return unless packagesMenu?.submenu? packagesMenu.submenu.sort (item1, item2) -> if item1.label and item2.label MenuHelpers.normalizeLabel(item1.label).localeCompare(MenuHelpers.normalizeLabel(item2.label)) else 0 @update() ================================================ FILE: src/menu-sort-helpers.js ================================================ // UTILS function splitArray(arr, predicate) { let lastArr = []; const multiArr = [lastArr]; arr.forEach(item => { if (predicate(item)) { if (lastArr.length > 0) { lastArr = []; multiArr.push(lastArr); } } else { lastArr.push(item); } }); return multiArr; } function joinArrays(arrays, joiner) { const joinedArr = []; arrays.forEach((arr, i) => { if (i > 0 && arr.length > 0) { joinedArr.push(joiner); } joinedArr.push(...arr); }); return joinedArr; } const pushOntoMultiMap = (map, key, value) => { if (!map.has(key)) { map.set(key, []); } map.get(key).push(value); }; function indexOfGroupContainingCommand(groups, command, ignoreGroup) { return groups.findIndex( candiateGroup => candiateGroup !== ignoreGroup && candiateGroup.some(candidateItem => candidateItem.command === command) ); } // Sort nodes topologically using a depth-first approach. Encountered cycles // are broken. function sortTopologically(originalOrder, edgesById) { const sorted = []; const marked = new Set(); function visit(id) { if (marked.has(id)) { // Either this node has already been placed, or we have encountered a // cycle and need to exit. return; } marked.add(id); const edges = edgesById.get(id); if (edges != null) { edges.forEach(visit); } sorted.push(id); } originalOrder.forEach(visit); return sorted; } function attemptToMergeAGroup(groups) { for (let i = 0; i < groups.length; i++) { const group = groups[i]; for (const item of group) { const toCommands = [...(item.before || []), ...(item.after || [])]; for (const command of toCommands) { const index = indexOfGroupContainingCommand(groups, command, group); if (index === -1) { // No valid edge for this command continue; } const mergeTarget = groups[index]; // Merge with group containing `command` mergeTarget.push(...group); groups.splice(i, 1); return true; } } } return false; } // Merge groups based on before/after positions // Mutates both the array of groups, and the individual group arrays. function mergeGroups(groups) { let mergedAGroup = true; while (mergedAGroup) { mergedAGroup = attemptToMergeAGroup(groups); } return groups; } function sortItemsInGroup(group) { const originalOrder = group.map((node, i) => i); const edges = new Map(); const commandToIndex = new Map(group.map((item, i) => [item.command, i])); group.forEach((item, i) => { if (item.before) { item.before.forEach(toCommand => { const to = commandToIndex.get(toCommand); if (to != null) { pushOntoMultiMap(edges, to, i); } }); } if (item.after) { item.after.forEach(toCommand => { const to = commandToIndex.get(toCommand); if (to != null) { pushOntoMultiMap(edges, i, to); } }); } }); const sortedNodes = sortTopologically(originalOrder, edges); return sortedNodes.map(i => group[i]); } function findEdgesInGroup(groups, i, edges) { const group = groups[i]; for (const item of group) { if (item.beforeGroupContaining) { for (const command of item.beforeGroupContaining) { const to = indexOfGroupContainingCommand(groups, command, group); if (to !== -1) { pushOntoMultiMap(edges, to, i); return; } } } if (item.afterGroupContaining) { for (const command of item.afterGroupContaining) { const to = indexOfGroupContainingCommand(groups, command, group); if (to !== -1) { pushOntoMultiMap(edges, i, to); return; } } } } } function sortGroups(groups) { const originalOrder = groups.map((item, i) => i); const edges = new Map(); for (let i = 0; i < groups.length; i++) { findEdgesInGroup(groups, i, edges); } const sortedGroupIndexes = sortTopologically(originalOrder, edges); return sortedGroupIndexes.map(i => groups[i]); } function isSeparator(item) { return item.type === 'separator'; } function sortMenuItems(menuItems) { // Split the items into their implicit groups based upon separators. const groups = splitArray(menuItems, isSeparator); // Merge groups that contain before/after references to eachother. const mergedGroups = mergeGroups(groups); // Sort each individual group internally. const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup); // Sort the groups based upon their beforeGroupContaining/afterGroupContaining // references. const sortedGroups = sortGroups(mergedGroupsWithSortedItems); // Join the groups back return joinArrays(sortedGroups, { type: 'separator' }); } module.exports = { sortMenuItems }; ================================================ FILE: src/model.coffee ================================================ nextInstanceId = 1 module.exports = class Model @resetNextInstanceId: -> nextInstanceId = 1 alive: true constructor: (params) -> @assignId(params?.id) assignId: (id) -> @id ?= id ? nextInstanceId++ nextInstanceId = id + 1 if id >= nextInstanceId destroy: -> return unless @isAlive() @alive = false @destroyed?() isAlive: -> @alive isDestroyed: -> not @isAlive() ================================================ FILE: src/module-cache.js ================================================ const Module = require('module'); const path = require('path'); const semver = require('semver'); // Extend semver.Range to memoize matched versions for speed class Range extends semver.Range { constructor() { super(...arguments); this.matchedVersions = new Set(); this.unmatchedVersions = new Set(); } test(version) { if (this.matchedVersions.has(version)) return true; if (this.unmatchedVersions.has(version)) return false; const matches = super.test(...arguments); if (matches) { this.matchedVersions.add(version); } else { this.unmatchedVersions.add(version); } return matches; } } let nativeModules = null; const cache = { builtins: {}, debug: false, dependencies: {}, extensions: {}, folders: {}, ranges: {}, registered: false, resourcePath: null, resourcePathWithTrailingSlash: null }; // isAbsolute is inlined from fs-plus so that fs-plus itself can be required // from this cache. let isAbsolute; if (process.platform === 'win32') { isAbsolute = pathToCheck => pathToCheck && (pathToCheck[1] === ':' || (pathToCheck[0] === '\\' && pathToCheck[1] === '\\')); } else { isAbsolute = pathToCheck => pathToCheck && pathToCheck[0] === '/'; } const isCorePath = pathToCheck => pathToCheck.startsWith(cache.resourcePathWithTrailingSlash); function loadDependencies(modulePath, rootPath, rootMetadata, moduleCache) { const fs = require('fs-plus'); for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) { if (path.basename(childPath) === '.bin') continue; if ( rootPath === modulePath && (rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty( path.basename(childPath) )) ) { continue; } const childMetadataPath = path.join(childPath, 'package.json'); if (!fs.isFileSync(childMetadataPath)) continue; const childMetadata = JSON.parse(fs.readFileSync(childMetadataPath)); if (childMetadata && childMetadata.version) { let mainPath; try { mainPath = require.resolve(childPath); } catch (error) { mainPath = null; } if (mainPath) { moduleCache.dependencies.push({ name: childMetadata.name, version: childMetadata.version, path: path.relative(rootPath, mainPath) }); } loadDependencies(childPath, rootPath, rootMetadata, moduleCache); } } } function loadFolderCompatibility( modulePath, rootPath, rootMetadata, moduleCache ) { const fs = require('fs-plus'); const metadataPath = path.join(modulePath, 'package.json'); if (!fs.isFileSync(metadataPath)) return; const metadata = JSON.parse(fs.readFileSync(metadataPath)); const dependencies = metadata.dependencies || {}; for (let name in dependencies) { if (!semver.validRange(dependencies[name])) { delete dependencies[name]; } } const onDirectory = childPath => path.basename(childPath) !== 'node_modules'; const extensions = ['.js', '.coffee', '.json', '.node']; let paths = {}; function onFile(childPath) { const needle = path.extname(childPath); if (extensions.includes(needle)) { const relativePath = path.relative(rootPath, path.dirname(childPath)); paths[relativePath] = true; } } fs.traverseTreeSync(modulePath, onFile, onDirectory); paths = Object.keys(paths); if (paths.length > 0 && Object.keys(dependencies).length > 0) { moduleCache.folders.push({ paths, dependencies }); } for (let childPath of fs.listSync(path.join(modulePath, 'node_modules'))) { if (path.basename(childPath) === '.bin') continue; if ( rootPath === modulePath && (rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty( path.basename(childPath) )) ) { continue; } loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache); } } function loadExtensions(modulePath, rootPath, rootMetadata, moduleCache) { const fs = require('fs-plus'); const extensions = ['.js', '.coffee', '.json', '.node']; const nodeModulesPath = path.join(rootPath, 'node_modules'); function onFile(filePath) { filePath = path.relative(rootPath, filePath); const segments = filePath.split(path.sep); if (segments.includes('test')) return; if (segments.includes('tests')) return; if (segments.includes('spec')) return; if (segments.includes('specs')) return; if ( segments.length > 1 && !['exports', 'lib', 'node_modules', 'src', 'static', 'vendor'].includes( segments[0] ) ) return; const extension = path.extname(filePath); if (extensions.includes(extension)) { if (moduleCache.extensions[extension] == null) { moduleCache.extensions[extension] = []; } moduleCache.extensions[extension].push(filePath); } } function onDirectory(childPath) { // Don't include extensions from bundled packages // These are generated and stored in the package's own metadata cache if (rootMetadata.name === 'atom') { const parentPath = path.dirname(childPath); if (parentPath === nodeModulesPath) { const packageName = path.basename(childPath); if ( rootMetadata.packageDependencies && rootMetadata.packageDependencies.hasOwnProperty(packageName) ) return false; } } return true; } fs.traverseTreeSync(rootPath, onFile, onDirectory); } function satisfies(version, rawRange) { let parsedRange; if (!(parsedRange = cache.ranges[rawRange])) { parsedRange = new Range(rawRange); cache.ranges[rawRange] = parsedRange; } return parsedRange.test(version); } function resolveFilePath(relativePath, parentModule) { if (!relativePath) return; if (!(parentModule && parentModule.filename)) return; if (relativePath[0] !== '.' && !isAbsolute(relativePath)) return; const resolvedPath = path.resolve( path.dirname(parentModule.filename), relativePath ); if (!isCorePath(resolvedPath)) return; let extension = path.extname(resolvedPath); if (extension) { if ( cache.extensions[extension] && cache.extensions[extension].has(resolvedPath) ) return resolvedPath; } else { for (extension in cache.extensions) { const paths = cache.extensions[extension]; const resolvedPathWithExtension = `${resolvedPath}${extension}`; if (paths.has(resolvedPathWithExtension)) { return resolvedPathWithExtension; } } } } function resolveModulePath(relativePath, parentModule) { if (!relativePath) return; if (!(parentModule && parentModule.filename)) return; if (!nativeModules) nativeModules = process.binding('natives'); if (nativeModules.hasOwnProperty(relativePath)) return; if (relativePath[0] === '.') return; if (isAbsolute(relativePath)) return; const folderPath = path.dirname(parentModule.filename); const range = cache.folders[folderPath] && cache.folders[folderPath][relativePath]; if (!range) { const builtinPath = cache.builtins[relativePath]; if (builtinPath) { return builtinPath; } else { return; } } const candidates = cache.dependencies[relativePath]; if (candidates == null) return; for (let version in candidates) { const resolvedPath = candidates[version]; if (Module._cache[resolvedPath] || isCorePath(resolvedPath)) { if (satisfies(version, range)) return resolvedPath; } } } function registerBuiltins(devMode) { if ( devMode || !cache.resourcePath.startsWith(`${process.resourcesPath}${path.sep}`) ) { const fs = require('fs-plus'); const atomJsPath = path.join(cache.resourcePath, 'exports', 'atom.js'); if (fs.isFileSync(atomJsPath)) { cache.builtins.atom = atomJsPath; } } if (cache.builtins.atom == null) { cache.builtins.atom = path.join(cache.resourcePath, 'exports', 'atom.js'); } } exports.create = function(modulePath) { const fs = require('fs-plus'); modulePath = fs.realpathSync(modulePath); const metadataPath = path.join(modulePath, 'package.json'); const metadata = JSON.parse(fs.readFileSync(metadataPath)); const moduleCache = { version: 1, dependencies: [], extensions: {}, folders: [] }; loadDependencies(modulePath, modulePath, metadata, moduleCache); loadFolderCompatibility(modulePath, modulePath, metadata, moduleCache); loadExtensions(modulePath, modulePath, metadata, moduleCache); metadata._atomModuleCache = moduleCache; fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)); }; exports.register = function({ resourcePath, devMode } = {}) { if (cache.registered) return; const originalResolveFilename = Module._resolveFilename; Module._resolveFilename = function(relativePath, parentModule) { let resolvedPath = resolveModulePath(relativePath, parentModule); if (!resolvedPath) { resolvedPath = resolveFilePath(relativePath, parentModule); } return resolvedPath || originalResolveFilename(relativePath, parentModule); }; cache.registered = true; cache.resourcePath = resourcePath; cache.resourcePathWithTrailingSlash = `${resourcePath}${path.sep}`; registerBuiltins(devMode); }; exports.add = function(directoryPath, metadata) { // path.join isn't used in this function for speed since path.join calls // path.normalize and all the paths are already normalized here. if (metadata == null) { try { metadata = require(`${directoryPath}${path.sep}package.json`); } catch (error) { return; } } const cacheToAdd = metadata && metadata._atomModuleCache; if (!cacheToAdd) return; for (const dependency of cacheToAdd.dependencies || []) { if (!cache.dependencies[dependency.name]) { cache.dependencies[dependency.name] = {}; } if (!cache.dependencies[dependency.name][dependency.version]) { cache.dependencies[dependency.name][ dependency.version ] = `${directoryPath}${path.sep}${dependency.path}`; } } for (const entry of cacheToAdd.folders || []) { for (const folderPath of entry.paths) { if (folderPath) { cache.folders[`${directoryPath}${path.sep}${folderPath}`] = entry.dependencies; } else { cache.folders[directoryPath] = entry.dependencies; } } } for (const extension in cacheToAdd.extensions) { const paths = cacheToAdd.extensions[extension]; if (!cache.extensions[extension]) { cache.extensions[extension] = new Set(); } for (let filePath of paths) { cache.extensions[extension].add(`${directoryPath}${path.sep}${filePath}`); } } }; exports.cache = cache; exports.Range = Range; ================================================ FILE: src/module-utils.js ================================================ // a require function with both ES5 and ES6 default export support function requireModule(path) { const modul = require(path); if (modul === null || modul === undefined) { // if null do not bother return modul; } else { if ( modul.__esModule === true && (modul.default !== undefined && modul.default !== null) ) { // __esModule flag is true and default is exported, which means that // an object containing the main functions (e.g. activate, etc) is default exported return modul.default; } else { return modul; } } } exports.requireModule = requireModule; ================================================ FILE: src/native-compile-cache.js ================================================ const Module = require('module'); const path = require('path'); const crypto = require('crypto'); const vm = require('vm'); function computeHash(contents) { return crypto .createHash('sha1') .update(contents, 'utf8') .digest('hex'); } class NativeCompileCache { constructor() { this.cacheStore = null; this.previousModuleCompile = null; } setCacheStore(store) { this.cacheStore = store; } setV8Version(v8Version) { this.v8Version = v8Version.toString(); } install() { this.savePreviousModuleCompile(); this.overrideModuleCompile(); } uninstall() { this.restorePreviousModuleCompile(); } savePreviousModuleCompile() { this.previousModuleCompile = Module.prototype._compile; } runInThisContext(code, filename) { const script = new vm.Script(code, filename); const cachedData = script.createCachedData(); return { result: script.runInThisContext(), cacheBuffer: typeof cachedData !== 'undefined' ? cachedData : null }; } runInThisContextCached(code, filename, cachedData) { const script = new vm.Script(code, { filename, cachedData }); return { result: script.runInThisContext(), wasRejected: script.cachedDataRejected }; } overrideModuleCompile() { let self = this; // Here we override Node's module.js // (https://github.com/atom/node/blob/atom/lib/module.js#L378), changing // only the bits that affect compilation in order to use the cached one. Module.prototype._compile = function(content, filename) { let moduleSelf = this; // remove shebang content = content.replace(/^#!.*/, ''); function require(path) { return moduleSelf.require(path); } require.resolve = function(request) { return Module._resolveFilename(request, moduleSelf); }; require.main = process.mainModule; // Enable support to add extra extension types require.extensions = Module._extensions; require.cache = Module._cache; let dirname = path.dirname(filename); // create wrapper function let wrapper = Module.wrap(content); let cacheKey = computeHash(wrapper + self.v8Version); let compiledWrapper = null; if (self.cacheStore.has(cacheKey)) { let buffer = self.cacheStore.get(cacheKey); let compilationResult = self.runInThisContextCached( wrapper, filename, buffer ); compiledWrapper = compilationResult.result; if (compilationResult.wasRejected) { self.cacheStore.delete(cacheKey); } } else { let compilationResult; try { compilationResult = self.runInThisContext(wrapper, filename); } catch (err) { console.error(`Error running script ${filename}`); throw err; } if (compilationResult.cacheBuffer) { self.cacheStore.set(cacheKey, compilationResult.cacheBuffer); } compiledWrapper = compilationResult.result; } let args = [ moduleSelf.exports, require, moduleSelf, filename, dirname, process, global, Buffer ]; return compiledWrapper.apply(moduleSelf.exports, args); }; } restorePreviousModuleCompile() { Module.prototype._compile = this.previousModuleCompile; } } module.exports = new NativeCompileCache(); ================================================ FILE: src/native-watcher-registry.js ================================================ const path = require('path'); // Private: re-join the segments split from an absolute path to form another absolute path. function absolute(...parts) { const candidate = path.join(...parts); return path.isAbsolute(candidate) ? candidate : path.join(path.sep, candidate); } // Private: Map userland filesystem watcher subscriptions efficiently to deliver filesystem change notifications to // each watcher with the most efficient coverage of native watchers. // // * If two watchers subscribe to the same directory, use a single native watcher for each. // * Re-use a native watcher watching a parent directory for a watcher on a child directory. If the parent directory // watcher is removed, it will be split into child watchers. // * If any child directories already being watched, stop and replace them with a watcher on the parent directory. // // Uses a trie whose structure mirrors the directory structure. class RegistryTree { // Private: Construct a tree with no native watchers. // // * `basePathSegments` the position of this tree's root relative to the filesystem's root as an {Array} of directory // names. // * `createNative` {Function} used to construct new native watchers. It should accept an absolute path as an argument // and return a new {NativeWatcher}. constructor(basePathSegments, createNative) { this.basePathSegments = basePathSegments; this.root = new RegistryNode(); this.createNative = createNative; } // Private: Identify the native watcher that should be used to produce events at a watched path, creating a new one // if necessary. // // * `pathSegments` the path to watch represented as an {Array} of directory names relative to this {RegistryTree}'s // root. // * `attachToNative` {Function} invoked with the appropriate native watcher and the absolute path to its watch root. add(pathSegments, attachToNative) { const absolutePathSegments = this.basePathSegments.concat(pathSegments); const absolutePath = absolute(...absolutePathSegments); const attachToNew = childPaths => { const native = this.createNative(absolutePath); const leaf = new RegistryWatcherNode( native, absolutePathSegments, childPaths ); this.root = this.root.insert(pathSegments, leaf); const sub = native.onWillStop(() => { sub.dispose(); this.root = this.root.remove(pathSegments, this.createNative) || new RegistryNode(); }); attachToNative(native, absolutePath); return native; }; this.root.lookup(pathSegments).when({ parent: (parent, remaining) => { // An existing NativeWatcher is watching the same directory or a parent directory of the requested path. // Attach this Watcher to it as a filtering watcher and record it as a dependent child path. const native = parent.getNativeWatcher(); parent.addChildPath(remaining); attachToNative(native, absolute(...parent.getAbsolutePathSegments())); }, children: children => { // One or more NativeWatchers exist on child directories of the requested path. Create a new native watcher // on the parent directory, note the subscribed child paths, and cleanly stop the child native watchers. const newNative = attachToNew(children.map(child => child.path)); for (let i = 0; i < children.length; i++) { const childNode = children[i].node; const childNative = childNode.getNativeWatcher(); childNative.reattachTo(newNative, absolutePath); childNative.dispose(); childNative.stop(); } }, missing: () => attachToNew([]) }); } // Private: Access the root node of the tree. getRoot() { return this.root; } // Private: Return a {String} representation of this tree's structure for diagnostics and testing. print() { return this.root.print(); } } // Private: Non-leaf node in a {RegistryTree} used by the {NativeWatcherRegistry} to cover the allocated {Watcher} // instances with the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory // in the filesystem tree. class RegistryNode { // Private: Construct a new, empty node representing a node with no watchers. constructor() { this.children = {}; } // Private: Recursively discover any existing watchers corresponding to a path. // // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. // // Returns: A {ParentResult} if the exact requested directory or a parent directory is being watched, a // {ChildrenResult} if one or more child paths are being watched, or a {MissingResult} if no relevant watchers // exist. lookup(pathSegments) { if (pathSegments.length === 0) { return new ChildrenResult(this.leaves([])); } const child = this.children[pathSegments[0]]; if (child === undefined) { return new MissingResult(this); } return child.lookup(pathSegments.slice(1)); } // Private: Insert a new {RegistryWatcherNode} into the tree, creating new intermediate {RegistryNode} instances as // needed. Any existing children of the watched directory are removed. // // * `pathSegments` filesystem path of the new {Watcher}, already split into an Array of directory names. // * `leaf` initialized {RegistryWatcherNode} to insert // // Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should // replace their node references with the returned value. insert(pathSegments, leaf) { if (pathSegments.length === 0) { return leaf; } const pathKey = pathSegments[0]; let child = this.children[pathKey]; if (child === undefined) { child = new RegistryNode(); } this.children[pathKey] = child.insert(pathSegments.slice(1), leaf); return this; } // Private: Remove a {RegistryWatcherNode} by its exact watched directory. // // * `pathSegments` absolute pre-split filesystem path of the node to remove. // * `createSplitNative` callback to be invoked with each child path segment {Array} if the {RegistryWatcherNode} // is split into child watchers rather than removed outright. See {RegistryWatcherNode.remove}. // // Returns: The root of a new tree with the {RegistryWatcherNode} removed. Callers should replace their node // references with the returned value. remove(pathSegments, createSplitNative) { if (pathSegments.length === 0) { // Attempt to remove a path with child watchers. Do nothing. return this; } const pathKey = pathSegments[0]; const child = this.children[pathKey]; if (child === undefined) { // Attempt to remove a path that isn't watched. Do nothing. return this; } // Recurse const newChild = child.remove(pathSegments.slice(1), createSplitNative); if (newChild === null) { delete this.children[pathKey]; } else { this.children[pathKey] = newChild; } // Remove this node if all of its children have been removed return Object.keys(this.children).length === 0 ? null : this; } // Private: Discover all {RegistryWatcherNode} instances beneath this tree node and the child paths // that they are watching. // // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. // // Returns: A possibly empty {Array} of `{node, path}` objects describing {RegistryWatcherNode} // instances beneath this node. leaves(prefix) { const results = []; for (const p of Object.keys(this.children)) { results.push(...this.children[p].leaves(prefix.concat([p]))); } return results; } // Private: Return a {String} representation of this subtree for diagnostics and testing. print(indent = 0) { let spaces = ''; for (let i = 0; i < indent; i++) { spaces += ' '; } let result = ''; for (const p of Object.keys(this.children)) { result += `${spaces}${p}\n${this.children[p].print(indent + 2)}`; } return result; } } // Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a // {NativeWatcher}. class RegistryWatcherNode { // Private: Allocate a new node to track a {NativeWatcher}. // // * `nativeWatcher` An existing {NativeWatcher} instance. // * `absolutePathSegments` The absolute path to this {NativeWatcher}'s directory as an {Array} of // path segments. // * `childPaths` {Array} of child directories that are currently the responsibility of this // {NativeWatcher}, if any. Directories are represented as arrays of the path segments between this // node's directory and the watched child path. constructor(nativeWatcher, absolutePathSegments, childPaths) { this.nativeWatcher = nativeWatcher; this.absolutePathSegments = absolutePathSegments; // Store child paths as joined strings so they work as Set members. this.childPaths = new Set(); for (let i = 0; i < childPaths.length; i++) { this.childPaths.add(path.join(...childPaths[i])); } } // Private: Assume responsibility for a new child path. If this node is removed, it will instead // split into a subtree with a new {RegistryWatcherNode} for each child path. // // * `childPathSegments` the {Array} of path segments between this node's directory and the watched // child directory. addChildPath(childPathSegments) { this.childPaths.add(path.join(...childPathSegments)); } // 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 // watched child directory. removeChildPath(childPathSegments) { this.childPaths.delete(path.join(...childPathSegments)); } // Private: Accessor for the {NativeWatcher}. getNativeWatcher() { return this.nativeWatcher; } // Private: Return the absolute path watched by this {NativeWatcher} as an {Array} of directory names. getAbsolutePathSegments() { return this.absolutePathSegments; } // Private: Identify how this watcher relates to a request to watch a directory tree. // // * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names. // // Returns: A {ParentResult} referencing this node. lookup(pathSegments) { return new ParentResult(this, pathSegments); } // Private: Remove this leaf node if the watcher's exact path matches. If this node is covering additional // {Watcher} instances on child paths, it will be split into a subtree. // // * `pathSegments` filesystem path of the node to remove. // * `createSplitNative` callback invoked with each {Array} of absolute child path segments to create a native // watcher on a subtree of this node. // // Returns: If `pathSegments` match this watcher's path exactly, returns `null` if this node has no `childPaths` // or a new {RegistryNode} on a newly allocated subtree if it did. If `pathSegments` does not match the watcher's // path, it's an attempt to remove a subnode that doesn't exist, so the remove call has no effect and returns // `this` unaltered. remove(pathSegments, createSplitNative) { if (pathSegments.length !== 0) { return this; } else if (this.childPaths.size > 0) { let newSubTree = new RegistryTree( this.absolutePathSegments, createSplitNative ); for (const childPath of this.childPaths) { const childPathSegments = childPath.split(path.sep); newSubTree.add(childPathSegments, (native, attachmentPath) => { this.nativeWatcher.reattachTo(native, attachmentPath); }); } return newSubTree.getRoot(); } else { return null; } } // Private: Discover this {RegistryWatcherNode} instance. // // * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths. // // Returns: An {Array} containing a `{node, path}` object describing this node. leaves(prefix) { return [{ node: this, path: prefix }]; } // Private: Return a {String} representation of this watcher for diagnostics and testing. Indicates the number of // child paths that this node's {NativeWatcher} is responsible for. print(indent = 0) { let result = ''; for (let i = 0; i < indent; i++) { result += ' '; } result += '[watcher'; if (this.childPaths.size > 0) { result += ` +${this.childPaths.size}`; } result += ']\n'; return result; } } // 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 successfully traversed {RegistryNode}. constructor(lastParent) { this.lastParent = lastParent; } // Private: Dispatch within a map of callback actions. // // * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned // by {RegistryNode.lookup}. The callback will be called with the last parent node that was encountered during the // traversal. // // Returns: the result of the `actions` callback. when(actions) { return actions.missing(this.lastParent); } } // Private: A {RegistryNode.lookup} traversal result that's returned when a parent or an exact match of the requested // directory is being watched by an existing {RegistryWatcherNode}. class ParentResult { // Private: Instantiate a new {ParentResult}. // // * `parent` the {RegistryWatcherNode} that was discovered. // * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and // the requested directory. This will be empty for exact matches. constructor(parent, remainingPathSegments) { this.parent = parent; this.remainingPathSegments = remainingPathSegments; } // Private: Dispatch within a map of callback actions. // // * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested // requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the // {RegistryWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node // and the requested directory. // // Returns: the result of the `actions` callback. when(actions) { return actions.parent(this.parent, this.remainingPathSegments); } } // Private: A {RegistryNode.lookup} traversal result that's returned when one or more children of the requested // directory are already being watched. class ChildrenResult { // Private: Instantiate a new {ChildrenResult}. // // * `children` {Array} of the {RegistryWatcherNode} instances that were discovered. constructor(children) { this.children = children; } // Private: Dispatch within a map of callback actions. // // * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested // requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the // {RegistryWatcherNode} instance. // // Returns: the result of the `actions` callback. when(actions) { return actions.children(this.children); } } // Private: Track the directories being monitored by native filesystem watchers. Minimize the number of native watchers // allocated to receive events for a desired set of directories by: // // 1. Subscribing to the same underlying {NativeWatcher} when watching the same directory multiple times. // 2. Subscribing to an existing {NativeWatcher} on a parent of a desired directory. // 3. Replacing multiple {NativeWatcher} instances on child directories with a single new {NativeWatcher} on the // parent. class NativeWatcherRegistry { // Private: Instantiate an empty registry. // // * `createNative` {Function} that will be called with a normalized filesystem path to create a new native // filesystem watcher. constructor(createNative) { this.tree = new RegistryTree([], createNative); } // Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already // exists, it will be attached to the new {Watcher} with an appropriate subpath configuration. Otherwise, the // `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree // and attached to the watcher. // // If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will // be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to // the new watcher. // // * `watcher` an unattached {Watcher}. async attach(watcher) { const normalizedDirectory = await watcher.getNormalizedPathPromise(); const pathSegments = normalizedDirectory .split(path.sep) .filter(segment => segment.length > 0); this.tree.add(pathSegments, (native, nativePath) => { watcher.attachToNative(native, nativePath); }); } // Private: Generate a visual representation of the currently active watchers managed by this // registry. // // Returns a {String} showing the tree structure. print() { return this.tree.print(); } } module.exports = { NativeWatcherRegistry }; ================================================ FILE: src/notification-manager.js ================================================ 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); } // Public: Invoke the given callback after the notifications have been cleared. // // * `callback` {Function} to be called after the notifications are cleared. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidClearNotifications(callback) { return this.emitter.on('did-clear-notifications', 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. // Requires `detail` to be set. // // 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. // Requires `detail` to be set. // // 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 */ // Public: Clear all the notifications. clear() { this.notifications = []; this.emitter.emit('did-clear-notifications'); } }; ================================================ FILE: src/notification.js ================================================ 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) || Array.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'; } } }; ================================================ FILE: src/null-grammar.js ================================================ const { Disposable } = require('event-kit'); module.exports = { name: 'Null Grammar', scopeName: 'text.plain.null-grammar', scopeForId(id) { if (id === -1 || id === -2) { return this.scopeName; } else { return null; } }, startIdForScope(scopeName) { if (scopeName === this.scopeName) { return -1; } else { return null; } }, endIdForScope(scopeName) { if (scopeName === this.scopeName) { return -2; } else { return null; } }, tokenizeLine(text) { return { tags: [ this.startIdForScope(this.scopeName), text.length, this.endIdForScope(this.scopeName) ], ruleStack: null }; }, onDidUpdate(callback) { return new Disposable(noop); } }; function noop() {} ================================================ FILE: src/overlay-manager.coffee ================================================ ElementResizeDetector = require('element-resize-detector') elementResizeDetector = null module.exports = class OverlayManager constructor: (@presenter, @container, @views) -> @overlaysById = {} render: (state) -> for decorationId, overlay of state.content.overlays if @shouldUpdateOverlay(decorationId, overlay) @renderOverlay(state, decorationId, overlay) for id, {overlayNode} of @overlaysById unless state.content.overlays.hasOwnProperty(id) delete @overlaysById[id] overlayNode.remove() elementResizeDetector.uninstall(overlayNode) shouldUpdateOverlay: (decorationId, overlay) -> cachedOverlay = @overlaysById[decorationId] return true unless cachedOverlay? cachedOverlay.pixelPosition?.top isnt overlay.pixelPosition?.top or cachedOverlay.pixelPosition?.left isnt overlay.pixelPosition?.left measureOverlay: (decorationId, itemView) -> contentMargin = parseInt(getComputedStyle(itemView)['margin-left']) ? 0 @presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin) renderOverlay: (state, decorationId, {item, pixelPosition, class: klass}) -> itemView = @views.getView(item) cachedOverlay = @overlaysById[decorationId] unless overlayNode = cachedOverlay?.overlayNode overlayNode = document.createElement('atom-overlay') overlayNode.classList.add(klass) if klass? elementResizeDetector ?= ElementResizeDetector({strategy: 'scroll'}) elementResizeDetector.listenTo(overlayNode, => if overlayNode.parentElement? @measureOverlay(decorationId, itemView) ) @container.appendChild(overlayNode) @overlaysById[decorationId] = cachedOverlay = {overlayNode, itemView} # The same node may be used in more than one overlay. This steals the node # back if it has been displayed in another overlay. overlayNode.appendChild(itemView) unless overlayNode.contains(itemView) cachedOverlay.pixelPosition = pixelPosition overlayNode.style.top = pixelPosition.top + 'px' overlayNode.style.left = pixelPosition.left + 'px' @measureOverlay(decorationId, itemView) ================================================ FILE: src/package-manager.js ================================================ const path = require('path'); let normalizePackageData = null; const _ = require('underscore-plus'); const { Emitter } = require('event-kit'); const fs = require('fs-plus'); const CSON = require('season'); const ServiceHub = require('service-hub'); const Package = require('./package'); const ThemePackage = require('./theme-package'); const ModuleCache = require('./module-cache'); const packageJSON = require('../package.json'); // Extended: Package manager for coordinating the lifecycle of Atom packages. // // An instance of this class is always available as the `atom.packages` global. // // Packages can be loaded, activated, and deactivated, and unloaded: // * Loading a package reads and parses the package's metadata and resources // such as keymaps, menus, stylesheets, etc. // * Activating a package registers the loaded resources and calls `activate()` // on the package's main module. // * Deactivating a package unregisters the package's resources and calls // `deactivate()` on the package's main module. // * Unloading a package removes it completely from the package manager. // // Packages can be enabled/disabled via the `core.disabledPackages` config // settings and also by calling `enablePackage()/disablePackage()`. 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, uriHandlerRegistry: this.uriHandlerRegistry } = params); this.emitter = new Emitter(); this.activationHookEmitter = new Emitter(); this.packageDirPaths = []; this.deferredActivationHooks = []; this.triggeredActivationHooks = new Set(); this.packagesCache = packageJSON._atomPackages != null ? packageJSON._atomPackages : {}; this.packageDependencies = packageJSON.packageDependencies != null ? packageJSON.packageDependencies : {}; this.deprecatedPackages = packageJSON._deprecatedPackages || {}; this.deprecatedPackageRanges = {}; this.initialPackagesLoaded = false; this.initialPackagesActivated = false; this.preloadedPackages = {}; this.loadedPackages = {}; this.activePackages = {}; this.activatingPackages = {}; this.packageStates = {}; this.serviceHub = new ServiceHub(); this.packageActivators = []; this.registerPackageActivator(this, ['atom', 'textmate']); } initialize(params) { this.devMode = params.devMode; this.resourcePath = params.resourcePath; if (params.configDirPath != null && !params.safeMode) { if (this.devMode) { this.packageDirPaths.push( path.join(params.configDirPath, 'dev', 'packages') ); this.packageDirPaths.push(path.join(this.resourcePath, 'packages')); } this.packageDirPaths.push(path.join(params.configDirPath, 'packages')); } } setContextMenuManager(contextMenuManager) { this.contextMenuManager = contextMenuManager; } setMenuManager(menuManager) { this.menuManager = menuManager; } setThemeManager(themeManager) { this.themeManager = themeManager; } async reset() { this.serviceHub.clear(); await this.deactivatePackages(); this.loadedPackages = {}; this.preloadedPackages = {}; this.packageStates = {}; this.packagesCache = packageJSON._atomPackages != null ? packageJSON._atomPackages : {}; this.packageDependencies = packageJSON.packageDependencies != null ? packageJSON.packageDependencies : {}; this.triggeredActivationHooks.clear(); this.activatePromise = null; } /* Section: Event Subscription */ // Public: Invoke the given callback when all packages have been loaded. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidLoadInitialPackages(callback) { return this.emitter.on('did-load-initial-packages', callback); } // Public: Invoke the given callback when all packages have been activated. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidActivateInitialPackages(callback) { return this.emitter.on('did-activate-initial-packages', callback); } getActivatePromise() { if (this.activatePromise) { return this.activatePromise; } else { return Promise.resolve(); } } // Public: Invoke the given callback when a package is activated. // // * `callback` A {Function} to be invoked when a package is activated. // * `package` The {Package} that was activated. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidActivatePackage(callback) { return this.emitter.on('did-activate-package', callback); } // Public: Invoke the given callback when a package is deactivated. // // * `callback` A {Function} to be invoked when a package is deactivated. // * `package` The {Package} that was deactivated. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDeactivatePackage(callback) { return this.emitter.on('did-deactivate-package', callback); } // Public: Invoke the given callback when a package is loaded. // // * `callback` A {Function} to be invoked when a package is loaded. // * `package` The {Package} that was loaded. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidLoadPackage(callback) { return this.emitter.on('did-load-package', callback); } // Public: Invoke the given callback when a package is unloaded. // // * `callback` A {Function} to be invoked when a package is unloaded. // * `package` The {Package} that was unloaded. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidUnloadPackage(callback) { return this.emitter.on('did-unload-package', callback); } /* Section: Package system data */ // Public: Get the path to the apm command. // // Uses the value of the `core.apmPath` config setting if it exists. // // Return a {String} file path to apm. getApmPath() { const configPath = atom.config.get('core.apmPath'); if (configPath || this.apmPath) { return configPath || this.apmPath; } const commandName = process.platform === 'win32' ? 'apm.cmd' : 'apm'; const apmRoot = path.join(process.resourcesPath, 'app', 'apm'); this.apmPath = path.join(apmRoot, 'bin', commandName); if (!fs.isFileSync(this.apmPath)) { this.apmPath = path.join( apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName ); } return this.apmPath; } // Public: Get the paths being used to look for packages. // // Returns an {Array} of {String} directory paths. getPackageDirPaths() { return _.clone(this.packageDirPaths); } /* Section: General package data */ // Public: Resolve the given package name to a path on disk. // // * `name` - The {String} package name. // // Return a {String} folder path or undefined if it could not be resolved. resolvePackagePath(name) { if (fs.isDirectorySync(name)) { return name; } let packagePath = fs.resolve(...this.packageDirPaths, name); if (fs.isDirectorySync(packagePath)) { return packagePath; } packagePath = path.join(this.resourcePath, 'node_modules', name); if (this.hasAtomEngine(packagePath)) { return packagePath; } return null; } // Public: Is the package with the given name bundled with Atom? // // * `name` - The {String} package name. // // Returns a {Boolean}. isBundledPackage(name) { return this.getPackageDependencies().hasOwnProperty(name); } isDeprecatedPackage(name, version) { const metadata = this.deprecatedPackages[name]; if (!metadata) return false; if (!metadata.version) return true; let range = this.deprecatedPackageRanges[metadata.version]; if (!range) { try { range = new ModuleCache.Range(metadata.version); } catch (error) { range = NullVersionRange; } this.deprecatedPackageRanges[metadata.version] = range; } return range.test(version); } getDeprecatedPackageMetadata(name) { const metadata = this.deprecatedPackages[name]; if (metadata) Object.freeze(metadata); return metadata; } /* Section: Enabling and disabling packages */ // Public: Enable the package with the given name. // // * `name` - The {String} package name. // // Returns the {Package} that was enabled or null if it isn't loaded. enablePackage(name) { const pack = this.loadPackage(name); if (pack != null) { pack.enable(); } return pack; } // Public: Disable the package with the given name. // // * `name` - The {String} package name. // // Returns the {Package} that was disabled or null if it isn't loaded. disablePackage(name) { const pack = this.loadPackage(name); if (!this.isPackageDisabled(name) && pack != null) { pack.disable(); } return pack; } // Public: Is the package with the given name disabled? // // * `name` - The {String} package name. // // Returns a {Boolean}. isPackageDisabled(name) { return _.include(this.config.get('core.disabledPackages') || [], name); } /* Section: Accessing active packages */ // Public: Get an {Array} of all the active {Package}s. getActivePackages() { return _.values(this.activePackages); } // Public: Get the active {Package} with the given name. // // * `name` - The {String} package name. // // Returns a {Package} or undefined. getActivePackage(name) { return this.activePackages[name]; } // Public: Is the {Package} with the given name active? // // * `name` - The {String} package name. // // Returns a {Boolean}. isPackageActive(name) { return this.getActivePackage(name) != null; } // Public: Returns a {Boolean} indicating whether package activation has occurred. hasActivatedInitialPackages() { return this.initialPackagesActivated; } /* Section: Accessing loaded packages */ // Public: Get an {Array} of all the loaded {Package}s getLoadedPackages() { return _.values(this.loadedPackages); } // Get packages for a certain package type // // * `types` an {Array} of {String}s like ['atom', 'textmate']. getLoadedPackagesForTypes(types) { return this.getLoadedPackages().filter(p => types.includes(p.getType())); } // Public: Get the loaded {Package} with the given name. // // * `name` - The {String} package name. // // Returns a {Package} or undefined. getLoadedPackage(name) { return this.loadedPackages[name]; } // Public: Is the package with the given name loaded? // // * `name` - The {String} package name. // // Returns a {Boolean}. isPackageLoaded(name) { return this.getLoadedPackage(name) != null; } // Public: Returns a {Boolean} indicating whether package loading has occurred. hasLoadedInitialPackages() { return this.initialPackagesLoaded; } /* Section: Accessing available packages */ // Public: Returns an {Array} of {String}s of all the available package paths. getAvailablePackagePaths() { return this.getAvailablePackages().map(a => a.path); } // Public: Returns an {Array} of {String}s of all the available package names. getAvailablePackageNames() { return this.getAvailablePackages().map(a => a.name); } // Public: Returns an {Array} of {String}s of all the available package metadata. getAvailablePackageMetadata() { const packages = []; for (const pack of this.getAvailablePackages()) { const loadedPackage = this.getLoadedPackage(pack.name); const metadata = loadedPackage != null ? loadedPackage.metadata : this.loadPackageMetadata(pack, true); packages.push(metadata); } return packages; } getAvailablePackages() { const packages = []; const packagesByName = new Set(); for (const packageDirPath of this.packageDirPaths) { if (fs.isDirectorySync(packageDirPath)) { // checks for directories. // dirent is faster, but for checking symbolic link we need stat. const packageNames = fs .readdirSync(packageDirPath, { withFileTypes: true }) .filter( dirent => dirent.isDirectory() || (dirent.isSymbolicLink() && fs.isDirectorySync(path.join(packageDirPath, dirent.name))) ) .map(dirent => dirent.name); for (const packageName of packageNames) { if ( !packageName.startsWith('.') && !packagesByName.has(packageName) ) { const packagePath = path.join(packageDirPath, packageName); packages.push({ name: packageName, path: packagePath, isBundled: false }); packagesByName.add(packageName); } } } } for (const packageName in this.packageDependencies) { if (!packagesByName.has(packageName)) { packages.push({ name: packageName, path: path.join(this.resourcePath, 'node_modules', packageName), isBundled: true }); } } return packages.sort((a, b) => a.name.localeCompare(b.name)); } /* Section: Private */ getPackageState(name) { return this.packageStates[name]; } setPackageState(name, state) { this.packageStates[name] = state; } getPackageDependencies() { return this.packageDependencies; } hasAtomEngine(packagePath) { const metadata = this.loadPackageMetadata(packagePath, true); return ( metadata != null && metadata.engines != null && metadata.engines.atom != null ); } unobserveDisabledPackages() { if (this.disabledPackagesSubscription != null) { this.disabledPackagesSubscription.dispose(); } this.disabledPackagesSubscription = null; } observeDisabledPackages() { if (this.disabledPackagesSubscription != null) { return; } this.disabledPackagesSubscription = this.config.onDidChange( 'core.disabledPackages', ({ newValue, oldValue }) => { const packagesToEnable = _.difference(oldValue, newValue); const packagesToDisable = _.difference(newValue, oldValue); packagesToDisable.forEach(name => { if (this.getActivePackage(name)) this.deactivatePackage(name); }); packagesToEnable.forEach(name => this.activatePackage(name)); return null; } ); } unobservePackagesWithKeymapsDisabled() { if (this.packagesWithKeymapsDisabledSubscription != null) { this.packagesWithKeymapsDisabledSubscription.dispose(); } this.packagesWithKeymapsDisabledSubscription = null; } observePackagesWithKeymapsDisabled() { if (this.packagesWithKeymapsDisabledSubscription != null) { return; } const performOnLoadedActivePackages = ( packageNames, disabledPackageNames, action ) => { for (const packageName of packageNames) { if (!disabledPackageNames.has(packageName)) { const pack = this.getLoadedPackage(packageName); if (pack != null) { action(pack); } } } }; this.packagesWithKeymapsDisabledSubscription = this.config.onDidChange( 'core.packagesWithKeymapsDisabled', ({ newValue, oldValue }) => { const keymapsToEnable = _.difference(oldValue, newValue); const keymapsToDisable = _.difference(newValue, oldValue); const disabledPackageNames = new Set( this.config.get('core.disabledPackages') ); performOnLoadedActivePackages( keymapsToDisable, disabledPackageNames, p => p.deactivateKeymaps() ); performOnLoadedActivePackages( keymapsToEnable, disabledPackageNames, p => p.activateKeymaps() ); return null; } ); } preloadPackages() { const result = []; for (const packageName in this.packagesCache) { result.push( this.preloadPackage(packageName, this.packagesCache[packageName]) ); } return result; } preloadPackage(packageName, pack) { const metadata = pack.metadata || {}; if (typeof metadata.name !== 'string' || metadata.name.length < 1) { metadata.name = packageName; } if ( metadata.repository != null && metadata.repository.type === 'git' && typeof metadata.repository.url === 'string' ) { metadata.repository.url = metadata.repository.url.replace( /(^git\+)|(\.git$)/g, '' ); } const options = { path: pack.rootDirPath, name: packageName, preloadedPackage: true, bundledPackage: true, metadata, packageManager: this, config: this.config, styleManager: this.styleManager, commandRegistry: this.commandRegistry, keymapManager: this.keymapManager, notificationManager: this.notificationManager, grammarRegistry: this.grammarRegistry, themeManager: this.themeManager, menuManager: this.menuManager, contextMenuManager: this.contextMenuManager, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry }; pack = metadata.theme ? new ThemePackage(options) : new Package(options); pack.preload(); this.preloadedPackages[packageName] = pack; return pack; } loadPackages() { // Ensure atom exports is already in the require cache so the load time // of the first package isn't skewed by being the first to require atom require('../exports/atom'); const disabledPackageNames = new Set( this.config.get('core.disabledPackages') ); this.config.transact(() => { for (const pack of this.getAvailablePackages()) { this.loadAvailablePackage(pack, disabledPackageNames); } }); this.initialPackagesLoaded = true; this.emitter.emit('did-load-initial-packages'); } loadPackage(nameOrPath) { if (path.basename(nameOrPath)[0].match(/^\./)) { // primarily to skip .git folder return null; } const pack = this.getLoadedPackage(nameOrPath); if (pack) { return pack; } const packagePath = this.resolvePackagePath(nameOrPath); if (packagePath) { const name = path.basename(nameOrPath); return this.loadAvailablePackage({ name, path: packagePath, isBundled: this.isBundledPackagePath(packagePath) }); } console.warn(`Could not resolve '${nameOrPath}' to a package path`); return null; } loadAvailablePackage(availablePackage, disabledPackageNames) { const preloadedPackage = this.preloadedPackages[availablePackage.name]; if ( disabledPackageNames != null && disabledPackageNames.has(availablePackage.name) ) { if (preloadedPackage != null) { preloadedPackage.deactivate(); delete preloadedPackage[availablePackage.name]; } return null; } const loadedPackage = this.getLoadedPackage(availablePackage.name); if (loadedPackage != null) { return loadedPackage; } if (preloadedPackage != null) { if (availablePackage.isBundled) { preloadedPackage.finishLoading(); this.loadedPackages[availablePackage.name] = preloadedPackage; return preloadedPackage; } else { preloadedPackage.deactivate(); delete preloadedPackage[availablePackage.name]; } } let metadata; try { metadata = this.loadPackageMetadata(availablePackage) || {}; } catch (error) { this.handleMetadataError(error, availablePackage.path); return null; } if ( !availablePackage.isBundled && this.isDeprecatedPackage(metadata.name, metadata.version) ) { console.warn( `Could not load ${metadata.name}@${ metadata.version } because it uses deprecated APIs that have been removed.` ); return null; } const options = { path: availablePackage.path, name: availablePackage.name, metadata, bundledPackage: availablePackage.isBundled, packageManager: this, config: this.config, styleManager: this.styleManager, commandRegistry: this.commandRegistry, keymapManager: this.keymapManager, notificationManager: this.notificationManager, grammarRegistry: this.grammarRegistry, themeManager: this.themeManager, menuManager: this.menuManager, contextMenuManager: this.contextMenuManager, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry }; const pack = metadata.theme ? new ThemePackage(options) : new Package(options); pack.load(); this.loadedPackages[pack.name] = pack; this.emitter.emit('did-load-package', pack); return pack; } unloadPackages() { _.keys(this.loadedPackages).forEach(name => this.unloadPackage(name)); } unloadPackage(name) { if (this.isPackageActive(name)) { throw new Error(`Tried to unload active package '${name}'`); } const pack = this.getLoadedPackage(name); if (pack) { delete this.loadedPackages[pack.name]; this.emitter.emit('did-unload-package', pack); } else { throw new Error(`No loaded package for name '${name}'`); } } // Activate all the packages that should be activated. activate() { let promises = []; for (let [activator, types] of this.packageActivators) { const packages = this.getLoadedPackagesForTypes(types); promises = promises.concat(activator.activatePackages(packages)); } this.activatePromise = Promise.all(promises).then(() => { this.triggerDeferredActivationHooks(); this.initialPackagesActivated = true; this.emitter.emit('did-activate-initial-packages'); this.activatePromise = null; }); return this.activatePromise; } registerURIHandlerForPackage(packageName, handler) { return this.uriHandlerRegistry.registerHostHandler(packageName, handler); } // another type of package manager can handle other package types. // See ThemeManager registerPackageActivator(activator, types) { this.packageActivators.push([activator, types]); } activatePackages(packages) { const promises = []; this.config.transactAsync(() => { for (const pack of packages) { const promise = this.activatePackage(pack.name); if (!pack.activationShouldBeDeferred()) { promises.push(promise); } } return Promise.all(promises); }); this.observeDisabledPackages(); this.observePackagesWithKeymapsDisabled(); return promises; } // Activate a single package by name activatePackage(name) { let pack = this.getActivePackage(name); if (pack) { return Promise.resolve(pack); } pack = this.loadPackage(name); if (!pack) { return Promise.reject(new Error(`Failed to load package '${name}'`)); } this.activatingPackages[pack.name] = pack; const activationPromise = pack.activate().then(() => { if (this.activatingPackages[pack.name] != null) { delete this.activatingPackages[pack.name]; this.activePackages[pack.name] = pack; this.emitter.emit('did-activate-package', pack); } return pack; }); if (this.deferredActivationHooks == null) { this.triggeredActivationHooks.forEach(hook => this.activationHookEmitter.emit(hook) ); } return activationPromise; } triggerDeferredActivationHooks() { if (this.deferredActivationHooks == null) { return; } for (const hook of this.deferredActivationHooks) { this.activationHookEmitter.emit(hook); } this.deferredActivationHooks = null; } triggerActivationHook(hook) { if (hook == null || !_.isString(hook) || hook.length <= 0) { return new Error('Cannot trigger an empty activation hook'); } this.triggeredActivationHooks.add(hook); if (this.deferredActivationHooks != null) { this.deferredActivationHooks.push(hook); } else { this.activationHookEmitter.emit(hook); } } onDidTriggerActivationHook(hook, callback) { if (hook == null || !_.isString(hook) || hook.length <= 0) { return; } return this.activationHookEmitter.on(hook, callback); } serialize() { for (const pack of this.getActivePackages()) { this.serializePackage(pack); } return this.packageStates; } serializePackage(pack) { if (typeof pack.serialize === 'function') { this.setPackageState(pack.name, pack.serialize()); } } // Deactivate all packages 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 async deactivatePackage(name, suppressSerialization) { const pack = this.getLoadedPackage(name); if (pack == null) { return; } if (!suppressSerialization && this.isPackageActive(pack.name)) { this.serializePackage(pack); } 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); } handleMetadataError(error, packagePath) { const metadataPath = path.join(packagePath, 'package.json'); const detail = `${error.message} in ${metadataPath}`; const stack = `${error.stack}\n at ${metadataPath}:1:1`; const message = `Failed to load the ${path.basename(packagePath)} package`; this.notificationManager.addError(message, { stack, detail, packageName: path.basename(packagePath), dismissable: true }); } uninstallDirectory(directory) { const symlinkPromise = new Promise(resolve => fs.isSymbolicLink(directory, isSymLink => resolve(isSymLink)) ); const dirPromise = new Promise(resolve => fs.isDirectory(directory, isDir => resolve(isDir)) ); return Promise.all([symlinkPromise, dirPromise]).then(values => { const [isSymLink, isDir] = values; if (!isSymLink && isDir) { return fs.remove(directory, function() {}); } }); } reloadActivePackageStyleSheets() { for (const pack of this.getActivePackages()) { if ( pack.getType() !== 'theme' && typeof pack.reloadStylesheets === 'function' ) { pack.reloadStylesheets(); } } } isBundledPackagePath(packagePath) { if ( this.devMode && !this.resourcePath.startsWith(`${process.resourcesPath}${path.sep}`) ) { return false; } if (this.resourcePathWithTrailingSlash == null) { this.resourcePathWithTrailingSlash = `${this.resourcePath}${path.sep}`; } return ( packagePath != null && packagePath.startsWith(this.resourcePathWithTrailingSlash) ); } loadPackageMetadata(packagePathOrAvailablePackage, ignoreErrors = false) { let isBundled, packageName, packagePath; if (typeof packagePathOrAvailablePackage === 'object') { const availablePackage = packagePathOrAvailablePackage; packageName = availablePackage.name; packagePath = availablePackage.path; isBundled = availablePackage.isBundled; } else { packagePath = packagePathOrAvailablePackage; packageName = path.basename(packagePath); isBundled = this.isBundledPackagePath(packagePath); } let metadata; if (isBundled && this.packagesCache[packageName] != null) { metadata = this.packagesCache[packageName].metadata; } if (metadata == null) { const metadataPath = CSON.resolve(path.join(packagePath, 'package')); if (metadataPath) { try { metadata = CSON.readFileSync(metadataPath); this.normalizePackageMetadata(metadata); } catch (error) { if (!ignoreErrors) { throw error; } } } } if (metadata == null) { metadata = {}; } if (typeof metadata.name !== 'string' || metadata.name.length <= 0) { metadata.name = packageName; } if ( metadata.repository && metadata.repository.type === 'git' && typeof metadata.repository.url === 'string' ) { metadata.repository.url = metadata.repository.url.replace( /(^git\+)|(\.git$)/g, '' ); } return metadata; } normalizePackageMetadata(metadata) { if (metadata != null) { normalizePackageData = normalizePackageData || require('normalize-package-data'); normalizePackageData(metadata); } } }; const NullVersionRange = { test() { return false; } }; ================================================ FILE: src/package-transpilation-registry.js ================================================ 'use strict'; // This file is required by compile-cache, which is required directly from // apm, so it can only use the subset of newer JavaScript features that apm's // version of Node supports. Strict mode is required for block scoped declarations. const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const minimatch = require('minimatch'); let Resolve = null; class PackageTranspilationRegistry { constructor() { this.configByPackagePath = {}; this.specByFilePath = {}; this.transpilerPaths = {}; } addTranspilerConfigForPath(packagePath, packageName, packageMeta, config) { this.configByPackagePath[packagePath] = { name: packageName, meta: packageMeta, path: packagePath, specs: config.map(spec => Object.assign({}, spec)) }; } removeTranspilerConfigForPath(packagePath) { delete this.configByPackagePath[packagePath]; const packagePathWithSep = packagePath.endsWith(path.sep) ? path.join(packagePath) : path.join(packagePath) + path.sep; Object.keys(this.specByFilePath).forEach(filePath => { if (path.join(filePath).startsWith(packagePathWithSep)) { delete this.specByFilePath[filePath]; } }); } // Wraps the transpiler in an object with the same interface // that falls back to the original transpiler implementation if and // only if a package hasn't registered its desire to transpile its own source. wrapTranspiler(transpiler) { return { getCachePath: (sourceCode, filePath) => { const spec = this.getPackageTranspilerSpecForFilePath(filePath); if (spec) { return this.getCachePath(sourceCode, filePath, spec); } return transpiler.getCachePath(sourceCode, filePath); }, compile: (sourceCode, filePath) => { const spec = this.getPackageTranspilerSpecForFilePath(filePath); if (spec) { return this.transpileWithPackageTranspiler( sourceCode, filePath, spec ); } return transpiler.compile(sourceCode, filePath); }, shouldCompile: (sourceCode, filePath) => { if (this.transpilerPaths[filePath]) { return false; } const spec = this.getPackageTranspilerSpecForFilePath(filePath); if (spec) { return true; } return transpiler.shouldCompile(sourceCode, filePath); } }; } getPackageTranspilerSpecForFilePath(filePath) { if (this.specByFilePath[filePath] !== undefined) return this.specByFilePath[filePath]; let thisPath = filePath; let lastPath = null; // Iterate parents from the file path to the root, checking at each level // to see if a package manages transpilation for that directory. // This means searching for a config for `/path/to/file/here.js` only // only iterates four times, even if there are hundreds of configs registered. while (thisPath !== lastPath) { // until we reach the root let config = this.configByPackagePath[thisPath]; if (config) { const relativePath = path.relative(thisPath, filePath); if ( relativePath.startsWith(`node_modules${path.sep}`) || relativePath.indexOf(`${path.sep}node_modules${path.sep}`) > -1 ) { return false; } for (let i = 0; i < config.specs.length; i++) { const spec = config.specs[i]; if (minimatch(filePath, path.join(config.path, spec.glob))) { spec._config = config; this.specByFilePath[filePath] = spec; return spec; } } } lastPath = thisPath; thisPath = path.join(thisPath, '..'); } this.specByFilePath[filePath] = null; return null; } getCachePath(sourceCode, filePath, spec) { const transpilerPath = this.getTranspilerPath(spec); const transpilerSource = spec._transpilerSource || fs.readFileSync(transpilerPath, 'utf8'); spec._transpilerSource = transpilerSource; const transpiler = this.getTranspiler(spec); let hash = crypto .createHash('sha1') .update(JSON.stringify(spec.options || {})) .update(transpilerSource, 'utf8') .update(sourceCode, 'utf8'); if (transpiler && transpiler.getCacheKeyData) { const meta = this.getMetadata(spec); const additionalCacheData = transpiler.getCacheKeyData( sourceCode, filePath, spec.options || {}, meta ); hash.update(additionalCacheData, 'utf8'); } return path.join( 'package-transpile', spec._config.name, hash.digest('hex') ); } transpileWithPackageTranspiler(sourceCode, filePath, spec) { const transpiler = this.getTranspiler(spec); if (transpiler) { const meta = this.getMetadata(spec); const result = transpiler.transpile( sourceCode, filePath, spec.options || {}, meta ); if (result === undefined || (result && result.code === undefined)) { return sourceCode; } else if (result.code) { return result.code.toString(); } else { throw new Error( 'Could not find a property `.code` on the transpilation results of ' + filePath ); } } else { const err = new Error( "Could not resolve transpiler '" + spec.transpiler + "' from '" + spec._config.path + "'" ); throw err; } } getMetadata(spec) { return { name: spec._config.name, path: spec._config.path, meta: spec._config.meta }; } getTranspilerPath(spec) { Resolve = Resolve || require('resolve'); return Resolve.sync(spec.transpiler, { basedir: spec._config.path, extensions: Object.keys(require.extensions) }); } getTranspiler(spec) { const transpilerPath = this.getTranspilerPath(spec); if (transpilerPath) { const transpiler = require(transpilerPath); this.transpilerPaths[transpilerPath] = true; return transpiler; } } } module.exports = PackageTranspilationRegistry; ================================================ FILE: src/package.js ================================================ const path = require('path'); const asyncEach = require('async/each'); const CSON = require('season'); const fs = require('fs-plus'); const { Emitter, CompositeDisposable } = require('event-kit'); const dedent = require('dedent'); const CompileCache = require('./compile-cache'); const ModuleCache = require('./module-cache'); const BufferedProcess = require('./buffered-process'); const { requireModule } = require('./module-utils'); // Extended: Loads and activates a package's main module and resources such as // stylesheets, keymaps, grammar, editor properties, and menus. module.exports = class Package { /* Section: Construction */ constructor(params) { this.config = params.config; this.packageManager = params.packageManager; this.styleManager = params.styleManager; this.commandRegistry = params.commandRegistry; this.keymapManager = params.keymapManager; this.notificationManager = params.notificationManager; this.grammarRegistry = params.grammarRegistry; this.themeManager = params.themeManager; this.menuManager = params.menuManager; this.contextMenuManager = params.contextMenuManager; this.deserializerManager = params.deserializerManager; this.viewRegistry = params.viewRegistry; this.emitter = new Emitter(); this.mainModule = null; this.path = params.path; this.preloadedPackage = params.preloadedPackage; this.metadata = params.metadata || this.packageManager.loadPackageMetadata(this.path); this.bundledPackage = params.bundledPackage != null ? params.bundledPackage : this.packageManager.isBundledPackagePath(this.path); this.name = (this.metadata && this.metadata.name) || params.name || path.basename(this.path); this.reset(); } /* Section: Event Subscription */ // Essential: Invoke the given callback when all packages have been activated. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDeactivate(callback) { return this.emitter.on('did-deactivate', callback); } /* Section: Instance Methods */ enable() { return this.config.removeAtKeyPath('core.disabledPackages', this.name); } disable() { return this.config.pushAtKeyPath('core.disabledPackages', this.name); } isTheme() { return this.metadata && this.metadata.theme; } measure(key, fn) { const startTime = window.performance.now(); const value = fn(); this[key] = Math.round(window.performance.now() - startTime); return value; } getType() { return 'atom'; } getStyleSheetPriority() { return 0; } preload() { this.loadKeymaps(); this.loadMenus(); this.registerDeserializerMethods(); this.activateCoreStartupServices(); this.registerURIHandler(); this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata(); this.requireMainModule(); this.settingsPromise = this.loadSettings(); this.activationDisposables = new CompositeDisposable(); this.activateKeymaps(); this.activateMenus(); for (let settings of this.settings) { settings.activate(this.config); } this.settingsActivated = true; } finishLoading() { this.measure('loadTime', () => { this.path = path.join(this.packageManager.resourcePath, this.path); ModuleCache.add(this.path, this.metadata); this.loadStylesheets(); // Unfortunately some packages are accessing `@mainModulePath`, so we need // to compute that variable eagerly also for preloaded packages. this.getMainModulePath(); }); } load() { this.measure('loadTime', () => { try { ModuleCache.add(this.path, this.metadata); this.loadKeymaps(); this.loadMenus(); this.loadStylesheets(); this.registerDeserializerMethods(); this.activateCoreStartupServices(); this.registerURIHandler(); this.registerTranspilerConfig(); this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata(); this.settingsPromise = this.loadSettings(); if (this.shouldRequireMainModuleOnLoad() && this.mainModule == null) { this.requireMainModule(); } } catch (error) { this.handleError(`Failed to load the ${this.name} package`, error); } }); return this; } unload() { this.unregisterTranspilerConfig(); } shouldRequireMainModuleOnLoad() { return !( this.metadata.deserializers || this.metadata.viewProviders || this.metadata.configSchema || this.activationShouldBeDeferred() || localStorage.getItem(this.getCanDeferMainModuleRequireStorageKey()) === 'true' ); } reset() { this.stylesheets = []; this.keymaps = []; this.menus = []; this.grammars = []; this.settings = []; this.mainInitialized = false; this.mainActivated = false; this.deserialized = false; } initializeIfNeeded() { if (this.mainInitialized) return; this.measure('initializeTime', () => { try { // The main module's `initialize()` method is guaranteed to be called // before its `activate()`. This gives you a chance to handle the // serialized package state before the package's derserializers and view // providers are used. if (!this.mainModule) this.requireMainModule(); if (typeof this.mainModule.initialize === 'function') { this.mainModule.initialize( this.packageManager.getPackageState(this.name) || {} ); } this.mainInitialized = true; } catch (error) { this.handleError( `Failed to initialize the ${this.name} package`, error ); } }); } activate() { if (!this.grammarsPromise) this.grammarsPromise = this.loadGrammars(); if (!this.activationPromise) { this.activationPromise = new Promise((resolve, reject) => { this.resolveActivationPromise = resolve; this.measure('activateTime', () => { try { this.activateResources(); if (this.activationShouldBeDeferred()) { return this.subscribeToDeferredActivation(); } else { return this.activateNow(); } } catch (error) { return this.handleError( `Failed to activate the ${this.name} package`, error ); } }); }); } return Promise.all([ this.grammarsPromise, this.settingsPromise, this.activationPromise ]); } activateNow() { try { if (!this.mainModule) this.requireMainModule(); this.configSchemaRegisteredOnActivate = this.registerConfigSchemaFromMainModule(); this.registerViewProviders(); this.activateStylesheets(); if (this.mainModule && !this.mainActivated) { this.initializeIfNeeded(); if (typeof this.mainModule.activateConfig === 'function') { this.mainModule.activateConfig(); } if (typeof this.mainModule.activate === 'function') { this.mainModule.activate( this.packageManager.getPackageState(this.name) || {} ); } this.mainActivated = true; this.activateServices(); } if (this.activationCommandSubscriptions) this.activationCommandSubscriptions.dispose(); if (this.activationHookSubscriptions) this.activationHookSubscriptions.dispose(); if (this.workspaceOpenerSubscriptions) this.workspaceOpenerSubscriptions.dispose(); } catch (error) { this.handleError(`Failed to activate the ${this.name} package`, error); } if (typeof this.resolveActivationPromise === 'function') this.resolveActivationPromise(); } registerConfigSchemaFromMetadata() { const configSchema = this.metadata.configSchema; if (configSchema) { this.config.setSchema(this.name, { type: 'object', properties: configSchema }); return true; } else { return false; } } registerConfigSchemaFromMainModule() { if (this.mainModule && !this.configSchemaRegisteredOnLoad) { if (typeof this.mainModule.config === 'object') { this.config.setSchema(this.name, { type: 'object', properties: this.mainModule.config }); return true; } } return false; } // TODO: Remove. Settings view calls this method currently. activateConfig() { if (this.configSchemaRegisteredOnLoad) return; this.requireMainModule(); this.registerConfigSchemaFromMainModule(); } activateStylesheets() { if (this.stylesheetsActivated) return; this.stylesheetDisposables = new CompositeDisposable(); const priority = this.getStyleSheetPriority(); for (let [sourcePath, source] of this.stylesheets) { const match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./); let context; if (match) { context = match[1]; } else if (this.metadata.theme === 'syntax') { context = 'atom-text-editor'; } this.stylesheetDisposables.add( this.styleManager.addStyleSheet(source, { sourcePath, priority, context, skipDeprecatedSelectorsTransformation: this.bundledPackage }) ); } this.stylesheetsActivated = true; } activateResources() { if (!this.activationDisposables) this.activationDisposables = new CompositeDisposable(); const packagesWithKeymapsDisabled = this.config.get( 'core.packagesWithKeymapsDisabled' ); if ( packagesWithKeymapsDisabled && packagesWithKeymapsDisabled.includes(this.name) ) { this.deactivateKeymaps(); } else if (!this.keymapActivated) { this.activateKeymaps(); } if (!this.menusActivated) { this.activateMenus(); } if (!this.grammarsActivated) { for (let grammar of this.grammars) { grammar.activate(); } this.grammarsActivated = true; } if (!this.settingsActivated) { for (let settings of this.settings) { settings.activate(this.config); } this.settingsActivated = true; } } activateKeymaps() { if (this.keymapActivated) return; this.keymapDisposables = new CompositeDisposable(); const validateSelectors = !this.preloadedPackage; for (let [keymapPath, map] of this.keymaps) { this.keymapDisposables.add( this.keymapManager.add(keymapPath, map, 0, validateSelectors) ); } this.menuManager.update(); this.keymapActivated = true; } deactivateKeymaps() { if (!this.keymapActivated) return; if (this.keymapDisposables) { this.keymapDisposables.dispose(); } this.menuManager.update(); this.keymapActivated = false; } hasKeymaps() { for (let [, map] of this.keymaps) { if (map.length > 0) return true; } return false; } activateMenus() { const validateSelectors = !this.preloadedPackage; for (const [menuPath, map] of this.menus) { if (map['context-menu']) { try { const itemsBySelector = map['context-menu']; this.activationDisposables.add( this.contextMenuManager.add(itemsBySelector, validateSelectors) ); } catch (error) { if (error.code === 'EBADSELECTOR') { error.message += ` in ${menuPath}`; error.stack += `\n at ${menuPath}:1:1`; } throw error; } } } for (const [, map] of this.menus) { if (map.menu) this.activationDisposables.add(this.menuManager.add(map.menu)); } this.menusActivated = true; } activateServices() { let methodName, version, versions; for (var name in this.metadata.providedServices) { ({ versions } = this.metadata.providedServices[name]); const servicesByVersion = {}; for (version in versions) { methodName = versions[version]; if (typeof this.mainModule[methodName] === 'function') { servicesByVersion[version] = this.mainModule[methodName](); } } this.activationDisposables.add( this.packageManager.serviceHub.provide(name, servicesByVersion) ); } for (name in this.metadata.consumedServices) { ({ versions } = this.metadata.consumedServices[name]); for (version in versions) { methodName = versions[version]; if (typeof this.mainModule[methodName] === 'function') { this.activationDisposables.add( this.packageManager.serviceHub.consume( name, version, this.mainModule[methodName].bind(this.mainModule) ) ); } } } } registerURIHandler() { const handlerConfig = this.getURIHandler(); const methodName = handlerConfig && handlerConfig.method; if (methodName) { this.uriHandlerSubscription = this.packageManager.registerURIHandlerForPackage( this.name, (...args) => this.handleURI(methodName, args) ); } } unregisterURIHandler() { if (this.uriHandlerSubscription) this.uriHandlerSubscription.dispose(); } handleURI(methodName, args) { this.activate().then(() => { if (this.mainModule[methodName]) this.mainModule[methodName].apply(this.mainModule, args); }); if (!this.mainActivated) this.activateNow(); } registerTranspilerConfig() { if (this.metadata.atomTranspilers) { CompileCache.addTranspilerConfigForPath( this.path, this.name, this.metadata, this.metadata.atomTranspilers ); } } unregisterTranspilerConfig() { if (this.metadata.atomTranspilers) { CompileCache.removeTranspilerConfigForPath(this.path); } } loadKeymaps() { if (this.bundledPackage && this.packageManager.packagesCache[this.name]) { this.keymaps = []; for (const keymapPath in this.packageManager.packagesCache[this.name] .keymaps) { const keymapObject = this.packageManager.packagesCache[this.name] .keymaps[keymapPath]; this.keymaps.push([`core:${keymapPath}`, keymapObject]); } } else { this.keymaps = this.getKeymapPaths().map(keymapPath => [ keymapPath, CSON.readFileSync(keymapPath, { allowDuplicateKeys: false }) || {} ]); } } loadMenus() { if (this.bundledPackage && this.packageManager.packagesCache[this.name]) { this.menus = []; for (const menuPath in this.packageManager.packagesCache[this.name] .menus) { const menuObject = this.packageManager.packagesCache[this.name].menus[ menuPath ]; this.menus.push([`core:${menuPath}`, menuObject]); } } else { this.menus = this.getMenuPaths().map(menuPath => [ menuPath, CSON.readFileSync(menuPath) || {} ]); } } getKeymapPaths() { const keymapsDirPath = path.join(this.path, 'keymaps'); if (this.metadata.keymaps) { return this.metadata.keymaps.map(name => fs.resolve(keymapsDirPath, name, ['json', 'cson', '']) ); } else { return fs.listSync(keymapsDirPath, ['cson', 'json']); } } getMenuPaths() { const menusDirPath = path.join(this.path, 'menus'); if (this.metadata.menus) { return this.metadata.menus.map(name => fs.resolve(menusDirPath, name, ['json', 'cson', '']) ); } else { return fs.listSync(menusDirPath, ['cson', 'json']); } } loadStylesheets() { this.stylesheets = this.getStylesheetPaths().map(stylesheetPath => [ stylesheetPath, this.themeManager.loadStylesheet(stylesheetPath, true) ]); } registerDeserializerMethods() { if (this.metadata.deserializers) { Object.keys(this.metadata.deserializers).forEach(deserializerName => { const methodName = this.metadata.deserializers[deserializerName]; this.deserializerManager.add({ name: deserializerName, deserialize: (state, atomEnvironment) => { this.registerViewProviders(); this.requireMainModule(); this.initializeIfNeeded(); if (atomEnvironment.packages.hasActivatedInitialPackages()) { // Only explicitly activate the package if initial packages // have finished activating. This is because deserialization // generally occurs at Atom startup, which happens before the // workspace element is added to the DOM and is inconsistent with // with when initial package activation occurs. Triggering activation // immediately may cause problems with packages that expect to // always have access to the workspace element. // Otherwise, we just set the deserialized flag and package-manager // will activate this package as normal during initial package activation. this.activateNow(); } this.deserialized = true; return this.mainModule[methodName](state, atomEnvironment); } }); }); } } activateCoreStartupServices() { const directoryProviderService = this.metadata.providedServices && this.metadata.providedServices['atom.directory-provider']; if (directoryProviderService) { this.requireMainModule(); const servicesByVersion = {}; for (let version in directoryProviderService.versions) { const methodName = directoryProviderService.versions[version]; if (typeof this.mainModule[methodName] === 'function') { servicesByVersion[version] = this.mainModule[methodName](); } } this.packageManager.serviceHub.provide( 'atom.directory-provider', servicesByVersion ); } } registerViewProviders() { if (this.metadata.viewProviders && !this.registeredViewProviders) { this.requireMainModule(); this.metadata.viewProviders.forEach(methodName => { this.viewRegistry.addViewProvider(model => { this.initializeIfNeeded(); return this.mainModule[methodName](model); }); }); this.registeredViewProviders = true; } } getStylesheetsPath() { return path.join(this.path, 'styles'); } getStylesheetPaths() { if ( this.bundledPackage && this.packageManager.packagesCache[this.name] && this.packageManager.packagesCache[this.name].styleSheetPaths ) { const { styleSheetPaths } = this.packageManager.packagesCache[this.name]; return styleSheetPaths.map(styleSheetPath => path.join(this.path, styleSheetPath) ); } else { let indexStylesheet; const stylesheetDirPath = this.getStylesheetsPath(); if (this.metadata.mainStyleSheet) { return [fs.resolve(this.path, this.metadata.mainStyleSheet)]; } else if (this.metadata.styleSheets) { return this.metadata.styleSheets.map(name => fs.resolve(stylesheetDirPath, name, ['css', 'less', '']) ); } else if ( (indexStylesheet = fs.resolve(this.path, 'index', ['css', 'less'])) ) { return [indexStylesheet]; } else { return fs.listSync(stylesheetDirPath, ['css', 'less']); } } } loadGrammarsSync() { if (this.grammarsLoaded) return; let grammarPaths; if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) { ({ grammarPaths } = this.packageManager.packagesCache[this.name]); } else { grammarPaths = fs.listSync(path.join(this.path, 'grammars'), [ 'json', 'cson' ]); } for (let grammarPath of grammarPaths) { if ( this.preloadedPackage && this.packageManager.packagesCache[this.name] ) { grammarPath = path.resolve( this.packageManager.resourcePath, grammarPath ); } try { const grammar = this.grammarRegistry.readGrammarSync(grammarPath); grammar.packageName = this.name; grammar.bundledPackage = this.bundledPackage; this.grammars.push(grammar); grammar.activate(); } catch (error) { console.warn( `Failed to load grammar: ${grammarPath}`, error.stack || error ); } } this.grammarsLoaded = true; this.grammarsActivated = true; } loadGrammars() { if (this.grammarsLoaded) return Promise.resolve(); const loadGrammar = (grammarPath, callback) => { if (this.preloadedPackage) { grammarPath = path.resolve( this.packageManager.resourcePath, grammarPath ); } return this.grammarRegistry.readGrammar(grammarPath, (error, grammar) => { if (error) { const detail = `${error.message} in ${grammarPath}`; const stack = `${error.stack}\n at ${grammarPath}:1:1`; this.notificationManager.addFatalError( `Failed to load a ${this.name} package grammar`, { stack, detail, packageName: this.name, dismissable: true } ); } else { grammar.packageName = this.name; grammar.bundledPackage = this.bundledPackage; this.grammars.push(grammar); if (this.grammarsActivated) grammar.activate(); } return callback(); }); }; return new Promise(resolve => { if ( this.preloadedPackage && this.packageManager.packagesCache[this.name] ) { const { grammarPaths } = this.packageManager.packagesCache[this.name]; return asyncEach(grammarPaths, loadGrammar, () => resolve()); } else { const grammarsDirPath = path.join(this.path, 'grammars'); fs.exists(grammarsDirPath, grammarsDirExists => { if (!grammarsDirExists) return resolve(); fs.list(grammarsDirPath, ['json', 'cson'], (error, grammarPaths) => { if (error || !grammarPaths) return resolve(); asyncEach(grammarPaths, loadGrammar, () => resolve()); }); }); } }); } loadSettings() { this.settings = []; const loadSettingsFile = (settingsPath, callback) => { return SettingsFile.load(settingsPath, (error, settingsFile) => { if (error) { const detail = `${error.message} in ${settingsPath}`; const stack = `${error.stack}\n at ${settingsPath}:1:1`; this.notificationManager.addFatalError( `Failed to load the ${this.name} package settings`, { stack, detail, packageName: this.name, dismissable: true } ); } else { this.settings.push(settingsFile); if (this.settingsActivated) settingsFile.activate(this.config); } return callback(); }); }; if (this.preloadedPackage && this.packageManager.packagesCache[this.name]) { for (let settingsPath in this.packageManager.packagesCache[this.name] .settings) { const properties = this.packageManager.packagesCache[this.name] .settings[settingsPath]; const settingsFile = new SettingsFile( `core:${settingsPath}`, properties || {} ); this.settings.push(settingsFile); if (this.settingsActivated) settingsFile.activate(this.config); } } else { return new Promise(resolve => { const settingsDirPath = path.join(this.path, 'settings'); fs.exists(settingsDirPath, settingsDirExists => { if (!settingsDirExists) return resolve(); fs.list(settingsDirPath, ['json', 'cson'], (error, settingsPaths) => { if (error || !settingsPaths) return resolve(); asyncEach(settingsPaths, loadSettingsFile, () => resolve()); }); }); }); } } serialize() { if (this.mainActivated) { if (typeof this.mainModule.serialize === 'function') { try { return this.mainModule.serialize(); } catch (error) { console.error( `Error serializing package '${this.name}'`, error.stack ); } } } } async deactivate() { this.activationPromise = null; this.resolveActivationPromise = null; if (this.activationCommandSubscriptions) this.activationCommandSubscriptions.dispose(); if (this.activationHookSubscriptions) this.activationHookSubscriptions.dispose(); this.configSchemaRegisteredOnActivate = false; this.unregisterURIHandler(); this.deactivateResources(); this.deactivateKeymaps(); if (!this.mainActivated) { this.emitter.emit('did-deactivate'); return; } if (typeof this.mainModule.deactivate === 'function') { try { const deactivationResult = this.mainModule.deactivate(); if ( deactivationResult && typeof deactivationResult.then === 'function' ) { await deactivationResult; } } catch (error) { console.error(`Error deactivating package '${this.name}'`, error.stack); } } if (typeof this.mainModule.deactivateConfig === 'function') { try { await this.mainModule.deactivateConfig(); } catch (error) { console.error(`Error deactivating package '${this.name}'`, error.stack); } } this.mainActivated = false; this.mainInitialized = false; this.emitter.emit('did-deactivate'); } deactivateResources() { for (let grammar of this.grammars) { grammar.deactivate(); } for (let settings of this.settings) { settings.deactivate(this.config); } if (this.stylesheetDisposables) this.stylesheetDisposables.dispose(); if (this.activationDisposables) this.activationDisposables.dispose(); if (this.keymapDisposables) this.keymapDisposables.dispose(); this.stylesheetsActivated = false; this.grammarsActivated = false; this.settingsActivated = false; this.menusActivated = false; } reloadStylesheets() { try { this.loadStylesheets(); } catch (error) { this.handleError( `Failed to reload the ${this.name} package stylesheets`, error ); } if (this.stylesheetDisposables) this.stylesheetDisposables.dispose(); this.stylesheetDisposables = new CompositeDisposable(); this.stylesheetsActivated = false; this.activateStylesheets(); } requireMainModule() { if (this.bundledPackage && this.packageManager.packagesCache[this.name]) { if (this.packageManager.packagesCache[this.name].main) { this.mainModule = requireModule( this.packageManager.packagesCache[this.name].main ); return this.mainModule; } } else if (this.mainModuleRequired) { return this.mainModule; } else if (!this.isCompatible()) { const nativeModuleNames = this.incompatibleModules .map(m => m.name) .join(', '); console.warn(dedent` Failed to require the main module of '${ this.name }' because it requires one or more incompatible native modules (${nativeModuleNames}). Run \`apm rebuild\` in the package directory and restart Atom to resolve.\ `); } else { const mainModulePath = this.getMainModulePath(); if (fs.isFileSync(mainModulePath)) { this.mainModuleRequired = true; const previousViewProviderCount = this.viewRegistry.getViewProviderCount(); const previousDeserializerCount = this.deserializerManager.getDeserializerCount(); this.mainModule = requireModule(mainModulePath); if ( this.viewRegistry.getViewProviderCount() === previousViewProviderCount && this.deserializerManager.getDeserializerCount() === previousDeserializerCount ) { localStorage.setItem( this.getCanDeferMainModuleRequireStorageKey(), 'true' ); } else { localStorage.removeItem( this.getCanDeferMainModuleRequireStorageKey() ); } return this.mainModule; } } } getMainModulePath() { if (this.resolvedMainModulePath) return this.mainModulePath; this.resolvedMainModulePath = true; if (this.bundledPackage && this.packageManager.packagesCache[this.name]) { if (this.packageManager.packagesCache[this.name].main) { this.mainModulePath = path.resolve( this.packageManager.resourcePath, 'static', this.packageManager.packagesCache[this.name].main ); } else { this.mainModulePath = null; } } else { const mainModulePath = this.metadata.main ? path.join(this.path, this.metadata.main) : path.join(this.path, 'index'); this.mainModulePath = fs.resolveExtension(mainModulePath, [ '', ...CompileCache.supportedExtensions ]); } return this.mainModulePath; } activationShouldBeDeferred() { return ( !this.deserialized && (this.hasActivationCommands() || this.hasActivationHooks() || this.hasWorkspaceOpeners() || this.hasDeferredURIHandler()) ); } hasActivationHooks() { const hooks = this.getActivationHooks(); return hooks && hooks.length > 0; } hasWorkspaceOpeners() { const openers = this.getWorkspaceOpeners(); return openers && openers.length > 0; } hasActivationCommands() { const object = this.getActivationCommands(); for (let selector in object) { const commands = object[selector]; if (commands.length > 0) return true; } return false; } hasDeferredURIHandler() { const handler = this.getURIHandler(); return handler && handler.deferActivation !== false; } subscribeToDeferredActivation() { this.subscribeToActivationCommands(); this.subscribeToActivationHooks(); this.subscribeToWorkspaceOpeners(); } subscribeToActivationCommands() { this.activationCommandSubscriptions = new CompositeDisposable(); const object = this.getActivationCommands(); for (let selector in object) { const commands = object[selector]; for (let command of commands) { ((selector, command) => { // Add dummy command so it appears in menu. // The real command will be registered on package activation try { this.activationCommandSubscriptions.add( this.commandRegistry.add(selector, command, function() {}) ); } catch (error) { if (error.code === 'EBADSELECTOR') { const metadataPath = path.join(this.path, 'package.json'); error.message += ` in ${metadataPath}`; error.stack += `\n at ${metadataPath}:1:1`; } throw error; } this.activationCommandSubscriptions.add( this.commandRegistry.onWillDispatch(event => { if (event.type !== command) return; let currentTarget = event.target; while (currentTarget) { if (currentTarget.webkitMatchesSelector(selector)) { this.activationCommandSubscriptions.dispose(); this.activateNow(); break; } currentTarget = currentTarget.parentElement; } }) ); })(selector, command); } } } getActivationCommands() { if (this.activationCommands) return this.activationCommands; this.activationCommands = {}; if (this.metadata.activationCommands) { for (let selector in this.metadata.activationCommands) { const commands = this.metadata.activationCommands[selector]; if (!this.activationCommands[selector]) this.activationCommands[selector] = []; if (typeof commands === 'string') { this.activationCommands[selector].push(commands); } else if (Array.isArray(commands)) { this.activationCommands[selector].push(...commands); } } } return this.activationCommands; } subscribeToActivationHooks() { this.activationHookSubscriptions = new CompositeDisposable(); for (let hook of this.getActivationHooks()) { if (typeof hook === 'string' && hook.trim().length > 0) { this.activationHookSubscriptions.add( this.packageManager.onDidTriggerActivationHook(hook, () => this.activateNow() ) ); } } } getActivationHooks() { if (this.metadata && this.activationHooks) return this.activationHooks; if (this.metadata.activationHooks) { if (Array.isArray(this.metadata.activationHooks)) { this.activationHooks = Array.from( new Set(this.metadata.activationHooks) ); } else if (typeof this.metadata.activationHooks === 'string') { this.activationHooks = [this.metadata.activationHooks]; } else { this.activationHooks = []; } } else { this.activationHooks = []; } return this.activationHooks; } subscribeToWorkspaceOpeners() { this.workspaceOpenerSubscriptions = new CompositeDisposable(); for (let opener of this.getWorkspaceOpeners()) { this.workspaceOpenerSubscriptions.add( atom.workspace.addOpener(filePath => { if (filePath === opener) { this.activateNow(); this.workspaceOpenerSubscriptions.dispose(); return atom.workspace.createItemForURI(opener); } }) ); } } getWorkspaceOpeners() { if (this.workspaceOpeners) return this.workspaceOpeners; if (this.metadata.workspaceOpeners) { if (Array.isArray(this.metadata.workspaceOpeners)) { this.workspaceOpeners = Array.from( new Set(this.metadata.workspaceOpeners) ); } else if (typeof this.metadata.workspaceOpeners === 'string') { this.workspaceOpeners = [this.metadata.workspaceOpeners]; } else { this.workspaceOpeners = []; } } else { this.workspaceOpeners = []; } return this.workspaceOpeners; } getURIHandler() { return this.metadata && this.metadata.uriHandler; } // Does the given module path contain native code? isNativeModule(modulePath) { try { return this.getModulePathNodeFiles(modulePath).length > 0; } catch (error) { return false; } } // get the list of `.node` files for the given module path getModulePathNodeFiles(modulePath) { try { const modulePathNodeFiles = fs.listSync( path.join(modulePath, 'build', 'Release'), ['.node'] ); return modulePathNodeFiles; } catch (error) { return []; } } // Get a Map of all the native modules => the `.node` files that this package depends on. // // First try to get this information from // @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't // exist, recurse through all dependencies. getNativeModuleDependencyPathsMap() { const nativeModulePaths = new Map(); if (this.metadata._atomModuleCache) { const nodeFilePaths = []; const relativeNativeModuleBindingPaths = (this.metadata._atomModuleCache.extensions && this.metadata._atomModuleCache.extensions['.node']) || []; for (let relativeNativeModuleBindingPath of relativeNativeModuleBindingPaths) { const nodeFilePath = path.join( this.path, relativeNativeModuleBindingPath, '..', '..', '..' ); nodeFilePaths.push(nodeFilePath); } nativeModulePaths.set(this.path, nodeFilePaths); return nativeModulePaths; } const traversePath = nodeModulesPath => { try { for (let modulePath of fs.listSync(nodeModulesPath)) { const modulePathNodeFiles = this.getModulePathNodeFiles(modulePath); if (modulePathNodeFiles) { nativeModulePaths.set(modulePath, modulePathNodeFiles); } traversePath(path.join(modulePath, 'node_modules')); } } catch (error) {} }; traversePath(path.join(this.path, 'node_modules')); return nativeModulePaths; } // Get an array of all the native modules that this package depends on. // See `getNativeModuleDependencyPathsMap` for more information getNativeModuleDependencyPaths() { return [...this.getNativeModuleDependencyPathsMap().keys()]; } /* Section: Native Module Compatibility */ // Extended: Are all native modules depended on by this package correctly // compiled against the current version of Atom? // // Incompatible packages cannot be activated. // // Returns a {Boolean}, true if compatible, false if incompatible. isCompatible() { if (this.compatible == null) { if (this.preloadedPackage) { this.compatible = true; } else if (this.getMainModulePath()) { this.incompatibleModules = this.getIncompatibleNativeModules(); this.compatible = this.incompatibleModules.length === 0 && this.getBuildFailureOutput() == null; } else { this.compatible = true; } } return this.compatible; } // Extended: Rebuild native modules in this package's dependencies for the // current version of Atom. // // Returns a {Promise} that resolves with an object containing `code`, // `stdout`, and `stderr` properties based on the results of running // `apm rebuild` on the package. rebuild() { return new Promise(resolve => this.runRebuildProcess(result => { if (result.code === 0) { global.localStorage.removeItem( this.getBuildFailureOutputStorageKey() ); } else { this.compatible = false; global.localStorage.setItem( this.getBuildFailureOutputStorageKey(), result.stderr ); } global.localStorage.setItem( this.getIncompatibleNativeModulesStorageKey(), '[]' ); resolve(result); }) ); } // Extended: If a previous rebuild failed, get the contents of stderr. // // Returns a {String} or null if no previous build failure occurred. getBuildFailureOutput() { return global.localStorage.getItem(this.getBuildFailureOutputStorageKey()); } runRebuildProcess(done) { let stderr = ''; let stdout = ''; return new BufferedProcess({ command: this.packageManager.getApmPath(), args: ['rebuild', '--no-color'], options: { cwd: this.path }, stderr(output) { stderr += output; }, stdout(output) { stdout += output; }, exit(code) { done({ code, stdout, stderr }); } }); } getBuildFailureOutputStorageKey() { return `installed-packages:${this.name}:${ this.metadata.version }:build-error`; } getIncompatibleNativeModulesStorageKey() { const electronVersion = process.versions.electron; return `installed-packages:${this.name}:${ this.metadata.version }:electron-${electronVersion}:incompatible-native-modules`; } getCanDeferMainModuleRequireStorageKey() { return `installed-packages:${this.name}:${ this.metadata.version }:can-defer-main-module-require`; } // Get the incompatible native modules that this package depends on. // This recurses through all dependencies and requires all `.node` files. // // This information is cached in local storage on a per package/version basis // to minimize the impact on startup time. getIncompatibleNativeModules() { if (!this.packageManager.devMode) { try { const arrayAsString = global.localStorage.getItem( this.getIncompatibleNativeModulesStorageKey() ); if (arrayAsString) return JSON.parse(arrayAsString); } catch (error1) {} } const incompatibleNativeModules = []; const nativeModulePaths = this.getNativeModuleDependencyPathsMap(); for (const [nativeModulePath, nodeFilesPaths] of nativeModulePaths) { try { // require each .node file for (const nodeFilePath of nodeFilesPaths) { require(nodeFilePath); } } catch (error) { let version; try { ({ version } = require(`${nativeModulePath}/package.json`)); } catch (error2) {} incompatibleNativeModules.push({ path: nativeModulePath, name: path.basename(nativeModulePath), version, error: error.message }); } } global.localStorage.setItem( this.getIncompatibleNativeModulesStorageKey(), JSON.stringify(incompatibleNativeModules) ); return incompatibleNativeModules; } handleError(message, error) { if (atom.inSpecMode()) throw error; let detail, location, stack; if (error.filename && error.location && error instanceof SyntaxError) { location = `${error.filename}:${error.location.first_line + 1}:${error .location.first_column + 1}`; detail = `${error.message} in ${location}`; stack = 'SyntaxError: ' + error.message + '\n' + 'at ' + location; } else if ( error.less && error.filename && error.column != null && error.line != null ) { location = `${error.filename}:${error.line}:${error.column}`; detail = `${error.message} in ${location}`; stack = 'LessError: ' + error.message + '\n' + 'at ' + location; } else { detail = error.message; stack = error.stack || error; } this.notificationManager.addFatalError(message, { stack, detail, packageName: this.name, dismissable: true }); } }; class SettingsFile { static load(path, callback) { CSON.readFile(path, (error, properties = {}) => { if (error) { callback(error); } else { callback(null, new SettingsFile(path, properties)); } }); } constructor(path, properties) { this.path = path; this.properties = properties; } activate(config) { for (let selector in this.properties) { config.set(null, this.properties[selector], { scopeSelector: selector, source: this.path }); } } deactivate(config) { for (let selector in this.properties) { config.unset(null, { scopeSelector: selector, source: this.path }); } } } ================================================ FILE: src/pane-axis-element.js ================================================ const { CompositeDisposable } = require('event-kit'); require('./pane-resize-handle-element'); class PaneAxisElement extends HTMLElement { connectedCallback() { if (this.subscriptions == null) { this.subscriptions = this.subscribeToModel(); } this.model .getChildren() .map((child, index) => this.childAdded({ child, index })); } disconnectedCallback() { this.subscriptions.dispose(); this.subscriptions = null; this.model.getChildren().map(child => this.childRemoved({ child })); } initialize(model, viewRegistry) { this.model = model; this.viewRegistry = viewRegistry; if (this.subscriptions == null) { this.subscriptions = this.subscribeToModel(); } const iterable = this.model.getChildren(); for (let index = 0; index < iterable.length; index++) { const child = iterable[index]; this.childAdded({ child, index }); } switch (this.model.getOrientation()) { case 'horizontal': this.classList.add('horizontal', 'pane-row'); break; case 'vertical': this.classList.add('vertical', 'pane-column'); break; } return this; } subscribeToModel() { const subscriptions = new CompositeDisposable(); subscriptions.add(this.model.onDidAddChild(this.childAdded.bind(this))); subscriptions.add( this.model.onDidRemoveChild(this.childRemoved.bind(this)) ); subscriptions.add( this.model.onDidReplaceChild(this.childReplaced.bind(this)) ); subscriptions.add( this.model.observeFlexScale(this.flexScaleChanged.bind(this)) ); return subscriptions; } isPaneResizeHandleElement(element) { return ( (element != null ? element.nodeName.toLowerCase() : undefined) === 'atom-pane-resize-handle' ); } childAdded({ child, index }) { let resizeHandle; const view = this.viewRegistry.getView(child); this.insertBefore(view, this.children[index * 2]); const prevElement = view.previousSibling; // if previous element is not pane resize element, then insert new resize element if (prevElement != null && !this.isPaneResizeHandleElement(prevElement)) { resizeHandle = document.createElement('atom-pane-resize-handle'); this.insertBefore(resizeHandle, view); } const nextElement = view.nextSibling; // if next element isnot resize element, then insert new resize element if (nextElement != null && !this.isPaneResizeHandleElement(nextElement)) { resizeHandle = document.createElement('atom-pane-resize-handle'); return this.insertBefore(resizeHandle, nextElement); } } childRemoved({ child }) { const view = this.viewRegistry.getView(child); const siblingView = view.previousSibling; // make sure next sibling view is pane resize view if (siblingView != null && this.isPaneResizeHandleElement(siblingView)) { siblingView.remove(); } return view.remove(); } childReplaced({ index, oldChild, newChild }) { let focusedElement; if (this.hasFocus()) { focusedElement = document.activeElement; } this.childRemoved({ child: oldChild, index }); this.childAdded({ child: newChild, index }); if (document.activeElement === document.body) { return focusedElement != null ? focusedElement.focus() : undefined; } } flexScaleChanged(flexScale) { this.style.flexGrow = flexScale; } hasFocus() { return ( this === document.activeElement || this.contains(document.activeElement) ); } } window.customElements.define('atom-pane-axis', PaneAxisElement); function createPaneAxisElement() { return document.createElement('atom-pane-axis'); } module.exports = { createPaneAxisElement }; ================================================ FILE: src/pane-axis.js ================================================ const { Emitter, CompositeDisposable } = require('event-kit'); const { flatten } = require('underscore-plus'); const Model = require('./model'); const { createPaneAxisElement } = require('./pane-axis-element'); class PaneAxis extends Model { static deserialize(state, { deserializers, views }) { state.children = state.children.map(childState => deserializers.deserialize(childState) ); return new PaneAxis(state, views); } constructor({ orientation, children, flexScale }, viewRegistry) { super(); this.parent = null; this.container = null; this.orientation = orientation; this.viewRegistry = viewRegistry; this.emitter = new Emitter(); this.subscriptionsByChild = new WeakMap(); this.subscriptions = new CompositeDisposable(); this.flexScale = flexScale != null ? flexScale : 1; this.children = []; if (children) { for (let child of children) { this.addChild(child); } } } serialize() { return { deserializer: 'PaneAxis', children: this.children.map(child => child.serialize()), orientation: this.orientation, flexScale: this.flexScale }; } getElement() { if (!this.element) { this.element = createPaneAxisElement().initialize( this, this.viewRegistry ); } return this.element; } getFlexScale() { return this.flexScale; } setFlexScale(flexScale) { this.flexScale = flexScale; this.emitter.emit('did-change-flex-scale', this.flexScale); return this.flexScale; } getParent() { return this.parent; } setParent(parent) { this.parent = parent; return this.parent; } getContainer() { return this.container; } setContainer(container) { if (container && container !== this.container) { this.container = container; this.children.forEach(child => child.setContainer(container)); } } getOrientation() { return this.orientation; } getChildren() { return this.children.slice(); } getPanes() { return flatten(this.children.map(child => child.getPanes())); } getItems() { return flatten(this.children.map(child => child.getItems())); } onDidAddChild(fn) { return this.emitter.on('did-add-child', fn); } onDidRemoveChild(fn) { return this.emitter.on('did-remove-child', fn); } onDidReplaceChild(fn) { return this.emitter.on('did-replace-child', fn); } onDidDestroy(fn) { return this.emitter.once('did-destroy', fn); } onDidChangeFlexScale(fn) { return this.emitter.on('did-change-flex-scale', fn); } observeFlexScale(fn) { fn(this.flexScale); return this.onDidChangeFlexScale(fn); } addChild(child, index = this.children.length) { this.children.splice(index, 0, child); child.setParent(this); child.setContainer(this.container); this.subscribeToChild(child); return this.emitter.emit('did-add-child', { child, index }); } adjustFlexScale() { // get current total flex scale of children let total = 0; for (var child of this.children) { total += child.getFlexScale(); } const needTotal = this.children.length; // set every child's flex scale by the ratio for (child of this.children) { child.setFlexScale((needTotal * child.getFlexScale()) / total); } } removeChild(child, replacing = false) { const index = this.children.indexOf(child); if (index === -1) { throw new Error('Removing non-existent child'); } this.unsubscribeFromChild(child); this.children.splice(index, 1); this.adjustFlexScale(); this.emitter.emit('did-remove-child', { child, index }); if (!replacing && this.children.length < 2) { this.reparentLastChild(); } } replaceChild(oldChild, newChild) { this.unsubscribeFromChild(oldChild); this.subscribeToChild(newChild); newChild.setParent(this); newChild.setContainer(this.container); const index = this.children.indexOf(oldChild); this.children.splice(index, 1, newChild); this.emitter.emit('did-replace-child', { oldChild, newChild, index }); } insertChildBefore(currentChild, newChild) { const index = this.children.indexOf(currentChild); return this.addChild(newChild, index); } insertChildAfter(currentChild, newChild) { const index = this.children.indexOf(currentChild); return this.addChild(newChild, index + 1); } reparentLastChild() { const lastChild = this.children[0]; lastChild.setFlexScale(this.flexScale); this.parent.replaceChild(this, lastChild); this.destroy(); } subscribeToChild(child) { const subscription = child.onDidDestroy(() => this.removeChild(child)); this.subscriptionsByChild.set(child, subscription); this.subscriptions.add(subscription); } unsubscribeFromChild(child) { const subscription = this.subscriptionsByChild.get(child); this.subscriptions.remove(subscription); subscription.dispose(); } destroyed() { this.subscriptions.dispose(); this.emitter.emit('did-destroy'); this.emitter.dispose(); } } module.exports = PaneAxis; ================================================ FILE: src/pane-container-element.js ================================================ const { CompositeDisposable } = require('event-kit'); class PaneContainerElement extends HTMLElement { constructor() { super(); this.subscriptions = new CompositeDisposable(); } initialize(model, { views }) { this.model = model; this.views = views; if (this.views == null) { throw new Error( 'Must pass a views parameter when initializing PaneContainerElements' ); } this.subscriptions.add(this.model.observeRoot(this.rootChanged.bind(this))); return this; } connectedCallback() { this.classList.add('panes'); } rootChanged(root) { const focusedElement = this.hasFocus() ? document.activeElement : null; if (this.firstChild != null) { this.firstChild.remove(); } if (root != null) { const view = this.views.getView(root); this.appendChild(view); if (focusedElement != null) { focusedElement.focus(); } } } hasFocus() { return ( this === document.activeElement || this.contains(document.activeElement) ); } } window.customElements.define('atom-pane-container', PaneContainerElement); function createPaneContainerElement() { return document.createElement('atom-pane-container'); } module.exports = { createPaneContainerElement }; ================================================ FILE: src/pane-container.js ================================================ const { find } = require('underscore-plus'); const { Emitter, CompositeDisposable } = require('event-kit'); const Pane = require('./pane'); const ItemRegistry = require('./item-registry'); const { createPaneContainerElement } = require('./pane-container-element'); const SERIALIZATION_VERSION = 1; const STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY = 100; module.exports = class PaneContainer { constructor(params) { let applicationDelegate, deserializerManager, notificationManager; ({ config: this.config, applicationDelegate, notificationManager, deserializerManager, viewRegistry: this.viewRegistry, location: this.location } = params); this.emitter = new Emitter(); this.subscriptions = new CompositeDisposable(); this.itemRegistry = new ItemRegistry(); this.alive = true; this.stoppedChangingActivePaneItemTimeout = null; this.setRoot( new Pane({ container: this, config: this.config, applicationDelegate, notificationManager, deserializerManager, viewRegistry: this.viewRegistry }) ); this.didActivatePane(this.getRoot()); } getLocation() { return this.location; } getElement() { return this.element != null ? this.element : (this.element = createPaneContainerElement().initialize(this, { views: this.viewRegistry })); } destroy() { this.alive = false; for (let pane of this.getRoot().getPanes()) { pane.destroy(); } this.cancelStoppedChangingActivePaneItemTimeout(); this.subscriptions.dispose(); this.emitter.dispose(); } isAlive() { return this.alive; } isDestroyed() { return !this.isAlive(); } serialize(params) { return { deserializer: 'PaneContainer', version: SERIALIZATION_VERSION, root: this.root ? this.root.serialize() : null, activePaneId: this.activePane.id }; } deserialize(state, deserializerManager) { if (state.version !== SERIALIZATION_VERSION) return; this.itemRegistry = new ItemRegistry(); this.setRoot(deserializerManager.deserialize(state.root)); this.activePane = find(this.getRoot().getPanes(), pane => pane.id === state.activePaneId) || this.getPanes()[0]; if (this.config.get('core.destroyEmptyPanes')) this.destroyEmptyPanes(); } onDidChangeRoot(fn) { return this.emitter.on('did-change-root', fn); } observeRoot(fn) { fn(this.getRoot()); return this.onDidChangeRoot(fn); } onDidAddPane(fn) { return this.emitter.on('did-add-pane', fn); } observePanes(fn) { for (let pane of this.getPanes()) { fn(pane); } return this.onDidAddPane(({ pane }) => fn(pane)); } onDidDestroyPane(fn) { return this.emitter.on('did-destroy-pane', fn); } onWillDestroyPane(fn) { return this.emitter.on('will-destroy-pane', fn); } onDidChangeActivePane(fn) { return this.emitter.on('did-change-active-pane', fn); } onDidActivatePane(fn) { return this.emitter.on('did-activate-pane', fn); } observeActivePane(fn) { fn(this.getActivePane()); return this.onDidChangeActivePane(fn); } onDidAddPaneItem(fn) { return this.emitter.on('did-add-pane-item', fn); } observePaneItems(fn) { for (let item of this.getPaneItems()) { fn(item); } return this.onDidAddPaneItem(({ item }) => fn(item)); } onDidChangeActivePaneItem(fn) { return this.emitter.on('did-change-active-pane-item', fn); } onDidStopChangingActivePaneItem(fn) { return this.emitter.on('did-stop-changing-active-pane-item', fn); } observeActivePaneItem(fn) { fn(this.getActivePaneItem()); return this.onDidChangeActivePaneItem(fn); } onWillDestroyPaneItem(fn) { return this.emitter.on('will-destroy-pane-item', fn); } onDidDestroyPaneItem(fn) { return this.emitter.on('did-destroy-pane-item', fn); } getRoot() { return this.root; } setRoot(root) { this.root = root; this.root.setParent(this); this.root.setContainer(this); this.emitter.emit('did-change-root', this.root); if (this.getActivePane() == null && this.root instanceof Pane) { this.didActivatePane(this.root); } } replaceChild(oldChild, newChild) { if (oldChild !== this.root) { throw new Error('Replacing non-existent child'); } this.setRoot(newChild); } getPanes() { if (this.alive) { return this.getRoot().getPanes(); } else { return []; } } getPaneItems() { return this.getRoot().getItems(); } getActivePane() { return this.activePane; } getActivePaneItem() { return this.getActivePane().getActiveItem(); } paneForURI(uri) { return find(this.getPanes(), pane => pane.itemForURI(uri) != null); } paneForItem(item) { return find(this.getPanes(), pane => pane.getItems().includes(item)); } saveAll() { for (let pane of this.getPanes()) { pane.saveItems(); } } confirmClose(options) { const promises = []; for (const pane of this.getPanes()) { for (const item of pane.getItems()) { promises.push(pane.promptToSaveItem(item, options)); } } return Promise.all(promises).then(results => !results.includes(false)); } activateNextPane() { const panes = this.getPanes(); if (panes.length > 1) { const currentIndex = panes.indexOf(this.activePane); const nextIndex = (currentIndex + 1) % panes.length; panes[nextIndex].activate(); return true; } else { return false; } } activatePreviousPane() { const panes = this.getPanes(); if (panes.length > 1) { const currentIndex = panes.indexOf(this.activePane); let previousIndex = currentIndex - 1; if (previousIndex < 0) { previousIndex = panes.length - 1; } panes[previousIndex].activate(); return true; } else { return false; } } moveActiveItemToPane(destPane) { const item = this.activePane.getActiveItem(); if (!destPane.isItemAllowed(item)) { return; } this.activePane.moveItemToPane(item, destPane); destPane.setActiveItem(item); } copyActiveItemToPane(destPane) { const item = this.activePane.copyActiveItem(); if (item && destPane.isItemAllowed(item)) { destPane.activateItem(item); } } destroyEmptyPanes() { for (let pane of this.getPanes()) { if (pane.items.length === 0) { pane.destroy(); } } } didAddPane(event) { this.emitter.emit('did-add-pane', event); const items = event.pane.getItems(); for (let i = 0, length = items.length; i < length; i++) { const item = items[i]; this.didAddPaneItem(item, event.pane, i); } } willDestroyPane(event) { this.emitter.emit('will-destroy-pane', event); } didDestroyPane(event) { this.emitter.emit('did-destroy-pane', event); } didActivatePane(activePane) { if (activePane !== this.activePane) { if (!this.getPanes().includes(activePane)) { throw new Error( 'Setting active pane that is not present in pane container' ); } this.activePane = activePane; this.emitter.emit('did-change-active-pane', this.activePane); this.didChangeActiveItemOnPane( this.activePane, this.activePane.getActiveItem() ); } this.emitter.emit('did-activate-pane', this.activePane); return this.activePane; } didAddPaneItem(item, pane, index) { this.itemRegistry.addItem(item); this.emitter.emit('did-add-pane-item', { item, pane, index }); } willDestroyPaneItem(event) { return this.emitter.emitAsync('will-destroy-pane-item', event); } didDestroyPaneItem(event) { this.itemRegistry.removeItem(event.item); this.emitter.emit('did-destroy-pane-item', event); } didChangeActiveItemOnPane(pane, activeItem) { if (pane === this.getActivePane()) { this.emitter.emit('did-change-active-pane-item', activeItem); this.cancelStoppedChangingActivePaneItemTimeout(); // `setTimeout()` isn't available during the snapshotting phase, but that's okay. if (!global.isGeneratingSnapshot) { this.stoppedChangingActivePaneItemTimeout = setTimeout(() => { this.stoppedChangingActivePaneItemTimeout = null; this.emitter.emit('did-stop-changing-active-pane-item', activeItem); }, STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY); } } } cancelStoppedChangingActivePaneItemTimeout() { if (this.stoppedChangingActivePaneItemTimeout != null) { clearTimeout(this.stoppedChangingActivePaneItemTimeout); } } }; ================================================ FILE: src/pane-element.js ================================================ const path = require('path'); const { CompositeDisposable } = require('event-kit'); class PaneElement extends HTMLElement { constructor() { super(); this.attached = false; this.subscriptions = new CompositeDisposable(); this.inlineDisplayStyles = new WeakMap(); this.subscribeToDOMEvents(); this.itemViews = document.createElement('div'); } connectedCallback() { this.initializeContent(); this.attached = true; if (this.model.isFocused()) { this.focus(); } } detachedCallback() { this.attached = false; } initializeContent() { this.setAttribute('class', 'pane'); this.setAttribute('tabindex', -1); this.appendChild(this.itemViews); this.itemViews.setAttribute('class', 'item-views'); } subscribeToDOMEvents() { const handleFocus = event => { if ( !( this.isActivating || this.model.isDestroyed() || this.contains(event.relatedTarget) ) ) { this.model.focus(); } if (event.target !== this) return; const view = this.getActiveView(); if (view) { view.focus(); event.stopPropagation(); } }; const handleBlur = event => { if (!this.contains(event.relatedTarget)) { this.model.blur(); } }; const handleDragOver = event => { event.preventDefault(); event.stopPropagation(); }; const handleDrop = event => { event.preventDefault(); event.stopPropagation(); this.getModel().activate(); const pathsToOpen = [...event.dataTransfer.files].map(file => file.path); if (pathsToOpen.length > 0) { this.applicationDelegate.open({ pathsToOpen, here: true }); } }; this.addEventListener('focus', handleFocus, { capture: true }); this.addEventListener('blur', handleBlur, { capture: true }); this.addEventListener('dragover', handleDragOver); this.addEventListener('drop', handleDrop); } initialize(model, { views, applicationDelegate }) { this.model = model; this.views = views; this.applicationDelegate = applicationDelegate; if (this.views == null) { throw new Error( 'Must pass a views parameter when initializing PaneElements' ); } if (this.applicationDelegate == null) { throw new Error( 'Must pass an applicationDelegate parameter when initializing PaneElements' ); } this.subscriptions.add(this.model.onDidActivate(this.activated.bind(this))); this.subscriptions.add( this.model.observeActive(this.activeStatusChanged.bind(this)) ); this.subscriptions.add( this.model.observeActiveItem(this.activeItemChanged.bind(this)) ); this.subscriptions.add( this.model.onDidRemoveItem(this.itemRemoved.bind(this)) ); this.subscriptions.add( this.model.onDidDestroy(this.paneDestroyed.bind(this)) ); this.subscriptions.add( this.model.observeFlexScale(this.flexScaleChanged.bind(this)) ); return this; } getModel() { return this.model; } activated() { this.isActivating = true; if (!this.hasFocus()) { // Don't steal focus from children. this.focus(); } this.isActivating = false; } activeStatusChanged(active) { if (active) { this.classList.add('active'); } else { this.classList.remove('active'); } } activeItemChanged(item) { delete this.dataset.activeItemName; delete this.dataset.activeItemPath; if (this.changePathDisposable != null) { this.changePathDisposable.dispose(); } if (item == null) { return; } const hasFocus = this.hasFocus(); const itemView = this.views.getView(item); const itemPath = typeof item.getPath === 'function' ? item.getPath() : null; if (itemPath) { this.dataset.activeItemName = path.basename(itemPath); this.dataset.activeItemPath = itemPath; if (item.onDidChangePath != null) { this.changePathDisposable = item.onDidChangePath(() => { const itemPath = item.getPath(); this.dataset.activeItemName = path.basename(itemPath); this.dataset.activeItemPath = itemPath; }); } } if (!this.itemViews.contains(itemView)) { this.itemViews.appendChild(itemView); } for (const child of this.itemViews.children) { if (child === itemView) { if (this.attached) { this.showItemView(child); } } else { this.hideItemView(child); } } if (hasFocus) { itemView.focus(); } } showItemView(itemView) { const inlineDisplayStyle = this.inlineDisplayStyles.get(itemView); if (inlineDisplayStyle != null) { itemView.style.display = inlineDisplayStyle; } else { itemView.style.display = ''; } } hideItemView(itemView) { const inlineDisplayStyle = itemView.style.display; if (inlineDisplayStyle !== 'none') { if (inlineDisplayStyle != null) { this.inlineDisplayStyles.set(itemView, inlineDisplayStyle); } itemView.style.display = 'none'; } } itemRemoved({ item, index, destroyed }) { const viewToRemove = this.views.getView(item); if (viewToRemove) { viewToRemove.remove(); } } paneDestroyed() { this.subscriptions.dispose(); if (this.changePathDisposable != null) { this.changePathDisposable.dispose(); } } flexScaleChanged(flexScale) { this.style.flexGrow = flexScale; } getActiveView() { return this.views.getView(this.model.getActiveItem()); } hasFocus() { return ( this === document.activeElement || this.contains(document.activeElement) ); } } function createPaneElement() { return document.createElement('atom-pane'); } window.customElements.define('atom-pane', PaneElement); module.exports = { createPaneElement }; ================================================ FILE: src/pane-resize-handle-element.js ================================================ class PaneResizeHandleElement extends HTMLElement { constructor() { super(); this.resizePane = this.resizePane.bind(this); this.resizeStopped = this.resizeStopped.bind(this); this.subscribeToDOMEvents(); } subscribeToDOMEvents() { this.addEventListener('dblclick', this.resizeToFitContent.bind(this)); this.addEventListener('mousedown', this.resizeStarted.bind(this)); } connectedCallback() { // For some reason Chromium 58 is firing the attached callback after the // element has been detached, so we ignore the callback when a parent element // can't be found. if (this.parentElement) { this.isHorizontal = this.parentElement.classList.contains('horizontal'); this.classList.add(this.isHorizontal ? 'horizontal' : 'vertical'); } } disconnectedCallback() { this.resizeStopped(); } resizeToFitContent() { // clear flex-grow css style of both pane if (this.previousSibling != null) { this.previousSibling.model.setFlexScale(1); } return this.nextSibling != null ? this.nextSibling.model.setFlexScale(1) : undefined; } resizeStarted(e) { e.stopPropagation(); if (!this.overlay) { this.overlay = document.createElement('div'); this.overlay.classList.add('atom-pane-cursor-overlay'); this.overlay.classList.add(this.isHorizontal ? 'horizontal' : 'vertical'); this.appendChild(this.overlay); } document.addEventListener('mousemove', this.resizePane); document.addEventListener('mouseup', this.resizeStopped); } resizeStopped() { document.removeEventListener('mousemove', this.resizePane); document.removeEventListener('mouseup', this.resizeStopped); if (this.overlay) { this.removeChild(this.overlay); this.overlay = undefined; } } calcRatio(ratio1, ratio2, total) { const allRatio = ratio1 + ratio2; return [(total * ratio1) / allRatio, (total * ratio2) / allRatio]; } setFlexGrow(prevSize, nextSize) { this.prevModel = this.previousSibling.model; this.nextModel = this.nextSibling.model; const totalScale = this.prevModel.getFlexScale() + this.nextModel.getFlexScale(); const flexGrows = this.calcRatio(prevSize, nextSize, totalScale); this.prevModel.setFlexScale(flexGrows[0]); this.nextModel.setFlexScale(flexGrows[1]); } fixInRange(val, minValue, maxValue) { return Math.min(Math.max(val, minValue), maxValue); } resizePane({ clientX, clientY, which }) { if (which !== 1) { return this.resizeStopped(); } if (this.previousSibling == null || this.nextSibling == null) { return this.resizeStopped(); } if (this.isHorizontal) { const totalWidth = this.previousSibling.clientWidth + this.nextSibling.clientWidth; // get the left and right width after move the resize view let leftWidth = clientX - this.previousSibling.getBoundingClientRect().left; leftWidth = this.fixInRange(leftWidth, 0, totalWidth); const rightWidth = totalWidth - leftWidth; // set the flex grow by the ratio of left width and right width // to change pane width this.setFlexGrow(leftWidth, rightWidth); } else { const totalHeight = this.previousSibling.clientHeight + this.nextSibling.clientHeight; let topHeight = clientY - this.previousSibling.getBoundingClientRect().top; topHeight = this.fixInRange(topHeight, 0, totalHeight); const bottomHeight = totalHeight - topHeight; this.setFlexGrow(topHeight, bottomHeight); } } } window.customElements.define( 'atom-pane-resize-handle', PaneResizeHandleElement ); function createPaneResizeHandleElement() { return document.createElement('atom-pane-resize-handle'); } module.exports = { createPaneResizeHandleElement }; ================================================ FILE: src/pane.js ================================================ const Grim = require('grim'); const { CompositeDisposable, Emitter } = require('event-kit'); const PaneAxis = require('./pane-axis'); const TextEditor = require('./text-editor'); const { createPaneElement } = 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( { deserializerManager: deserializers, notificationManager: notifications, viewRegistry: views, config, applicationDelegate }, state ) ); } 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 = createPaneElement().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() { if (this.getContainer().getPanes().length > 1) { this.setFlexScale(this.getFlexScale() * 1.1); } } decreaseSize() { if (this.getContainer().getPanes().length > 1) { 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() { 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; const itemSubscriptions = new CompositeDisposable(); this.subscriptionsPerItem.set(item, itemSubscriptions); if (typeof item.onDidDestroy === 'function') { itemSubscriptions.add( item.onDidDestroy(() => this.removeItem(item, false)) ); } if (typeof item.onDidTerminatePendingState === 'function') { itemSubscriptions.add( item.onDidTerminatePendingState(() => { if (this.getPendingItem() === item) this.clearPendingItem(); }) ); } 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`. // // This action can be prevented by onWillDestroyPaneItem callbacks in which // case nothing happens. // // * `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 ) { let preventClosing = false; await this.container.willDestroyPaneItem({ item, index, pane: this, prevent: () => { preventClosing = true; } }); if (preventClosing) return false; } 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 = {}) { return new Promise((resolve, reject) => { if ( typeof item.shouldPromptToSave !== 'function' || !item.shouldPromptToSave(options) ) { return resolve(true); } let uri; if (typeof item.getURI === 'function') { uri = item.getURI(); } else if (typeof item.getUri === 'function') { uri = item.getUri(); } else { return resolve(true); } const title = (typeof item.getTitle === 'function' && item.getTitle()) || uri; const saveDialog = (saveButtonText, saveFn, message) => { this.applicationDelegate.confirm( { message, detail: 'Your changes will be lost if you close this item without saving.', buttons: [saveButtonText, 'Cancel', "&Don't Save"] }, response => { switch (response) { case 0: 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 )}` ); } else { resolve(true); } }); case 1: return resolve(false); case 2: return resolve(true); } } ); }; 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 async 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; let resolveSaveDialogPromise = null; const saveDialogPromise = new Promise(resolve => { resolveSaveDialogPromise = resolve; }); this.applicationDelegate.showSaveDialog(saveOptions, newItemPath => { if (newItemPath) { promisify(() => item.saveAs(newItemPath)) .then(() => { if (nextAction) { resolveSaveDialogPromise(nextAction()); } else { resolveSaveDialogPromise(); } }) .catch(error => { if (nextAction) { resolveSaveDialogPromise(nextAction(error)); } else { this.handleSaveError(error, item); resolveSaveDialogPromise(); } }); } else if (nextAction) { resolveSaveDialogPromise( nextAction(new SaveCancelledError('Save Cancelled')) ); } else { resolveSaveDialogPromise(); } }); return saveDialogPromise; } // 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'); this.focused = true; 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); } } ================================================ FILE: src/panel-container-element.js ================================================ 'use strict'; const { createFocusTrap } = require('focus-trap'); const { CompositeDisposable } = require('event-kit'); class PanelContainerElement extends HTMLElement { constructor() { super(); this.subscriptions = new CompositeDisposable(); } connectedCallback() { if (this.model.dock) { this.model.dock.elementAttached(); } } initialize(model, viewRegistry) { this.model = model; this.viewRegistry = viewRegistry; this.subscriptions.add( this.model.onDidAddPanel(this.panelAdded.bind(this)) ); this.subscriptions.add(this.model.onDidDestroy(this.destroyed.bind(this))); this.classList.add(this.model.getLocation()); // Add the dock. if (this.model.dock != null) { this.appendChild(this.model.dock.getElement()); } return this; } getModel() { return this.model; } panelAdded({ panel, index }) { const panelElement = panel.getElement(); panelElement.classList.add(this.model.getLocation()); if (this.model.isModal()) { panelElement.classList.add('overlay', 'from-top'); } else { panelElement.classList.add( 'tool-panel', `panel-${this.model.getLocation()}` ); } if (index >= this.childNodes.length) { this.appendChild(panelElement); } else { const referenceItem = this.childNodes[index]; this.insertBefore(panelElement, referenceItem); } if (this.model.isModal()) { this.hideAllPanelsExcept(panel); this.subscriptions.add( panel.onDidChangeVisible(visible => { if (visible) { this.hideAllPanelsExcept(panel); } }) ); if (panel.autoFocus) { const focusOptions = { // focus-trap will attempt to give focus to the first tabbable element // on activation. If there aren't any tabbable elements, // give focus to the panel element itself fallbackFocus: panelElement, // closing is handled by core Atom commands and this already deactivates // on visibility changes escapeDeactivates: false, delayInitialFocus: false }; if (panel.autoFocus !== true) { focusOptions.initialFocus = panel.autoFocus; } const modalFocusTrap = createFocusTrap(panelElement, focusOptions); this.subscriptions.add( panel.onDidChangeVisible(visible => { if (visible) { modalFocusTrap.activate(); } else { modalFocusTrap.deactivate(); } }) ); } } } destroyed() { this.subscriptions.dispose(); if (this.parentNode != null) { this.parentNode.removeChild(this); } } hideAllPanelsExcept(excludedPanel) { for (let panel of this.model.getPanels()) { if (panel !== excludedPanel) { panel.hide(); } } } } window.customElements.define('atom-panel-container', PanelContainerElement); function createPanelContainerElement() { return document.createElement('atom-panel-container'); } module.exports = { createPanelContainerElement }; ================================================ FILE: src/panel-container.js ================================================ 'use strict'; const { Emitter, CompositeDisposable } = require('event-kit'); const { createPanelContainerElement } = require('./panel-container-element'); module.exports = class PanelContainer { constructor({ location, dock, viewRegistry } = {}) { this.location = location; this.emitter = new Emitter(); this.subscriptions = new CompositeDisposable(); this.panels = []; this.dock = dock; this.viewRegistry = viewRegistry; } destroy() { for (let panel of this.getPanels()) { panel.destroy(); } this.subscriptions.dispose(); this.emitter.emit('did-destroy', this); this.emitter.dispose(); } getElement() { if (!this.element) { this.element = createPanelContainerElement().initialize( this, this.viewRegistry ); } return this.element; } /* Section: Event Subscription */ onDidAddPanel(callback) { return this.emitter.on('did-add-panel', callback); } onDidRemovePanel(callback) { return this.emitter.on('did-remove-panel', callback); } onDidDestroy(callback) { return this.emitter.once('did-destroy', callback); } /* Section: Panels */ getLocation() { return this.location; } isModal() { return this.location === 'modal'; } getPanels() { return this.panels.slice(); } addPanel(panel) { this.subscriptions.add(panel.onDidDestroy(this.panelDestroyed.bind(this))); const index = this.getPanelIndex(panel); if (index === this.panels.length) { this.panels.push(panel); } else { this.panels.splice(index, 0, panel); } this.emitter.emit('did-add-panel', { panel, index }); return panel; } panelForItem(item) { for (let panel of this.panels) { if (panel.getItem() === item) { return panel; } } return null; } panelDestroyed(panel) { const index = this.panels.indexOf(panel); if (index > -1) { this.panels.splice(index, 1); this.emitter.emit('did-remove-panel', { panel, index }); } } getPanelIndex(panel) { const priority = panel.getPriority(); if (['bottom', 'right'].includes(this.location)) { for (let i = this.panels.length - 1; i >= 0; i--) { const p = this.panels[i]; if (priority < p.getPriority()) { return i + 1; } } return 0; } else { for (let i = 0; i < this.panels.length; i++) { const p = this.panels[i]; if (priority < p.getPriority()) { return i; } } return this.panels.length; } } }; ================================================ FILE: src/panel.js ================================================ const { Emitter } = require('event-kit'); // Extended: A container representing a panel on the edges of the editor window. // You should not create a `Panel` directly, instead use {Workspace::addTopPanel} // and friends to add panels. // // Examples: [status-bar](https://github.com/atom/status-bar) // and [find-and-replace](https://github.com/atom/find-and-replace) both use // panels. module.exports = class Panel { /* Section: Construction and Destruction */ constructor({ item, autoFocus, visible, priority, className }, viewRegistry) { this.destroyed = false; this.item = item; this.autoFocus = autoFocus == null ? false : autoFocus; this.visible = visible == null ? true : visible; this.priority = priority == null ? 100 : priority; this.className = className; this.viewRegistry = viewRegistry; this.emitter = new Emitter(); } // Public: Destroy and remove this panel from the UI. destroy() { if (this.destroyed) return; this.destroyed = true; this.hide(); if (this.element) this.element.remove(); this.emitter.emit('did-destroy', this); return this.emitter.dispose(); } getElement() { if (!this.element) { this.element = document.createElement('atom-panel'); if (!this.visible) this.element.style.display = 'none'; if (this.className) this.element.classList.add(...this.className.split(' ')); this.element.appendChild(this.viewRegistry.getView(this.item)); } return this.element; } /* Section: Event Subscription */ // Public: Invoke the given callback when the pane hidden or shown. // // * `callback` {Function} to be called when the pane is destroyed. // * `visible` {Boolean} true when the panel has been shown // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeVisible(callback) { return this.emitter.on('did-change-visible', callback); } // Public: Invoke the given callback when the pane is destroyed. // // * `callback` {Function} to be called when the pane is destroyed. // * `panel` {Panel} this panel // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy(callback) { return this.emitter.once('did-destroy', callback); } /* Section: Panel Details */ // Public: Returns the panel's item. getItem() { return this.item; } // Public: Returns a {Number} indicating this panel's priority. getPriority() { return this.priority; } getClassName() { return this.className; } // Public: Returns a {Boolean} true when the panel is visible. isVisible() { return this.visible; } // Public: Hide this panel hide() { let wasVisible = this.visible; this.visible = false; if (this.element) this.element.style.display = 'none'; if (wasVisible) this.emitter.emit('did-change-visible', this.visible); } // Public: Show this panel show() { let wasVisible = this.visible; this.visible = true; if (this.element) this.element.style.display = null; if (!wasVisible) this.emitter.emit('did-change-visible', this.visible); } }; ================================================ FILE: src/path-watcher.js ================================================ const fs = require('fs'); const path = require('path'); const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); const nsfw = require('@atom/nsfw'); const watcher = require('@atom/watcher'); const { NativeWatcherRegistry } = require('./native-watcher-registry'); // Private: Associate native watcher action flags with descriptive String equivalents. const ACTION_MAP = new Map([ [nsfw.actions.MODIFIED, 'modified'], [nsfw.actions.CREATED, 'created'], [nsfw.actions.DELETED, 'deleted'], [nsfw.actions.RENAMED, 'renamed'] ]); // Private: Possible states of a {NativeWatcher}. const WATCHER_STATE = { STOPPED: Symbol('stopped'), STARTING: Symbol('starting'), RUNNING: Symbol('running'), STOPPING: Symbol('stopping') }; // Private: Interface with and normalize events from a filesystem watcher implementation. class NativeWatcher { // Private: Initialize a native watcher on a path. // // Events will not be produced until {start()} is called. constructor(normalizedPath) { this.normalizedPath = normalizedPath; this.emitter = new Emitter(); this.subs = new CompositeDisposable(); this.state = WATCHER_STATE.STOPPED; this.onEvents = this.onEvents.bind(this); this.onError = this.onError.bind(this); } // Private: Begin watching for filesystem events. // // Has no effect if the watcher has already been started. async start() { if (this.state !== WATCHER_STATE.STOPPED) { return; } this.state = WATCHER_STATE.STARTING; await this.doStart(); this.state = WATCHER_STATE.RUNNING; this.emitter.emit('did-start'); } doStart() { return Promise.reject(new Error('doStart() not overridden')); } // Private: Return true if the underlying watcher is actively listening for filesystem events. isRunning() { return this.state === WATCHER_STATE.RUNNING; } // Private: Register a callback to be invoked when the filesystem watcher has been initialized. // // Returns: A {Disposable} to revoke the subscription. onDidStart(callback) { return this.emitter.on('did-start', callback); } // Private: Register a callback to be invoked with normalized filesystem events as they arrive. Starts the watcher // automatically if it is not already running. The watcher will be stopped automatically when all subscribers // dispose their subscriptions. // // Returns: A {Disposable} to revoke the subscription. onDidChange(callback) { this.start(); const sub = this.emitter.on('did-change', callback); return new Disposable(() => { sub.dispose(); if (this.emitter.listenerCountForEventName('did-change') === 0) { this.stop(); } }); } // Private: Register a callback to be invoked when a {Watcher} should attach to a different {NativeWatcher}. // // Returns: A {Disposable} to revoke the subscription. onShouldDetach(callback) { return this.emitter.on('should-detach', callback); } // Private: Register a callback to be invoked when a {NativeWatcher} is about to be stopped. // // Returns: A {Disposable} to revoke the subscription. onWillStop(callback) { return this.emitter.on('will-stop', callback); } // Private: Register a callback to be invoked when the filesystem watcher has been stopped. // // Returns: A {Disposable} to revoke the subscription. onDidStop(callback) { return this.emitter.on('did-stop', callback); } // Private: Register a callback to be invoked with any errors reported from the watcher. // // Returns: A {Disposable} to revoke the subscription. onDidError(callback) { return this.emitter.on('did-error', callback); } // Private: Broadcast an `onShouldDetach` event to prompt any {Watcher} instances bound here to attach to a new // {NativeWatcher} instead. // // * `replacement` the new {NativeWatcher} instance that a live {Watcher} instance should reattach to instead. // * `watchedPath` absolute path watched by the new {NativeWatcher}. reattachTo(replacement, watchedPath, options) { this.emitter.emit('should-detach', { replacement, watchedPath, options }); } // Private: Stop the native watcher and release any operating system resources associated with it. // // Has no effect if the watcher is not running. async stop() { if (this.state !== WATCHER_STATE.RUNNING) { return; } this.state = WATCHER_STATE.STOPPING; this.emitter.emit('will-stop'); await this.doStop(); this.state = WATCHER_STATE.STOPPED; this.emitter.emit('did-stop'); } doStop() { return Promise.resolve(); } // Private: Detach any event subscribers. dispose() { this.emitter.dispose(); } // Private: Callback function invoked by the native watcher when a debounced group of filesystem events arrive. // Normalize and re-broadcast them to any subscribers. // // * `events` An Array of filesystem events. onEvents(events) { this.emitter.emit('did-change', events); } // Private: Callback function invoked by the native watcher when an error occurs. // // * `err` The native filesystem error. onError(err) { this.emitter.emit('did-error', err); } } // Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss // any changes made to files outside of Atom, but it also has no overhead. class AtomNativeWatcher extends NativeWatcher { async doStart() { const getRealPath = givenPath => { if (!givenPath) { return Promise.resolve(null); } return new Promise(resolve => { fs.realpath(givenPath, (err, resolvedPath) => { err ? resolve(null) : resolve(resolvedPath); }); }); }; this.subs.add( atom.workspace.observeTextEditors(async editor => { let realPath = await getRealPath(editor.getPath()); if (!realPath || !realPath.startsWith(this.normalizedPath)) { return; } const announce = (action, oldPath) => { const payload = { action, path: realPath }; if (oldPath) payload.oldPath = oldPath; this.onEvents([payload]); }; const buffer = editor.getBuffer(); this.subs.add(buffer.onDidConflict(() => announce('modified'))); this.subs.add(buffer.onDidReload(() => announce('modified'))); this.subs.add( buffer.onDidSave(event => { if (event.path === realPath) { announce('modified'); } else { const oldPath = realPath; realPath = event.path; announce('renamed', oldPath); } }) ); this.subs.add(buffer.onDidDelete(() => announce('deleted'))); this.subs.add( buffer.onDidChangePath(newPath => { if (newPath !== this.normalizedPath) { const oldPath = this.normalizedPath; this.normalizedPath = newPath; announce('renamed', oldPath); } }) ); }) ); // Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView. const treeViewPackage = await atom.packages.getLoadedPackage('tree-view'); if (!treeViewPackage) return; await treeViewPackage.activationPromise; const treeViewModule = treeViewPackage.mainModule; if (!treeViewModule) return; const treeView = treeViewModule.getTreeViewInstance(); const isOpenInEditor = async eventPath => { const openPaths = await Promise.all( atom.workspace .getTextEditors() .map(editor => getRealPath(editor.getPath())) ); return openPaths.includes(eventPath); }; this.subs.add( treeView.onFileCreated(async event => { const realPath = await getRealPath(event.path); if (!realPath) return; this.onEvents([{ action: 'added', path: realPath }]); }) ); this.subs.add( treeView.onEntryDeleted(async event => { const realPath = await getRealPath(event.path); if (!realPath || (await isOpenInEditor(realPath))) return; this.onEvents([{ action: 'deleted', path: realPath }]); }) ); this.subs.add( treeView.onEntryMoved(async event => { const [realNewPath, realOldPath] = await Promise.all([ getRealPath(event.newPath), getRealPath(event.initialPath) ]); if ( !realNewPath || !realOldPath || (await isOpenInEditor(realNewPath)) || (await isOpenInEditor(realOldPath)) ) return; this.onEvents([ { action: 'renamed', path: realNewPath, oldPath: realOldPath } ]); }) ); } } // Private: Implement a native watcher by translating events from an NSFW watcher. class NSFWNativeWatcher extends NativeWatcher { async doStart(rootPath, eventCallback, errorCallback) { const handler = events => { this.onEvents( events.map(event => { const action = ACTION_MAP.get(event.action) || `unexpected (${event.action})`; const payload = { action }; if (event.file) { payload.path = path.join(event.directory, event.file); } else { payload.oldPath = path.join( event.directory, typeof event.oldFile === 'undefined' ? '' : event.oldFile ); payload.path = path.join( event.directory, typeof event.newFile === 'undefined' ? '' : event.newFile ); } return payload; }) ); }; this.watcher = await nsfw(this.normalizedPath, handler, { debounceMS: 100, errorCallback: this.onError }); await this.watcher.start(); } doStop() { return this.watcher.stop(); } } // Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by // calling `watchPath`. To watch for events within active project directories, use {Project::onDidChangeFiles} // instead. // // Multiple PathWatchers may be backed by a single native watcher to conserve operation system resources. // // Call {::dispose} to stop receiving events and, if possible, release underlying resources. A PathWatcher may be // added to a {CompositeDisposable} to manage its lifetime along with other {Disposable} resources like event // subscriptions. // // ```js // const {watchPath} = require('atom') // // const disposable = await watchPath('/var/log', {}, events => { // console.log(`Received batch of ${events.length} events.`) // for (const event of events) { // // "created", "modified", "deleted", "renamed" // console.log(`Event action: ${event.action}`) // // // absolute path to the filesystem entry that was touched // console.log(`Event path: ${event.path}`) // // if (event.action === 'renamed') { // console.log(`.. renamed from: ${event.oldPath}`) // } // } // }) // // // Immediately stop receiving filesystem events. If this is the last // // watcher, asynchronously release any OS resources required to // // subscribe to these events. // disposable.dispose() // ``` // // `watchPath` accepts the following arguments: // // `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. // // `options` Control the watcher's behavior. Currently a placeholder. // // `eventCallback` {Function} to be called each time a batch of filesystem events is observed. Each event object has // the keys: `action`, a {String} describing the filesystem action that occurred, one of `"created"`, `"modified"`, // `"deleted"`, or `"renamed"`; `path`, a {String} containing the absolute path to the filesystem entry that was acted // upon; for rename events only, `oldPath`, a {String} containing the filesystem entry's former absolute path. class PathWatcher { // Private: Instantiate a new PathWatcher. Call {watchPath} instead. // // * `nativeWatcherRegistry` {NativeWatcherRegistry} used to find and consolidate redundant watchers. // * `watchedPath` {String} containing the absolute path to the root of the watched filesystem tree. // * `options` See {watchPath} for options. // constructor(nativeWatcherRegistry, watchedPath, options) { this.watchedPath = watchedPath; this.nativeWatcherRegistry = nativeWatcherRegistry; this.normalizedPath = null; this.native = null; this.changeCallbacks = new Map(); this.attachedPromise = new Promise(resolve => { this.resolveAttachedPromise = resolve; }); this.startPromise = new Promise((resolve, reject) => { this.resolveStartPromise = resolve; this.rejectStartPromise = reject; }); this.normalizedPathPromise = new Promise((resolve, reject) => { fs.realpath(watchedPath, (err, real) => { if (err) { reject(err); return; } this.normalizedPath = real; resolve(real); }); }); this.normalizedPathPromise.catch(err => this.rejectStartPromise(err)); this.emitter = new Emitter(); this.subs = new CompositeDisposable(); } // Private: Return a {Promise} that will resolve with the normalized root path. getNormalizedPathPromise() { return this.normalizedPathPromise; } // Private: Return a {Promise} that will resolve the first time that this watcher is attached to a native watcher. getAttachedPromise() { return this.attachedPromise; } // Extended: Return a {Promise} that will resolve when the underlying native watcher is ready to begin sending events. // When testing filesystem watchers, it's important to await this promise before making filesystem changes that you // intend to assert about because there will be a delay between the instantiation of the watcher and the activation // of the underlying OS resources that feed its events. // // PathWatchers acquired through `watchPath` are already started. // // ```js // const {watchPath} = require('atom') // const ROOT = path.join(__dirname, 'fixtures') // const FILE = path.join(ROOT, 'filename.txt') // // describe('something', function () { // it("doesn't miss events", async function () { // const watcher = watchPath(ROOT, {}, events => {}) // await watcher.getStartPromise() // fs.writeFile(FILE, 'contents\n', err => { // // The watcher is listening and the event should be // // received asynchronously // } // }) // }) // ``` getStartPromise() { return this.startPromise; } // Private: Attach another {Function} to be called with each batch of filesystem events. See {watchPath} for the // spec of the callback's argument. // // * `callback` {Function} to be called with each batch of filesystem events. // // Returns a {Disposable} that will stop the underlying watcher when all callbacks mapped to it have been disposed. onDidChange(callback) { if (this.native) { const sub = this.native.onDidChange(events => this.onNativeEvents(events, callback) ); this.changeCallbacks.set(callback, sub); this.native.start(); } else { // Attach to a new native listener and retry this.nativeWatcherRegistry.attach(this).then(() => { this.onDidChange(callback); }); } return new Disposable(() => { const sub = this.changeCallbacks.get(callback); this.changeCallbacks.delete(callback); sub.dispose(); }); } // Extended: Invoke a {Function} when any errors related to this watcher are reported. // // * `callback` {Function} to be called when an error occurs. // * `err` An {Error} describing the failure condition. // // Returns a {Disposable}. onDidError(callback) { return this.emitter.on('did-error', callback); } // Private: Wire this watcher to an operating system-level native watcher implementation. attachToNative(native) { this.subs.dispose(); this.native = native; if (native.isRunning()) { this.resolveStartPromise(); } else { this.subs.add( native.onDidStart(() => { this.resolveStartPromise(); }) ); } // Transfer any native event subscriptions to the new NativeWatcher. for (const [callback, formerSub] of this.changeCallbacks) { const newSub = native.onDidChange(events => this.onNativeEvents(events, callback) ); this.changeCallbacks.set(callback, newSub); formerSub.dispose(); } this.subs.add( native.onDidError(err => { this.emitter.emit('did-error', err); }) ); this.subs.add( native.onShouldDetach(({ replacement, watchedPath }) => { if ( this.native === native && replacement !== native && this.normalizedPath.startsWith(watchedPath) ) { this.attachToNative(replacement); } }) ); this.subs.add( native.onWillStop(() => { if (this.native === native) { this.subs.dispose(); this.native = null; } }) ); this.resolveAttachedPromise(); } // Private: Invoked when the attached native watcher creates a batch of native filesystem events. The native watcher's // events may include events for paths above this watcher's root path, so filter them to only include the relevant // ones, then re-broadcast them to our subscribers. onNativeEvents(events, callback) { const isWatchedPath = eventPath => eventPath.startsWith(this.normalizedPath); const filtered = []; for (let i = 0; i < events.length; i++) { const event = events[i]; if (event.action === 'renamed') { const srcWatched = isWatchedPath(event.oldPath); const destWatched = isWatchedPath(event.path); if (srcWatched && destWatched) { filtered.push(event); } else if (srcWatched && !destWatched) { filtered.push({ action: 'deleted', kind: event.kind, path: event.oldPath }); } else if (!srcWatched && destWatched) { filtered.push({ action: 'created', kind: event.kind, path: event.path }); } } else { if (isWatchedPath(event.path)) { filtered.push(event); } } } if (filtered.length > 0) { callback(filtered); } } // Extended: Unsubscribe all subscribers from filesystem events. Native resources will be released asynchronously, // but this watcher will stop broadcasting events immediately. dispose() { for (const sub of this.changeCallbacks.values()) { sub.dispose(); } this.emitter.dispose(); this.subs.dispose(); } } // Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher} backed by emulated Atom // events or NSFW. class PathWatcherManager { // Private: Access the currently active manager instance, creating one if necessary. static active() { if (!this.activeManager) { this.activeManager = new PathWatcherManager( atom.config.get('core.fileSystemWatcher') ); this.sub = atom.config.onDidChange( 'core.fileSystemWatcher', ({ newValue }) => { this.transitionTo(newValue); } ); } return this.activeManager; } // Private: Replace the active {PathWatcherManager} with a new one that creates [NativeWatchers]{NativeWatcher} // based on the value of `setting`. static async transitionTo(setting) { const current = this.active(); if (this.transitionPromise) { await this.transitionPromise; } if (current.setting === setting) { return; } current.isShuttingDown = true; let resolveTransitionPromise = () => {}; this.transitionPromise = new Promise(resolve => { resolveTransitionPromise = resolve; }); const replacement = new PathWatcherManager(setting); this.activeManager = replacement; await Promise.all( Array.from(current.live, async ([root, native]) => { const w = await replacement.createWatcher(root, {}, () => {}); native.reattachTo(w.native, root, w.native.options || {}); }) ); current.stopAllWatchers(); resolveTransitionPromise(); this.transitionPromise = null; } // Private: Initialize global {PathWatcher} state. constructor(setting) { this.setting = setting; this.live = new Map(); const initLocal = NativeConstructor => { this.nativeRegistry = new NativeWatcherRegistry(normalizedPath => { const nativeWatcher = new NativeConstructor(normalizedPath); this.live.set(normalizedPath, nativeWatcher); const sub = nativeWatcher.onWillStop(() => { this.live.delete(normalizedPath); sub.dispose(); }); return nativeWatcher; }); }; if (setting === 'atom') { initLocal(AtomNativeWatcher); } else if (setting === 'experimental') { // } else if (setting === 'poll') { // } else { initLocal(NSFWNativeWatcher); } this.isShuttingDown = false; } useExperimentalWatcher() { return this.setting === 'experimental' || this.setting === 'poll'; } // Private: Create a {PathWatcher} tied to this global state. See {watchPath} for detailed arguments. async createWatcher(rootPath, options, eventCallback) { if (this.isShuttingDown) { await this.constructor.transitionPromise; return PathWatcherManager.active().createWatcher( rootPath, options, eventCallback ); } if (this.useExperimentalWatcher()) { if (this.setting === 'poll') { options.poll = true; } const w = await watcher.watchPath(rootPath, options, eventCallback); this.live.set(rootPath, w.native); return w; } const w = new PathWatcher(this.nativeRegistry, rootPath, options); w.onDidChange(eventCallback); await w.getStartPromise(); return w; } // Private: Directly access the {NativeWatcherRegistry}. getRegistry() { if (this.useExperimentalWatcher()) { return watcher.getRegistry(); } return this.nativeRegistry; } // Private: Sample watcher usage statistics. Only available for experimental watchers. status() { if (this.useExperimentalWatcher()) { return watcher.status(); } return {}; } // Private: Return a {String} depicting the currently active native watchers. print() { if (this.useExperimentalWatcher()) { return watcher.printWatchers(); } return this.nativeRegistry.print(); } // Private: Stop all living watchers. // // Returns a {Promise} that resolves when all native watcher resources are disposed. stopAllWatchers() { if (this.useExperimentalWatcher()) { return watcher.stopAllWatchers(); } return Promise.all(Array.from(this.live, ([, w]) => w.stop())); } } // Extended: Invoke a callback with each filesystem event that occurs beneath a specified path. If you only need to // watch events within the project's root paths, use {Project::onDidChangeFiles} instead. // // watchPath handles the efficient re-use of operating system resources across living watchers. Watching the same path // more than once, or the child of a watched path, will re-use the existing native watcher. // // * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch. // * `options` Control the watcher's behavior. // * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed. // * `events` {Array} of objects that describe the events that have occurred. // * `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 {Promise} that will resolve to a {PathWatcher} once it has started. Note that every {PathWatcher} // is a {Disposable}, so they can be managed by a {CompositeDisposable} if desired. // // ```js // const {watchPath} = require('atom') // // const disposable = await watchPath('/var/log', {}, events => { // console.log(`Received batch of ${events.length} events.`) // for (const event of events) { // // "created", "modified", "deleted", "renamed" // console.log(`Event action: ${event.action}`) // // absolute path to the filesystem entry that was touched // console.log(`Event path: ${event.path}`) // if (event.action === 'renamed') { // console.log(`.. renamed from: ${event.oldPath}`) // } // } // }) // // // Immediately stop receiving filesystem events. If this is the last watcher, asynchronously release any OS // // resources required to subscribe to these events. // disposable.dispose() // ``` // function watchPath(rootPath, options, eventCallback) { return PathWatcherManager.active().createWatcher( rootPath, options, eventCallback ); } // Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager // have stopped listening. This is useful for `afterEach()` blocks in unit tests. function stopAllWatchers() { return PathWatcherManager.active().stopAllWatchers(); } // Private: Show the currently active native watchers in a formatted {String}. watchPath.printWatchers = function() { return PathWatcherManager.active().print(); }; // Private: Access the active {NativeWatcherRegistry}. watchPath.getRegistry = function() { return PathWatcherManager.active().getRegistry(); }; // Private: Sample usage statistics for the active watcher. watchPath.status = function() { return PathWatcherManager.active().status(); }; // Private: Configure @atom/watcher ("experimental") directly. watchPath.configure = function(...args) { return watcher.configure(...args); }; module.exports = { watchPath, stopAllWatchers }; ================================================ FILE: src/project.js ================================================ const path = require('path'); const _ = require('underscore-plus'); const fs = require('fs-plus'); const { Emitter, Disposable, CompositeDisposable } = 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, grammarRegistry }) { super(); this.notificationManager = notificationManager; this.applicationDelegate = applicationDelegate; this.grammarRegistry = grammarRegistry; 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.subscriptions = new CompositeDisposable(); 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(); this.subscriptions.dispose(); this.subscriptions = new CompositeDisposable(); 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(); } } // Layers the contents of a project's file's config // on top of the current global config. replace(projectSpecification) { if (projectSpecification == null) { atom.config.clearProjectSettings(); this.setPaths([]); } else { if (projectSpecification.originPath == null) { return; } // If no path is specified, set to directory of originPath. if (!Array.isArray(projectSpecification.paths)) { projectSpecification.paths = [ path.dirname(projectSpecification.originPath) ]; } atom.config.resetProjectSettings( projectSpecification.config, projectSpecification.originPath ); this.setPaths(projectSpecification.paths); } this.emitter.emit('did-replace', projectSpecification); } onDidReplace(callback) { return this.emitter.on('did-replace', callback); } /* 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.grammarRegistry.maintainLanguageMode(buffer); 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.action}`) // // // absolute path to the filesystem entry that was touched // console.log(`Event path: ${event.path}`) // // if (event.action === '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); } // Public: Invoke the given callback with all current and future // repositories in the project. // // * `callback` {Function} to be called with current and future // repositories. // * `repository` A {GitRepository} that is present at the time of // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to // unsubscribe. observeRepositories(callback) { for (const repo of this.repositories) { if (repo != null) { callback(repo); } } return this.onDidAddRepository(callback); } // Public: Invoke the given callback when a repository is added to the // project. // // * `callback` {Function} to be called when a repository is added. // * `repository` A {GitRepository}. // // Returns a {Disposable} on which `.dispose()` can be called to // unsubscribe. onDidAddRepository(callback) { return this.emitter.on('did-add-repository', 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 {GitRepository} 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 {GitRepository}. // // Returns a {Promise} that resolves with either: // * {GitRepository} 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() { try { return this.rootDirectories.map(rootDirectory => rootDirectory.getPath()); } catch (e) { atom.notifications.addError( "Please clear Atom's window state with: atom --clear-window-state" ); } } // 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); const didChangeCallback = events => { // Stop event delivery immediately on removal of a rootDirectory, even if its watcher // promise has yet to resolve at the time of removal if (this.rootDirectories.includes(directory)) { this.emitter.emit('did-change-files', events); } }; // We'll use the directory's custom onDidChangeFiles callback, if available. // CustomDirectory::onDidChangeFiles should match the signature of // Project::onDidChangeFiles below (although it may resolve asynchronously) this.watcherPromisesByPath[directory.getPath()] = directory.onDidChangeFiles != null ? Promise.resolve(directory.onDidChangeFiles(didChangeCallback)) : watchPath(directory.getPath(), {}, didChangeCallback); for (let watchedPath in this.watcherPromisesByPath) { if (!this.rootDirectories.find(dir => dir.getPath() === watchedPath)) { 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 (repo != null) { this.emitter.emit('did-add-repository', repo); } if (options.emitEvent !== false) { this.emitter.emit('did-change-paths', this.getPaths()); } } getProvidedDirectoryForProjectPath(projectPath) { for (let provider of this.directoryProviders) { if (typeof provider.directoryForURISync === 'function') { const directory = provider.directoryForURISync(projectPath); if (directory) { return directory; } } } return null; } getDirectoryForProjectPath(projectPath) { let directory = this.getProvidedDirectoryForProjectPath(projectPath); 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}. async buildBuffer(absoluteFilePath) { const params = { shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete }; let buffer; if (absoluteFilePath != null) { if (this.loadPromisesByPath[absoluteFilePath] == null) { this.loadPromisesByPath[absoluteFilePath] = TextBuffer.load( absoluteFilePath, params ) .then(result => { delete this.loadPromisesByPath[absoluteFilePath]; return result; }) .catch(error => { delete this.loadPromisesByPath[absoluteFilePath]; throw error; }); } buffer = await this.loadPromisesByPath[absoluteFilePath]; } else { buffer = new TextBuffer(params); } this.grammarRegistry.autoAssignLanguageMode(buffer); this.addBuffer(buffer); return buffer; } addBuffer(buffer, options = {}) { this.buffers.push(buffer); this.subscriptions.add(this.grammarRegistry.maintainLanguageMode(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(async ({ 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 }); }); } }; ================================================ FILE: src/protocol-handler-installer.js ================================================ const { ipcRenderer } = 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); } async isDefaultProtocolClient() { return ipcRenderer.invoke('isDefaultProtocolClient', { protocol: 'atom', path: process.execPath, args: ['--uri-handler', '--'] }); } async 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() && ipcRenderer.invoke('setAsDefaultProtocolClient', { protocol: 'atom', path: process.execPath, args: ['--uri-handler', '--'] }) ); } async initialize(config, notifications) { if (!this.isSupported()) { return; } const behaviorWhenNotProtocolClient = config.get(SETTING); switch (behaviorWhenNotProtocolClient) { case PROMPT: if (await !this.isDefaultProtocolClient()) { this.promptToBecomeProtocolClient(config, notifications); } break; case ALWAYS: if (await !this.isDefaultProtocolClient()) { this.setAsDefaultProtocolClient(); } break; case NEVER: if (process.platform === 'win32') { // Only win32 supports deregistration const Registry = require('winreg'); const commandKey = new Registry({ hive: 'HKCR', key: `\\atom` }); commandKey.destroy((_err, _val) => { /* no op */ }); } break; 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 default 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) } ] } ); } }; ================================================ FILE: src/register-default-commands.coffee ================================================ {ipcRenderer} = require 'electron' Grim = require 'grim' module.exports = ({commandRegistry, commandInstaller, config, notificationManager, project, clipboard}) -> commandRegistry.add( 'atom-workspace', { 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() 'pane:move-active-item-to-top-of-stack': -> @getModel().getActivePane().moveActiveItemToTopOfStack() 'pane:show-next-item': -> @getModel().getActivePane().activateNextItem() 'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem() 'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0) 'pane:show-item-2': -> @getModel().getActivePane().activateItemAtIndex(1) 'pane:show-item-3': -> @getModel().getActivePane().activateItemAtIndex(2) 'pane:show-item-4': -> @getModel().getActivePane().activateItemAtIndex(3) 'pane:show-item-5': -> @getModel().getActivePane().activateItemAtIndex(4) 'pane:show-item-6': -> @getModel().getActivePane().activateItemAtIndex(5) 'pane:show-item-7': -> @getModel().getActivePane().activateItemAtIndex(6) 'pane:show-item-8': -> @getModel().getActivePane().activateItemAtIndex(7) 'pane:show-item-9': -> @getModel().getActivePane().activateLastItem() 'pane:move-item-right': -> @getModel().getActivePane().moveItemRight() 'pane:move-item-left': -> @getModel().getActivePane().moveItemLeft() 'window:increase-font-size': -> @getModel().increaseFontSize() 'window:decrease-font-size': -> @getModel().decreaseFontSize() 'window:reset-font-size': -> @getModel().resetFontSize() 'application:about': -> ipcRenderer.send('command', 'application:about') 'application:show-preferences': -> ipcRenderer.send('command', 'application:show-settings') 'application:show-settings': -> ipcRenderer.send('command', 'application:show-settings') 'application:quit': -> ipcRenderer.send('command', 'application:quit') 'application:hide': -> ipcRenderer.send('command', 'application:hide') 'application:hide-other-applications': -> ipcRenderer.send('command', 'application:hide-other-applications') 'application:install-update': -> ipcRenderer.send('command', 'application:install-update') 'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications') 'application:new-window': -> ipcRenderer.send('command', 'application:new-window') 'application:new-file': -> ipcRenderer.send('command', 'application:new-file') 'application:open': -> defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] ipcRenderer.send('open-chosen-any', defaultPath) 'application:open-file': -> defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] ipcRenderer.send('open-chosen-file', defaultPath) 'application:open-folder': -> defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0] ipcRenderer.send('open-chosen-folder', defaultPath) 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev') 'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe') 'application:add-project-folder': -> atom.addProjectFolder() 'application:minimize': -> ipcRenderer.send('command', 'application:minimize') 'application:zoom': -> ipcRenderer.send('command', 'application:zoom') 'application:bring-all-windows-to-front': -> ipcRenderer.send('command', 'application:bring-all-windows-to-front') 'application:open-your-config': -> ipcRenderer.send('command', 'application:open-your-config') 'application:open-your-init-script': -> ipcRenderer.send('command', 'application:open-your-init-script') 'application:open-your-keymap': -> ipcRenderer.send('command', 'application:open-your-keymap') 'application:open-your-snippets': -> ipcRenderer.send('command', 'application:open-your-snippets') 'application:open-your-stylesheet': -> ipcRenderer.send('command', 'application:open-your-stylesheet') 'application:open-license': -> @getModel().openLicense() 'window:run-package-specs': -> @runPackageSpecs() 'window:run-benchmarks': -> @runBenchmarks() 'window:toggle-left-dock': -> @getModel().getLeftDock().toggle() 'window:toggle-right-dock': -> @getModel().getRightDock().toggle() 'window:toggle-bottom-dock': -> @getModel().getBottomDock().toggle() 'window:focus-next-pane': -> @getModel().activateNextPane() 'window:focus-previous-pane': -> @getModel().activatePreviousPane() 'window:focus-pane-above': -> @focusPaneViewAbove() 'window:focus-pane-below': -> @focusPaneViewBelow() 'window:focus-pane-on-left': -> @focusPaneViewOnLeft() 'window:focus-pane-on-right': -> @focusPaneViewOnRight() 'window:move-active-item-to-pane-above': -> @moveActiveItemToPaneAbove() 'window:move-active-item-to-pane-below': -> @moveActiveItemToPaneBelow() 'window:move-active-item-to-pane-on-left': -> @moveActiveItemToPaneOnLeft() 'window:move-active-item-to-pane-on-right': -> @moveActiveItemToPaneOnRight() 'window:copy-active-item-to-pane-above': -> @moveActiveItemToPaneAbove(keepOriginal: true) 'window:copy-active-item-to-pane-below': -> @moveActiveItemToPaneBelow(keepOriginal: true) 'window:copy-active-item-to-pane-on-left': -> @moveActiveItemToPaneOnLeft(keepOriginal: true) 'window:copy-active-item-to-pane-on-right': -> @moveActiveItemToPaneOnRight(keepOriginal: true) 'window:save-all': -> @getModel().saveAll() 'window:toggle-invisibles': -> config.set("editor.showInvisibles", not config.get("editor.showInvisibles")) 'window:log-deprecation-warnings': -> Grim.logDeprecations() 'window:toggle-auto-indent': -> config.set("editor.autoIndent", not config.get("editor.autoIndent")) 'pane:reopen-closed-item': -> @getModel().reopenItem() 'core:close': -> @getModel().closeActivePaneItemOrEmptyPaneOrWindow() 'core:save': -> @getModel().saveActivePaneItem() 'core:save-as': -> @getModel().saveActivePaneItemAs() }, false ) if process.platform is 'darwin' commandRegistry.add( 'atom-workspace', 'window:install-shell-commands', (-> commandInstaller.installShellCommandsInteractively()), false ) commandRegistry.add( 'atom-pane', { 'pane:save-items': -> @getModel().saveItems() 'pane:split-left': -> @getModel().splitLeft() 'pane:split-right': -> @getModel().splitRight() 'pane:split-up': -> @getModel().splitUp() 'pane:split-down': -> @getModel().splitDown() 'pane:split-left-and-copy-active-item': -> @getModel().splitLeft(copyActiveItem: true) 'pane:split-right-and-copy-active-item': -> @getModel().splitRight(copyActiveItem: true) 'pane:split-up-and-copy-active-item': -> @getModel().splitUp(copyActiveItem: true) 'pane:split-down-and-copy-active-item': -> @getModel().splitDown(copyActiveItem: true) 'pane:split-left-and-move-active-item': -> @getModel().splitLeft(moveActiveItem: true) 'pane:split-right-and-move-active-item': -> @getModel().splitRight(moveActiveItem: true) 'pane:split-up-and-move-active-item': -> @getModel().splitUp(moveActiveItem: true) 'pane:split-down-and-move-active-item': -> @getModel().splitDown(moveActiveItem: true) 'pane:close': -> @getModel().close() 'pane:close-other-items': -> @getModel().destroyInactiveItems() 'pane:increase-size': -> @getModel().increaseSize() 'pane:decrease-size': -> @getModel().decreaseSize() }, false ) commandRegistry.add( 'atom-text-editor', stopEventPropagation({ 'core:move-left': -> @moveLeft() 'core:move-right': -> @moveRight() 'core:select-left': -> @selectLeft() 'core:select-right': -> @selectRight() 'core:select-up': -> @selectUp() 'core:select-down': -> @selectDown() 'core:select-all': -> @selectAll() 'editor:select-word': -> @selectWordsContainingCursors() 'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections() 'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph() 'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph() 'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine() 'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine() 'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine() 'editor:move-to-end-of-line': -> @moveToEndOfLine() 'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine() 'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord() 'editor:move-to-end-of-word': -> @moveToEndOfWord() 'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord() 'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary() 'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary() 'editor:move-to-previous-subword-boundary': -> @moveToPreviousSubwordBoundary() 'editor:move-to-next-subword-boundary': -> @moveToNextSubwordBoundary() 'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph() 'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph() 'editor:select-to-end-of-line': -> @selectToEndOfLine() 'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine() 'editor:select-to-end-of-word': -> @selectToEndOfWord() 'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord() 'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord() 'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary() 'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary() 'editor:select-to-next-subword-boundary': -> @selectToNextSubwordBoundary() 'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary() 'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine() 'editor:select-line': -> @selectLinesContainingCursors() 'editor:select-larger-syntax-node': -> @selectLargerSyntaxNode() 'editor:select-smaller-syntax-node': -> @selectSmallerSyntaxNode() }), false ) commandRegistry.add( 'atom-text-editor:not([readonly])', stopEventPropagation({ 'core:undo': -> @undo() 'core:redo': -> @redo() }), false ) commandRegistry.add( 'atom-text-editor', stopEventPropagationAndGroupUndo( config, { 'core:copy': -> @copySelectedText() 'editor:copy-selection': -> @copyOnlySelectedText() } ), false ) commandRegistry.add( 'atom-text-editor:not([readonly])', stopEventPropagationAndGroupUndo( config, { 'core:backspace': -> @backspace() 'core:delete': -> @delete() 'core:cut': -> @cutSelectedText() '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() 'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine() 'editor:delete-to-end-of-line': -> @deleteToEndOfLine() 'editor:delete-to-end-of-word': -> @deleteToEndOfWord() 'editor:delete-to-beginning-of-subword': -> @deleteToBeginningOfSubword() 'editor:delete-to-end-of-subword': -> @deleteToEndOfSubword() 'editor:delete-line': -> @deleteLine() 'editor:cut-to-end-of-line': -> @cutToEndOfLine() 'editor:cut-to-end-of-buffer-line': -> @cutToEndOfBufferLine() 'editor:transpose': -> @transpose() 'editor:upper-case': -> @upperCase() 'editor:lower-case': -> @lowerCase() } ), false ) commandRegistry.add( 'atom-text-editor:not([mini])', stopEventPropagation({ 'core:move-up': -> @moveUp() 'core:move-down': -> @moveDown() 'core:move-to-top': -> @moveToTop() 'core:move-to-bottom': -> @moveToBottom() 'core:page-up': -> @pageUp() 'core:page-down': -> @pageDown() 'core:select-to-top': -> @selectToTop() 'core:select-to-bottom': -> @selectToBottom() 'core:select-page-up': -> @selectPageUp() 'core:select-page-down': -> @selectPageDown() 'editor:add-selection-below': -> @addSelectionBelow() 'editor:add-selection-above': -> @addSelectionAbove() 'editor:split-selections-into-lines': -> @splitSelectionsIntoLines() 'editor:toggle-soft-tabs': -> @toggleSoftTabs() 'editor:toggle-soft-wrap': -> @toggleSoftWrapped() 'editor:fold-all': -> @foldAll() 'editor:unfold-all': -> @unfoldAll() 'editor:fold-current-row': -> @foldCurrentRow() @scrollToCursorPosition() 'editor:unfold-current-row': -> @unfoldCurrentRow() @scrollToCursorPosition() 'editor:fold-selection': -> @foldSelectedLines() 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0) @scrollToCursorPosition() 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1) @scrollToCursorPosition() 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2) @scrollToCursorPosition() 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3) @scrollToCursorPosition() 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4) @scrollToCursorPosition() 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5) @scrollToCursorPosition() 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6) @scrollToCursorPosition() 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7) @scrollToCursorPosition() 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8) @scrollToCursorPosition() 'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager) 'editor:log-cursor-syntax-tree-scope': -> showSyntaxTree(@getCursorSyntaxTreeScope(), notificationManager) 'editor:copy-path': -> copyPathToClipboard(this, project, clipboard, false) 'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true) 'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide')) 'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers')) 'editor:scroll-to-cursor': -> @scrollToCursorPosition() }), false ) commandRegistry.add( 'atom-text-editor:not([mini]):not([readonly])', stopEventPropagationAndGroupUndo( config, { 'editor:indent': -> @indent() 'editor:auto-indent': -> @autoIndentSelectedRows() 'editor:indent-selected-rows': -> @indentSelectedRows() 'editor:outdent-selected-rows': -> @outdentSelectedRows() 'editor:newline': -> @insertNewline() 'editor:newline-below': -> @insertNewlineBelow() 'editor:newline-above': -> @insertNewlineAbove() 'editor:toggle-line-comments': -> @toggleLineCommentsInSelection() 'editor:checkout-head-revision': -> atom.workspace.checkoutHeadRevision(this) 'editor:move-line-up': -> @moveLineUp() 'editor:move-line-down': -> @moveLineDown() 'editor:move-selection-left': -> @moveSelectionLeft() 'editor:move-selection-right': -> @moveSelectionRight() 'editor:duplicate-lines': -> @duplicateLines() 'editor:join-lines': -> @joinLines() } ), false ) stopEventPropagation = (commandListeners) -> newCommandListeners = {} for commandName, commandListener of commandListeners do (commandListener) -> newCommandListeners[commandName] = (event) -> event.stopPropagation() commandListener.call(@getModel(), event) newCommandListeners stopEventPropagationAndGroupUndo = (config, commandListeners) -> newCommandListeners = {} for commandName, commandListener of commandListeners do (commandListener) -> newCommandListeners[commandName] = (event) -> event.stopPropagation() model = @getModel() model.transact model.getUndoGroupingInterval(), -> commandListener.call(model, event) newCommandListeners showCursorScope = (descriptor, notificationManager) -> list = descriptor.scopes.toString().split(',') list = list.map (item) -> "* #{item}" content = "Scopes at Cursor\n#{list.join('\n')}" notificationManager.addInfo(content, dismissable: true) showSyntaxTree = (descriptor, notificationManager) -> list = descriptor.scopes.toString().split(',') list = list.map (item) -> "* #{item}" content = "Syntax tree at Cursor\n#{list.join('\n')}" notificationManager.addInfo(content, dismissable: true) copyPathToClipboard = (editor, project, clipboard, relative) -> if filePath = editor.getPath() filePath = project.relativize(filePath) if relative clipboard.write(filePath) ================================================ FILE: src/reopen-project-list-view.js ================================================ const SelectListView = require('atom-select-list'); module.exports = class ReopenProjectListView { constructor(callback) { this.callback = callback; this.selectListView = new SelectListView({ emptyMessage: 'No projects in history.', itemsClassList: ['mark-active'], items: [], filterKeyForItem: project => project.name, elementForItem: project => { let element = document.createElement('li'); if (project.name === this.currentProjectName) { element.classList.add('active'); } element.textContent = project.name; return element; }, didConfirmSelection: project => { this.cancel(); this.callback(project.value); }, didCancelSelection: () => { this.cancel(); } }); this.selectListView.element.classList.add('reopen-project'); } get element() { return this.selectListView.element; } dispose() { this.cancel(); return this.selectListView.destroy(); } cancel() { if (this.panel != null) { this.panel.destroy(); } this.panel = null; this.currentProjectName = null; if (this.previouslyFocusedElement) { this.previouslyFocusedElement.focus(); this.previouslyFocusedElement = null; } } attach() { this.previouslyFocusedElement = document.activeElement; if (this.panel == null) { this.panel = atom.workspace.addModalPanel({ item: this }); } this.selectListView.focus(); this.selectListView.reset(); } async toggle() { if (this.panel != null) { this.cancel(); } else { this.currentProjectName = atom.project != null ? this.makeName(atom.project.getPaths()) : null; const projects = atom.history .getProjects() .map(p => ({ name: this.makeName(p.paths), value: p.paths })); await this.selectListView.update({ items: projects }); this.attach(); } } makeName(paths) { return paths.join(', '); } }; ================================================ FILE: src/reopen-project-menu-manager.js ================================================ const { CompositeDisposable } = require('event-kit'); const path = require('path'); module.exports = class ReopenProjectMenuManager { constructor({ menu, commands, history, config, open }) { this.menuManager = menu; this.historyManager = history; this.config = config; this.open = open; this.projects = []; this.subscriptions = new CompositeDisposable(); this.subscriptions.add( history.onDidChangeProjects(this.update.bind(this)), config.onDidChange( 'core.reopenProjectMenuCount', ({ oldValue, newValue }) => { this.update(); } ), commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) }) ); this.applyWindowsJumpListRemovals(); } reopenProjectCommand(e) { if (e.detail != null && e.detail.index != null) { this.open(this.projects[e.detail.index].paths); } else { this.createReopenProjectListView(); } } createReopenProjectListView() { if (this.reopenProjectListView == null) { const ReopenProjectListView = require('./reopen-project-list-view'); this.reopenProjectListView = new ReopenProjectListView(paths => { if (paths != null) { this.open(paths); } }); } this.reopenProjectListView.toggle(); } update() { this.disposeProjectMenu(); this.projects = this.historyManager .getProjects() .slice(0, this.config.get('core.reopenProjectMenuCount')); const newMenu = ReopenProjectMenuManager.createProjectsMenu(this.projects); this.lastProjectMenu = this.menuManager.add([newMenu]); this.updateWindowsJumpList(); } static taskDescription(paths) { return paths .map(path => `${ReopenProjectMenuManager.betterBaseName(path)} (${path})`) .join(' '); } // Windows users can right-click Atom taskbar and remove project from the jump list. // We have to honor that or the group stops working. As we only get a partial list // each time we remove them from history entirely. async applyWindowsJumpListRemovals() { if (process.platform !== 'win32') return; if (this.app === undefined) { this.app = require('electron').remote.app; } const removed = this.app .getJumpListSettings() .removedItems.map(i => i.description); if (removed.length === 0) return; for (let project of this.historyManager.getProjects()) { if ( removed.includes( ReopenProjectMenuManager.taskDescription(project.paths) ) ) { await this.historyManager.removeProject(project.paths); } } } updateWindowsJumpList() { if (process.platform !== 'win32') return; if (this.app === undefined) { this.app = require('electron').remote.app; } this.app.setJumpList([ { type: 'custom', name: 'Recent Projects', items: this.projects.map(project => ({ type: 'task', title: project.paths .map(ReopenProjectMenuManager.betterBaseName) .join(', '), description: ReopenProjectMenuManager.taskDescription(project.paths), program: process.execPath, args: project.paths.map(path => `"${path}"`).join(' '), iconPath: path.join( path.dirname(process.execPath), 'resources', 'cli', 'folder.ico' ), iconIndex: 0 })) }, { type: 'recent' }, { items: [ { type: 'task', title: 'New Window', program: process.execPath, args: '--new-window', description: 'Opens a new Atom window' } ] } ]); } dispose() { this.subscriptions.dispose(); this.disposeProjectMenu(); if (this.reopenProjectListView != null) { this.reopenProjectListView.dispose(); } } disposeProjectMenu() { if (this.lastProjectMenu) { this.lastProjectMenu.dispose(); this.lastProjectMenu = null; } } static createProjectsMenu(projects) { return { label: 'File', id: 'File', submenu: [ { label: 'Reopen Project', id: 'Reopen Project', submenu: projects.map((project, index) => ({ label: this.createLabel(project), command: 'application:reopen-project', commandDetail: { index: index, paths: project.paths } })) } ] }; } static createLabel(project) { return project.paths.length === 1 ? project.paths[0] : project.paths.map(this.betterBaseName).join(', '); } static betterBaseName(directory) { // Handles Windows roots better than path.basename which returns '' for 'd:' and 'd:\' const match = directory.match(/^([a-z]:)[\\]?$/i); return match ? match[1] + '\\' : path.basename(directory); } }; ================================================ FILE: src/replace-handler.coffee ================================================ {PathReplacer} = require 'scandal' module.exports = (filePaths, regexSource, regexFlags, replacementText) -> callback = @async() replacer = new PathReplacer() regex = new RegExp(regexSource, regexFlags) replacer.on 'file-error', ({code, path, message}) -> emit('replace:file-error', {code, path, message}) replacer.on 'path-replaced', (result) -> emit('replace:path-replaced', result) replacer.replacePaths(regex, replacementText, filePaths, -> callback()) ================================================ FILE: src/ripgrep-directory-searcher.js ================================================ const { spawn } = require('child_process'); const path = require('path'); // `ripgrep` and `scandal` have a different way of handling the trailing and leading // context lines: // * `scandal` returns all the context lines that are requested, even if they include // previous or future results. // * `ripgrep` is a bit smarter and only returns the context lines that do not correspond // to any result (in a similar way that is shown in the find and replace UI). // // For example, if we have the following file and we request to leading context lines: // // line 1 // line 2 // result 1 // result 2 // line 3 // line 4 // // `scandal` will return two results: // * First result with `['line 1', line 2']` as leading context. // * Second result with `['line 2', result 1']` as leading context. // `ripgrep` on the other hand will return a JS object that is more similar to the way that // the results are shown: // [ // {type: 'begin', ...}, // {type: 'context', ...}, // context for line 1 // {type: 'context', ...}, // context for line 2 // {type: 'match', ...}, // result 1 // {type: 'match', ...}, // result 2 // {type: 'end', ...}, // ] // // In order to keep backwards compatibility, and avoid doing changes to the find and replace logic, // for `ripgrep` we need to keep some state with the context lines (and matches) to be able to build // a data structure that has the same behaviour as the `scandal` one. // // We use the `pendingLeadingContext` array to generate the leading context. This array gets mutated // to always contain the leading `n` lines and is cloned every time a match is found. It's currently // implemented as a standard array but we can easily change it to use a linked list if we find that // the shift operations are slow. // // We use the `pendingTrailingContexts` Set to generate the trailing context. Since the trailing // context needs to be generated after receiving a match, we keep all trailing context arrays that // haven't been fulfilled in this Set, and mutate them adding new lines until they are fulfilled. function updateLeadingContext(message, pendingLeadingContext, options) { if (message.type !== 'match' && message.type !== 'context') { return; } if (options.leadingContextLineCount) { pendingLeadingContext.push(cleanResultLine(message.data.lines)); if (pendingLeadingContext.length > options.leadingContextLineCount) { pendingLeadingContext.shift(); } } } function updateTrailingContexts(message, pendingTrailingContexts, options) { if (message.type !== 'match' && message.type !== 'context') { return; } if (options.trailingContextLineCount) { for (const trailingContextLines of pendingTrailingContexts) { trailingContextLines.push(cleanResultLine(message.data.lines)); if (trailingContextLines.length === options.trailingContextLineCount) { pendingTrailingContexts.delete(trailingContextLines); } } } } function cleanResultLine(resultLine) { resultLine = getText(resultLine); return resultLine[resultLine.length - 1] === '\n' ? resultLine.slice(0, -1) : resultLine; } function getPositionFromColumn(lines, column) { let currentLength = 0; let currentLine = 0; let previousLength = 0; while (column >= currentLength) { previousLength = currentLength; currentLength += lines[currentLine].length + 1; currentLine++; } return [currentLine - 1, column - previousLength]; } function processUnicodeMatch(match) { const text = getText(match.lines); if (text.length === Buffer.byteLength(text)) { // fast codepath for lines that only contain characters of 1 byte length. return; } let remainingBuffer = Buffer.from(text); let currentLength = 0; let previousPosition = 0; function convertPosition(position) { const currentBuffer = remainingBuffer.slice(0, position - previousPosition); currentLength = currentBuffer.toString().length + currentLength; remainingBuffer = remainingBuffer.slice(position); previousPosition = position; return currentLength; } // Iterate over all the submatches to find the convert the start and end values // (which come as bytes from ripgrep) to character positions. // We can do this because submatches come ordered by position. for (const submatch of match.submatches) { submatch.start = convertPosition(submatch.start); submatch.end = convertPosition(submatch.end); } } // This function processes a ripgrep submatch to create the correct // range. This is mostly needed for multi-line results, since the range // will have different start and end rows and we need to calculate these // based on the lines that ripgrep returns. function processSubmatch(submatch, lineText, offsetRow) { const lineParts = lineText.split('\n'); const start = getPositionFromColumn(lineParts, submatch.start); const end = getPositionFromColumn(lineParts, submatch.end); // Make sure that the lineText string only contains lines that are // relevant to this submatch. This means getting rid of lines above // the start row and below the end row. for (let i = start[0]; i > 0; i--) { lineParts.shift(); } while (end[0] < lineParts.length - 1) { lineParts.pop(); } start[0] += offsetRow; end[0] += offsetRow; return { range: [start, end], lineText: cleanResultLine({ text: lineParts.join('\n') }) }; } function getText(input) { return 'text' in input ? input.text : Buffer.from(input.bytes, 'base64').toString(); } module.exports = class RipgrepDirectorySearcher { canSearchDirectory() { return true; } // Performs a text search for files in the specified `Directory`s, subject to the // specified parameters. // // Results are streamed back to the caller by invoking methods on the specified `options`, // such as `didMatch` and `didError`. // // * `directories` {Array} of {Directory} objects to search, all of which have been accepted by // this searcher's `canSearchDirectory()` predicate. // * `regex` {RegExp} to search with. // * `options` {Object} with the following properties: // * `didMatch` {Function} call with a search result structured as follows: // * `searchResult` {Object} with the following keys: // * `filePath` {String} absolute path to the matching file. // * `matches` {Array} with object elements with the following keys: // * `lineText` {String} The full text of the matching line (without a line terminator character). // * `lineTextOffset` {Number} Always 0, present for backwards compatibility // * `matchText` {String} The text that matched the `regex` used for the search. // * `range` {Range} Identifies the matching region in the file. (Likely as an array of numeric arrays.) // * `didError` {Function} call with an Error if there is a problem during the search. // * `didSearchPaths` {Function} periodically call with the number of paths searched that contain results thus far. // * `inclusions` {Array} of glob patterns (as strings) to search within. Note that this // array may be empty, indicating that all files should be searched. // // Each item in the array is a file/directory pattern, e.g., `src` to search in the "src" // directory or `*.js` to search all JavaScript files. In practice, this often comes from the // comma-delimited list of patterns in the bottom text input of the ProjectFindView dialog. // * `includeHidden` {boolean} whether to ignore hidden files. // * `excludeVcsIgnores` {boolean} whether to exclude VCS ignored paths. // * `exclusions` {Array} similar to inclusions // * `follow` {boolean} whether symlinks should be followed. // // Returns a *thenable* `DirectorySearch` that includes a `cancel()` method. If `cancel()` is // invoked before the `DirectorySearch` is determined, it will resolve the `DirectorySearch`. search(directories, regexp, options) { const numPathsFound = { num: 0 }; const allPromises = directories.map(directory => this.searchInDirectory(directory, regexp, options, numPathsFound) ); const promise = Promise.all(allPromises); promise.cancel = () => { for (const promise of allPromises) { promise.cancel(); } }; return promise; } searchInDirectory(directory, regexp, options, numPathsFound) { // Delay the require of vscode-ripgrep to not mess with the snapshot creation. if (!this.rgPath) { this.rgPath = require('vscode-ripgrep').rgPath.replace( /\bapp\.asar\b/, 'app.asar.unpacked' ); } const directoryPath = directory.getPath(); const regexpStr = this.prepareRegexp(regexp.source); const args = ['--json', '--regexp', regexpStr]; if (options.leadingContextLineCount) { args.push('--before-context', options.leadingContextLineCount); } if (options.trailingContextLineCount) { args.push('--after-context', options.trailingContextLineCount); } if (regexp.ignoreCase) { args.push('--ignore-case'); } for (const inclusion of this.prepareGlobs( options.inclusions, directoryPath )) { args.push('--glob', inclusion); } for (const exclusion of this.prepareGlobs( options.exclusions, directoryPath )) { args.push('--glob', '!' + exclusion); } if (this.isMultilineRegexp(regexpStr)) { args.push('--multiline'); } if (options.includeHidden) { args.push('--hidden'); } if (options.follow) { args.push('--follow'); } if (!options.excludeVcsIgnores) { args.push('--no-ignore-vcs'); } if (options.PCRE2) { args.push('--pcre2'); } args.push('.'); const child = spawn(this.rgPath, args, { cwd: directoryPath, stdio: ['pipe', 'pipe', 'pipe'] }); const didMatch = options.didMatch || (() => {}); let cancelled = false; const returnedPromise = new Promise((resolve, reject) => { let buffer = ''; let bufferError = ''; let pendingEvent; let pendingLeadingContext; let pendingTrailingContexts; child.on('close', (code, signal) => { // code 1 is used when no results are found. if (code !== null && code > 1) { reject(new Error(bufferError)); } else { resolve(); } }); child.stderr.on('data', chunk => { bufferError += chunk; }); child.stdout.on('data', chunk => { if (cancelled) { return; } buffer += chunk; const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { const message = JSON.parse(line); updateTrailingContexts(message, pendingTrailingContexts, options); if (message.type === 'begin') { pendingEvent = { filePath: path.join(directoryPath, getText(message.data.path)), matches: [] }; pendingLeadingContext = []; pendingTrailingContexts = new Set(); } else if (message.type === 'match') { const trailingContextLines = []; pendingTrailingContexts.add(trailingContextLines); processUnicodeMatch(message.data); for (const submatch of message.data.submatches) { const { lineText, range } = processSubmatch( submatch, getText(message.data.lines), message.data.line_number - 1 ); pendingEvent.matches.push({ matchText: getText(submatch.match), lineText, lineTextOffset: 0, range, leadingContextLines: [...pendingLeadingContext], trailingContextLines }); } } else if (message.type === 'end') { options.didSearchPaths(++numPathsFound.num); didMatch(pendingEvent); pendingEvent = null; } updateLeadingContext(message, pendingLeadingContext, options); } }); }); returnedPromise.cancel = () => { child.kill(); cancelled = true; }; return returnedPromise; } // We need to prepare the "globs" that we receive from the user to make their behaviour more // user-friendly (e.g when adding `src/` the user probably means `src/**/*`). // This helper function takes care of that. prepareGlobs(globs, projectRootPath) { const output = []; for (let pattern of globs) { // we need to replace path separators by slashes since globs should // always use always slashes as path separators. pattern = pattern.replace(new RegExp(`\\${path.sep}`, 'g'), '/'); if (pattern.length === 0) { continue; } const projectName = path.basename(projectRootPath); // The user can just search inside one of the opened projects. When we detect // this scenario we just consider the glob to include every file. if (pattern === projectName) { output.push('**/*'); continue; } if (pattern.startsWith(projectName + '/')) { pattern = pattern.slice(projectName.length + 1); } if (pattern.endsWith('/')) { pattern = pattern.slice(0, -1); } output.push(pattern); output.push(pattern.endsWith('/**') ? pattern : `${pattern}/**`); } return output; } prepareRegexp(regexpStr) { // ripgrep handles `--` as the arguments separator, so we need to escape it if the // user searches for that exact same string. if (regexpStr === '--') { return '\\-\\-'; } // ripgrep is quite picky about unnecessarily escaped sequences, so we need to unescape // them: https://github.com/BurntSushi/ripgrep/issues/434. regexpStr = regexpStr.replace(/\\\//g, '/'); return regexpStr; } isMultilineRegexp(regexpStr) { if (regexpStr.includes('\\n')) { return true; } return false; } }; ================================================ FILE: src/scan-handler.coffee ================================================ path = require "path" async = require "async" {PathSearcher, PathScanner, search} = require 'scandal' module.exports = (rootPaths, regexSource, options, searchOptions={}) -> callback = @async() PATHS_COUNTER_SEARCHED_CHUNK = 50 pathsSearched = 0 searcher = new PathSearcher(searchOptions) searcher.on 'file-error', ({code, path, message}) -> emit('scan:file-error', {code, path, message}) searcher.on 'results-found', (result) -> emit('scan:result-found', result) flags = "g" flags += "i" if options.ignoreCase regex = new RegExp(regexSource, flags) async.each( rootPaths, (rootPath, next) -> options2 = Object.assign {}, options, inclusions: processPaths(rootPath, options.inclusions) globalExclusions: processPaths(rootPath, options.globalExclusions) scanner = new PathScanner(rootPath, options2) scanner.on 'path-found', -> pathsSearched++ if pathsSearched % PATHS_COUNTER_SEARCHED_CHUNK is 0 emit('scan:paths-searched', pathsSearched) search regex, scanner, searcher, -> emit('scan:paths-searched', pathsSearched) next() callback ) processPaths = (rootPath, paths) -> return paths unless paths?.length > 0 rootPathBase = path.basename(rootPath) results = [] for givenPath in paths segments = givenPath.split(path.sep) firstSegment = segments.shift() results.push(givenPath) if firstSegment is rootPathBase if segments.length is 0 results.push(path.join("**", "*")) else results.push(path.join(segments...)) results ================================================ FILE: src/scope-descriptor.js ================================================ // Extended: Wraps an {Array} of `String`s. The Array describes a path from the // root of the syntax tree to a token including _all_ scope names for the entire // path. // // Methods that take a `ScopeDescriptor` will also accept an {Array} of {String} // scope names e.g. `['.source.js']`. // // You can use `ScopeDescriptor`s to get language-specific config settings via // {Config::get}. // // You should not need to create a `ScopeDescriptor` directly. // // * {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. // // See the [scopes and scope descriptor guide](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/) // for more information. module.exports = class ScopeDescriptor { static fromObject(scopes) { if (scopes instanceof ScopeDescriptor) { return scopes; } else { return new ScopeDescriptor({ scopes }); } } /* Section: Construction and Destruction */ // Public: Create a {ScopeDescriptor} object. // // * `object` {Object} // * `scopes` {Array} of {String}s constructor({ scopes }) { this.scopes = scopes; } // Public: Returns an {Array} of {String}s getScopesArray() { return this.scopes; } getScopeChain() { // For backward compatibility, prefix TextMate-style scope names with // leading dots (e.g. 'source.js' -> '.source.js'). if (this.scopes[0] != null && this.scopes[0].includes('.')) { let result = ''; for (let i = 0; i < this.scopes.length; i++) { const scope = this.scopes[i]; if (i > 0) { result += ' '; } if (scope[0] !== '.') { result += '.'; } result += scope; } return result; } else { return this.scopes.join(' '); } } toString() { return this.getScopeChain(); } isEqual(other) { if (this.scopes.length !== other.scopes.length) { return false; } for (let i = 0; i < this.scopes.length; i++) { const scope = this.scopes[i]; if (scope !== other.scopes[i]) { return false; } } return true; } }; ================================================ FILE: src/selection.js ================================================ const { Point, Range } = require('text-buffer'); const { pick } = require('underscore-plus'); const { Emitter } = require('event-kit'); const NonWhitespaceRegExp = /\S/; let nextId = 0; // Extended: Represents a selection in the {TextEditor}. module.exports = class Selection { constructor({ cursor, marker, editor, id }) { this.id = id != null ? id : nextId++; this.cursor = cursor; this.marker = marker; this.editor = editor; this.emitter = new Emitter(); this.initialScreenRange = null; this.wordwise = false; this.cursor.selection = this; this.decoration = this.editor.decorateMarker(this.marker, { type: 'highlight', class: 'selection' }); this.marker.onDidChange(e => this.markerDidChange(e)); this.marker.onDidDestroy(() => this.markerDidDestroy()); } destroy() { this.marker.destroy(); } isLastSelection() { return this === this.editor.getLastSelection(); } /* Section: Event Subscription */ // Extended: Calls your `callback` when the selection was moved. // // * `callback` {Function} // * `event` {Object} // * `oldBufferRange` {Range} // * `oldScreenRange` {Range} // * `newBufferRange` {Range} // * `newScreenRange` {Range} // * `selection` {Selection} that triggered the event // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeRange(callback) { return this.emitter.on('did-change-range', callback); } // Extended: Calls your `callback` when the selection was destroyed // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy(callback) { return this.emitter.once('did-destroy', callback); } /* Section: Managing the selection range */ // Public: Returns the screen {Range} for the selection. getScreenRange() { return this.marker.getScreenRange(); } // Public: Modifies the screen range for the selection. // // * `screenRange` The new {Range} to use. // * `options` (optional) {Object} options matching those found in {::setBufferRange}. setScreenRange(screenRange, options) { return this.setBufferRange( this.editor.bufferRangeForScreenRange(screenRange), options ); } // Public: Returns the buffer {Range} for the selection. getBufferRange() { return this.marker.getBufferRange(); } // Public: Modifies the buffer {Range} for the selection. // // * `bufferRange` The new {Range} to select. // * `options` (optional) {Object} with the keys: // * `reversed` {Boolean} indicating whether to set the selection in a // reversed orientation. // * `preserveFolds` if `true`, the fold settings are preserved after the // selection moves. // * `autoscroll` {Boolean} indicating whether to autoscroll to the new // range. Defaults to `true` if this is the most recently added selection, // `false` otherwise. setBufferRange(bufferRange, options = {}) { bufferRange = Range.fromObject(bufferRange); if (options.reversed == null) options.reversed = this.isReversed(); if (!options.preserveFolds) this.editor.destroyFoldsContainingBufferPositions( [bufferRange.start, bufferRange.end], true ); this.modifySelection(() => { const needsFlash = options.flash; options.flash = null; this.marker.setBufferRange(bufferRange, options); const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection(); if (autoscroll) this.autoscroll(); if (needsFlash) this.decoration.flash('flash', this.editor.selectionFlashDuration); }); } // Public: Returns the starting and ending buffer rows the selection is // highlighting. // // Returns an {Array} of two {Number}s: the starting row, and the ending row. getBufferRowRange() { const range = this.getBufferRange(); const start = range.start.row; let end = range.end.row; if (range.end.column === 0) end = Math.max(start, end - 1); return [start, end]; } getTailScreenPosition() { return this.marker.getTailScreenPosition(); } getTailBufferPosition() { return this.marker.getTailBufferPosition(); } getHeadScreenPosition() { return this.marker.getHeadScreenPosition(); } getHeadBufferPosition() { return this.marker.getHeadBufferPosition(); } /* Section: Info about the selection */ // Public: Determines if the selection contains anything. isEmpty() { return this.getBufferRange().isEmpty(); } // Public: Determines if the ending position of a marker is greater than the // starting position. // // This can happen when, for example, you highlight text "up" in a {TextBuffer}. isReversed() { return this.marker.isReversed(); } // Public: Returns whether the selection is a single line or not. isSingleScreenLine() { return this.getScreenRange().isSingleLine(); } // Public: Returns the text in the selection. getText() { return this.editor.buffer.getTextInRange(this.getBufferRange()); } // Public: Identifies if a selection intersects with a given buffer range. // // * `bufferRange` A {Range} to check against. // // Returns a {Boolean} intersectsBufferRange(bufferRange) { return this.getBufferRange().intersectsWith(bufferRange); } intersectsScreenRowRange(startRow, endRow) { return this.getScreenRange().intersectsRowRange(startRow, endRow); } intersectsScreenRow(screenRow) { return this.getScreenRange().intersectsRow(screenRow); } // Public: Identifies if a selection intersects with another selection. // // * `otherSelection` A {Selection} to check against. // // Returns a {Boolean} intersectsWith(otherSelection, exclusive) { return this.getBufferRange().intersectsWith( otherSelection.getBufferRange(), exclusive ); } /* Section: Modifying the selected range */ // Public: Clears the selection, moving the marker to the head. // // * `options` (optional) {Object} with the following keys: // * `autoscroll` {Boolean} indicating whether to autoscroll to the new // range. Defaults to `true` if this is the most recently added selection, // `false` otherwise. clear(options) { this.goalScreenRange = null; if (!this.retainSelection) this.marker.clearTail(); const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection(); if (autoscroll) this.autoscroll(); this.finalize(); } // Public: Selects the text from the current cursor position to a given screen // position. // // * `position` An instance of {Point}, with a given `row` and `column`. selectToScreenPosition(position, options) { position = Point.fromObject(position); this.modifySelection(() => { if (this.initialScreenRange) { if (position.isLessThan(this.initialScreenRange.start)) { this.marker.setScreenRange([position, this.initialScreenRange.end], { reversed: true }); } else { this.marker.setScreenRange( [this.initialScreenRange.start, position], { reversed: false } ); } } else { this.cursor.setScreenPosition(position, options); } if (this.linewise) { this.expandOverLine(options); } else if (this.wordwise) { this.expandOverWord(options); } }); } // Public: Selects the text from the current cursor position to a given buffer // position. // // * `position` An instance of {Point}, with a given `row` and `column`. selectToBufferPosition(position) { this.modifySelection(() => this.cursor.setBufferPosition(position)); } // Public: Selects the text one position right of the cursor. // // * `columnCount` (optional) {Number} number of columns to select (default: 1) selectRight(columnCount) { this.modifySelection(() => this.cursor.moveRight(columnCount)); } // Public: Selects the text one position left of the cursor. // // * `columnCount` (optional) {Number} number of columns to select (default: 1) selectLeft(columnCount) { this.modifySelection(() => this.cursor.moveLeft(columnCount)); } // Public: Selects all the text one position above the cursor. // // * `rowCount` (optional) {Number} number of rows to select (default: 1) selectUp(rowCount) { this.modifySelection(() => this.cursor.moveUp(rowCount)); } // Public: Selects all the text one position below the cursor. // // * `rowCount` (optional) {Number} number of rows to select (default: 1) selectDown(rowCount) { this.modifySelection(() => this.cursor.moveDown(rowCount)); } // Public: Selects all the text from the current cursor position to the top of // the buffer. selectToTop() { this.modifySelection(() => this.cursor.moveToTop()); } // Public: Selects all the text from the current cursor position to the bottom // of the buffer. selectToBottom() { this.modifySelection(() => this.cursor.moveToBottom()); } // Public: Selects all the text in the buffer. selectAll() { this.setBufferRange(this.editor.buffer.getRange(), { autoscroll: false }); } // Public: Selects all the text from the current cursor position to the // beginning of the line. selectToBeginningOfLine() { this.modifySelection(() => this.cursor.moveToBeginningOfLine()); } // Public: Selects all the text from the current cursor position to the first // character of the line. selectToFirstCharacterOfLine() { this.modifySelection(() => this.cursor.moveToFirstCharacterOfLine()); } // Public: Selects all the text from the current cursor position to the end of // the screen line. selectToEndOfLine() { this.modifySelection(() => this.cursor.moveToEndOfScreenLine()); } // Public: Selects all the text from the current cursor position to the end of // the buffer line. selectToEndOfBufferLine() { this.modifySelection(() => this.cursor.moveToEndOfLine()); } // Public: Selects all the text from the current cursor position to the // beginning of the word. selectToBeginningOfWord() { this.modifySelection(() => this.cursor.moveToBeginningOfWord()); } // Public: Selects all the text from the current cursor position to the end of // the word. selectToEndOfWord() { this.modifySelection(() => this.cursor.moveToEndOfWord()); } // Public: Selects all the text from the current cursor position to the // beginning of the next word. selectToBeginningOfNextWord() { this.modifySelection(() => this.cursor.moveToBeginningOfNextWord()); } // Public: Selects text to the previous word boundary. selectToPreviousWordBoundary() { this.modifySelection(() => this.cursor.moveToPreviousWordBoundary()); } // Public: Selects text to the next word boundary. selectToNextWordBoundary() { this.modifySelection(() => this.cursor.moveToNextWordBoundary()); } // Public: Selects text to the previous subword boundary. selectToPreviousSubwordBoundary() { this.modifySelection(() => this.cursor.moveToPreviousSubwordBoundary()); } // Public: Selects text to the next subword boundary. selectToNextSubwordBoundary() { this.modifySelection(() => this.cursor.moveToNextSubwordBoundary()); } // Public: Selects all the text from the current cursor position to the // beginning of the next paragraph. selectToBeginningOfNextParagraph() { this.modifySelection(() => this.cursor.moveToBeginningOfNextParagraph()); } // Public: Selects all the text from the current cursor position to the // beginning of the previous paragraph. selectToBeginningOfPreviousParagraph() { this.modifySelection(() => this.cursor.moveToBeginningOfPreviousParagraph() ); } // Public: Modifies the selection to encompass the current word. // // Returns a {Range}. selectWord(options = {}) { if (this.cursor.isSurroundedByWhitespace()) options.wordRegex = /[\t ]*/; if (this.cursor.isBetweenWordAndNonWord()) { options.includeNonWordCharacters = false; } this.setBufferRange( this.cursor.getCurrentWordBufferRange(options), options ); this.wordwise = true; this.initialScreenRange = this.getScreenRange(); } // Public: Expands the newest selection to include the entire word on which // the cursors rests. expandOverWord(options) { this.setBufferRange( this.getBufferRange().union(this.cursor.getCurrentWordBufferRange()), { autoscroll: false } ); const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection(); if (autoscroll) this.cursor.autoscroll(); } // Public: Selects an entire line in the buffer. // // * `row` The line {Number} to select (default: the row of the cursor). selectLine(row, options) { if (row != null) { this.setBufferRange( this.editor.bufferRangeForBufferRow(row, { includeNewline: true }), options ); } else { const startRange = this.editor.bufferRangeForBufferRow( this.marker.getStartBufferPosition().row ); const endRange = this.editor.bufferRangeForBufferRow( this.marker.getEndBufferPosition().row, { includeNewline: true } ); this.setBufferRange(startRange.union(endRange), options); } this.linewise = true; this.wordwise = false; this.initialScreenRange = this.getScreenRange(); } // Public: Expands the newest selection to include the entire line on which // the cursor currently rests. // // It also includes the newline character. expandOverLine(options) { const range = this.getBufferRange().union( this.cursor.getCurrentLineBufferRange({ includeNewline: true }) ); this.setBufferRange(range, { autoscroll: false }); const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection(); if (autoscroll) this.cursor.autoscroll(); } // Private: Ensure that the {TextEditor} is not marked read-only before allowing a buffer modification to occur. if // the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error. ensureWritable(methodName, opts) { if (!opts.bypassReadOnly && this.editor.isReadOnly()) { if (atom.inDevMode() || atom.inSpecMode()) { const e = new Error( 'Attempt to mutate a read-only TextEditor through a Selection' ); e.detail = `Your package is attempting to call ${methodName} on a selection within an editor that has been marked ` + ' read-only. Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before ' + ' attempting modifications.'; throw e; } return false; } return true; } /* Section: Modifying the selected text */ // Public: Replaces text at the current selection. // // * `text` A {String} representing the text to add // * `options` (optional) {Object} with keys: // * `select` If `true`, selects the newly added text. // * `autoIndent` If `true`, indents all inserted text appropriately. // * `autoIndentNewline` If `true`, indent newline appropriately. // * `autoDecreaseIndent` If `true`, decreases indent level appropriately // (for example, when a closing bracket is inserted). // * `preserveTrailingLineIndentation` By default, when pasting multiple // lines, Atom attempts to preserve the relative indent level between the // first line and trailing lines, even if the indent level of the first // line has changed from the copied text. If this option is `true`, this // behavior is suppressed. // level between the first lines and the trailing lines. // * `normalizeLineEndings` (optional) {Boolean} (default: true) // * `undo` *Deprecated* If `skip`, skips the undo stack for this operation. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead. // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) insertText(text, options = {}) { if (!this.ensureWritable('insertText', options)) return; let desiredIndentLevel, indentAdjustment; const oldBufferRange = this.getBufferRange(); const wasReversed = this.isReversed(); this.clear(options); let autoIndentFirstLine = false; const precedingText = this.editor.getTextInRange([ [oldBufferRange.start.row, 0], oldBufferRange.start ]); const remainingLines = text.split('\n'); const firstInsertedLine = remainingLines.shift(); if ( options.indentBasis != null && !options.preserveTrailingLineIndentation ) { indentAdjustment = this.editor.indentLevelForLine(precedingText) - options.indentBasis; this.adjustIndent(remainingLines, indentAdjustment); } const textIsAutoIndentable = text === '\n' || text === '\r\n' || NonWhitespaceRegExp.test(text); if ( options.autoIndent && textIsAutoIndentable && !NonWhitespaceRegExp.test(precedingText) && remainingLines.length > 0 ) { autoIndentFirstLine = true; const firstLine = precedingText + firstInsertedLine; const languageMode = this.editor.buffer.getLanguageMode(); desiredIndentLevel = languageMode.suggestedIndentForLineAtBufferRow && languageMode.suggestedIndentForLineAtBufferRow( oldBufferRange.start.row, firstLine, this.editor.getTabLength() ); if (desiredIndentLevel != null) { indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine); this.adjustIndent(remainingLines, indentAdjustment); } } text = firstInsertedLine; if (remainingLines.length > 0) text += `\n${remainingLines.join('\n')}`; const newBufferRange = this.editor.buffer.setTextInRange( oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings') ); if (options.select) { this.setBufferRange(newBufferRange, { reversed: wasReversed }); } else { if (wasReversed) this.cursor.setBufferPosition(newBufferRange.end); } if (autoIndentFirstLine) { this.editor.setIndentationForBufferRow( oldBufferRange.start.row, desiredIndentLevel ); } if (options.autoIndentNewline && text === '\n') { this.editor.autoIndentBufferRow(newBufferRange.end.row, { preserveLeadingWhitespace: true, skipBlankLines: false }); } else if (options.autoDecreaseIndent && NonWhitespaceRegExp.test(text)) { this.editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row); } const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection(); if (autoscroll) this.autoscroll(); return newBufferRange; } // Public: Removes the first character before the selection if the selection // is empty otherwise it deletes the selection. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) backspace(options = {}) { if (!this.ensureWritable('backspace', options)) return; if (this.isEmpty()) this.selectLeft(); this.deleteSelectedText(options); } // Public: Removes the selection or, if nothing is selected, then all // characters from the start of the selection back to the previous word // boundary. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToPreviousWordBoundary(options = {}) { if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return; if (this.isEmpty()) this.selectToPreviousWordBoundary(); this.deleteSelectedText(options); } // Public: Removes the selection or, if nothing is selected, then all // characters from the start of the selection up to the next word // boundary. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToNextWordBoundary(options = {}) { if (!this.ensureWritable('deleteToNextWordBoundary', options)) return; if (this.isEmpty()) this.selectToNextWordBoundary(); this.deleteSelectedText(options); } // Public: Removes from the start of the selection to the beginning of the // current word if the selection is empty otherwise it deletes the selection. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToBeginningOfWord(options = {}) { if (!this.ensureWritable('deleteToBeginningOfWord', options)) return; if (this.isEmpty()) this.selectToBeginningOfWord(); this.deleteSelectedText(options); } // Public: Removes from the beginning of the line which the selection begins on // all the way through to the end of the selection. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToBeginningOfLine(options = {}) { if (!this.ensureWritable('deleteToBeginningOfLine', options)) return; if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) { this.selectLeft(); } else { this.selectToBeginningOfLine(); } this.deleteSelectedText(options); } // Public: Removes the selection or the next character after the start of the // selection if the selection is empty. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) delete(options = {}) { if (!this.ensureWritable('delete', options)) return; if (this.isEmpty()) this.selectRight(); this.deleteSelectedText(options); } // Public: If the selection is empty, removes all text from the cursor to the // end of the line. If the cursor is already at the end of the line, it // removes the following newline. If the selection isn't empty, only deletes // the contents of the selection. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToEndOfLine(options = {}) { if (!this.ensureWritable('deleteToEndOfLine', options)) return; if (this.isEmpty()) { if (this.cursor.isAtEndOfLine()) { this.delete(options); return; } this.selectToEndOfLine(); } this.deleteSelectedText(options); } // Public: Removes the selection or all characters from the start of the // selection to the end of the current word if nothing is selected. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToEndOfWord(options = {}) { if (!this.ensureWritable('deleteToEndOfWord', options)) return; if (this.isEmpty()) this.selectToEndOfWord(); this.deleteSelectedText(options); } // Public: Removes the selection or all characters from the start of the // selection to the end of the current word if nothing is selected. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToBeginningOfSubword(options = {}) { if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return; if (this.isEmpty()) this.selectToPreviousSubwordBoundary(); this.deleteSelectedText(options); } // Public: Removes the selection or all characters from the start of the // selection to the end of the current word if nothing is selected. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToEndOfSubword(options = {}) { if (!this.ensureWritable('deleteToEndOfSubword', options)) return; if (this.isEmpty()) this.selectToNextSubwordBoundary(); this.deleteSelectedText(options); } // Public: Removes only the selected text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteSelectedText(options = {}) { if (!this.ensureWritable('deleteSelectedText', options)) return; const bufferRange = this.getBufferRange(); if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange); if (this.cursor) this.cursor.setBufferPosition(bufferRange.start); } // Public: Removes the line at the beginning of the selection if the selection // is empty unless the selection spans multiple lines in which case all lines // are removed. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteLine(options = {}) { if (!this.ensureWritable('deleteLine', options)) return; const range = this.getBufferRange(); if (range.isEmpty()) { const start = this.cursor.getScreenRow(); const range = this.editor.bufferRowsForScreenRows(start, start + 1); if (range[1] > range[0]) { this.editor.buffer.deleteRows(range[0], range[1] - 1); } else { this.editor.buffer.deleteRow(range[0]); } } else { const start = range.start.row; let end = range.end.row; if (end !== this.editor.buffer.getLastRow() && range.end.column === 0) end--; this.editor.buffer.deleteRows(start, end); } this.cursor.setBufferPosition({ row: this.cursor.getBufferRow(), column: range.start.column }); } // Public: Joins the current line with the one below it. Lines will // be separated by a single space. // // If there selection spans more than one line, all the lines are joined together. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) joinLines(options = {}) { if (!this.ensureWritable('joinLines', options)) return; let joinMarker; const selectedRange = this.getBufferRange(); if (selectedRange.isEmpty()) { if (selectedRange.start.row === this.editor.buffer.getLastRow()) return; } else { joinMarker = this.editor.markBufferRange(selectedRange, { invalidate: 'never' }); } const rowCount = Math.max(1, selectedRange.getRowCount() - 1); for (let i = 0; i < rowCount; i++) { this.cursor.setBufferPosition([selectedRange.start.row]); this.cursor.moveToEndOfLine(); // Remove trailing whitespace from the current line const scanRange = this.cursor.getCurrentLineBufferRange(); let trailingWhitespaceRange = null; this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({ range }) => { trailingWhitespaceRange = range; }); if (trailingWhitespaceRange) { this.setBufferRange(trailingWhitespaceRange); this.deleteSelectedText(options); } const currentRow = selectedRange.start.row; const nextRow = currentRow + 1; const insertSpace = nextRow <= this.editor.buffer.getLastRow() && this.editor.buffer.lineLengthForRow(nextRow) > 0 && this.editor.buffer.lineLengthForRow(currentRow) > 0; if (insertSpace) this.insertText(' ', options); this.cursor.moveToEndOfLine(); // Remove leading whitespace from the line below this.modifySelection(() => { this.cursor.moveRight(); this.cursor.moveToFirstCharacterOfLine(); }); this.deleteSelectedText(options); if (insertSpace) this.cursor.moveLeft(); } if (joinMarker) { const newSelectedRange = joinMarker.getBufferRange(); this.setBufferRange(newSelectedRange); joinMarker.destroy(); } } // Public: Removes one level of indent from the currently selected rows. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) outdentSelectedRows(options = {}) { if (!this.ensureWritable('outdentSelectedRows', options)) return; const [start, end] = this.getBufferRowRange(); const { buffer } = this.editor; const leadingTabRegex = new RegExp( `^( {1,${this.editor.getTabLength()}}|\t)` ); for (let row = start; row <= end; row++) { const match = buffer.lineForRow(row).match(leadingTabRegex); if (match && match[0].length > 0) { buffer.delete([[row, 0], [row, match[0].length]]); } } } // Public: Sets the indentation level of all selected rows to values suggested // by the relevant grammars. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) autoIndentSelectedRows(options = {}) { if (!this.ensureWritable('autoIndentSelectedRows', options)) return; const [start, end] = this.getBufferRowRange(); return this.editor.autoIndentBufferRows(start, end); } // Public: Wraps the selected lines in comments if they aren't currently part // of a comment. // // Removes the comment if they are currently wrapped in a comment. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) toggleLineComments(options = {}) { if (!this.ensureWritable('toggleLineComments', options)) return; let bufferRowRange = this.getBufferRowRange() || [null, null]; this.editor.toggleLineCommentsForBufferRows(...bufferRowRange, { correctSelection: true, selection: this }); } // Public: Cuts the selection until the end of the screen line. // // * `maintainClipboard` {Boolean} // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) cutToEndOfLine(maintainClipboard, options = {}) { if (!this.ensureWritable('cutToEndOfLine', options)) return; if (this.isEmpty()) this.selectToEndOfLine(); return this.cut(maintainClipboard, false, options.bypassReadOnly); } // Public: Cuts the selection until the end of the buffer line. // // * `maintainClipboard` {Boolean} // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) cutToEndOfBufferLine(maintainClipboard, options = {}) { if (!this.ensureWritable('cutToEndOfBufferLine', options)) return; if (this.isEmpty()) this.selectToEndOfBufferLine(); this.cut(maintainClipboard, false, options.bypassReadOnly); } // Public: Copies the selection to the clipboard and then deletes it. // // * `maintainClipboard` {Boolean} (default: false) See {::copy} // * `fullLine` {Boolean} (default: false) See {::copy} // * `bypassReadOnly` {Boolean} (default: false) Must be `true` to modify text within a read-only editor. cut(maintainClipboard = false, fullLine = false, bypassReadOnly = false) { if (!this.ensureWritable('cut', { bypassReadOnly })) return; this.copy(maintainClipboard, fullLine); this.delete({ bypassReadOnly }); } // Public: Copies the current selection to the clipboard. // // * `maintainClipboard` {Boolean} if `true`, a specific metadata property // is created to store each content copied to the clipboard. The clipboard // `text` still contains the concatenation of the clipboard with the // current selection. (default: false) // * `fullLine` {Boolean} if `true`, the copied text will always be pasted // at the beginning of the line containing the cursor, regardless of the // cursor's horizontal position. (default: false) copy(maintainClipboard = false, fullLine = false) { if (this.isEmpty()) return; const { start, end } = this.getBufferRange(); const selectionText = this.editor.getTextInRange([start, end]); const precedingText = this.editor.getTextInRange([[start.row, 0], start]); const startLevel = this.editor.indentLevelForLine(precedingText); if (maintainClipboard) { let { text: clipboardText, metadata } = this.editor.constructor.clipboard.readWithMetadata(); if (!metadata) metadata = {}; if (!metadata.selections) { metadata.selections = [ { text: clipboardText, indentBasis: metadata.indentBasis, fullLine: metadata.fullLine } ]; } metadata.selections.push({ text: selectionText, indentBasis: startLevel, fullLine }); this.editor.constructor.clipboard.write( [clipboardText, selectionText].join('\n'), metadata ); } else { this.editor.constructor.clipboard.write(selectionText, { indentBasis: startLevel, fullLine }); } } // Public: Creates a fold containing the current selection. fold() { const range = this.getBufferRange(); if (!range.isEmpty()) { this.editor.foldBufferRange(range); this.cursor.setBufferPosition(range.end); } } // Private: Increase the indentation level of the given text by given number // of levels. Leaves the first line unchanged. adjustIndent(lines, indentAdjustment) { for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (indentAdjustment === 0 || line === '') { continue; } else if (indentAdjustment > 0) { lines[i] = this.editor.buildIndentString(indentAdjustment) + line; } else { const currentIndentLevel = this.editor.indentLevelForLine(lines[i]); const indentLevel = Math.max(0, currentIndentLevel + indentAdjustment); lines[i] = line.replace( /^[\t ]+/, this.editor.buildIndentString(indentLevel) ); } } } // Indent the current line(s). // // If the selection is empty, indents the current line if the cursor precedes // non-whitespace characters, and otherwise inserts a tab. If the selection is // non empty, calls {::indentSelectedRows}. // // * `options` (optional) {Object} with the keys: // * `autoIndent` If `true`, the line is indented to an automatically-inferred // level. Otherwise, {TextEditor::getTabText} is inserted. // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) indent({ autoIndent, bypassReadOnly } = {}) { if (!this.ensureWritable('indent', { bypassReadOnly })) return; const { row } = this.cursor.getBufferPosition(); if (this.isEmpty()) { this.cursor.skipLeadingWhitespace(); const desiredIndent = this.editor.suggestedIndentForBufferRow(row); let delta = desiredIndent - this.cursor.getIndentLevel(); if (autoIndent && delta > 0) { if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1); this.insertText(this.editor.buildIndentString(delta), { bypassReadOnly }); } else { this.insertText( this.editor.buildIndentString(1, this.cursor.getBufferColumn()), { bypassReadOnly } ); } } else { this.indentSelectedRows({ bypassReadOnly }); } } // Public: If the selection spans multiple rows, indent all of them. // // * `options` (optional) {Object} with the keys: // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) indentSelectedRows(options = {}) { if (!this.ensureWritable('indentSelectedRows', options)) return; const [start, end] = this.getBufferRowRange(); for (let row = start; row <= end; row++) { if (this.editor.buffer.lineLengthForRow(row) !== 0) { this.editor.buffer.insert([row, 0], this.editor.getTabText()); } } } /* Section: Managing multiple selections */ // Public: Moves the selection down one row. addSelectionBelow() { const range = this.getGoalScreenRange().copy(); const nextRow = range.end.row + 1; for ( let row = nextRow, end = this.editor.getLastScreenRow(); row <= end; row++ ) { range.start.row = row; range.end.row = row; const clippedRange = this.editor.clipScreenRange(range, { skipSoftWrapIndentation: true }); if (range.isEmpty()) { if (range.end.column > 0 && clippedRange.end.column === 0) continue; } else { if (clippedRange.isEmpty()) continue; } const containingSelections = this.editor.selectionsMarkerLayer.findMarkers( { containsScreenRange: clippedRange } ); if (containingSelections.length === 0) { const selection = this.editor.addSelectionForScreenRange(clippedRange); selection.setGoalScreenRange(range); } break; } } // Public: Moves the selection up one row. addSelectionAbove() { const range = this.getGoalScreenRange().copy(); const previousRow = range.end.row - 1; for (let row = previousRow; row >= 0; row--) { range.start.row = row; range.end.row = row; const clippedRange = this.editor.clipScreenRange(range, { skipSoftWrapIndentation: true }); if (range.isEmpty()) { if (range.end.column > 0 && clippedRange.end.column === 0) continue; } else { if (clippedRange.isEmpty()) continue; } const containingSelections = this.editor.selectionsMarkerLayer.findMarkers( { containsScreenRange: clippedRange } ); if (containingSelections.length === 0) { const selection = this.editor.addSelectionForScreenRange(clippedRange); selection.setGoalScreenRange(range); } break; } } // Public: Combines the given selection into this selection and then destroys // the given selection. // // * `otherSelection` A {Selection} to merge with. // * `options` (optional) {Object} options matching those found in {::setBufferRange}. merge(otherSelection, options = {}) { const myGoalScreenRange = this.getGoalScreenRange(); const otherGoalScreenRange = otherSelection.getGoalScreenRange(); if (myGoalScreenRange && otherGoalScreenRange) { options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange); } else { options.goalScreenRange = myGoalScreenRange || otherGoalScreenRange; } const bufferRange = this.getBufferRange().union( otherSelection.getBufferRange() ); this.setBufferRange( bufferRange, Object.assign({ autoscroll: false }, options) ); otherSelection.destroy(); } /* Section: Comparing to other selections */ // Public: Compare this selection's buffer range to another selection's buffer // range. // // See {Range::compare} for more details. // // * `otherSelection` A {Selection} to compare against compare(otherSelection) { return this.marker.compare(otherSelection.marker); } /* Section: Private Utilities */ setGoalScreenRange(range) { this.goalScreenRange = Range.fromObject(range); } getGoalScreenRange() { return this.goalScreenRange || this.getScreenRange(); } markerDidChange(e) { const { oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition } = e; const { oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition } = e; const { textChanged } = e; if (!oldHeadScreenPosition.isEqual(newHeadScreenPosition)) { this.cursor.goalColumn = null; const cursorMovedEvent = { oldBufferPosition: oldHeadBufferPosition, oldScreenPosition: oldHeadScreenPosition, newBufferPosition: newHeadBufferPosition, newScreenPosition: newHeadScreenPosition, textChanged, cursor: this.cursor }; this.cursor.emitter.emit('did-change-position', cursorMovedEvent); this.editor.cursorMoved(cursorMovedEvent); } const rangeChangedEvent = { oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition), oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition), newBufferRange: this.getBufferRange(), newScreenRange: this.getScreenRange(), selection: this }; this.emitter.emit('did-change-range', rangeChangedEvent); this.editor.selectionRangeChanged(rangeChangedEvent); } markerDidDestroy() { if (this.editor.isDestroyed()) return; this.destroyed = true; this.cursor.destroyed = true; this.editor.removeSelection(this); this.cursor.emitter.emit('did-destroy'); this.emitter.emit('did-destroy'); this.cursor.emitter.dispose(); this.emitter.dispose(); } finalize() { if ( !this.initialScreenRange || !this.initialScreenRange.isEqual(this.getScreenRange()) ) { this.initialScreenRange = null; } if (this.isEmpty()) { this.wordwise = false; this.linewise = false; } } autoscroll(options) { if (this.marker.hasTail()) { this.editor.scrollToScreenRange( this.getScreenRange(), Object.assign({ reversed: this.isReversed() }, options) ); } else { this.cursor.autoscroll(options); } } clearAutoscroll() {} modifySelection(fn) { this.retainSelection = true; this.plantTail(); fn(); this.retainSelection = false; } // Sets the marker's tail to the same position as the marker's head. // // This only works if there isn't already a tail position. // // Returns a {Point} representing the new tail position. plantTail() { this.marker.plantTail(); } }; ================================================ FILE: src/selectors.js ================================================ module.exports = { selectorMatchesAnyScope, matcherForSelector }; const { isSubset } = require('underscore-plus'); // Private: Parse a selector into parts. // If already parsed, returns the selector unmodified. // // * `selector` a {String|Array} specifying what to match // Returns selector parts, an {Array}. function parse(selector) { return typeof selector === 'string' ? selector.replace(/^\./, '').split('.') : selector; } const always = scope => true; // Essential: Return a matcher function for a selector. // // * selector, a {String} selector // Returns {(scope: String) -> Boolean}, a matcher function returning // true iff the scope matches the selector. function matcherForSelector(selector) { const parts = parse(selector); if (typeof parts === 'function') return parts; return selector ? scope => isSubset(parts, parse(scope)) : always; } // Essential: Return true iff the selector matches any provided scope. // // * {String} selector // * {Array} scopes // Returns {Boolean} true if any scope matches the selector. function selectorMatchesAnyScope(selector, scopes) { return !selector || scopes.some(matcherForSelector(selector)); } ================================================ FILE: src/special-token-symbols.coffee ================================================ module.exports = { SoftTab: Symbol('SoftTab') HardTab: Symbol('HardTab') PairedCharacter: Symbol('PairedCharacter') SoftWrapIndent: Symbol('SoftWrapIndent') } ================================================ FILE: src/startup-time.js ================================================ let startTime; let markers = []; module.exports = { setStartTime() { if (!startTime) { startTime = Date.now(); } }, addMarker(label, dateTime) { if (!startTime) { return; } dateTime = dateTime || Date.now(); markers.push({ label, time: dateTime - startTime }); }, importData(data) { startTime = data.startTime; markers = data.markers; }, exportData() { if (!startTime) { return undefined; } return { startTime, markers }; }, deleteData() { startTime = undefined; markers = []; } }; ================================================ FILE: src/state-store.js ================================================ 'use strict'; module.exports = class StateStore { constructor(databaseName, version) { this.connected = false; this.databaseName = databaseName; this.version = version; } get dbPromise() { if (!this._dbPromise) { this._dbPromise = new Promise(resolve => { const dbOpenRequest = indexedDB.open(this.databaseName, this.version); dbOpenRequest.onupgradeneeded = event => { let db = event.target.result; db.onerror = error => { atom.notifications.addFatalError('Error loading database', { stack: new Error('Error loading database').stack, dismissable: true }); console.error('Error loading database', error); }; db.createObjectStore('states'); }; dbOpenRequest.onsuccess = () => { this.connected = true; resolve(dbOpenRequest.result); }; dbOpenRequest.onerror = error => { atom.notifications.addFatalError('Could not connect to indexedDB', { stack: new Error('Could not connect to indexedDB').stack, dismissable: true }); console.error('Could not connect to indexedDB', error); this.connected = false; resolve(null); }; }); } return this._dbPromise; } isConnected() { return this.connected; } connect() { return this.dbPromise.then(db => !!db); } save(key, value) { return new Promise((resolve, reject) => { this.dbPromise.then(db => { if (db == null) return resolve(); const request = db .transaction(['states'], 'readwrite') .objectStore('states') .put({ value: value, storedAt: new Date().toString() }, key); request.onsuccess = resolve; request.onerror = reject; }); }); } load(key) { return this.dbPromise.then(db => { if (!db) return; return new Promise((resolve, reject) => { const request = db .transaction(['states']) .objectStore('states') .get(key); request.onsuccess = event => { let result = event.target.result; if (result && !result.isJSON) { resolve(result.value); } else { resolve(null); } }; request.onerror = event => reject(event); }); }); } delete(key) { return new Promise((resolve, reject) => { this.dbPromise.then(db => { if (db == null) return resolve(); const request = db .transaction(['states'], 'readwrite') .objectStore('states') .delete(key); request.onsuccess = resolve; request.onerror = reject; }); }); } clear() { return this.dbPromise.then(db => { if (!db) return; return new Promise((resolve, reject) => { const request = db .transaction(['states'], 'readwrite') .objectStore('states') .clear(); request.onsuccess = resolve; request.onerror = reject; }); }); } count() { return this.dbPromise.then(db => { if (!db) return; return new Promise((resolve, reject) => { const request = db .transaction(['states']) .objectStore('states') .count(); request.onsuccess = () => { resolve(request.result); }; request.onerror = reject; }); }); } }; ================================================ FILE: src/storage-folder.js ================================================ const path = require('path'); const fs = require('fs-plus'); module.exports = class StorageFolder { constructor(containingPath) { if (containingPath) { this.path = path.join(containingPath, 'storage'); } } store(name, object) { return new Promise((resolve, reject) => { if (!this.path) return resolve(); fs.writeFile( this.pathForKey(name), JSON.stringify(object), 'utf8', error => (error ? reject(error) : resolve()) ); }); } load(name) { return new Promise(resolve => { if (!this.path) return resolve(null); const statePath = this.pathForKey(name); fs.readFile(statePath, 'utf8', (error, stateString) => { if (error && error.code !== 'ENOENT') { console.warn( `Error reading state file: ${statePath}`, error.stack, error ); } if (!stateString) return resolve(null); try { resolve(JSON.parse(stateString)); } catch (error) { console.warn( `Error parsing state file: ${statePath}`, error.stack, error ); resolve(null); } }); }); } pathForKey(name) { return path.join(this.getPath(), name); } getPath() { return this.path; } }; ================================================ FILE: src/style-manager.js ================================================ const { Emitter, Disposable } = require('event-kit'); const crypto = require('crypto'); const fs = require('fs-plus'); const path = require('path'); const postcss = require('postcss'); const selectorParser = require('postcss-selector-parser'); const { createStylesElement } = require('./styles-element'); const DEPRECATED_SYNTAX_SELECTORS = require('./deprecated-syntax-selectors'); // Extended: A singleton instance of this class available via `atom.styles`, // which you can use to globally query and observe the set of active style // sheets. The `StyleManager` doesn't add any style elements to the DOM on its // own, but is instead subscribed to by individual `` elements, // which clone and attach style elements in different contexts. module.exports = class StyleManager { constructor() { this.emitter = new Emitter(); this.styleElements = []; this.styleElementsBySourcePath = {}; this.deprecationsBySourcePath = {}; } initialize({ configDirPath }) { this.configDirPath = configDirPath; if (this.configDirPath != null) { this.cacheDirPath = path.join( this.configDirPath, 'compile-cache', 'style-manager' ); } } /* Section: Event Subscription */ // Extended: Invoke `callback` for all current and future style elements. // // * `callback` {Function} that is called with style elements. // * `styleElement` An `HTMLStyleElement` instance. The `.sheet` property // will be null because this element isn't attached to the DOM. If you want // to attach this element to the DOM, be sure to clone it first by calling // `.cloneNode(true)` on it. The style element will also have the following // non-standard properties: // * `sourcePath` A {String} containing the path from which the style // element was loaded. // * `context` A {String} indicating the target context of the style // element. // // Returns a {Disposable} on which `.dispose()` can be called to cancel the // subscription. observeStyleElements(callback) { for (let styleElement of this.getStyleElements()) { callback(styleElement); } return this.onDidAddStyleElement(callback); } // Extended: Invoke `callback` when a style element is added. // // * `callback` {Function} that is called with style elements. // * `styleElement` An `HTMLStyleElement` instance. The `.sheet` property // will be null because this element isn't attached to the DOM. If you want // to attach this element to the DOM, be sure to clone it first by calling // `.cloneNode(true)` on it. The style element will also have the following // non-standard properties: // * `sourcePath` A {String} containing the path from which the style // element was loaded. // * `context` A {String} indicating the target context of the style // element. // // Returns a {Disposable} on which `.dispose()` can be called to cancel the // subscription. onDidAddStyleElement(callback) { return this.emitter.on('did-add-style-element', callback); } // Extended: Invoke `callback` when a style element is removed. // // * `callback` {Function} that is called with style elements. // * `styleElement` An `HTMLStyleElement` instance. // // Returns a {Disposable} on which `.dispose()` can be called to cancel the // subscription. onDidRemoveStyleElement(callback) { return this.emitter.on('did-remove-style-element', callback); } // Extended: Invoke `callback` when an existing style element is updated. // // * `callback` {Function} that is called with style elements. // * `styleElement` An `HTMLStyleElement` instance. The `.sheet` property // will be null because this element isn't attached to the DOM. The style // element will also have the following non-standard properties: // * `sourcePath` A {String} containing the path from which the style // element was loaded. // * `context` A {String} indicating the target context of the style // element. // // Returns a {Disposable} on which `.dispose()` can be called to cancel the // subscription. onDidUpdateStyleElement(callback) { return this.emitter.on('did-update-style-element', callback); } onDidUpdateDeprecations(callback) { return this.emitter.on('did-update-deprecations', callback); } /* Section: Reading Style Elements */ // Extended: Get all loaded style elements. getStyleElements() { return this.styleElements.slice(); } addStyleSheet(source, params = {}) { let styleElement; let updated; if ( params.sourcePath != null && this.styleElementsBySourcePath[params.sourcePath] != null ) { updated = true; styleElement = this.styleElementsBySourcePath[params.sourcePath]; } else { updated = false; styleElement = document.createElement('style'); if (params.sourcePath != null) { styleElement.sourcePath = params.sourcePath; styleElement.setAttribute('source-path', params.sourcePath); } if (params.context != null) { styleElement.context = params.context; styleElement.setAttribute('context', params.context); } if (params.priority != null) { styleElement.priority = params.priority; styleElement.setAttribute('priority', params.priority); } } if (params.skipDeprecatedSelectorsTransformation) { styleElement.textContent = source; } else { const transformed = this.upgradeDeprecatedSelectorsForStyleSheet( source, params.context ); styleElement.textContent = transformed.source; if (transformed.deprecationMessage) { this.deprecationsBySourcePath[params.sourcePath] = { message: transformed.deprecationMessage }; this.emitter.emit('did-update-deprecations'); } } if (updated) { this.emitter.emit('did-update-style-element', styleElement); } else { this.addStyleElement(styleElement); } return new Disposable(() => { this.removeStyleElement(styleElement); }); } addStyleElement(styleElement) { let insertIndex = this.styleElements.length; if (styleElement.priority != null) { for (let i = 0; i < this.styleElements.length; i++) { const existingElement = this.styleElements[i]; if (existingElement.priority > styleElement.priority) { insertIndex = i; break; } } } this.styleElements.splice(insertIndex, 0, styleElement); if ( styleElement.sourcePath != null && this.styleElementsBySourcePath[styleElement.sourcePath] == null ) { this.styleElementsBySourcePath[styleElement.sourcePath] = styleElement; } this.emitter.emit('did-add-style-element', styleElement); } removeStyleElement(styleElement) { const index = this.styleElements.indexOf(styleElement); if (index !== -1) { this.styleElements.splice(index, 1); if (styleElement.sourcePath != null) { delete this.styleElementsBySourcePath[styleElement.sourcePath]; } this.emitter.emit('did-remove-style-element', styleElement); } } upgradeDeprecatedSelectorsForStyleSheet(styleSheet, context) { if (this.cacheDirPath != null) { const hash = crypto.createHash('sha1'); if (context != null) { hash.update(context); } hash.update(styleSheet); const cacheFilePath = path.join(this.cacheDirPath, hash.digest('hex')); try { return JSON.parse(fs.readFileSync(cacheFilePath)); } catch (e) { const transformed = transformDeprecatedShadowDOMSelectors( styleSheet, context ); fs.writeFileSync(cacheFilePath, JSON.stringify(transformed)); return transformed; } } else { return transformDeprecatedShadowDOMSelectors(styleSheet, context); } } getDeprecations() { return this.deprecationsBySourcePath; } clearDeprecations() { this.deprecationsBySourcePath = {}; } getSnapshot() { return this.styleElements.slice(); } restoreSnapshot(styleElementsToRestore) { for (let styleElement of this.getStyleElements()) { if (!styleElementsToRestore.includes(styleElement)) { this.removeStyleElement(styleElement); } } const existingStyleElements = this.getStyleElements(); for (let styleElement of styleElementsToRestore) { if (!existingStyleElements.includes(styleElement)) { this.addStyleElement(styleElement); } } } buildStylesElement() { const stylesElement = createStylesElement(); stylesElement.initialize(this); return stylesElement; } /* Section: Paths */ // Extended: Get the path of the user style sheet in `~/.atom`. // // Returns a {String}. getUserStyleSheetPath() { if (this.configDirPath == null) { return ''; } else { const stylesheetPath = fs.resolve( path.join(this.configDirPath, 'styles'), ['css', 'less'] ); if (fs.isFileSync(stylesheetPath)) { return stylesheetPath; } else { return path.join(this.configDirPath, 'styles.less'); } } } }; function transformDeprecatedShadowDOMSelectors(css, context) { const transformedSelectors = []; let transformedSource; try { transformedSource = postcss.parse(css); } catch (e) { transformedSource = null; } if (transformedSource) { transformedSource.walkRules(rule => { const transformedSelector = selectorParser(selectors => { selectors.each(selector => { const firstNode = selector.nodes[0]; if ( context === 'atom-text-editor' && firstNode.type === 'pseudo' && firstNode.value === ':host' ) { const atomTextEditorElementNode = selectorParser.tag({ value: 'atom-text-editor' }); firstNode.replaceWith(atomTextEditorElementNode); } let previousNodeIsAtomTextEditor = false; let targetsAtomTextEditorShadow = context === 'atom-text-editor'; let previousNode; selector.each(node => { if (targetsAtomTextEditorShadow && node.type === 'class') { if (DEPRECATED_SYNTAX_SELECTORS.has(node.value)) { node.value = `syntax--${node.value}`; } } else { if ( previousNodeIsAtomTextEditor && node.type === 'pseudo' && node.value === '::shadow' ) { node.type = 'className'; node.value = '.editor'; targetsAtomTextEditorShadow = true; } } previousNode = node; if (node.type === 'combinator') { previousNodeIsAtomTextEditor = false; } else if ( previousNode.type === 'tag' && previousNode.value === 'atom-text-editor' ) { previousNodeIsAtomTextEditor = true; } }); }); }).processSync(rule.selector, { lossless: true }); if (transformedSelector !== rule.selector) { transformedSelectors.push({ before: rule.selector, after: transformedSelector }); rule.selector = transformedSelector; } }); let deprecationMessage; if (transformedSelectors.length > 0) { deprecationMessage = 'Starting from Atom v1.13.0, the contents of `atom-text-editor` elements '; deprecationMessage += 'are no longer encapsulated within a shadow DOM boundary. '; deprecationMessage += 'This means you should stop using `:host` and `::shadow` '; deprecationMessage += 'pseudo-selectors, and prepend all your syntax selectors with `syntax--`. '; deprecationMessage += 'To prevent breakage with existing style sheets, Atom will automatically '; deprecationMessage += 'upgrade the following selectors:\n\n'; deprecationMessage += transformedSelectors .map(selector => `* \`${selector.before}\` => \`${selector.after}\``) .join('\n\n') + '\n\n'; deprecationMessage += 'Automatic translation of selectors will be removed in a few release cycles to minimize startup time. '; deprecationMessage += 'Please, make sure to upgrade the above selectors as soon as possible.'; } return { source: transformedSource.toString(), deprecationMessage }; } else { // CSS was malformed so we don't transform it. return { source: css }; } } ================================================ FILE: src/styles-element.js ================================================ const { Emitter, CompositeDisposable } = require('event-kit'); class StylesElement extends HTMLElement { constructor() { super(); this.subscriptions = new CompositeDisposable(); this.emitter = new Emitter(); this.styleElementClonesByOriginalElement = new WeakMap(); this.context = null; } onDidAddStyleElement(callback) { this.emitter.on('did-add-style-element', callback); } onDidRemoveStyleElement(callback) { this.emitter.on('did-remove-style-element', callback); } onDidUpdateStyleElement(callback) { this.emitter.on('did-update-style-element', callback); } connectedCallback() { let left; this.context = (left = this.getAttribute('context')) != null ? left : undefined; } disconnectedCallback() { this.subscriptions.dispose(); this.subscriptions = new CompositeDisposable(); } static get observedAttributes() { return ['context']; } attributeChangedCallback(attrName) { if (attrName === 'context') { return this.contextChanged(); } } initialize(styleManager) { this.styleManager = styleManager; if (this.styleManager == null) { throw new Error( 'Must pass a styleManager parameter when initializing a StylesElement' ); } this.subscriptions.add( this.styleManager.observeStyleElements(this.styleElementAdded.bind(this)) ); this.subscriptions.add( this.styleManager.onDidRemoveStyleElement( this.styleElementRemoved.bind(this) ) ); this.subscriptions.add( this.styleManager.onDidUpdateStyleElement( this.styleElementUpdated.bind(this) ) ); } contextChanged() { if (this.subscriptions == null) { return; } for (let child of Array.from(Array.prototype.slice.call(this.children))) { this.styleElementRemoved(child); } this.context = this.getAttribute('context'); for (let styleElement of Array.from(this.styleManager.getStyleElements())) { this.styleElementAdded(styleElement); } } styleElementAdded(styleElement) { let insertBefore; if (!this.styleElementMatchesContext(styleElement)) { return; } const styleElementClone = styleElement.cloneNode(true); styleElementClone.sourcePath = styleElement.sourcePath; styleElementClone.context = styleElement.context; styleElementClone.priority = styleElement.priority; this.styleElementClonesByOriginalElement.set( styleElement, styleElementClone ); const { priority } = styleElement; if (priority != null) { for (let child of this.children) { if (child.priority > priority) { insertBefore = child; break; } } } this.insertBefore(styleElementClone, insertBefore); this.emitter.emit('did-add-style-element', styleElementClone); } styleElementRemoved(styleElement) { let left; if (!this.styleElementMatchesContext(styleElement)) { return; } const styleElementClone = (left = this.styleElementClonesByOriginalElement.get(styleElement)) != null ? left : styleElement; styleElementClone.remove(); this.emitter.emit('did-remove-style-element', styleElementClone); } styleElementUpdated(styleElement) { if (!this.styleElementMatchesContext(styleElement)) { return; } const styleElementClone = this.styleElementClonesByOriginalElement.get( styleElement ); styleElementClone.textContent = styleElement.textContent; this.emitter.emit('did-update-style-element', styleElementClone); } styleElementMatchesContext(styleElement) { return this.context == null || styleElement.context === this.context; } } window.customElements.define('atom-styles', StylesElement); function createStylesElement() { return document.createElement('atom-styles'); } module.exports = { createStylesElement }; ================================================ FILE: src/syntax-scope-map.js ================================================ const parser = require('postcss-selector-parser'); module.exports = class SyntaxScopeMap { constructor(resultsBySelector) { this.namedScopeTable = {}; this.anonymousScopeTable = {}; for (let selector in resultsBySelector) { this.addSelector(selector, resultsBySelector[selector]); } setTableDefaults(this.namedScopeTable, true); setTableDefaults(this.anonymousScopeTable, false); } addSelector(selector, result) { parser(parseResult => { for (let selectorNode of parseResult.nodes) { let currentTable = null; let currentIndexValue = null; for (let i = selectorNode.nodes.length - 1; i >= 0; i--) { const termNode = selectorNode.nodes[i]; switch (termNode.type) { case 'tag': if (!currentTable) currentTable = this.namedScopeTable; if (!currentTable[termNode.value]) currentTable[termNode.value] = {}; currentTable = currentTable[termNode.value]; if (currentIndexValue != null) { if (!currentTable.indices) currentTable.indices = {}; if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}; currentTable = currentTable.indices[currentIndexValue]; currentIndexValue = null; } break; case 'string': if (!currentTable) currentTable = this.anonymousScopeTable; const value = termNode.value.slice(1, -1).replace(/\\"/g, '"'); if (!currentTable[value]) currentTable[value] = {}; currentTable = currentTable[value]; if (currentIndexValue != null) { if (!currentTable.indices) currentTable.indices = {}; if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}; currentTable = currentTable.indices[currentIndexValue]; currentIndexValue = null; } break; case 'universal': if (currentTable) { if (!currentTable['*']) currentTable['*'] = {}; currentTable = currentTable['*']; } else { if (!this.namedScopeTable['*']) { this.namedScopeTable['*'] = this.anonymousScopeTable[ '*' ] = {}; } currentTable = this.namedScopeTable['*']; } if (currentIndexValue != null) { if (!currentTable.indices) currentTable.indices = {}; if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}; currentTable = currentTable.indices[currentIndexValue]; currentIndexValue = null; } break; case 'combinator': if (currentIndexValue != null) { rejectSelector(selector); } if (termNode.value === '>') { if (!currentTable.parents) currentTable.parents = {}; currentTable = currentTable.parents; } else { rejectSelector(selector); } break; case 'pseudo': if (termNode.value === ':nth-child') { currentIndexValue = termNode.nodes[0].nodes[0].value; } else { rejectSelector(selector); } break; default: rejectSelector(selector); } } currentTable.result = result; } }).process(selector); } get(nodeTypes, childIndices, leafIsNamed = true) { let result; let i = nodeTypes.length - 1; let currentTable = leafIsNamed ? this.namedScopeTable[nodeTypes[i]] : this.anonymousScopeTable[nodeTypes[i]]; if (!currentTable) currentTable = this.namedScopeTable['*']; while (currentTable) { if (currentTable.indices && currentTable.indices[childIndices[i]]) { currentTable = currentTable.indices[childIndices[i]]; } if (currentTable.result != null) { result = currentTable.result; } if (i === 0) break; i--; currentTable = currentTable.parents && (currentTable.parents[nodeTypes[i]] || currentTable.parents['*']); } return result; } }; function setTableDefaults(table, allowWildcardSelector) { const defaultTypeTable = allowWildcardSelector ? table['*'] : null; for (let type in table) { let typeTable = table[type]; if (typeTable === defaultTypeTable) continue; if (defaultTypeTable) { mergeTable(typeTable, defaultTypeTable); } if (typeTable.parents) { setTableDefaults(typeTable.parents, true); } for (let key in typeTable.indices) { const indexTable = typeTable.indices[key]; mergeTable(indexTable, typeTable, false); if (indexTable.parents) { setTableDefaults(indexTable.parents, true); } } } } function mergeTable(table, defaultTable, mergeIndices = true) { if (mergeIndices && defaultTable.indices) { if (!table.indices) table.indices = {}; for (let key in defaultTable.indices) { if (!table.indices[key]) table.indices[key] = {}; mergeTable(table.indices[key], defaultTable.indices[key]); } } if (defaultTable.parents) { if (!table.parents) table.parents = {}; for (let key in defaultTable.parents) { if (!table.parents[key]) table.parents[key] = {}; mergeTable(table.parents[key], defaultTable.parents[key]); } } if (defaultTable.result != null && table.result == null) { table.result = defaultTable.result; } } function rejectSelector(selector) { throw new TypeError(`Unsupported selector '${selector}'`); } ================================================ FILE: src/task-bootstrap.js ================================================ const { userAgent } = process.env; const [compileCachePath, taskPath] = process.argv.slice(2); const CompileCache = require('./compile-cache'); CompileCache.setCacheDirectory(compileCachePath); CompileCache.install(`${process.resourcesPath}`, require); const setupGlobals = function() { global.attachEvent = function() {}; const console = { warn() { return global.emit('task:warn', ...arguments); }, log() { return global.emit('task:log', ...arguments); }, error() { return global.emit('task:error', ...arguments); }, trace() {} }; global.__defineGetter__('console', () => console); global.document = { createElement() { return { setAttribute() {}, getElementsByTagName() { return []; }, appendChild() {} }; }, documentElement: { insertBefore() {}, removeChild() {} }, getElementById() { return {}; }, createComment() { return {}; }, createDocumentFragment() { return {}; } }; global.emit = (event, ...args) => process.send({ event, args }); global.navigator = { userAgent }; return (global.window = global); }; const handleEvents = function() { process.on('uncaughtException', error => console.error(error.message, error.stack) ); return process.on('message', function({ event, args } = {}) { if (event !== 'start') { return; } let isAsync = false; const async = function() { isAsync = true; return result => global.emit('task:completed', result); }; const result = handler.bind({ async })(...args); if (!isAsync) { return global.emit('task:completed', result); } }); }; const setupDeprecations = function() { const Grim = require('grim'); return Grim.on('updated', function() { const deprecations = Grim.getDeprecations().map(deprecation => deprecation.serialize() ); Grim.clearDeprecations(); return global.emit('task:deprecations', deprecations); }); }; setupGlobals(); handleEvents(); setupDeprecations(); const handler = require(taskPath); ================================================ FILE: src/task.coffee ================================================ _ = require 'underscore-plus' ChildProcess = require 'child_process' {Emitter} = require 'event-kit' Grim = require 'grim' # Extended: Run a node script in a separate process. # # Used by the fuzzy-finder and [find in project](https://github.com/atom/atom/blob/master/src/scan-handler.coffee). # # For a real-world example, see the [scan-handler](https://github.com/atom/atom/blob/master/src/scan-handler.coffee) # and the [instantiation of the task](https://github.com/atom/atom/blob/4a20f13162f65afc816b512ad7201e528c3443d7/src/project.coffee#L245). # # ## Examples # # In your package code: # # ```coffee # {Task} = require 'atom' # # task = Task.once '/path/to/task-file.coffee', parameter1, parameter2, -> # console.log 'task has finished' # # task.on 'some-event-from-the-task', (data) => # console.log data.someString # prints 'yep this is it' # ``` # # In `'/path/to/task-file.coffee'`: # # ```coffee # module.exports = (parameter1, parameter2) -> # # Indicates that this task will be async. # # Call the `callback` to finish the task # callback = @async() # # emit('some-event-from-the-task', {someString: 'yep this is it'}) # # callback() # ``` module.exports = class Task # Public: A helper method to easily launch and run a task once. # # * `taskPath` The {String} path to the CoffeeScript/JavaScript file which # exports a single {Function} to execute. # * `args` The arguments to pass to the exported function. # # Returns the created {Task}. @once: (taskPath, args...) -> task = new Task(taskPath) task.once 'task:completed', -> task.terminate() task.start(args...) task # Called upon task completion. # # It receives the same arguments that were passed to the task. # # If subclassed, this is intended to be overridden. However if {::start} # receives a completion callback, this is overridden. callback: null # Public: Creates a task. You should probably use {.once} # # * `taskPath` The {String} path to the CoffeeScript/JavaScript file that # exports a single {Function} to execute. constructor: (taskPath) -> @emitter = new Emitter compileCachePath = require('./compile-cache').getCacheDirectory() taskPath = require.resolve(taskPath) env = Object.assign({}, process.env, {userAgent: navigator.userAgent}) @childProcess = ChildProcess.fork require.resolve('./task-bootstrap'), [compileCachePath, taskPath], {env, silent: true} @on "task:log", -> console.log(arguments...) @on "task:warn", -> console.warn(arguments...) @on "task:error", -> console.error(arguments...) @on "task:deprecations", (deprecations) -> Grim.addSerializedDeprecation(deprecation) for deprecation in deprecations return @on "task:completed", (args...) => @callback?(args...) @handleEvents() # Routes messages from the child to the appropriate event. handleEvents: -> @childProcess.removeAllListeners() @childProcess.on 'message', ({event, args}) => @emitter.emit(event, args) if @childProcess? # Catch the errors that happened before task-bootstrap. if @childProcess.stdout? @childProcess.stdout.removeAllListeners() @childProcess.stdout.on 'data', (data) -> console.log data.toString() if @childProcess.stderr? @childProcess.stderr.removeAllListeners() @childProcess.stderr.on 'data', (data) -> console.error data.toString() # Public: Starts the task. # # Throws an error if this task has already been terminated or if sending a # message to the child process fails. # # * `args` The arguments to pass to the function exported by this task's script. # * `callback` (optional) A {Function} to call when the task completes. start: (args..., callback) -> throw new Error('Cannot start terminated process') unless @childProcess? @handleEvents() if _.isFunction(callback) @callback = callback else args.push(callback) @send({event: 'start', args}) undefined # Public: Send message to the task. # # Throws an error if this task has already been terminated or if sending a # message to the child process fails. # # * `message` The message to send to the task. send: (message) -> if @childProcess? @childProcess.send(message) else throw new Error('Cannot send message to terminated process') undefined # Public: Call a function when an event is emitted by the child process # # * `eventName` The {String} name of the event to handle. # * `callback` The {Function} to call when the event is emitted. # # Returns a {Disposable} that can be used to stop listening for the event. on: (eventName, callback) -> @emitter.on eventName, (args) -> callback(args...) once: (eventName, callback) -> disposable = @on eventName, (args...) -> disposable.dispose() callback(args...) # Public: Forcefully stop the running task. # # No more events are emitted once this method is called. terminate: -> return false unless @childProcess? @childProcess.removeAllListeners() @childProcess.stdout?.removeAllListeners() @childProcess.stderr?.removeAllListeners() @childProcess.kill() @childProcess = null true # Public: Cancel the running task and emit an event if it was canceled. # # Returns a {Boolean} indicating whether the task was terminated. cancel: -> didForcefullyTerminate = @terminate() if didForcefullyTerminate @emitter.emit('task:cancelled') didForcefullyTerminate ================================================ FILE: src/test.ejs ================================================ <% if something() { %>
          <%= html `ok how about this` %>
          <% } %> ================================================ FILE: src/text-editor-component.js ================================================ /* global ResizeObserver */ const etch = require('etch'); const { Point, Range } = require('text-buffer'); const LineTopIndex = require('line-top-index'); const TextEditor = require('./text-editor'); const { isPairedCharacter } = require('./text-utils'); const electron = require('electron'); const clipboard = electron.clipboard; const $ = etch.dom; let TextEditorElement; const DEFAULT_ROWS_PER_TILE = 6; const NORMAL_WIDTH_CHARACTER = 'x'; const DOUBLE_WIDTH_CHARACTER = '我'; const HALF_WIDTH_CHARACTER = 'ハ'; const KOREAN_CHARACTER = '세'; const NBSP_CHARACTER = '\u00a0'; const ZERO_WIDTH_NBSP_CHARACTER = '\ufeff'; const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40; const CURSOR_BLINK_RESUME_DELAY = 300; const CURSOR_BLINK_PERIOD = 800; function scaleMouseDragAutoscrollDelta(delta) { return Math.pow(delta / 3, 3) / 280; } module.exports = class TextEditorComponent { static setScheduler(scheduler) { etch.setScheduler(scheduler); } static getScheduler() { return etch.getScheduler(); } static didUpdateStyles() { if (this.attachedComponents) { this.attachedComponents.forEach(component => { component.didUpdateStyles(); }); } } static didUpdateScrollbarStyles() { if (this.attachedComponents) { this.attachedComponents.forEach(component => { component.didUpdateScrollbarStyles(); }); } } constructor(props) { this.props = props; if (!props.model) { props.model = new TextEditor({ mini: props.mini, readOnly: props.readOnly }); } this.props.model.component = this; if (props.element) { this.element = props.element; } else { if (!TextEditorElement) TextEditorElement = require('./text-editor-element'); this.element = TextEditorElement.createTextEditorElement(); } this.element.initialize(this); this.virtualNode = $('atom-text-editor'); this.virtualNode.domNode = this.element; this.refs = {}; this.updateSync = this.updateSync.bind(this); this.didBlurHiddenInput = this.didBlurHiddenInput.bind(this); this.didFocusHiddenInput = this.didFocusHiddenInput.bind(this); this.didPaste = this.didPaste.bind(this); this.didTextInput = this.didTextInput.bind(this); this.didKeydown = this.didKeydown.bind(this); this.didKeyup = this.didKeyup.bind(this); this.didKeypress = this.didKeypress.bind(this); this.didCompositionStart = this.didCompositionStart.bind(this); this.didCompositionUpdate = this.didCompositionUpdate.bind(this); this.didCompositionEnd = this.didCompositionEnd.bind(this); this.updatedSynchronously = this.props.updatedSynchronously; this.didScrollDummyScrollbar = this.didScrollDummyScrollbar.bind(this); this.didMouseDownOnContent = this.didMouseDownOnContent.bind(this); this.debouncedResumeCursorBlinking = debounce( this.resumeCursorBlinking.bind(this), this.props.cursorBlinkResumeDelay || CURSOR_BLINK_RESUME_DELAY ); this.lineTopIndex = new LineTopIndex(); this.lineNodesPool = new NodePool(); this.updateScheduled = false; this.suppressUpdates = false; this.hasInitialMeasurements = false; this.measurements = { lineHeight: 0, baseCharacterWidth: 0, doubleWidthCharacterWidth: 0, halfWidthCharacterWidth: 0, koreanCharacterWidth: 0, gutterContainerWidth: 0, lineNumberGutterWidth: 0, clientContainerHeight: 0, clientContainerWidth: 0, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, longestLineWidth: 0 }; this.derivedDimensionsCache = {}; this.visible = false; this.cursorsBlinking = false; this.cursorsBlinkedOff = false; this.nextUpdateOnlyBlinksCursors = null; 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 horizontal pixel positions this.blockDecorationsToMeasure = new Set(); this.blockDecorationsByElement = new WeakMap(); this.blockDecorationSentinel = document.createElement('div'); this.blockDecorationSentinel.style.height = '1px'; this.heightsByBlockDecoration = new WeakMap(); this.blockDecorationResizeObserver = new ResizeObserver( this.didResizeBlockDecorations.bind(this) ); this.lineComponentsByScreenLineId = new Map(); this.overlayComponents = new Set(); this.shouldRenderDummyScrollbars = true; this.remeasureScrollbars = false; this.pendingAutoscroll = null; this.scrollTopPending = false; this.scrollLeftPending = false; this.scrollTop = 0; this.scrollLeft = 0; this.previousScrollWidth = 0; this.previousScrollHeight = 0; this.lastKeydown = null; this.lastKeydownBeforeKeypress = null; this.accentedCharacterMenuIsOpen = false; this.remeasureGutterDimensions = false; this.guttersToRender = [this.props.model.getLineNumberGutter()]; this.guttersVisibility = [this.guttersToRender[0].visible]; this.idsByTileStartRow = new Map(); this.nextTileId = 0; this.renderedTileStartRows = []; this.showLineNumbers = this.props.model.doesShowLineNumbers(); this.lineNumbersToRender = { maxDigits: 2, bufferRows: [], screenRows: [], keys: [], softWrappedFlags: [], foldableFlags: [] }; this.decorationsToRender = { lineNumbers: new Map(), lines: null, highlights: [], cursors: [], overlays: [], customGutter: new Map(), blocks: new Map(), text: [] }; this.decorationsToMeasure = { highlights: [], cursors: new Map() }; this.textDecorationsByMarker = new Map(); this.textDecorationBoundaries = []; this.pendingScrollTopRow = this.props.initialScrollTopRow; this.pendingScrollLeftColumn = this.props.initialScrollLeftColumn; this.tabIndex = this.props.element && this.props.element.tabIndex ? this.props.element.tabIndex : -1; this.measuredContent = false; this.queryGuttersToRender(); this.queryMaxLineNumberDigits(); this.observeBlockDecorations(); this.updateClassList(); etch.updateSync(this); } update(props) { if (props.model !== this.props.model) { this.props.model.component = null; props.model.component = this; } this.props = props; this.scheduleUpdate(); } pixelPositionForScreenPosition({ row, column }) { const top = this.pixelPositionAfterBlocksForRow(row); let left = column === 0 ? 0 : this.pixelLeftForRowAndColumn(row, column); if (left == null) { this.requestHorizontalMeasurement(row, column); this.updateSync(); left = this.pixelLeftForRowAndColumn(row, column); } return { top, left }; } scheduleUpdate(nextUpdateOnlyBlinksCursors = false) { if (!this.visible) return; if (this.suppressUpdates) return; this.nextUpdateOnlyBlinksCursors = this.nextUpdateOnlyBlinksCursors !== false && nextUpdateOnlyBlinksCursors === true; if (this.updatedSynchronously) { this.updateSync(); } else if (!this.updateScheduled) { this.updateScheduled = true; etch.getScheduler().updateDocument(() => { if (this.updateScheduled) this.updateSync(true); }); } } updateSync(useScheduler = false) { // Don't proceed if we know we are not visible if (!this.visible) { this.updateScheduled = false; return; } // Don't proceed if we have to pay for a measurement anyway and detect // that we are no longer visible. if ( (this.remeasureCharacterDimensions || this.remeasureAllBlockDecorations) && !this.isVisible() ) { if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise(); this.updateScheduled = false; return; } const onlyBlinkingCursors = this.nextUpdateOnlyBlinksCursors; this.nextUpdateOnlyBlinksCursors = null; if (useScheduler && onlyBlinkingCursors) { this.refs.cursorsAndInput.updateCursorBlinkSync(this.cursorsBlinkedOff); if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise(); this.updateScheduled = false; return; } if (this.remeasureCharacterDimensions) { const originalLineHeight = this.getLineHeight(); const originalBaseCharacterWidth = this.getBaseCharacterWidth(); const scrollTopRow = this.getScrollTopRow(); const scrollLeftColumn = this.getScrollLeftColumn(); this.measureCharacterDimensions(); this.measureGutterDimensions(); this.queryLongestLine(); if (this.getLineHeight() !== originalLineHeight) { this.setScrollTopRow(scrollTopRow); } if (this.getBaseCharacterWidth() !== originalBaseCharacterWidth) { this.setScrollLeftColumn(scrollLeftColumn); } this.remeasureCharacterDimensions = false; } this.measureBlockDecorations(); this.updateSyncBeforeMeasuringContent(); if (useScheduler === true) { const scheduler = etch.getScheduler(); scheduler.readDocument(() => { const restartFrame = this.measureContentDuringUpdateSync(); scheduler.updateDocument(() => { if (restartFrame) { this.updateSync(true); } else { this.updateSyncAfterMeasuringContent(); } }); }); } else { const restartFrame = this.measureContentDuringUpdateSync(); if (restartFrame) { this.updateSync(false); } else { this.updateSyncAfterMeasuringContent(); } } this.updateScheduled = false; } measureBlockDecorations() { if (this.remeasureAllBlockDecorations) { this.remeasureAllBlockDecorations = false; const decorations = this.props.model.getDecorations(); for (let i = 0; i < decorations.length; i++) { const decoration = decorations[i]; const marker = decoration.getMarker(); if (marker.isValid() && decoration.getProperties().type === 'block') { this.blockDecorationsToMeasure.add(decoration); } } // Update the width of the line tiles to ensure block decorations are // measured with the most recent width. if (this.blockDecorationsToMeasure.size > 0) { this.updateSyncBeforeMeasuringContent(); } } if (this.blockDecorationsToMeasure.size > 0) { const { blockDecorationMeasurementArea } = this.refs; const sentinelElements = new Set(); blockDecorationMeasurementArea.appendChild(document.createElement('div')); this.blockDecorationsToMeasure.forEach(decoration => { const { item } = decoration.getProperties(); const decorationElement = TextEditor.viewForItem(item); if (document.contains(decorationElement)) { const parentElement = decorationElement.parentElement; if (!decorationElement.previousSibling) { const sentinelElement = this.blockDecorationSentinel.cloneNode(); parentElement.insertBefore(sentinelElement, decorationElement); sentinelElements.add(sentinelElement); } if (!decorationElement.nextSibling) { const sentinelElement = this.blockDecorationSentinel.cloneNode(); parentElement.appendChild(sentinelElement); sentinelElements.add(sentinelElement); } this.didMeasureVisibleBlockDecoration = true; } else { blockDecorationMeasurementArea.appendChild( this.blockDecorationSentinel.cloneNode() ); blockDecorationMeasurementArea.appendChild(decorationElement); blockDecorationMeasurementArea.appendChild( this.blockDecorationSentinel.cloneNode() ); } }); if (this.resizeBlockDecorationMeasurementsArea) { this.resizeBlockDecorationMeasurementsArea = false; this.refs.blockDecorationMeasurementArea.style.width = this.getScrollWidth() + 'px'; } this.blockDecorationsToMeasure.forEach(decoration => { const { item } = decoration.getProperties(); const decorationElement = TextEditor.viewForItem(item); const { previousSibling, nextSibling } = decorationElement; const height = nextSibling.getBoundingClientRect().top - previousSibling.getBoundingClientRect().bottom; this.heightsByBlockDecoration.set(decoration, height); this.lineTopIndex.resizeBlock(decoration, height); }); sentinelElements.forEach(sentinelElement => sentinelElement.remove()); while (blockDecorationMeasurementArea.firstChild) { blockDecorationMeasurementArea.firstChild.remove(); } this.blockDecorationsToMeasure.clear(); } } updateSyncBeforeMeasuringContent() { this.measuredContent = false; this.derivedDimensionsCache = {}; this.updateModelSoftWrapColumn(); if (this.pendingAutoscroll) { let { screenRange, options } = this.pendingAutoscroll; this.autoscrollVertically(screenRange, options); this.requestHorizontalMeasurement( screenRange.start.row, screenRange.start.column ); this.requestHorizontalMeasurement( screenRange.end.row, screenRange.end.column ); } this.populateVisibleRowRange(this.getRenderedStartRow()); this.populateVisibleTiles(); this.queryScreenLinesToRender(); this.queryLongestLine(); this.queryLineNumbersToRender(); this.queryGuttersToRender(); this.queryDecorationsToRender(); this.queryExtraScreenLinesToRender(); this.shouldRenderDummyScrollbars = !this.remeasureScrollbars; etch.updateSync(this); this.updateClassList(); this.shouldRenderDummyScrollbars = true; this.didMeasureVisibleBlockDecoration = false; } measureContentDuringUpdateSync() { let gutterDimensionsChanged = false; if (this.remeasureGutterDimensions) { gutterDimensionsChanged = this.measureGutterDimensions(); this.remeasureGutterDimensions = false; } const wasHorizontalScrollbarVisible = this.canScrollHorizontally() && this.getHorizontalScrollbarHeight() > 0; this.measureLongestLineWidth(); this.measureHorizontalPositions(); this.updateAbsolutePositionedDecorations(); const isHorizontalScrollbarVisible = this.canScrollHorizontally() && this.getHorizontalScrollbarHeight() > 0; if (this.pendingAutoscroll) { this.derivedDimensionsCache = {}; const { screenRange, options } = this.pendingAutoscroll; this.autoscrollHorizontally(screenRange, options); if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) { this.autoscrollVertically(screenRange, options); } this.pendingAutoscroll = null; } this.linesToMeasure.clear(); this.measuredContent = true; return ( gutterDimensionsChanged || wasHorizontalScrollbarVisible !== isHorizontalScrollbarVisible ); } updateSyncAfterMeasuringContent() { this.derivedDimensionsCache = {}; etch.updateSync(this); this.currentFrameLineNumberGutterProps = null; this.scrollTopPending = false; this.scrollLeftPending = false; if (this.remeasureScrollbars) { // Flush stored scroll positions to the vertical and the horizontal // scrollbars. This is because they have just been destroyed and recreated // as a result of their remeasurement, but we could not assign the scroll // top while they were initialized because they were not attached to the // DOM yet. this.refs.verticalScrollbar.flushScrollPosition(); this.refs.horizontalScrollbar.flushScrollPosition(); this.measureScrollbarDimensions(); this.remeasureScrollbars = false; etch.updateSync(this); } this.derivedDimensionsCache = {}; if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise(); } render() { const { model } = this.props; const style = {}; if (!model.getAutoHeight() && !model.getAutoWidth()) { style.contain = 'size'; } let clientContainerHeight = '100%'; let clientContainerWidth = '100%'; if (this.hasInitialMeasurements) { if (model.getAutoHeight()) { clientContainerHeight = this.getContentHeight() + this.getHorizontalScrollbarHeight() + 'px'; } if (model.getAutoWidth()) { style.width = 'min-content'; clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() + this.getVerticalScrollbarWidth() + 'px'; } else { style.width = this.element.style.width; } } let attributes = {}; if (model.isMini()) { attributes.mini = ''; } if (model.isReadOnly()) { attributes.readonly = ''; } const dataset = { encoding: model.getEncoding() }; const grammar = model.getGrammar(); if (grammar && grammar.scopeName) { dataset.grammar = grammar.scopeName.replace(/\./g, ' '); } return $( 'atom-text-editor', { // See this.updateClassList() for construction of the class name style, attributes, dataset, tabIndex: -1, on: { mousewheel: this.didMouseWheel } }, $.div( { ref: 'clientContainer', style: { position: 'relative', contain: 'strict', overflow: 'hidden', backgroundColor: 'inherit', height: clientContainerHeight, width: clientContainerWidth } }, this.renderGutterContainer(), this.renderScrollContainer() ), this.renderOverlayDecorations() ); } renderGutterContainer() { if (this.props.model.isMini()) { return null; } else { return $(GutterContainerComponent, { ref: 'gutterContainer', key: 'gutterContainer', rootComponent: this, hasInitialMeasurements: this.hasInitialMeasurements, measuredContent: this.measuredContent, scrollTop: this.getScrollTop(), scrollHeight: this.getScrollHeight(), lineNumberGutterWidth: this.getLineNumberGutterWidth(), lineHeight: this.getLineHeight(), renderedStartRow: this.getRenderedStartRow(), renderedEndRow: this.getRenderedEndRow(), rowsPerTile: this.getRowsPerTile(), guttersToRender: this.guttersToRender, decorationsToRender: this.decorationsToRender, isLineNumberGutterVisible: this.props.model.isLineNumberGutterVisible(), showLineNumbers: this.showLineNumbers, lineNumbersToRender: this.lineNumbersToRender, didMeasureVisibleBlockDecoration: this.didMeasureVisibleBlockDecoration }); } } renderScrollContainer() { const style = { position: 'absolute', contain: 'strict', overflow: 'hidden', top: 0, bottom: 0, backgroundColor: 'inherit' }; if (this.hasInitialMeasurements) { style.left = this.getGutterContainerWidth() + 'px'; style.width = this.getScrollContainerWidth() + 'px'; } return $.div( { ref: 'scrollContainer', key: 'scrollContainer', className: 'scroll-view', style }, this.renderContent(), this.renderDummyScrollbars() ); } renderContent() { let style = { contain: 'strict', overflow: 'hidden', backgroundColor: 'inherit' }; if (this.hasInitialMeasurements) { style.width = ceilToPhysicalPixelBoundary(this.getScrollWidth()) + 'px'; style.height = ceilToPhysicalPixelBoundary(this.getScrollHeight()) + 'px'; style.willChange = 'transform'; style.transform = `translate(${-roundToPhysicalPixelBoundary( this.getScrollLeft() )}px, ${-roundToPhysicalPixelBoundary(this.getScrollTop())}px)`; } return $.div( { ref: 'content', on: { mousedown: this.didMouseDownOnContent }, style }, this.renderLineTiles(), this.renderBlockDecorationMeasurementArea(), this.renderCharacterMeasurementLine() ); } renderHighlightDecorations() { return $(HighlightsComponent, { hasInitialMeasurements: this.hasInitialMeasurements, highlightDecorations: this.decorationsToRender.highlights.slice(), width: this.getScrollWidth(), height: this.getScrollHeight(), lineHeight: this.getLineHeight() }); } renderLineTiles() { const style = { position: 'absolute', contain: 'strict', overflow: 'hidden' }; const children = []; children.push(this.renderHighlightDecorations()); if (this.hasInitialMeasurements) { const { lineComponentsByScreenLineId } = this; const startRow = this.getRenderedStartRow(); const endRow = this.getRenderedEndRow(); const rowsPerTile = this.getRowsPerTile(); const tileWidth = this.getScrollWidth(); for (let i = 0; i < this.renderedTileStartRows.length; i++) { const tileStartRow = this.renderedTileStartRows[i]; const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile); const tileHeight = this.pixelPositionBeforeBlocksForRow(tileEndRow) - this.pixelPositionBeforeBlocksForRow(tileStartRow); children.push( $(LinesTileComponent, { key: this.idsByTileStartRow.get(tileStartRow), measuredContent: this.measuredContent, height: tileHeight, width: tileWidth, top: this.pixelPositionBeforeBlocksForRow(tileStartRow), lineHeight: this.getLineHeight(), renderedStartRow: startRow, tileStartRow, tileEndRow, screenLines: this.renderedScreenLines.slice( tileStartRow - startRow, tileEndRow - startRow ), lineDecorations: this.decorationsToRender.lines.slice( tileStartRow - startRow, tileEndRow - startRow ), textDecorations: this.decorationsToRender.text.slice( tileStartRow - startRow, tileEndRow - startRow ), blockDecorations: this.decorationsToRender.blocks.get(tileStartRow), displayLayer: this.props.model.displayLayer, nodePool: this.lineNodesPool, lineComponentsByScreenLineId }) ); } this.extraRenderedScreenLines.forEach((screenLine, screenRow) => { if (screenRow < startRow || screenRow >= endRow) { children.push( $(LineComponent, { key: 'extra-' + screenLine.id, offScreen: true, screenLine, screenRow, displayLayer: this.props.model.displayLayer, nodePool: this.lineNodesPool, lineComponentsByScreenLineId }) ); } }); style.width = this.getScrollWidth() + 'px'; style.height = this.getScrollHeight() + 'px'; } children.push(this.renderPlaceholderText()); children.push(this.renderCursorsAndInput()); return $.div( { key: 'lineTiles', ref: 'lineTiles', className: 'lines', style }, children ); } renderCursorsAndInput() { return $(CursorsAndInputComponent, { ref: 'cursorsAndInput', key: 'cursorsAndInput', didBlurHiddenInput: this.didBlurHiddenInput, didFocusHiddenInput: this.didFocusHiddenInput, didTextInput: this.didTextInput, didPaste: this.didPaste, didKeydown: this.didKeydown, didKeyup: this.didKeyup, didKeypress: this.didKeypress, didCompositionStart: this.didCompositionStart, didCompositionUpdate: this.didCompositionUpdate, didCompositionEnd: this.didCompositionEnd, measuredContent: this.measuredContent, lineHeight: this.getLineHeight(), scrollHeight: this.getScrollHeight(), scrollWidth: this.getScrollWidth(), decorationsToRender: this.decorationsToRender, cursorsBlinkedOff: this.cursorsBlinkedOff, hiddenInputPosition: this.hiddenInputPosition, tabIndex: this.tabIndex }); } renderPlaceholderText() { const { model } = this.props; if (model.isEmpty()) { const placeholderText = model.getPlaceholderText(); if (placeholderText != null) { return $.div({ className: 'placeholder-text' }, placeholderText); } } return null; } renderCharacterMeasurementLine() { return $.div( { key: 'characterMeasurementLine', ref: 'characterMeasurementLine', className: 'line dummy', style: { position: 'absolute', visibility: 'hidden' } }, $.span({ ref: 'normalWidthCharacterSpan' }, NORMAL_WIDTH_CHARACTER), $.span({ ref: 'doubleWidthCharacterSpan' }, DOUBLE_WIDTH_CHARACTER), $.span({ ref: 'halfWidthCharacterSpan' }, HALF_WIDTH_CHARACTER), $.span({ ref: 'koreanCharacterSpan' }, KOREAN_CHARACTER) ); } renderBlockDecorationMeasurementArea() { return $.div({ ref: 'blockDecorationMeasurementArea', key: 'blockDecorationMeasurementArea', style: { contain: 'strict', position: 'absolute', visibility: 'hidden', width: this.getScrollWidth() + 'px' } }); } renderDummyScrollbars() { if (this.shouldRenderDummyScrollbars && !this.props.model.isMini()) { let scrollHeight, scrollTop, horizontalScrollbarHeight; let scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible; let canScrollHorizontally, canScrollVertically; if (this.hasInitialMeasurements) { scrollHeight = this.getScrollHeight(); scrollWidth = this.getScrollWidth(); scrollTop = this.getScrollTop(); scrollLeft = this.getScrollLeft(); canScrollHorizontally = this.canScrollHorizontally(); canScrollVertically = this.canScrollVertically(); horizontalScrollbarHeight = this.getHorizontalScrollbarHeight(); verticalScrollbarWidth = this.getVerticalScrollbarWidth(); forceScrollbarVisible = this.remeasureScrollbars; } else { forceScrollbarVisible = true; } return [ $(DummyScrollbarComponent, { ref: 'verticalScrollbar', orientation: 'vertical', didScroll: this.didScrollDummyScrollbar, didMouseDown: this.didMouseDownOnContent, canScroll: canScrollVertically, scrollHeight, scrollTop, horizontalScrollbarHeight, forceScrollbarVisible }), $(DummyScrollbarComponent, { ref: 'horizontalScrollbar', orientation: 'horizontal', didScroll: this.didScrollDummyScrollbar, didMouseDown: this.didMouseDownOnContent, canScroll: canScrollHorizontally, scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible }), // Force a "corner" to render where the two scrollbars meet at the lower right $.div({ ref: 'scrollbarCorner', className: 'scrollbar-corner', style: { position: 'absolute', height: '20px', width: '20px', bottom: 0, right: 0, overflow: 'scroll' } }) ]; } else { return null; } } renderOverlayDecorations() { return this.decorationsToRender.overlays.map(overlayProps => $( OverlayComponent, Object.assign( { key: overlayProps.element, overlayComponents: this.overlayComponents, didResize: overlayComponent => { this.updateOverlayToRender(overlayProps); overlayComponent.update(overlayProps); } }, overlayProps ) ) ); } // Imperatively manipulate the class list of the root element to avoid // clearing classes assigned by package authors. updateClassList() { const { model } = this.props; const oldClassList = this.classList; const newClassList = ['editor']; if (this.focused) newClassList.push('is-focused'); if (model.isMini()) newClassList.push('mini'); for (var i = 0; i < model.selections.length; i++) { if (!model.selections[i].isEmpty()) { newClassList.push('has-selection'); break; } } if (oldClassList) { for (let i = 0; i < oldClassList.length; i++) { const className = oldClassList[i]; if (!newClassList.includes(className)) { this.element.classList.remove(className); } } } for (let i = 0; i < newClassList.length; i++) { this.element.classList.add(newClassList[i]); } this.classList = newClassList; } queryScreenLinesToRender() { const { model } = this.props; this.renderedScreenLines = model.displayLayer.getScreenLines( this.getRenderedStartRow(), this.getRenderedEndRow() ); } queryLongestLine() { const { model } = this.props; const longestLineRow = model.getApproximateLongestScreenRow(); const longestLine = model.screenLineForScreenRow(longestLineRow); if ( longestLine !== this.previousLongestLine || this.remeasureCharacterDimensions ) { this.requestLineToMeasure(longestLineRow, longestLine); this.longestLineToMeasure = longestLine; this.previousLongestLine = longestLine; } } queryExtraScreenLinesToRender() { this.extraRenderedScreenLines.clear(); this.linesToMeasure.forEach((screenLine, row) => { if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) { this.extraRenderedScreenLines.set(row, screenLine); } }); } queryLineNumbersToRender() { const { model } = this.props; if (!model.anyLineNumberGutterVisible()) return; if (this.showLineNumbers !== model.doesShowLineNumbers()) { this.remeasureGutterDimensions = true; this.showLineNumbers = model.doesShowLineNumbers(); } this.queryMaxLineNumberDigits(); const startRow = this.getRenderedStartRow(); const endRow = this.getRenderedEndRow(); const renderedRowCount = this.getRenderedRowCount(); const bufferRows = model.bufferRowsForScreenRows(startRow, endRow); const screenRows = new Array(renderedRowCount); const keys = new Array(renderedRowCount); const foldableFlags = new Array(renderedRowCount); const softWrappedFlags = new Array(renderedRowCount); let previousBufferRow = startRow > 0 ? model.bufferRowForScreenRow(startRow - 1) : -1; let softWrapCount = 0; for (let row = startRow; row < endRow; row++) { const i = row - startRow; const bufferRow = bufferRows[i]; if (bufferRow === previousBufferRow) { softWrapCount++; softWrappedFlags[i] = true; keys[i] = bufferRow + '-' + softWrapCount; } else { softWrapCount = 0; softWrappedFlags[i] = false; keys[i] = bufferRow; } const nextBufferRow = bufferRows[i + 1]; if (bufferRow !== nextBufferRow) { foldableFlags[i] = model.isFoldableAtBufferRow(bufferRow); } else { foldableFlags[i] = false; } screenRows[i] = row; previousBufferRow = bufferRow; } // Delete extra buffer row at the end because it's not currently on screen. bufferRows.pop(); this.lineNumbersToRender.bufferRows = bufferRows; this.lineNumbersToRender.screenRows = screenRows; this.lineNumbersToRender.keys = keys; this.lineNumbersToRender.foldableFlags = foldableFlags; this.lineNumbersToRender.softWrappedFlags = softWrappedFlags; } queryMaxLineNumberDigits() { const { model } = this.props; if (model.anyLineNumberGutterVisible()) { const maxDigits = Math.max(2, model.getLineCount().toString().length); if (maxDigits !== this.lineNumbersToRender.maxDigits) { this.remeasureGutterDimensions = true; this.lineNumbersToRender.maxDigits = maxDigits; } } } renderedScreenLineForRow(row) { return ( this.renderedScreenLines[row - this.getRenderedStartRow()] || this.extraRenderedScreenLines.get(row) ); } queryGuttersToRender() { const oldGuttersToRender = this.guttersToRender; const oldGuttersVisibility = this.guttersVisibility; this.guttersToRender = this.props.model.getGutters(); this.guttersVisibility = this.guttersToRender.map(g => g.visible); if ( !oldGuttersToRender || oldGuttersToRender.length !== this.guttersToRender.length ) { this.remeasureGutterDimensions = true; } else { for (let i = 0, length = this.guttersToRender.length; i < length; i++) { if ( this.guttersToRender[i] !== oldGuttersToRender[i] || this.guttersVisibility[i] !== oldGuttersVisibility[i] ) { this.remeasureGutterDimensions = true; break; } } } } queryDecorationsToRender() { this.decorationsToRender.lineNumbers.clear(); this.decorationsToRender.lines = []; this.decorationsToRender.overlays.length = 0; this.decorationsToRender.customGutter.clear(); this.decorationsToRender.blocks = new Map(); this.decorationsToRender.text = []; this.decorationsToMeasure.highlights.length = 0; this.decorationsToMeasure.cursors.clear(); this.textDecorationsByMarker.clear(); this.textDecorationBoundaries.length = 0; const decorationsByMarker = this.props.model.decorationManager.decorationPropertiesByMarkerForScreenRowRange( this.getRenderedStartRow(), this.getRenderedEndRow() ); decorationsByMarker.forEach((decorations, marker) => { const screenRange = marker.getScreenRange(); const reversed = marker.isReversed(); for (let i = 0; i < decorations.length; i++) { const decoration = decorations[i]; this.addDecorationToRender( decoration.type, decoration, marker, screenRange, reversed ); } }); this.populateTextDecorationsToRender(); } addDecorationToRender(type, decoration, marker, screenRange, reversed) { if (Array.isArray(type)) { for (let i = 0, length = type.length; i < length; i++) { this.addDecorationToRender( type[i], decoration, marker, screenRange, reversed ); } } else { switch (type) { case 'line': case 'line-number': this.addLineDecorationToRender( type, decoration, screenRange, reversed ); break; case 'highlight': this.addHighlightDecorationToMeasure( decoration, screenRange, marker.id ); break; case 'cursor': this.addCursorDecorationToMeasure( decoration, marker, screenRange, reversed ); break; case 'overlay': this.addOverlayDecorationToRender(decoration, marker); break; case 'gutter': this.addCustomGutterDecorationToRender(decoration, screenRange); break; case 'block': this.addBlockDecorationToRender(decoration, screenRange, reversed); break; case 'text': this.addTextDecorationToRender(decoration, screenRange, marker); break; } } } addLineDecorationToRender(type, decoration, screenRange, reversed) { let decorationsToRender; if (type === 'line') { decorationsToRender = this.decorationsToRender.lines; } else { const gutterName = decoration.gutterName || 'line-number'; decorationsToRender = this.decorationsToRender.lineNumbers.get( gutterName ); if (!decorationsToRender) { decorationsToRender = []; this.decorationsToRender.lineNumbers.set( gutterName, decorationsToRender ); } } let omitLastRow = false; if (screenRange.isEmpty()) { if (decoration.onlyNonEmpty) return; } else { if (decoration.onlyEmpty) return; if (decoration.omitEmptyLastRow !== false) { omitLastRow = screenRange.end.column === 0; } } const renderedStartRow = this.getRenderedStartRow(); let rangeStartRow = screenRange.start.row; let rangeEndRow = screenRange.end.row; if (decoration.onlyHead) { if (reversed) { rangeEndRow = rangeStartRow; } else { rangeStartRow = rangeEndRow; } } rangeStartRow = Math.max(rangeStartRow, this.getRenderedStartRow()); rangeEndRow = Math.min(rangeEndRow, this.getRenderedEndRow() - 1); for (let row = rangeStartRow; row <= rangeEndRow; row++) { if (omitLastRow && row === screenRange.end.row) break; const currentClassName = decorationsToRender[row - renderedStartRow]; const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class; decorationsToRender[row - renderedStartRow] = newClassName; } } addHighlightDecorationToMeasure(decoration, screenRange, key) { screenRange = constrainRangeToRows( screenRange, this.getRenderedStartRow(), this.getRenderedEndRow() ); if (screenRange.isEmpty()) return; const { class: className, flashRequested, flashClass, flashDuration } = decoration; decoration.flashRequested = false; this.decorationsToMeasure.highlights.push({ screenRange, key, className, flashRequested, flashClass, flashDuration }); this.requestHorizontalMeasurement( screenRange.start.row, screenRange.start.column ); this.requestHorizontalMeasurement( screenRange.end.row, screenRange.end.column ); } addCursorDecorationToMeasure(decoration, marker, screenRange, reversed) { const { model } = this.props; if (!model.getShowCursorOnSelection() && !screenRange.isEmpty()) return; let decorationToMeasure = this.decorationsToMeasure.cursors.get(marker); if (!decorationToMeasure) { const isLastCursor = model.getLastCursor().getMarker() === marker; const screenPosition = reversed ? screenRange.start : screenRange.end; const { row, column } = screenPosition; if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) return; this.requestHorizontalMeasurement(row, column); let columnWidth = 0; if (model.lineLengthForScreenRow(row) > column) { columnWidth = 1; this.requestHorizontalMeasurement(row, column + 1); } decorationToMeasure = { screenPosition, columnWidth, isLastCursor }; this.decorationsToMeasure.cursors.set(marker, decorationToMeasure); } if (decoration.class) { if (decorationToMeasure.className) { decorationToMeasure.className += ' ' + decoration.class; } else { decorationToMeasure.className = decoration.class; } } if (decoration.style) { if (decorationToMeasure.style) { Object.assign(decorationToMeasure.style, decoration.style); } else { decorationToMeasure.style = Object.assign({}, decoration.style); } } } addOverlayDecorationToRender(decoration, marker) { const { class: className, item, position, avoidOverflow } = decoration; const element = TextEditor.viewForItem(item); const screenPosition = position === 'tail' ? marker.getTailScreenPosition() : marker.getHeadScreenPosition(); this.requestHorizontalMeasurement( screenPosition.row, screenPosition.column ); this.decorationsToRender.overlays.push({ className, element, avoidOverflow, screenPosition }); } addCustomGutterDecorationToRender(decoration, screenRange) { let decorations = this.decorationsToRender.customGutter.get( decoration.gutterName ); if (!decorations) { decorations = []; this.decorationsToRender.customGutter.set( decoration.gutterName, decorations ); } const top = this.pixelPositionAfterBlocksForRow(screenRange.start.row); const height = this.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1) - top; decorations.push({ className: 'decoration' + (decoration.class ? ' ' + decoration.class : ''), element: TextEditor.viewForItem(decoration.item), top, height }); } addBlockDecorationToRender(decoration, screenRange, reversed) { 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) { decorationsByScreenLine = new Map(); this.decorationsToRender.blocks.set( tileStartRow, decorationsByScreenLine ); } let decorations = decorationsByScreenLine.get(screenLine.id); if (!decorations) { decorations = []; decorationsByScreenLine.set(screenLine.id, decorations); } decorations.push(decoration); // Order block decorations by increasing values of their "order" property. Break ties with "id", which mirrors // their creation sequence. decorations.sort((a, b) => a.order !== b.order ? a.order - b.order : a.id - b.id ); } addTextDecorationToRender(decoration, screenRange, marker) { if (screenRange.isEmpty()) return; let decorationsForMarker = this.textDecorationsByMarker.get(marker); if (!decorationsForMarker) { decorationsForMarker = []; this.textDecorationsByMarker.set(marker, decorationsForMarker); this.textDecorationBoundaries.push({ position: screenRange.start, starting: [marker] }); this.textDecorationBoundaries.push({ position: screenRange.end, ending: [marker] }); } decorationsForMarker.push(decoration); } populateTextDecorationsToRender() { // Sort all boundaries in ascending order of position this.textDecorationBoundaries.sort((a, b) => a.position.compare(b.position) ); // Combine adjacent boundaries with the same position for (let i = 0; i < this.textDecorationBoundaries.length; ) { const boundary = this.textDecorationBoundaries[i]; const nextBoundary = this.textDecorationBoundaries[i + 1]; if (nextBoundary && nextBoundary.position.isEqual(boundary.position)) { if (nextBoundary.starting) { if (boundary.starting) { boundary.starting.push(...nextBoundary.starting); } else { boundary.starting = nextBoundary.starting; } } if (nextBoundary.ending) { if (boundary.ending) { boundary.ending.push(...nextBoundary.ending); } else { boundary.ending = nextBoundary.ending; } } this.textDecorationBoundaries.splice(i + 1, 1); } else { i++; } } const renderedStartRow = this.getRenderedStartRow(); const renderedEndRow = this.getRenderedEndRow(); const containingMarkers = []; // Iterate over boundaries to build up text decorations. for (let i = 0; i < this.textDecorationBoundaries.length; i++) { const boundary = this.textDecorationBoundaries[i]; // If multiple markers start here, sort them by order of nesting (markers ending later come first) if (boundary.starting && boundary.starting.length > 1) { boundary.starting.sort((a, b) => a.compare(b)); } // If multiple markers start here, sort them by order of nesting (markers starting earlier come first) if (boundary.ending && boundary.ending.length > 1) { boundary.ending.sort((a, b) => b.compare(a)); } // Remove markers ending here from containing markers array if (boundary.ending) { for (let j = boundary.ending.length - 1; j >= 0; j--) { containingMarkers.splice( containingMarkers.lastIndexOf(boundary.ending[j]), 1 ); } } // Add markers starting here to containing markers array if (boundary.starting) containingMarkers.push(...boundary.starting); // Determine desired className and style based on containing markers let className, style; for (let j = 0; j < containingMarkers.length; j++) { const marker = containingMarkers[j]; const decorations = this.textDecorationsByMarker.get(marker); for (let k = 0; k < decorations.length; k++) { const decoration = decorations[k]; if (decoration.class) { if (className) { className += ' ' + decoration.class; } else { className = decoration.class; } } if (decoration.style) { if (style) { Object.assign(style, decoration.style); } else { style = Object.assign({}, decoration.style); } } } } // Add decoration start with className/style for current position's column, // and also for the start of every row up until the next decoration boundary if (boundary.position.row >= renderedStartRow) { this.addTextDecorationStart( boundary.position.row, boundary.position.column, className, style ); } const nextBoundary = this.textDecorationBoundaries[i + 1]; if (nextBoundary) { let row = Math.max(boundary.position.row + 1, renderedStartRow); const endRow = Math.min(nextBoundary.position.row, renderedEndRow); for (; row < endRow; row++) { this.addTextDecorationStart(row, 0, className, style); } if ( row === nextBoundary.position.row && nextBoundary.position.column !== 0 ) { this.addTextDecorationStart(row, 0, className, style); } } } } addTextDecorationStart(row, column, className, style) { const renderedStartRow = this.getRenderedStartRow(); let decorationStarts = this.decorationsToRender.text[ row - renderedStartRow ]; if (!decorationStarts) { decorationStarts = []; this.decorationsToRender.text[row - renderedStartRow] = decorationStarts; } decorationStarts.push({ column, className, style }); } updateAbsolutePositionedDecorations() { this.updateHighlightsToRender(); this.updateCursorsToRender(); this.updateOverlaysToRender(); } updateHighlightsToRender() { this.decorationsToRender.highlights.length = 0; for (let i = 0; i < this.decorationsToMeasure.highlights.length; i++) { const highlight = this.decorationsToMeasure.highlights[i]; const { start, end } = highlight.screenRange; highlight.startPixelTop = this.pixelPositionAfterBlocksForRow(start.row); highlight.startPixelLeft = this.pixelLeftForRowAndColumn( start.row, start.column ); highlight.endPixelTop = this.pixelPositionAfterBlocksForRow(end.row) + this.getLineHeight(); highlight.endPixelLeft = this.pixelLeftForRowAndColumn( end.row, end.column ); this.decorationsToRender.highlights.push(highlight); } } updateCursorsToRender() { this.decorationsToRender.cursors.length = 0; this.decorationsToMeasure.cursors.forEach(cursor => { const { screenPosition, className, style } = cursor; const { row, column } = screenPosition; const pixelTop = this.pixelPositionAfterBlocksForRow(row); const pixelLeft = this.pixelLeftForRowAndColumn(row, column); let pixelWidth; if (cursor.columnWidth === 0) { pixelWidth = this.getBaseCharacterWidth(); } else { pixelWidth = this.pixelLeftForRowAndColumn(row, column + 1) - pixelLeft; } const cursorPosition = { pixelTop, pixelLeft, pixelWidth, className, style }; this.decorationsToRender.cursors.push(cursorPosition); if (cursor.isLastCursor) this.hiddenInputPosition = cursorPosition; }); } 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(); 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; for (let i = 0; i < overlayCount; i++) { const decoration = this.decorationsToRender.overlays[i]; this.updateOverlayToRender(decoration); } } didAttach() { if (!this.attached) { this.attached = true; this.intersectionObserver = new IntersectionObserver(entries => { const { intersectionRect } = entries[entries.length - 1]; if (intersectionRect.width > 0 || intersectionRect.height > 0) { this.didShow(); } else { this.didHide(); } }); this.intersectionObserver.observe(this.element); this.resizeObserver = new ResizeObserver(this.didResize.bind(this)); this.resizeObserver.observe(this.element); if (this.refs.gutterContainer) { this.gutterContainerResizeObserver = new ResizeObserver( this.didResizeGutterContainer.bind(this) ); this.gutterContainerResizeObserver.observe( this.refs.gutterContainer.element ); } this.overlayComponents.forEach(component => component.didAttach()); if (this.isVisible()) { this.didShow(); if (this.refs.verticalScrollbar) this.refs.verticalScrollbar.flushScrollPosition(); if (this.refs.horizontalScrollbar) this.refs.horizontalScrollbar.flushScrollPosition(); } else { this.didHide(); } if (!this.constructor.attachedComponents) { this.constructor.attachedComponents = new Set(); } this.constructor.attachedComponents.add(this); } } didDetach() { if (this.attached) { this.intersectionObserver.disconnect(); this.resizeObserver.disconnect(); if (this.gutterContainerResizeObserver) this.gutterContainerResizeObserver.disconnect(); this.overlayComponents.forEach(component => component.didDetach()); this.didHide(); this.attached = false; this.constructor.attachedComponents.delete(this); } } didShow() { if (!this.visible && this.isVisible()) { if (!this.hasInitialMeasurements) this.measureDimensions(); this.visible = true; this.props.model.setVisible(true); this.resizeBlockDecorationMeasurementsArea = true; this.updateSync(); this.flushPendingLogicalScrollPosition(); } } didHide() { if (this.visible) { this.visible = false; this.props.model.setVisible(false); } } // Called by TextEditorElement so that focus events can be handled before // the element is attached to the DOM. didFocus() { if (!this.visible) this.didShow(); if (!this.focused) { this.focused = true; this.startCursorBlinking(); this.scheduleUpdate(); } this.getHiddenInput().focus({ preventScroll: true }); } // Called by TextEditorElement so that this function is always the first // listener to be fired, even if other listeners are bound before creating // the component. didBlur(event) { if (event.relatedTarget === this.getHiddenInput()) { event.stopImmediatePropagation(); } } didBlurHiddenInput(event) { if ( this.element !== event.relatedTarget && !this.element.contains(event.relatedTarget) ) { this.focused = false; this.stopCursorBlinking(); this.scheduleUpdate(); this.element.dispatchEvent(new FocusEvent(event.type, event)); } } didFocusHiddenInput() { if (!this.focused) { this.focused = true; this.startCursorBlinking(); this.scheduleUpdate(); } } didMouseWheel(event) { const scrollSensitivity = this.props.model.getScrollSensitivity() / 100; let { wheelDeltaX, wheelDeltaY } = event; if (Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)) { wheelDeltaX = wheelDeltaX * scrollSensitivity; wheelDeltaY = 0; } else { wheelDeltaX = 0; wheelDeltaY = wheelDeltaY * scrollSensitivity; } if (this.getPlatform() !== 'darwin' && event.shiftKey) { let temp = wheelDeltaX; wheelDeltaX = wheelDeltaY; wheelDeltaY = temp; } const scrollLeftChanged = wheelDeltaX !== 0 && this.setScrollLeft(this.getScrollLeft() - wheelDeltaX); const scrollTopChanged = wheelDeltaY !== 0 && this.setScrollTop(this.getScrollTop() - wheelDeltaY); if (scrollLeftChanged || scrollTopChanged) { event.preventDefault(); this.updateSync(); } } didResize() { // Prevent the component from measuring the client container dimensions when // getting spurious resize events. if (this.isVisible()) { const clientContainerWidthChanged = this.measureClientContainerWidth(); const clientContainerHeightChanged = this.measureClientContainerHeight(); if (clientContainerWidthChanged || clientContainerHeightChanged) { if (clientContainerWidthChanged) { this.remeasureAllBlockDecorations = true; } this.resizeObserver.disconnect(); this.scheduleUpdate(); process.nextTick(() => { this.resizeObserver.observe(this.element); }); } } } didResizeGutterContainer() { // Prevent the component from measuring the gutter dimensions when getting // spurious resize events. if (this.isVisible() && this.measureGutterDimensions()) { this.gutterContainerResizeObserver.disconnect(); this.scheduleUpdate(); process.nextTick(() => { this.gutterContainerResizeObserver.observe( this.refs.gutterContainer.element ); }); } } didScrollDummyScrollbar() { let scrollTopChanged = false; let scrollLeftChanged = false; if (!this.scrollTopPending) { scrollTopChanged = this.setScrollTop( this.refs.verticalScrollbar.element.scrollTop ); } if (!this.scrollLeftPending) { scrollLeftChanged = this.setScrollLeft( this.refs.horizontalScrollbar.element.scrollLeft ); } if (scrollTopChanged || scrollLeftChanged) this.updateSync(); } didUpdateStyles() { this.remeasureCharacterDimensions = true; this.horizontalPixelPositionsByScreenLineId.clear(); this.scheduleUpdate(); } didUpdateScrollbarStyles() { if (!this.props.model.isMini()) { this.remeasureScrollbars = true; this.scheduleUpdate(); } } didPaste(event) { // On Linux, Chromium translates a middle-button mouse click into a // mousedown event *and* a paste event. Since Atom supports the middle mouse // click as a way of closing a tab, we only want the mousedown event, not // the paste event. And since we don't use the `paste` event for any // behavior in Atom, we can no-op the event to eliminate this issue. // See https://github.com/atom/atom/pull/15183#issue-248432413. if (this.getPlatform() === 'linux') event.preventDefault(); } didTextInput(event) { if (this.compositionCheckpoint) { this.props.model.revertToCheckpoint(this.compositionCheckpoint); this.compositionCheckpoint = null; } if (this.isInputEnabled()) { event.stopPropagation(); // WARNING: If we call preventDefault on the input of a space // character, then the browser interprets the spacebar keypress as a // page-down command, causing spaces to scroll elements containing // editors. This means typing space will actually change the contents // of the hidden input, which will cause the browser to autoscroll the // scroll container to reveal the input if it is off screen (See // https://github.com/atom/atom/issues/16046). To correct for this // situation, we automatically reset the scroll position to 0,0 after // typing a space. None of this can really be tested. if (event.data === ' ') { window.setImmediate(() => { this.refs.scrollContainer.scrollTop = 0; this.refs.scrollContainer.scrollLeft = 0; }); } else { event.preventDefault(); } // If the input event is fired while the accented character menu is open it // means that the user has chosen one of the accented alternatives. Thus, we // 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 // opened on macOS. Usually, every keydown event that could cause input is // followed by a corresponding keypress. However, pressing and holding // long enough to open the accented character menu causes additional keydown // events to fire that aren't followed by their own keypress and textInput // events. // // Therefore, we assume the accented character menu has been deployed if, // before observing any keyup event, we observe events in the following // sequence: // // keydown(code: X), keypress, keydown(code: X) // // The code X must be the same in the keydown events that bracket the // keypress, meaning we're *holding* the _same_ key we initially 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. Modifier keys are exempt to preserve the ability to // add selections, shift-scroll horizontally while selecting. if ( this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta' && event.key !== 'Shift' ) { this.stopDragging(); } if (this.lastKeydownBeforeKeypress != null) { if (this.lastKeydownBeforeKeypress.code === event.code) { this.accentedCharacterMenuIsOpen = true; } this.lastKeydownBeforeKeypress = null; } this.lastKeydown = event; } didKeypress(event) { this.lastKeydownBeforeKeypress = this.lastKeydown; // This cancels the accented character behavior if we type a key normally // with the menu open. this.accentedCharacterMenuIsOpen = false; } didKeyup(event) { if ( this.lastKeydownBeforeKeypress && this.lastKeydownBeforeKeypress.code === event.code ) { this.lastKeydownBeforeKeypress = null; } } // The IME composition events work like this: // // User types 's', chromium pops up the completion helper // 1. compositionstart fired // 2. compositionupdate fired; event.data == 's' // User hits arrow keys to move around in completion helper // 3. compositionupdate fired; event.data == 's' for each arry key press // User escape to cancel OR User chooses a completion // 4. compositionend fired // 5. textInput fired; event.data == the completion string didCompositionStart() { // Workaround for Chromium not preventing composition events when // preventDefault is called on the keydown event that precipitated them. if (this.lastKeydown && this.lastKeydown.defaultPrevented) { this.getHiddenInput().disabled = true; process.nextTick(() => { // Disabling the hidden input makes it lose focus as well, so we have to // re-enable and re-focus it. this.getHiddenInput().disabled = false; this.getHiddenInput().focus({ preventScroll: true }); }); return; } this.compositionCheckpoint = this.props.model.createCheckpoint(); if (this.accentedCharacterMenuIsOpen) { this.props.model.selectLeft(); } } didCompositionUpdate(event) { this.props.model.insertText(event.data, { select: true }); } didCompositionEnd(event) { event.target.value = ''; } didMouseDownOnContent(event) { const { model } = this.props; const { target, button, detail, ctrlKey, shiftKey, metaKey } = event; const platform = this.getPlatform(); // Ignore clicks on block decorations. if (target) { let element = target; while (element && element !== this.element) { if (this.blockDecorationsByElement.has(element)) { return; } element = element.parentElement; } } const screenPosition = this.screenPositionForMouseEvent(event); if (button === 1) { model.setCursorScreenPosition(screenPosition, { autoscroll: false }); // On Linux, pasting happens on middle click. A textInput event with the // contents of the selection clipboard will be dispatched by the browser // automatically on mouseup if editor.selectionClipboard is set to true. if ( platform === 'linux' && this.isInputEnabled() && atom.config.get('editor.selectionClipboard') ) model.insertText(clipboard.readText('selection')); return; } if (button !== 0) return; // Ctrl-click brings up the context menu on macOS if (platform === 'darwin' && ctrlKey) return; if (target && target.matches('.fold-marker')) { const bufferPosition = model.bufferPositionForScreenPosition( screenPosition ); model.destroyFoldsContainingBufferPositions([bufferPosition], false); return; } const allowMultiCursor = atom.config.get('editor.multiCursorOnClick'); const addOrRemoveSelection = allowMultiCursor && (metaKey || (ctrlKey && platform !== 'darwin')); switch (detail) { case 1: if (addOrRemoveSelection) { const existingSelection = model.getSelectionAtScreenPosition( screenPosition ); if (existingSelection) { if (model.hasMultipleCursors()) existingSelection.destroy(); } else { model.addCursorAtScreenPosition(screenPosition, { autoscroll: false }); } } else { if (shiftKey) { model.selectToScreenPosition(screenPosition, { autoscroll: false }); } else { model.setCursorScreenPosition(screenPosition, { autoscroll: false }); } } break; case 2: if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition, { autoscroll: false }); model.getLastSelection().selectWord({ autoscroll: false }); break; case 3: if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition, { autoscroll: false }); model.getLastSelection().selectLine(null, { autoscroll: false }); break; } this.handleMouseDragUntilMouseUp({ didDrag: event => { this.autoscrollOnMouseDrag(event); const screenPosition = this.screenPositionForMouseEvent(event); model.selectToScreenPosition(screenPosition, { suppressSelectionMerge: true, autoscroll: false }); this.updateSync(); }, didStopDragging: () => { model.finalizeSelections(); model.mergeIntersectingSelections(); this.updateSync(); } }); } didMouseDownOnLineNumberGutter(event) { const { model } = this.props; const { target, button, ctrlKey, shiftKey, metaKey } = event; // Only handle mousedown events for left mouse button if (button !== 0) return; const clickedScreenRow = this.screenPositionForMouseEvent(event).row; const startBufferRow = model.bufferPositionForScreenPosition([ clickedScreenRow, 0 ]).row; if ( target && (target.matches('.foldable .icon-right') || target.matches('.folded .icon-right')) ) { model.toggleFoldAtBufferRow(startBufferRow); return; } const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin'); const endBufferRow = model.bufferPositionForScreenPosition([ clickedScreenRow, Infinity ]).row; const clickedLineBufferRange = Range( Point(startBufferRow, 0), Point(endBufferRow + 1, 0) ); let initialBufferRange; if (shiftKey) { const lastSelection = model.getLastSelection(); initialBufferRange = lastSelection.getBufferRange(); lastSelection.setBufferRange( initialBufferRange.union(clickedLineBufferRange), { reversed: clickedScreenRow < lastSelection.getScreenRange().start.row, autoscroll: false, preserveFolds: true, suppressSelectionMerge: true } ); } else { initialBufferRange = clickedLineBufferRange; if (addOrRemoveSelection) { model.addSelectionForBufferRange(clickedLineBufferRange, { autoscroll: false, preserveFolds: true }); } else { model.setSelectedBufferRange(clickedLineBufferRange, { autoscroll: false, preserveFolds: true }); } } const initialScreenRange = model.screenRangeForBufferRange( initialBufferRange ); this.handleMouseDragUntilMouseUp({ didDrag: event => { this.autoscrollOnMouseDrag(event, true); const dragRow = this.screenPositionForMouseEvent(event).row; const draggedLineScreenRange = Range( Point(dragRow, 0), Point(dragRow + 1, 0) ); model .getLastSelection() .setScreenRange(draggedLineScreenRange.union(initialScreenRange), { reversed: dragRow < initialScreenRange.start.row, autoscroll: false, preserveFolds: true }); this.updateSync(); }, didStopDragging: () => { model.mergeIntersectingSelections(); this.updateSync(); } }); } handleMouseDragUntilMouseUp({ didDrag, didStopDragging }) { let dragging = false; let lastMousemoveEvent; const animationFrameLoop = () => { window.requestAnimationFrame(() => { if (dragging && this.visible) { didDrag(lastMousemoveEvent); animationFrameLoop(); } }); }; function didMouseMove(event) { lastMousemoveEvent = event; if (!dragging) { dragging = true; animationFrameLoop(); } } function didMouseUp() { this.stopDragging = null; window.removeEventListener('mousemove', didMouseMove); window.removeEventListener('mouseup', didMouseUp, { capture: true }); if (dragging) { dragging = false; didStopDragging(); } } window.addEventListener('mousemove', didMouseMove); window.addEventListener('mouseup', didMouseUp, { capture: true }); this.stopDragging = didMouseUp; } autoscrollOnMouseDrag({ clientX, clientY }, verticalOnly = false) { let { top, bottom, left, right } = this.refs.scrollContainer.getBoundingClientRect(); // Using var to avoid deopt on += assignments below top += MOUSE_DRAG_AUTOSCROLL_MARGIN; bottom -= MOUSE_DRAG_AUTOSCROLL_MARGIN; left += MOUSE_DRAG_AUTOSCROLL_MARGIN; right -= MOUSE_DRAG_AUTOSCROLL_MARGIN; let yDelta, yDirection; if (clientY < top) { yDelta = top - clientY; yDirection = -1; } else if (clientY > bottom) { yDelta = clientY - bottom; yDirection = 1; } let xDelta, xDirection; if (clientX < left) { xDelta = left - clientX; xDirection = -1; } else if (clientX > right) { xDelta = clientX - right; xDirection = 1; } let scrolled = false; if (yDelta != null) { const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection; scrolled = this.setScrollTop(this.getScrollTop() + scaledDelta); } if (!verticalOnly && xDelta != null) { const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection; scrolled = this.setScrollLeft(this.getScrollLeft() + scaledDelta); } if (scrolled) this.updateSync(); } screenPositionForMouseEvent(event) { return this.screenPositionForPixelPosition( this.pixelPositionForMouseEvent(event) ); } pixelPositionForMouseEvent({ clientX, clientY }) { const scrollContainerRect = this.refs.scrollContainer.getBoundingClientRect(); clientX = Math.min( scrollContainerRect.right, Math.max(scrollContainerRect.left, clientX) ); clientY = Math.min( scrollContainerRect.bottom, Math.max(scrollContainerRect.top, clientY) ); const linesRect = this.refs.lineTiles.getBoundingClientRect(); return { top: clientY - linesRect.top, left: clientX - linesRect.left }; } didUpdateSelections() { this.pauseCursorBlinking(); this.scheduleUpdate(); } pauseCursorBlinking() { this.stopCursorBlinking(); this.debouncedResumeCursorBlinking(); } resumeCursorBlinking() { this.cursorsBlinkedOff = true; this.startCursorBlinking(); } stopCursorBlinking() { if (this.cursorsBlinking) { this.cursorsBlinkedOff = false; this.cursorsBlinking = false; window.clearInterval(this.cursorBlinkIntervalHandle); this.cursorBlinkIntervalHandle = null; this.scheduleUpdate(); } } startCursorBlinking() { if (!this.cursorsBlinking) { this.cursorBlinkIntervalHandle = window.setInterval(() => { this.cursorsBlinkedOff = !this.cursorsBlinkedOff; this.scheduleUpdate(true); }, (this.props.cursorBlinkPeriod || CURSOR_BLINK_PERIOD) / 2); this.cursorsBlinking = true; this.scheduleUpdate(true); } } didRequestAutoscroll(autoscroll) { this.pendingAutoscroll = autoscroll; this.scheduleUpdate(); } flushPendingLogicalScrollPosition() { let changedScrollTop = false; if (this.pendingScrollTopRow > 0) { changedScrollTop = this.setScrollTopRow(this.pendingScrollTopRow, false); this.pendingScrollTopRow = null; } let changedScrollLeft = false; if (this.pendingScrollLeftColumn > 0) { changedScrollLeft = this.setScrollLeftColumn( this.pendingScrollLeftColumn, false ); this.pendingScrollLeftColumn = null; } if (changedScrollTop || changedScrollLeft) { this.updateSync(); } } autoscrollVertically(screenRange, options) { const screenRangeTop = this.pixelPositionAfterBlocksForRow( screenRange.start.row ); const screenRangeBottom = this.pixelPositionAfterBlocksForRow(screenRange.end.row) + this.getLineHeight(); const verticalScrollMargin = this.getVerticalAutoscrollMargin(); let desiredScrollTop, desiredScrollBottom; if (options && options.center) { const desiredScrollCenter = (screenRangeTop + screenRangeBottom) / 2; desiredScrollTop = desiredScrollCenter - this.getScrollContainerClientHeight() / 2; desiredScrollBottom = desiredScrollCenter + this.getScrollContainerClientHeight() / 2; } else { desiredScrollTop = screenRangeTop - verticalScrollMargin; desiredScrollBottom = screenRangeBottom + verticalScrollMargin; } if (!options || options.reversed !== false) { if (desiredScrollBottom > this.getScrollBottom()) { this.setScrollBottom(desiredScrollBottom); } if (desiredScrollTop < this.getScrollTop()) { this.setScrollTop(desiredScrollTop); } } else { if (desiredScrollTop < this.getScrollTop()) { this.setScrollTop(desiredScrollTop); } if (desiredScrollBottom > this.getScrollBottom()) { this.setScrollBottom(desiredScrollBottom); } } return false; } autoscrollHorizontally(screenRange, options) { const horizontalScrollMargin = this.getHorizontalAutoscrollMargin(); const gutterContainerWidth = this.getGutterContainerWidth(); let left = this.pixelLeftForRowAndColumn( screenRange.start.row, screenRange.start.column ) + gutterContainerWidth; let right = this.pixelLeftForRowAndColumn( screenRange.end.row, screenRange.end.column ) + gutterContainerWidth; const desiredScrollLeft = Math.max( 0, left - horizontalScrollMargin - gutterContainerWidth ); const desiredScrollRight = Math.min( this.getScrollWidth(), right + horizontalScrollMargin ); if (!options || options.reversed !== false) { if (desiredScrollRight > this.getScrollRight()) { this.setScrollRight(desiredScrollRight); } if (desiredScrollLeft < this.getScrollLeft()) { this.setScrollLeft(desiredScrollLeft); } } else { if (desiredScrollLeft < this.getScrollLeft()) { this.setScrollLeft(desiredScrollLeft); } if (desiredScrollRight > this.getScrollRight()) { this.setScrollRight(desiredScrollRight); } } } getVerticalAutoscrollMargin() { const maxMarginInLines = Math.floor( (this.getScrollContainerClientHeight() / this.getLineHeight() - 1) / 2 ); const marginInLines = Math.min( this.props.model.verticalScrollMargin, maxMarginInLines ); return marginInLines * this.getLineHeight(); } getHorizontalAutoscrollMargin() { const maxMarginInBaseCharacters = Math.floor( (this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() - 1) / 2 ); const marginInBaseCharacters = Math.min( this.props.model.horizontalScrollMargin, maxMarginInBaseCharacters ); 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); // 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; } } // This method exists because it existed in the previous implementation and some // package tests relied on it measureDimensions() { this.measureCharacterDimensions(); this.measureGutterDimensions(); this.measureClientContainerHeight(); this.measureClientContainerWidth(); this.measureScrollbarDimensions(); this.hasInitialMeasurements = true; } measureCharacterDimensions() { this.measurements.lineHeight = Math.max( 1, this.refs.characterMeasurementLine.getBoundingClientRect().height ); this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width; this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width; this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width; this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().width; this.props.model.setLineHeightInPixels(this.measurements.lineHeight); this.props.model.setDefaultCharWidth( this.measurements.baseCharacterWidth, this.measurements.doubleWidthCharacterWidth, this.measurements.halfWidthCharacterWidth, this.measurements.koreanCharacterWidth ); this.lineTopIndex.setDefaultLineHeight(this.measurements.lineHeight); } measureGutterDimensions() { let dimensionsChanged = false; if (this.refs.gutterContainer) { const gutterContainerWidth = this.refs.gutterContainer.element .offsetWidth; if (gutterContainerWidth !== this.measurements.gutterContainerWidth) { dimensionsChanged = true; this.measurements.gutterContainerWidth = gutterContainerWidth; } } else { this.measurements.gutterContainerWidth = 0; } if ( this.refs.gutterContainer && this.refs.gutterContainer.refs.lineNumberGutter ) { const lineNumberGutterWidth = this.refs.gutterContainer.refs .lineNumberGutter.element.offsetWidth; if (lineNumberGutterWidth !== this.measurements.lineNumberGutterWidth) { dimensionsChanged = true; this.measurements.lineNumberGutterWidth = lineNumberGutterWidth; } } else { this.measurements.lineNumberGutterWidth = 0; } return dimensionsChanged; } measureClientContainerHeight() { const clientContainerHeight = this.refs.clientContainer.offsetHeight; if (clientContainerHeight !== this.measurements.clientContainerHeight) { this.measurements.clientContainerHeight = clientContainerHeight; return true; } else { return false; } } measureClientContainerWidth() { const clientContainerWidth = this.refs.clientContainer.offsetWidth; if (clientContainerWidth !== this.measurements.clientContainerWidth) { this.measurements.clientContainerWidth = clientContainerWidth; return true; } else { return false; } } measureScrollbarDimensions() { if (this.props.model.isMini()) { this.measurements.verticalScrollbarWidth = 0; this.measurements.horizontalScrollbarHeight = 0; } else { this.measurements.verticalScrollbarWidth = this.refs.verticalScrollbar.getRealScrollbarWidth(); this.measurements.horizontalScrollbarHeight = this.refs.horizontalScrollbar.getRealScrollbarHeight(); } } measureLongestLineWidth() { if (this.longestLineToMeasure) { const lineComponent = this.lineComponentsByScreenLineId.get( this.longestLineToMeasure.id ); this.measurements.longestLineWidth = lineComponent.element.firstChild.offsetWidth; this.longestLineToMeasure = null; } } requestLineToMeasure(row, screenLine) { this.linesToMeasure.set(row, screenLine); } requestHorizontalMeasurement(row, column) { if (column === 0) return; const screenLine = this.props.model.screenLineForScreenRow(row); if (screenLine) { this.requestLineToMeasure(row, screenLine); let columns = this.horizontalPositionsToMeasure.get(row); if (columns == null) { columns = []; this.horizontalPositionsToMeasure.set(row, columns); } columns.push(column); } } measureHorizontalPositions() { this.horizontalPositionsToMeasure.forEach((columnsToMeasure, row) => { columnsToMeasure.sort((a, b) => a - b); const screenLine = this.renderedScreenLineForRow(row); const lineComponent = this.lineComponentsByScreenLineId.get( screenLine.id ); if (!lineComponent) { const error = new Error( 'Requested measurement of a line component that is not currently rendered' ); error.metadata = { row, columnsToMeasure, renderedScreenLineIds: this.renderedScreenLines.map(line => line.id), extraRenderedScreenLineIds: Array.from( this.extraRenderedScreenLines.keys() ), lineComponentScreenLineIds: Array.from( this.lineComponentsByScreenLineId.keys() ), renderedStartRow: this.getRenderedStartRow(), renderedEndRow: this.getRenderedEndRow(), requestedScreenLineId: screenLine.id }; throw error; } const lineNode = lineComponent.element; const textNodes = lineComponent.textNodes; let positionsForLine = this.horizontalPixelPositionsByScreenLineId.get( screenLine.id ); if (positionsForLine == null) { positionsForLine = new Map(); this.horizontalPixelPositionsByScreenLineId.set( screenLine.id, positionsForLine ); } this.measureHorizontalPositionsOnLine( lineNode, textNodes, columnsToMeasure, positionsForLine ); }); this.horizontalPositionsToMeasure.clear(); } measureHorizontalPositionsOnLine( lineNode, textNodes, columnsToMeasure, positions ) { let lineNodeClientLeft = -1; let textNodeStartColumn = 0; let textNodesIndex = 0; let lastTextNodeRight = null; // eslint-disable-next-line no-labels columnLoop: for ( let columnsIndex = 0; columnsIndex < columnsToMeasure.length; columnsIndex++ ) { const nextColumnToMeasure = columnsToMeasure[columnsIndex]; while (textNodesIndex < textNodes.length) { if (nextColumnToMeasure === 0) { positions.set(0, 0); continue columnLoop; // eslint-disable-line no-labels } if (positions.has(nextColumnToMeasure)) continue columnLoop; // eslint-disable-line no-labels const textNode = textNodes[textNodesIndex]; const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length; if (nextColumnToMeasure < textNodeEndColumn) { let clientPixelPosition; if (nextColumnToMeasure === textNodeStartColumn) { clientPixelPosition = clientRectForRange(textNode, 0, 1).left; } else { clientPixelPosition = clientRectForRange( textNode, 0, nextColumnToMeasure - textNodeStartColumn ).right; } if (lineNodeClientLeft === -1) { lineNodeClientLeft = lineNode.getBoundingClientRect().left; } positions.set( nextColumnToMeasure, Math.round(clientPixelPosition - lineNodeClientLeft) ); continue columnLoop; // eslint-disable-line no-labels } else { textNodesIndex++; textNodeStartColumn = textNodeEndColumn; } } if (lastTextNodeRight == null) { const lastTextNode = textNodes[textNodes.length - 1]; lastTextNodeRight = clientRectForRange( lastTextNode, 0, lastTextNode.textContent.length ).right; } if (lineNodeClientLeft === -1) { lineNodeClientLeft = lineNode.getBoundingClientRect().left; } positions.set( nextColumnToMeasure, Math.round(lastTextNodeRight - lineNodeClientLeft) ); } } rowForPixelPosition(pixelPosition) { return Math.max(0, this.lineTopIndex.rowForPixelPosition(pixelPosition)); } heightForBlockDecorationsBeforeRow(row) { return ( this.pixelPositionAfterBlocksForRow(row) - this.pixelPositionBeforeBlocksForRow(row) ); } heightForBlockDecorationsAfterRow(row) { const currentRowBottom = this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight(); const nextRowTop = this.pixelPositionBeforeBlocksForRow(row + 1); return nextRowTop - currentRowBottom; } pixelPositionBeforeBlocksForRow(row) { return this.lineTopIndex.pixelPositionBeforeBlocksForRow(row); } pixelPositionAfterBlocksForRow(row) { return this.lineTopIndex.pixelPositionAfterBlocksForRow(row); } pixelLeftForRowAndColumn(row, column) { if (column === 0) return 0; const screenLine = this.renderedScreenLineForRow(row); if (screenLine) { const horizontalPositionsByColumn = this.horizontalPixelPositionsByScreenLineId.get( screenLine.id ); if (horizontalPositionsByColumn) { return horizontalPositionsByColumn.get(column); } } } screenPositionForPixelPosition({ top, left }) { const { model } = this.props; const row = Math.min( this.rowForPixelPosition(top), model.getApproximateScreenLineCount() - 1 ); let screenLine = this.renderedScreenLineForRow(row); if (!screenLine) { this.requestLineToMeasure(row, model.screenLineForScreenRow(row)); this.updateSyncBeforeMeasuringContent(); this.measureContentDuringUpdateSync(); screenLine = this.renderedScreenLineForRow(row); } const linesClientLeft = this.refs.lineTiles.getBoundingClientRect().left; const targetClientLeft = linesClientLeft + Math.max(0, left); const { textNodes } = this.lineComponentsByScreenLineId.get(screenLine.id); let containingTextNodeIndex; { let low = 0; let high = textNodes.length - 1; while (low <= high) { const mid = low + ((high - low) >> 1); const textNode = textNodes[mid]; const textNodeRect = clientRectForRange(textNode, 0, textNode.length); if (targetClientLeft < textNodeRect.left) { high = mid - 1; containingTextNodeIndex = Math.max(0, mid - 1); } else if (targetClientLeft > textNodeRect.right) { low = mid + 1; containingTextNodeIndex = Math.min(textNodes.length - 1, mid + 1); } else { containingTextNodeIndex = mid; break; } } } const containingTextNode = textNodes[containingTextNodeIndex]; let characterIndex = 0; { let low = 0; let high = containingTextNode.length - 1; while (low <= high) { const charIndex = low + ((high - low) >> 1); const nextCharIndex = isPairedCharacter( containingTextNode.textContent, charIndex ) ? charIndex + 2 : charIndex + 1; const rangeRect = clientRectForRange( containingTextNode, charIndex, nextCharIndex ); if (targetClientLeft < rangeRect.left) { high = charIndex - 1; characterIndex = Math.max(0, charIndex - 1); } else if (targetClientLeft > rangeRect.right) { low = nextCharIndex; characterIndex = Math.min( containingTextNode.textContent.length, nextCharIndex ); } else { if (targetClientLeft <= (rangeRect.left + rangeRect.right) / 2) { characterIndex = charIndex; } else { characterIndex = nextCharIndex; } break; } } } let textNodeStartColumn = 0; for (let i = 0; i < containingTextNodeIndex; i++) { textNodeStartColumn = textNodeStartColumn + textNodes[i].length; } const column = textNodeStartColumn + characterIndex; return Point(row, column); } didResetDisplayLayer() { this.spliceLineTopIndex(0, Infinity, Infinity); this.scheduleUpdate(); } didChangeDisplayLayer(changes) { for (let i = 0; i < changes.length; i++) { const { oldRange, newRange } = changes[i]; this.spliceLineTopIndex( newRange.start.row, oldRange.end.row - oldRange.start.row, newRange.end.row - newRange.start.row ); } this.scheduleUpdate(); } didChangeSelectionRange() { const { model } = this.props; if (this.getPlatform() === 'linux') { if (this.selectionClipboardImmediateId) { clearImmediate(this.selectionClipboardImmediateId); } this.selectionClipboardImmediateId = setImmediate(() => { this.selectionClipboardImmediateId = null; if (model.isDestroyed()) return; const selectedText = model.getSelectedText(); if (selectedText) { // This uses ipcRenderer.send instead of clipboard.writeText because // clipboard.writeText is a sync ipcRenderer call on Linux and that // will slow down selections. electron.ipcRenderer.send( 'write-text-to-selection-clipboard', selectedText ); } }); } } observeBlockDecorations() { const { model } = this.props; const decorations = model.getDecorations({ type: 'block' }); for (let i = 0; i < decorations.length; i++) { this.addBlockDecoration(decorations[i]); } } addBlockDecoration(decoration, subscribeToChanges = true) { const marker = decoration.getMarker(); const { item, position } = decoration.getProperties(); const element = TextEditor.viewForItem(item); if (marker.isValid()) { const row = marker.getHeadScreenPosition().row; this.lineTopIndex.insertBlock(decoration, row, 0, position === 'after'); this.blockDecorationsToMeasure.add(decoration); this.blockDecorationsByElement.set(element, decoration); this.blockDecorationResizeObserver.observe(element); this.scheduleUpdate(); } if (subscribeToChanges) { let wasValid = marker.isValid(); const didUpdateDisposable = marker.bufferMarker.onDidChange( ({ textChanged }) => { const isValid = marker.isValid(); if (wasValid && !isValid) { wasValid = false; this.blockDecorationsToMeasure.delete(decoration); this.heightsByBlockDecoration.delete(decoration); this.blockDecorationsByElement.delete(element); this.blockDecorationResizeObserver.unobserve(element); this.lineTopIndex.removeBlock(decoration); this.scheduleUpdate(); } else if (!wasValid && isValid) { wasValid = true; this.addBlockDecoration(decoration, false); } else if (isValid && !textChanged) { this.lineTopIndex.moveBlock( decoration, marker.getHeadScreenPosition().row ); this.scheduleUpdate(); } } ); const didDestroyDisposable = decoration.onDidDestroy(() => { didUpdateDisposable.dispose(); didDestroyDisposable.dispose(); if (wasValid) { wasValid = false; this.blockDecorationsToMeasure.delete(decoration); this.heightsByBlockDecoration.delete(decoration); this.blockDecorationsByElement.delete(element); this.blockDecorationResizeObserver.unobserve(element); this.lineTopIndex.removeBlock(decoration); this.scheduleUpdate(); } }); } } didResizeBlockDecorations(entries) { if (!this.visible) return; for (let i = 0; i < entries.length; i++) { const { target, contentRect } = entries[i]; const decoration = this.blockDecorationsByElement.get(target); const previousHeight = this.heightsByBlockDecoration.get(decoration); if ( this.element.contains(target) && contentRect.height !== previousHeight ) { this.invalidateBlockDecorationDimensions(decoration); } } } invalidateBlockDecorationDimensions(decoration) { this.blockDecorationsToMeasure.add(decoration); this.scheduleUpdate(); } spliceLineTopIndex(startRow, oldExtent, newExtent) { const invalidatedBlockDecorations = this.lineTopIndex.splice( startRow, oldExtent, newExtent ); invalidatedBlockDecorations.forEach(decoration => { const newPosition = decoration.getMarker().getHeadScreenPosition(); this.lineTopIndex.moveBlock(decoration, newPosition.row); }); } isVisible() { return this.element.offsetWidth > 0 || this.element.offsetHeight > 0; } getWindowInnerHeight() { return window.innerHeight; } getWindowInnerWidth() { return window.innerWidth; } getLineHeight() { return this.measurements.lineHeight; } getBaseCharacterWidth() { return this.measurements.baseCharacterWidth; } getLongestLineWidth() { return this.measurements.longestLineWidth; } getClientContainerHeight() { return this.measurements.clientContainerHeight; } getClientContainerWidth() { return this.measurements.clientContainerWidth; } getScrollContainerWidth() { if (this.props.model.getAutoWidth()) { return this.getScrollWidth(); } else { return this.getClientContainerWidth() - this.getGutterContainerWidth(); } } getScrollContainerHeight() { if (this.props.model.getAutoHeight()) { return this.getScrollHeight() + this.getHorizontalScrollbarHeight(); } else { return this.getClientContainerHeight(); } } getScrollContainerClientWidth() { return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth(); } getScrollContainerClientHeight() { return ( this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() ); } canScrollVertically() { const { model } = this.props; if (model.isMini()) return false; if (model.getAutoHeight()) return false; return this.getContentHeight() > this.getScrollContainerClientHeight(); } canScrollHorizontally() { const { model } = this.props; if (model.isMini()) return false; if (model.getAutoWidth()) return false; if (model.isSoftWrapped()) return false; return this.getContentWidth() > this.getScrollContainerClientWidth(); } getScrollHeight() { if (this.props.model.getScrollPastEnd()) { return ( this.getContentHeight() + Math.max( 3 * this.getLineHeight(), this.getScrollContainerClientHeight() - 3 * this.getLineHeight() ) ); } else if (this.props.model.getAutoHeight()) { return this.getContentHeight(); } else { return Math.max( this.getContentHeight(), this.getScrollContainerClientHeight() ); } } getScrollWidth() { const { model } = this.props; if (model.isSoftWrapped()) { return this.getScrollContainerClientWidth(); } else if (model.getAutoWidth()) { return this.getContentWidth(); } else { return Math.max( this.getContentWidth(), this.getScrollContainerClientWidth() ); } } getContentHeight() { return this.pixelPositionAfterBlocksForRow( this.props.model.getApproximateScreenLineCount() ); } getContentWidth() { return Math.ceil(this.getLongestLineWidth() + this.getBaseCharacterWidth()); } getScrollContainerClientWidthInBaseCharacters() { return Math.floor( this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() ); } getGutterContainerWidth() { return this.measurements.gutterContainerWidth; } getLineNumberGutterWidth() { return this.measurements.lineNumberGutterWidth; } getVerticalScrollbarWidth() { return this.measurements.verticalScrollbarWidth; } getHorizontalScrollbarHeight() { return this.measurements.horizontalScrollbarHeight; } getRowsPerTile() { return this.props.rowsPerTile || DEFAULT_ROWS_PER_TILE; } tileStartRowForRow(row) { return row - (row % this.getRowsPerTile()); } getRenderedStartRow() { if (this.derivedDimensionsCache.renderedStartRow == null) { this.derivedDimensionsCache.renderedStartRow = this.tileStartRowForRow( this.getFirstVisibleRow() ); } return this.derivedDimensionsCache.renderedStartRow; } getRenderedEndRow() { if (this.derivedDimensionsCache.renderedEndRow == null) { this.derivedDimensionsCache.renderedEndRow = Math.min( this.props.model.getApproximateScreenLineCount(), this.getRenderedStartRow() + this.getVisibleTileCount() * this.getRowsPerTile() ); } return this.derivedDimensionsCache.renderedEndRow; } getRenderedRowCount() { if (this.derivedDimensionsCache.renderedRowCount == null) { this.derivedDimensionsCache.renderedRowCount = Math.max( 0, this.getRenderedEndRow() - this.getRenderedStartRow() ); } return this.derivedDimensionsCache.renderedRowCount; } getRenderedTileCount() { if (this.derivedDimensionsCache.renderedTileCount == null) { this.derivedDimensionsCache.renderedTileCount = Math.ceil( this.getRenderedRowCount() / this.getRowsPerTile() ); } return this.derivedDimensionsCache.renderedTileCount; } getFirstVisibleRow() { if (this.derivedDimensionsCache.firstVisibleRow == null) { this.derivedDimensionsCache.firstVisibleRow = this.rowForPixelPosition( this.getScrollTop() ); } return this.derivedDimensionsCache.firstVisibleRow; } getLastVisibleRow() { if (this.derivedDimensionsCache.lastVisibleRow == null) { this.derivedDimensionsCache.lastVisibleRow = Math.min( this.props.model.getApproximateScreenLineCount() - 1, this.rowForPixelPosition(this.getScrollBottom()) ); } return this.derivedDimensionsCache.lastVisibleRow; } // We may render more tiles than needed if some contain block decorations, // 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 // combinations of editor height and line height. getVisibleTileCount() { if (this.derivedDimensionsCache.visibleTileCount == null) { const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() / this.getRowsPerTile(); this.derivedDimensionsCache.visibleTileCount = Math.ceil(editorHeightInTiles) + 1; } return this.derivedDimensionsCache.visibleTileCount; } getFirstVisibleColumn() { return Math.floor(this.getScrollLeft() / this.getBaseCharacterWidth()); } getScrollTop() { this.scrollTop = Math.min(this.getMaxScrollTop(), this.scrollTop); return this.scrollTop; } setScrollTop(scrollTop) { if (Number.isNaN(scrollTop) || scrollTop == null) return false; scrollTop = roundToPhysicalPixelBoundary( Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop)) ); if (scrollTop !== this.scrollTop) { this.derivedDimensionsCache = {}; this.scrollTopPending = true; this.scrollTop = scrollTop; this.element.emitter.emit('did-change-scroll-top', scrollTop); return true; } else { return false; } } getMaxScrollTop() { return Math.round( Math.max( 0, this.getScrollHeight() - this.getScrollContainerClientHeight() ) ); } getScrollBottom() { return this.getScrollTop() + this.getScrollContainerClientHeight(); } setScrollBottom(scrollBottom) { return this.setScrollTop( scrollBottom - this.getScrollContainerClientHeight() ); } getScrollLeft() { return this.scrollLeft; } setScrollLeft(scrollLeft) { if (Number.isNaN(scrollLeft) || scrollLeft == null) return false; scrollLeft = roundToPhysicalPixelBoundary( Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft)) ); if (scrollLeft !== this.scrollLeft) { this.scrollLeftPending = true; this.scrollLeft = scrollLeft; this.element.emitter.emit('did-change-scroll-left', scrollLeft); return true; } else { return false; } } getMaxScrollLeft() { return Math.round( Math.max(0, this.getScrollWidth() - this.getScrollContainerClientWidth()) ); } getScrollRight() { return this.getScrollLeft() + this.getScrollContainerClientWidth(); } setScrollRight(scrollRight) { return this.setScrollLeft( scrollRight - this.getScrollContainerClientWidth() ); } setScrollTopRow(scrollTopRow, scheduleUpdate = true) { if (this.hasInitialMeasurements) { const didScroll = this.setScrollTop( this.pixelPositionBeforeBlocksForRow(scrollTopRow) ); if (didScroll && scheduleUpdate) { this.scheduleUpdate(); } return didScroll; } else { this.pendingScrollTopRow = scrollTopRow; return false; } } getScrollTopRow() { if (this.hasInitialMeasurements) { return this.rowForPixelPosition(this.getScrollTop()); } else { return this.pendingScrollTopRow || 0; } } setScrollLeftColumn(scrollLeftColumn, scheduleUpdate = true) { if (this.hasInitialMeasurements && this.getLongestLineWidth() != null) { const didScroll = this.setScrollLeft( scrollLeftColumn * this.getBaseCharacterWidth() ); if (didScroll && scheduleUpdate) { this.scheduleUpdate(); } return didScroll; } else { this.pendingScrollLeftColumn = scrollLeftColumn; return false; } } getScrollLeftColumn() { if (this.hasInitialMeasurements && this.getLongestLineWidth() != null) { return Math.round(this.getScrollLeft() / this.getBaseCharacterWidth()); } else { return this.pendingScrollLeftColumn || 0; } } // 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() { const startRow = this.getRenderedStartRow(); const endRow = this.getRenderedEndRow(); const freeTileIds = []; for (let i = 0; i < this.renderedTileStartRows.length; i++) { const tileStartRow = this.renderedTileStartRows[i]; if (tileStartRow < startRow || tileStartRow >= endRow) { const tileId = this.idsByTileStartRow.get(tileStartRow); freeTileIds.push(tileId); this.idsByTileStartRow.delete(tileStartRow); } } const rowsPerTile = this.getRowsPerTile(); this.renderedTileStartRows.length = this.getRenderedTileCount(); for ( let tileStartRow = startRow, i = 0; tileStartRow < endRow; tileStartRow = tileStartRow + rowsPerTile, i++ ) { this.renderedTileStartRows[i] = tileStartRow; if (!this.idsByTileStartRow.has(tileStartRow)) { if (freeTileIds.length > 0) { this.idsByTileStartRow.set(tileStartRow, freeTileIds.shift()); } else { this.idsByTileStartRow.set(tileStartRow, this.nextTileId++); } } } this.renderedTileStartRows.sort( (a, b) => this.idsByTileStartRow.get(a) - this.idsByTileStartRow.get(b) ); } getNextUpdatePromise() { if (!this.nextUpdatePromise) { this.nextUpdatePromise = new Promise(resolve => { this.resolveNextUpdatePromise = () => { this.nextUpdatePromise = null; this.resolveNextUpdatePromise = null; resolve(); }; }); } return this.nextUpdatePromise; } setInputEnabled(inputEnabled) { this.props.model.update({ keyboardInputEnabled: inputEnabled }); } isInputEnabled() { return ( !this.props.model.isReadOnly() && this.props.model.isKeyboardInputEnabled() ); } getHiddenInput() { return this.refs.cursorsAndInput.refs.hiddenInput; } getPlatform() { return this.props.platform || process.platform; } getChromeVersion() { return this.props.chromeVersion || parseInt(process.versions.chrome); } }; class DummyScrollbarComponent { constructor(props) { this.props = props; etch.initialize(this); } update(newProps) { const oldProps = this.props; this.props = newProps; etch.updateSync(this); const shouldFlushScrollPosition = newProps.scrollTop !== oldProps.scrollTop || newProps.scrollLeft !== oldProps.scrollLeft; if (shouldFlushScrollPosition) this.flushScrollPosition(); } flushScrollPosition() { if (this.props.orientation === 'horizontal') { this.element.scrollLeft = this.props.scrollLeft; } else { this.element.scrollTop = this.props.scrollTop; } } render() { const { orientation, scrollWidth, scrollHeight, verticalScrollbarWidth, horizontalScrollbarHeight, canScroll, forceScrollbarVisible, didScroll } = this.props; const outerStyle = { position: 'absolute', contain: 'content', zIndex: 1, willChange: 'transform' }; if (!canScroll) outerStyle.visibility = 'hidden'; const innerStyle = {}; if (orientation === 'horizontal') { let right = verticalScrollbarWidth || 0; outerStyle.bottom = 0; outerStyle.left = 0; outerStyle.right = right + 'px'; outerStyle.height = '15px'; outerStyle.overflowY = 'hidden'; outerStyle.overflowX = forceScrollbarVisible ? 'scroll' : 'auto'; outerStyle.cursor = 'default'; innerStyle.height = '15px'; innerStyle.width = (scrollWidth || 0) + 'px'; } else { let bottom = horizontalScrollbarHeight || 0; outerStyle.right = 0; outerStyle.top = 0; outerStyle.bottom = bottom + 'px'; outerStyle.width = '15px'; outerStyle.overflowX = 'hidden'; outerStyle.overflowY = forceScrollbarVisible ? 'scroll' : 'auto'; outerStyle.cursor = 'default'; innerStyle.width = '15px'; innerStyle.height = (scrollHeight || 0) + 'px'; } return $.div( { className: `${orientation}-scrollbar`, style: outerStyle, on: { scroll: didScroll, mousedown: this.didMouseDown } }, $.div({ style: innerStyle }) ); } didMouseDown(event) { let { bottom, right } = this.element.getBoundingClientRect(); const clickedOnScrollbar = this.props.orientation === 'horizontal' ? event.clientY >= bottom - this.getRealScrollbarHeight() : event.clientX >= right - this.getRealScrollbarWidth(); if (!clickedOnScrollbar) this.props.didMouseDown(event); } getRealScrollbarWidth() { return this.element.offsetWidth - this.element.clientWidth; } getRealScrollbarHeight() { return this.element.offsetHeight - this.element.clientHeight; } } class GutterContainerComponent { constructor(props) { this.props = props; etch.initialize(this); } update(props) { if (this.shouldUpdate(props)) { this.props = props; etch.updateSync(this); } } shouldUpdate(props) { return ( !props.measuredContent || props.lineNumberGutterWidth !== this.props.lineNumberGutterWidth ); } render() { const { hasInitialMeasurements, scrollTop, scrollHeight, guttersToRender, decorationsToRender } = this.props; const innerStyle = { willChange: 'transform', display: 'flex' }; if (hasInitialMeasurements) { innerStyle.transform = `translateY(${-roundToPhysicalPixelBoundary( scrollTop )}px)`; } return $.div( { ref: 'gutterContainer', key: 'gutterContainer', className: 'gutter-container', style: { position: 'relative', zIndex: 1, backgroundColor: 'inherit' } }, $.div( { style: innerStyle }, guttersToRender.map(gutter => { if (gutter.type === 'line-number') { return this.renderLineNumberGutter(gutter); } else { return $(CustomGutterComponent, { key: gutter, element: gutter.getElement(), name: gutter.name, visible: gutter.isVisible(), height: scrollHeight, decorations: decorationsToRender.customGutter.get(gutter.name) }); } }) ) ); } renderLineNumberGutter(gutter) { const { rootComponent, showLineNumbers, hasInitialMeasurements, lineNumbersToRender, renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, scrollHeight, lineNumberGutterWidth, lineHeight } = this.props; if (!gutter.isVisible()) { return null; } const oneTrueLineNumberGutter = gutter.name === 'line-number'; const ref = oneTrueLineNumberGutter ? 'lineNumberGutter' : undefined; const width = oneTrueLineNumberGutter ? lineNumberGutterWidth : undefined; if (hasInitialMeasurements) { const { maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags } = lineNumbersToRender; return $(LineNumberGutterComponent, { ref, element: gutter.getElement(), name: gutter.name, className: gutter.className, labelFn: gutter.labelFn, onMouseDown: gutter.onMouseDown, onMouseMove: gutter.onMouseMove, rootComponent: rootComponent, startRow: renderedStartRow, endRow: renderedEndRow, rowsPerTile: rowsPerTile, maxDigits: maxDigits, keys: keys, bufferRows: bufferRows, screenRows: screenRows, softWrappedFlags: softWrappedFlags, foldableFlags: foldableFlags, decorations: decorationsToRender.lineNumbers.get(gutter.name) || [], blockDecorations: decorationsToRender.blocks, didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, height: scrollHeight, width, lineHeight: lineHeight, showLineNumbers }); } else { return $(LineNumberGutterComponent, { ref, element: gutter.getElement(), name: gutter.name, className: gutter.className, onMouseDown: gutter.onMouseDown, onMouseMove: gutter.onMouseMove, maxDigits: lineNumbersToRender.maxDigits, showLineNumbers }); } } } class LineNumberGutterComponent { constructor(props) { this.props = props; this.element = this.props.element; this.virtualNode = $.div(null); this.virtualNode.domNode = this.element; this.nodePool = new NodePool(); etch.updateSync(this); } update(newProps) { if (this.shouldUpdate(newProps)) { this.props = newProps; etch.updateSync(this); } } render() { const { rootComponent, showLineNumbers, height, width, startRow, endRow, rowsPerTile, maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations, className } = this.props; let children = null; if (bufferRows) { children = new Array(rootComponent.renderedTileStartRows.length); for (let i = 0; i < rootComponent.renderedTileStartRows.length; i++) { const tileStartRow = rootComponent.renderedTileStartRows[i]; const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile); const tileChildren = new Array(tileEndRow - tileStartRow); for (let row = tileStartRow; row < tileEndRow; row++) { const indexInTile = row - tileStartRow; const j = row - startRow; const key = keys[j]; const softWrapped = softWrappedFlags[j]; const foldable = foldableFlags[j]; const bufferRow = bufferRows[j]; const screenRow = screenRows[j]; let className = 'line-number'; if (foldable) className = className + ' foldable'; const decorationsForRow = decorations[row - startRow]; if (decorationsForRow) className = className + ' ' + decorationsForRow; let number = null; if (showLineNumbers) { if (this.props.labelFn == null) { number = softWrapped ? '•' : bufferRow + 1; number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number; } else { number = this.props.labelFn({ bufferRow, screenRow, foldable, softWrapped, maxDigits }); } } // We need to adjust the line number position to account for block // decorations preceding the current row and following the preceding // row. Note that we ignore the latter when the line number starts at // the beginning of the tile, because the tile will already be // positioned to take into account block decorations added after the // last row of the previous tile. let marginTop = rootComponent.heightForBlockDecorationsBeforeRow(row); if (indexInTile > 0) marginTop += rootComponent.heightForBlockDecorationsAfterRow( row - 1 ); tileChildren[row - tileStartRow] = $(LineNumberComponent, { key, className, width, bufferRow, screenRow, number, marginTop, nodePool: this.nodePool }); } const tileTop = rootComponent.pixelPositionBeforeBlocksForRow( tileStartRow ); const tileBottom = rootComponent.pixelPositionBeforeBlocksForRow( tileEndRow ); const tileHeight = tileBottom - tileTop; const tileWidth = width != null && width > 0 ? width + 'px' : ''; children[i] = $.div( { key: rootComponent.idsByTileStartRow.get(tileStartRow), style: { contain: 'layout style', position: 'absolute', top: 0, height: tileHeight + 'px', width: tileWidth, transform: `translateY(${tileTop}px)` } }, ...tileChildren ); } } let rootClassName = 'gutter line-numbers'; if (className) { rootClassName += ' ' + className; } return $.div( { className: rootClassName, attributes: { 'gutter-name': this.props.name }, style: { position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px' }, on: { mousedown: this.didMouseDown, mousemove: this.didMouseMove } }, $.div( { key: 'placeholder', className: 'line-number dummy', style: { visibility: 'hidden' } }, showLineNumbers ? '0'.repeat(maxDigits) : null, $.div({ className: 'icon-right' }) ), children ); } shouldUpdate(newProps) { const oldProps = this.props; if (oldProps.showLineNumbers !== newProps.showLineNumbers) return true; if (oldProps.height !== newProps.height) return true; if (oldProps.width !== newProps.width) return true; if (oldProps.lineHeight !== newProps.lineHeight) return true; if (oldProps.startRow !== newProps.startRow) return true; if (oldProps.endRow !== newProps.endRow) return true; if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true; if (oldProps.maxDigits !== newProps.maxDigits) return true; if (oldProps.labelFn !== newProps.labelFn) return true; if (oldProps.className !== newProps.className) return true; if (newProps.didMeasureVisibleBlockDecoration) return true; if (!arraysEqual(oldProps.keys, newProps.keys)) return true; if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true; if (!arraysEqual(oldProps.foldableFlags, newProps.foldableFlags)) return true; if (!arraysEqual(oldProps.decorations, newProps.decorations)) return true; let oldTileStartRow = oldProps.startRow; let newTileStartRow = newProps.startRow; while ( oldTileStartRow < oldProps.endRow || newTileStartRow < newProps.endRow ) { let oldTileBlockDecorations = oldProps.blockDecorations.get( oldTileStartRow ); let newTileBlockDecorations = newProps.blockDecorations.get( newTileStartRow ); if (oldTileBlockDecorations && newTileBlockDecorations) { if (oldTileBlockDecorations.size !== newTileBlockDecorations.size) return true; let blockDecorationsChanged = false; oldTileBlockDecorations.forEach((oldDecorations, screenLineId) => { if (!blockDecorationsChanged) { const newDecorations = newTileBlockDecorations.get(screenLineId); blockDecorationsChanged = newDecorations == null || !arraysEqual(oldDecorations, newDecorations); } }); if (blockDecorationsChanged) return true; newTileBlockDecorations.forEach((newDecorations, screenLineId) => { if (!blockDecorationsChanged) { const oldDecorations = oldTileBlockDecorations.get(screenLineId); blockDecorationsChanged = oldDecorations == null; } }); if (blockDecorationsChanged) return true; } else if (oldTileBlockDecorations) { return true; } else if (newTileBlockDecorations) { return true; } oldTileStartRow += oldProps.rowsPerTile; newTileStartRow += newProps.rowsPerTile; } return false; } didMouseDown(event) { if (this.props.onMouseDown == null) { this.props.rootComponent.didMouseDownOnLineNumberGutter(event); } else { const { bufferRow, screenRow } = event.target.dataset; this.props.onMouseDown({ bufferRow: parseInt(bufferRow, 10), screenRow: parseInt(screenRow, 10), domEvent: event }); } } didMouseMove(event) { if (this.props.onMouseMove != null) { const { bufferRow, screenRow } = event.target.dataset; this.props.onMouseMove({ bufferRow: parseInt(bufferRow, 10), screenRow: parseInt(screenRow, 10), domEvent: event }); } } } class LineNumberComponent { constructor(props) { const { className, width, marginTop, bufferRow, screenRow, number, nodePool } = props; this.props = props; const style = {}; if (width != null && width > 0) style.width = width + 'px'; if (marginTop != null && marginTop > 0) style.marginTop = marginTop + 'px'; this.element = nodePool.getElement('DIV', className, style); this.element.dataset.bufferRow = bufferRow; this.element.dataset.screenRow = screenRow; if (number) this.element.appendChild(nodePool.getTextNode(number)); this.element.appendChild(nodePool.getElement('DIV', 'icon-right', null)); } destroy() { this.element.remove(); this.props.nodePool.release(this.element); } update(props) { const { nodePool, className, width, marginTop, bufferRow, screenRow, number } = props; if (this.props.bufferRow !== bufferRow) this.element.dataset.bufferRow = bufferRow; if (this.props.screenRow !== screenRow) this.element.dataset.screenRow = screenRow; if (this.props.className !== className) this.element.className = className; if (this.props.width !== width) { if (width != null && width > 0) { this.element.style.width = width + 'px'; } else { this.element.style.width = ''; } } if (this.props.marginTop !== marginTop) { if (marginTop != null && marginTop > 0) { this.element.style.marginTop = marginTop + 'px'; } else { this.element.style.marginTop = ''; } } if (this.props.number !== number) { if (this.props.number != null) { const numberNode = this.element.firstChild; numberNode.remove(); nodePool.release(numberNode); } if (number != null) { this.element.insertBefore( nodePool.getTextNode(number), this.element.firstChild ); } } this.props = props; } } class CustomGutterComponent { constructor(props) { this.props = props; this.element = this.props.element; this.virtualNode = $.div(null); this.virtualNode.domNode = this.element; etch.updateSync(this); } update(props) { this.props = props; etch.updateSync(this); } destroy() { etch.destroy(this); } render() { let className = 'gutter'; if (this.props.className) { className += ' ' + this.props.className; } return $.div( { className, attributes: { 'gutter-name': this.props.name }, style: { display: this.props.visible ? '' : 'none' } }, $.div( { className: 'custom-decorations', style: { height: this.props.height + 'px' } }, this.renderDecorations() ) ); } renderDecorations() { if (!this.props.decorations) return null; return this.props.decorations.map(({ className, element, top, height }) => { return $(CustomGutterDecorationComponent, { className, element, top, height }); }); } } class CustomGutterDecorationComponent { constructor(props) { this.props = props; this.element = document.createElement('div'); const { top, height, className, element } = this.props; this.element.style.position = 'absolute'; this.element.style.top = top + 'px'; this.element.style.height = height + 'px'; if (className != null) this.element.className = className; if (element != null) { this.element.appendChild(element); element.style.height = height + 'px'; } } update(newProps) { const oldProps = this.props; this.props = newProps; if (newProps.top !== oldProps.top) this.element.style.top = newProps.top + 'px'; if (newProps.height !== oldProps.height) { this.element.style.height = newProps.height + 'px'; if (newProps.element) newProps.element.style.height = newProps.height + 'px'; } if (newProps.className !== oldProps.className) this.element.className = newProps.className || ''; if (newProps.element !== oldProps.element) { if (this.element.firstChild) this.element.firstChild.remove(); if (newProps.element != null) { this.element.appendChild(newProps.element); newProps.element.style.height = newProps.height + 'px'; } } } } class CursorsAndInputComponent { constructor(props) { this.props = props; etch.initialize(this); } update(props) { if (props.measuredContent) { this.props = props; etch.updateSync(this); } } updateCursorBlinkSync(cursorsBlinkedOff) { this.props.cursorsBlinkedOff = cursorsBlinkedOff; const className = this.getCursorsClassName(); this.refs.cursors.className = className; this.virtualNode.props.className = className; } render() { const { lineHeight, decorationsToRender, scrollHeight, scrollWidth } = this.props; const className = this.getCursorsClassName(); const cursorHeight = lineHeight + 'px'; const children = [this.renderHiddenInput()]; for (let i = 0; i < decorationsToRender.cursors.length; i++) { const { pixelLeft, pixelTop, pixelWidth, className: extraCursorClassName, style: extraCursorStyle } = decorationsToRender.cursors[i]; let cursorClassName = 'cursor'; if (extraCursorClassName) cursorClassName += ' ' + extraCursorClassName; const cursorStyle = { height: cursorHeight, width: Math.min(pixelWidth, scrollWidth - pixelLeft) + 'px', transform: `translate(${pixelLeft}px, ${pixelTop}px)` }; if (extraCursorStyle) Object.assign(cursorStyle, extraCursorStyle); children.push( $.div({ className: cursorClassName, style: cursorStyle }) ); } return $.div( { key: 'cursors', ref: 'cursors', className, style: { position: 'absolute', contain: 'strict', zIndex: 1, width: scrollWidth + 'px', height: scrollHeight + 'px', pointerEvents: 'none', userSelect: 'none' } }, children ); } getCursorsClassName() { return this.props.cursorsBlinkedOff ? 'cursors blink-off' : 'cursors'; } renderHiddenInput() { const { lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput, didPaste, didTextInput, didKeydown, didKeyup, didKeypress, didCompositionStart, didCompositionUpdate, didCompositionEnd, tabIndex } = this.props; let top, left; if (hiddenInputPosition) { top = hiddenInputPosition.pixelTop; left = hiddenInputPosition.pixelLeft; } else { top = 0; left = 0; } return $.input({ ref: 'hiddenInput', key: 'hiddenInput', className: 'hidden-input', on: { blur: didBlurHiddenInput, focus: didFocusHiddenInput, paste: didPaste, textInput: didTextInput, keydown: didKeydown, keyup: didKeyup, keypress: didKeypress, compositionstart: didCompositionStart, compositionupdate: didCompositionUpdate, compositionend: didCompositionEnd }, tabIndex: tabIndex, style: { position: 'absolute', width: '1px', height: lineHeight + 'px', top: top + 'px', left: left + 'px', opacity: 0, padding: 0, border: 0 } }); } } class LinesTileComponent { constructor(props) { this.props = props; etch.initialize(this); this.createLines(); this.updateBlockDecorations({}, props); } update(newProps) { if (this.shouldUpdate(newProps)) { const oldProps = this.props; this.props = newProps; etch.updateSync(this); if (!newProps.measuredContent) { this.updateLines(oldProps, newProps); this.updateBlockDecorations(oldProps, newProps); } } } destroy() { for (let i = 0; i < this.lineComponents.length; i++) { this.lineComponents[i].destroy(); } this.lineComponents.length = 0; return etch.destroy(this); } render() { const { height, width, top } = this.props; return $.div( { style: { contain: 'layout style', position: 'absolute', height: height + 'px', width: width + 'px', transform: `translateY(${top}px)` } } // Lines and block decorations will be manually inserted here for efficiency ); } createLines() { const { tileStartRow, screenLines, lineDecorations, textDecorations, nodePool, displayLayer, lineComponentsByScreenLineId } = this.props; this.lineComponents = []; for (let i = 0, length = screenLines.length; i < length; i++) { const component = new LineComponent({ screenLine: screenLines[i], screenRow: tileStartRow + i, lineDecoration: lineDecorations[i], textDecorations: textDecorations[i], displayLayer, nodePool, lineComponentsByScreenLineId }); this.element.appendChild(component.element); this.lineComponents.push(component); } } updateLines(oldProps, newProps) { const { screenLines, tileStartRow, lineDecorations, textDecorations, nodePool, displayLayer, lineComponentsByScreenLineId } = newProps; const oldScreenLines = oldProps.screenLines; const newScreenLines = screenLines; const oldScreenLinesEndIndex = oldScreenLines.length; const newScreenLinesEndIndex = newScreenLines.length; let oldScreenLineIndex = 0; let newScreenLineIndex = 0; let lineComponentIndex = 0; while ( oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex ) { const oldScreenLine = oldScreenLines[oldScreenLineIndex]; const newScreenLine = newScreenLines[newScreenLineIndex]; if (oldScreenLineIndex >= oldScreenLinesEndIndex) { var newScreenLineComponent = new LineComponent({ screenLine: newScreenLine, screenRow: tileStartRow + newScreenLineIndex, lineDecoration: lineDecorations[newScreenLineIndex], textDecorations: textDecorations[newScreenLineIndex], displayLayer, nodePool, lineComponentsByScreenLineId }); this.element.appendChild(newScreenLineComponent.element); this.lineComponents.push(newScreenLineComponent); newScreenLineIndex++; lineComponentIndex++; } else if (newScreenLineIndex >= newScreenLinesEndIndex) { this.lineComponents[lineComponentIndex].destroy(); this.lineComponents.splice(lineComponentIndex, 1); oldScreenLineIndex++; } else if (oldScreenLine === newScreenLine) { const lineComponent = this.lineComponents[lineComponentIndex]; lineComponent.update({ screenRow: tileStartRow + newScreenLineIndex, lineDecoration: lineDecorations[newScreenLineIndex], textDecorations: textDecorations[newScreenLineIndex] }); oldScreenLineIndex++; newScreenLineIndex++; lineComponentIndex++; } else { const oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf( oldScreenLine ); const newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf( newScreenLine ); if ( newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex ) { const newScreenLineComponents = []; while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { // eslint-disable-next-line no-redeclare var newScreenLineComponent = new LineComponent({ screenLine: newScreenLines[newScreenLineIndex], screenRow: tileStartRow + newScreenLineIndex, lineDecoration: lineDecorations[newScreenLineIndex], textDecorations: textDecorations[newScreenLineIndex], displayLayer, nodePool, lineComponentsByScreenLineId }); this.element.insertBefore( newScreenLineComponent.element, this.getFirstElementForScreenLine(oldProps, oldScreenLine) ); newScreenLineComponents.push(newScreenLineComponent); newScreenLineIndex++; } this.lineComponents.splice( lineComponentIndex, 0, ...newScreenLineComponents ); lineComponentIndex = lineComponentIndex + newScreenLineComponents.length; } else if ( oldScreenLineIndex < newScreenLineIndexInOldScreenLines && newScreenLineIndexInOldScreenLines < oldScreenLinesEndIndex ) { while (oldScreenLineIndex < newScreenLineIndexInOldScreenLines) { this.lineComponents[lineComponentIndex].destroy(); this.lineComponents.splice(lineComponentIndex, 1); oldScreenLineIndex++; } } else { const oldScreenLineComponent = this.lineComponents[ lineComponentIndex ]; // eslint-disable-next-line no-redeclare var newScreenLineComponent = new LineComponent({ screenLine: newScreenLines[newScreenLineIndex], screenRow: tileStartRow + newScreenLineIndex, lineDecoration: lineDecorations[newScreenLineIndex], textDecorations: textDecorations[newScreenLineIndex], displayLayer, nodePool, lineComponentsByScreenLineId }); this.element.insertBefore( newScreenLineComponent.element, oldScreenLineComponent.element ); oldScreenLineComponent.destroy(); this.lineComponents[lineComponentIndex] = newScreenLineComponent; oldScreenLineIndex++; newScreenLineIndex++; lineComponentIndex++; } } } } getFirstElementForScreenLine(oldProps, screenLine) { const blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null; if (blockDecorations) { const blockDecorationElementsBeforeOldScreenLine = []; for (let i = 0; i < blockDecorations.length; i++) { const decoration = blockDecorations[i]; if (decoration.position !== 'after') { blockDecorationElementsBeforeOldScreenLine.push( TextEditor.viewForItem(decoration.item) ); } } for ( let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++ ) { const blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i]; if ( !blockDecorationElementsBeforeOldScreenLine.includes( blockDecorationElement.previousSibling ) ) { return blockDecorationElement; } } } return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element; } updateBlockDecorations(oldProps, newProps) { const { blockDecorations, lineComponentsByScreenLineId } = newProps; if (oldProps.blockDecorations) { oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { const newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null; for (let i = 0; i < oldDecorations.length; i++) { const oldDecoration = oldDecorations[i]; if (newDecorations && newDecorations.includes(oldDecoration)) continue; const element = TextEditor.viewForItem(oldDecoration.item); if (element.parentElement !== this.element) continue; element.remove(); } }); } if (blockDecorations) { blockDecorations.forEach((newDecorations, screenLineId) => { const oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null; const lineNode = lineComponentsByScreenLineId.get(screenLineId).element; let lastAfter = lineNode; for (let i = 0; i < newDecorations.length; i++) { const newDecoration = newDecorations[i]; const element = TextEditor.viewForItem(newDecoration.item); if (oldDecorations && oldDecorations.includes(newDecoration)) { if (newDecoration.position === 'after') { lastAfter = element; } continue; } if (newDecoration.position === 'after') { this.element.insertBefore(element, lastAfter.nextSibling); lastAfter = element; } else { this.element.insertBefore(element, lineNode); } } }); } } shouldUpdate(newProps) { const oldProps = this.props; if (oldProps.top !== newProps.top) return true; if (oldProps.height !== newProps.height) return true; if (oldProps.width !== newProps.width) return true; if (oldProps.lineHeight !== newProps.lineHeight) return true; if (oldProps.tileStartRow !== newProps.tileStartRow) return true; if (oldProps.tileEndRow !== newProps.tileEndRow) return true; if (!arraysEqual(oldProps.screenLines, newProps.screenLines)) return true; if (!arraysEqual(oldProps.lineDecorations, newProps.lineDecorations)) return true; if (oldProps.blockDecorations && newProps.blockDecorations) { if (oldProps.blockDecorations.size !== newProps.blockDecorations.size) return true; let blockDecorationsChanged = false; oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { if (!blockDecorationsChanged) { const newDecorations = newProps.blockDecorations.get(screenLineId); blockDecorationsChanged = newDecorations == null || !arraysEqual(oldDecorations, newDecorations); } }); if (blockDecorationsChanged) return true; newProps.blockDecorations.forEach((newDecorations, screenLineId) => { if (!blockDecorationsChanged) { const oldDecorations = oldProps.blockDecorations.get(screenLineId); blockDecorationsChanged = oldDecorations == null; } }); if (blockDecorationsChanged) return true; } else if (oldProps.blockDecorations) { return true; } else if (newProps.blockDecorations) { return true; } if (oldProps.textDecorations.length !== newProps.textDecorations.length) return true; for (let i = 0; i < oldProps.textDecorations.length; i++) { if ( !textDecorationsEqual( oldProps.textDecorations[i], newProps.textDecorations[i] ) ) return true; } return false; } } class LineComponent { constructor(props) { const { nodePool, screenRow, screenLine, lineComponentsByScreenLineId, offScreen } = props; this.props = props; this.element = nodePool.getElement('DIV', this.buildClassName(), null); this.element.dataset.screenRow = screenRow; this.textNodes = []; if (offScreen) { this.element.style.position = 'absolute'; this.element.style.visibility = 'hidden'; this.element.dataset.offScreen = true; } this.appendContents(); lineComponentsByScreenLineId.set(screenLine.id, this); } update(newProps) { if (this.props.lineDecoration !== newProps.lineDecoration) { this.props.lineDecoration = newProps.lineDecoration; this.element.className = this.buildClassName(); } if (this.props.screenRow !== newProps.screenRow) { this.props.screenRow = newProps.screenRow; this.element.dataset.screenRow = newProps.screenRow; } if ( !textDecorationsEqual( this.props.textDecorations, newProps.textDecorations ) ) { this.props.textDecorations = newProps.textDecorations; this.element.firstChild.remove(); this.appendContents(); } } destroy() { const { nodePool, lineComponentsByScreenLineId, screenLine } = this.props; if (lineComponentsByScreenLineId.get(screenLine.id) === this) { lineComponentsByScreenLineId.delete(screenLine.id); } this.element.remove(); nodePool.release(this.element); } appendContents() { const { displayLayer, nodePool, screenLine, textDecorations } = this.props; this.textNodes.length = 0; const { lineText, tags } = screenLine; let openScopeNode = nodePool.getElement('SPAN', null, null); this.element.appendChild(openScopeNode); let decorationIndex = 0; let column = 0; let activeClassName = null; let activeStyle = null; let nextDecoration = textDecorations ? textDecorations[decorationIndex] : null; if (nextDecoration && nextDecoration.column === 0) { column = nextDecoration.column; activeClassName = nextDecoration.className; activeStyle = nextDecoration.style; nextDecoration = textDecorations[++decorationIndex]; } for (let i = 0; i < tags.length; i++) { const tag = tags[i]; if (tag !== 0) { if (displayLayer.isCloseTag(tag)) { openScopeNode = openScopeNode.parentElement; } else if (displayLayer.isOpenTag(tag)) { const newScopeNode = nodePool.getElement( 'SPAN', displayLayer.classNameForTag(tag), null ); openScopeNode.appendChild(newScopeNode); openScopeNode = newScopeNode; } else { const nextTokenColumn = column + tag; while (nextDecoration && nextDecoration.column <= nextTokenColumn) { const text = lineText.substring(column, nextDecoration.column); this.appendTextNode( openScopeNode, text, activeClassName, activeStyle ); column = nextDecoration.column; activeClassName = nextDecoration.className; activeStyle = nextDecoration.style; nextDecoration = textDecorations[++decorationIndex]; } if (column < nextTokenColumn) { const text = lineText.substring(column, nextTokenColumn); this.appendTextNode( openScopeNode, text, activeClassName, activeStyle ); column = nextTokenColumn; } } } } if (column === 0) { const textNode = nodePool.getTextNode(' '); this.element.appendChild(textNode); this.textNodes.push(textNode); } if (lineText.endsWith(displayLayer.foldCharacter)) { // Insert a zero-width non-breaking whitespace, so that LinesYardstick can // take the fold-marker::after pseudo-element into account during // measurements when such marker is the last character on the line. const textNode = nodePool.getTextNode(ZERO_WIDTH_NBSP_CHARACTER); this.element.appendChild(textNode); this.textNodes.push(textNode); } } appendTextNode(openScopeNode, text, activeClassName, activeStyle) { const { nodePool } = this.props; if (activeClassName || activeStyle) { const decorationNode = nodePool.getElement( 'SPAN', activeClassName, activeStyle ); openScopeNode.appendChild(decorationNode); openScopeNode = decorationNode; } const textNode = nodePool.getTextNode(text); openScopeNode.appendChild(textNode); this.textNodes.push(textNode); } buildClassName() { const { lineDecoration } = this.props; let className = 'line'; if (lineDecoration != null) className = className + ' ' + lineDecoration; return className; } } class HighlightsComponent { constructor(props) { this.props = {}; this.element = document.createElement('div'); this.element.className = 'highlights'; this.element.style.contain = 'strict'; this.element.style.position = 'absolute'; this.element.style.overflow = 'hidden'; this.element.style.userSelect = 'none'; this.highlightComponentsByKey = new Map(); this.update(props); } destroy() { this.highlightComponentsByKey.forEach(highlightComponent => { highlightComponent.destroy(); }); this.highlightComponentsByKey.clear(); } update(newProps) { if (this.shouldUpdate(newProps)) { this.props = newProps; const { height, width, lineHeight, highlightDecorations } = this.props; this.element.style.height = height + 'px'; this.element.style.width = width + 'px'; const visibleHighlightDecorations = new Set(); if (highlightDecorations) { for (let i = 0; i < highlightDecorations.length; i++) { const highlightDecoration = highlightDecorations[i]; const highlightProps = Object.assign( { lineHeight }, highlightDecorations[i] ); let highlightComponent = this.highlightComponentsByKey.get( highlightDecoration.key ); if (highlightComponent) { highlightComponent.update(highlightProps); } else { highlightComponent = new HighlightComponent(highlightProps); this.element.appendChild(highlightComponent.element); this.highlightComponentsByKey.set( highlightDecoration.key, highlightComponent ); } highlightDecorations[i].flashRequested = false; visibleHighlightDecorations.add(highlightDecoration.key); } } this.highlightComponentsByKey.forEach((highlightComponent, key) => { if (!visibleHighlightDecorations.has(key)) { highlightComponent.destroy(); this.highlightComponentsByKey.delete(key); } }); } } shouldUpdate(newProps) { const oldProps = this.props; if (!newProps.hasInitialMeasurements) return false; if (oldProps.width !== newProps.width) return true; if (oldProps.height !== newProps.height) return true; if (oldProps.lineHeight !== newProps.lineHeight) return true; if (!oldProps.highlightDecorations && newProps.highlightDecorations) return true; if (oldProps.highlightDecorations && !newProps.highlightDecorations) return true; if (oldProps.highlightDecorations && newProps.highlightDecorations) { if ( oldProps.highlightDecorations.length !== newProps.highlightDecorations.length ) return true; for ( let i = 0, length = oldProps.highlightDecorations.length; i < length; i++ ) { const oldHighlight = oldProps.highlightDecorations[i]; const newHighlight = newProps.highlightDecorations[i]; if (oldHighlight.className !== newHighlight.className) return true; if (newHighlight.flashRequested) return true; if (oldHighlight.startPixelTop !== newHighlight.startPixelTop) return true; if (oldHighlight.startPixelLeft !== newHighlight.startPixelLeft) return true; if (oldHighlight.endPixelTop !== newHighlight.endPixelTop) return true; if (oldHighlight.endPixelLeft !== newHighlight.endPixelLeft) return true; if (!oldHighlight.screenRange.isEqual(newHighlight.screenRange)) return true; } } } } class HighlightComponent { constructor(props) { this.props = props; etch.initialize(this); if (this.props.flashRequested) this.performFlash(); } destroy() { if (this.timeoutsByClassName) { this.timeoutsByClassName.forEach(timeout => { window.clearTimeout(timeout); }); this.timeoutsByClassName.clear(); } return etch.destroy(this); } update(newProps) { this.props = newProps; etch.updateSync(this); if (newProps.flashRequested) this.performFlash(); } performFlash() { const { flashClass, flashDuration } = this.props; if (!this.timeoutsByClassName) this.timeoutsByClassName = new Map(); // If a flash of this class is already in progress, clear it early and // flash again on the next frame to ensure CSS transitions apply to the // second flash. if (this.timeoutsByClassName.has(flashClass)) { window.clearTimeout(this.timeoutsByClassName.get(flashClass)); this.timeoutsByClassName.delete(flashClass); this.element.classList.remove(flashClass); requestAnimationFrame(() => this.performFlash()); } else { this.element.classList.add(flashClass); this.timeoutsByClassName.set( flashClass, window.setTimeout(() => { this.element.classList.remove(flashClass); }, flashDuration) ); } } render() { const { className, screenRange, lineHeight, startPixelTop, startPixelLeft, endPixelTop, endPixelLeft } = this.props; const regionClassName = 'region ' + className; let children; if (screenRange.start.row === screenRange.end.row) { children = $.div({ className: regionClassName, style: { position: 'absolute', boxSizing: 'border-box', top: startPixelTop + 'px', left: startPixelLeft + 'px', width: endPixelLeft - startPixelLeft + 'px', height: lineHeight + 'px' } }); } else { children = []; children.push( $.div({ className: regionClassName, style: { position: 'absolute', boxSizing: 'border-box', top: startPixelTop + 'px', left: startPixelLeft + 'px', right: 0, height: lineHeight + 'px' } }) ); if (screenRange.end.row - screenRange.start.row > 1) { children.push( $.div({ className: regionClassName, style: { position: 'absolute', boxSizing: 'border-box', top: startPixelTop + lineHeight + 'px', left: 0, right: 0, height: endPixelTop - startPixelTop - lineHeight * 2 + 'px' } }) ); } if (endPixelLeft > 0) { children.push( $.div({ className: regionClassName, style: { position: 'absolute', boxSizing: 'border-box', top: endPixelTop - lineHeight + 'px', left: 0, width: endPixelLeft + 'px', height: lineHeight + 'px' } }) ); } } return $.div({ className: 'highlight ' + className }, children); } } class OverlayComponent { constructor(props) { this.props = props; this.element = document.createElement('atom-overlay'); if (this.props.className != null) this.element.classList.add(this.props.className); this.element.appendChild(this.props.element); this.element.style.position = 'fixed'; this.element.style.zIndex = 4; this.element.style.top = (this.props.pixelTop || 0) + 'px'; this.element.style.left = (this.props.pixelLeft || 0) + 'px'; this.currentContentRect = null; // Synchronous DOM updates in response to resize events might trigger a // "loop limit exceeded" error. We disconnect the observer before // potentially mutating the DOM, and then reconnect it on the next tick. // Note: ResizeObserver calls its callback when .observe is called this.resizeObserver = new ResizeObserver(entries => { const { contentRect } = entries[0]; if ( this.currentContentRect && (this.currentContentRect.width !== contentRect.width || this.currentContentRect.height !== contentRect.height) ) { this.resizeObserver.disconnect(); this.props.didResize(this); process.nextTick(() => { this.resizeObserver.observe(this.props.element); }); } this.currentContentRect = contentRect; }); this.didAttach(); this.props.overlayComponents.add(this); } destroy() { this.props.overlayComponents.delete(this); this.didDetach(); } getNextUpdatePromise() { if (!this.nextUpdatePromise) { this.nextUpdatePromise = new Promise(resolve => { this.resolveNextUpdatePromise = () => { this.nextUpdatePromise = null; this.resolveNextUpdatePromise = null; resolve(); }; }); } return this.nextUpdatePromise; } update(newProps) { const oldProps = this.props; this.props = Object.assign({}, oldProps, newProps); 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.props.element); } didDetach() { this.resizeObserver.disconnect(); } } let rangeForMeasurement; function clientRectForRange(textNode, startIndex, endIndex) { if (!rangeForMeasurement) rangeForMeasurement = document.createRange(); rangeForMeasurement.setStart(textNode, startIndex); rangeForMeasurement.setEnd(textNode, endIndex); return rangeForMeasurement.getBoundingClientRect(); } function textDecorationsEqual(oldDecorations, newDecorations) { if (!oldDecorations && newDecorations) return false; if (oldDecorations && !newDecorations) return false; if (oldDecorations && newDecorations) { if (oldDecorations.length !== newDecorations.length) return false; for (let j = 0; j < oldDecorations.length; j++) { if (oldDecorations[j].column !== newDecorations[j].column) return false; if (oldDecorations[j].className !== newDecorations[j].className) return false; if (!objectsEqual(oldDecorations[j].style, newDecorations[j].style)) return false; } } return true; } function arraysEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0, length = a.length; i < length; i++) { if (a[i] !== b[i]) return false; } return true; } function objectsEqual(a, b) { if (!a && b) return false; if (a && !b) return false; if (a && b) { for (const key in a) { if (a[key] !== b[key]) return false; } for (const key in b) { if (a[key] !== b[key]) return false; } } return true; } function constrainRangeToRows(range, startRow, endRow) { if (range.start.row < startRow || range.end.row >= endRow) { range = range.copy(); if (range.start.row < startRow) { range.start.row = startRow; range.start.column = 0; } if (range.end.row >= endRow) { range.end.row = endRow; range.end.column = 0; } } return range; } function debounce(fn, wait) { let timestamp, timeout; function later() { const last = Date.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; fn(); } } return function() { timestamp = Date.now(); if (!timeout) timeout = setTimeout(later, wait); }; } class NodePool { constructor() { this.elementsByType = {}; this.textNodes = []; } getElement(type, className, style) { let element; const elementsByDepth = this.elementsByType[type]; if (elementsByDepth) { while (elementsByDepth.length > 0) { const elements = elementsByDepth[elementsByDepth.length - 1]; if (elements && elements.length > 0) { element = elements.pop(); if (elements.length === 0) elementsByDepth.pop(); break; } else { elementsByDepth.pop(); } } } if (element) { element.className = className || ''; element.attributeStyleMap.forEach((value, key) => { if (!style || style[key] == null) element.style[key] = ''; }); if (style) Object.assign(element.style, style); for (const key in element.dataset) delete element.dataset[key]; while (element.firstChild) element.firstChild.remove(); return element; } else { const newElement = document.createElement(type); if (className) newElement.className = className; if (style) Object.assign(newElement.style, style); return newElement; } } getTextNode(text) { if (this.textNodes.length > 0) { const node = this.textNodes.pop(); node.textContent = text; return node; } else { return document.createTextNode(text); } } release(node, depth = 0) { const { nodeName } = node; if (nodeName === '#text') { this.textNodes.push(node); } else { let elementsByDepth = this.elementsByType[nodeName]; if (!elementsByDepth) { elementsByDepth = []; this.elementsByType[nodeName] = elementsByDepth; } let elements = elementsByDepth[depth]; if (!elements) { elements = []; elementsByDepth[depth] = elements; } elements.push(node); for (let i = 0; i < node.childNodes.length; i++) { this.release(node.childNodes[i], depth + 1); } } } } function roundToPhysicalPixelBoundary(virtualPixelPosition) { const virtualPixelsPerPhysicalPixel = 1 / window.devicePixelRatio; return ( Math.round(virtualPixelPosition / virtualPixelsPerPhysicalPixel) * virtualPixelsPerPhysicalPixel ); } function ceilToPhysicalPixelBoundary(virtualPixelPosition) { const virtualPixelsPerPhysicalPixel = 1 / window.devicePixelRatio; return ( Math.ceil(virtualPixelPosition / virtualPixelsPerPhysicalPixel) * virtualPixelsPerPhysicalPixel ); } ================================================ FILE: src/text-editor-element.js ================================================ const { Emitter, Range } = require('atom'); const Grim = require('grim'); const TextEditorComponent = require('./text-editor-component'); const dedent = require('dedent'); class TextEditorElement extends HTMLElement { initialize(component) { this.component = component; return this; } get shadowRoot() { Grim.deprecate(dedent` The contents of \`atom-text-editor\` elements are no longer encapsulated within a shadow DOM boundary. Please, stop using \`shadowRoot\` and access the editor contents directly instead. `); return this; } get rootElement() { Grim.deprecate(dedent` The contents of \`atom-text-editor\` elements are no longer encapsulated within a shadow DOM boundary. Please, stop using \`rootElement\` and access the editor contents directly instead. `); return this; } constructor() { super(); this.emitter = new Emitter(); this.initialText = this.textContent; if (this.tabIndex == null) this.tabIndex = -1; this.addEventListener('focus', event => this.getComponent().didFocus(event) ); this.addEventListener('blur', event => this.getComponent().didBlur(event)); } connectedCallback() { this.getComponent().didAttach(); this.emitter.emit('did-attach'); } disconnectedCallback() { this.emitter.emit('did-detach'); this.getComponent().didDetach(); } static get observedAttributes() { return ['mini', 'placeholder-text', 'gutter-hidden', 'readonly']; } attributeChangedCallback(name, oldValue, newValue) { if (this.component) { switch (name) { case 'mini': this.getModel().update({ mini: newValue != null }); break; case 'placeholder-text': this.getModel().update({ placeholderText: newValue }); break; case 'gutter-hidden': this.getModel().update({ lineNumberGutterVisible: newValue == null }); break; case 'readonly': this.getModel().update({ readOnly: newValue != null }); break; } } } // Extended: Get a promise that resolves the next time the element's DOM // is updated in any way. // // This can be useful when you've made a change to the model and need to // be sure this change has been flushed to the DOM. // // Returns a {Promise}. getNextUpdatePromise() { return this.getComponent().getNextUpdatePromise(); } getModel() { return this.getComponent().props.model; } setModel(model) { this.getComponent().update({ model }); this.updateModelFromAttributes(); } updateModelFromAttributes() { const props = { mini: this.hasAttribute('mini') }; if (this.hasAttribute('placeholder-text')) props.placeholderText = this.getAttribute('placeholder-text'); if (this.hasAttribute('gutter-hidden')) props.lineNumberGutterVisible = false; this.getModel().update(props); if (this.initialText) this.getModel().setText(this.initialText); } onDidAttach(callback) { return this.emitter.on('did-attach', callback); } onDidDetach(callback) { return this.emitter.on('did-detach', callback); } measureDimensions() { this.getComponent().measureDimensions(); } setWidth(width) { this.style.width = this.getComponent().getGutterContainerWidth() + width + 'px'; } getWidth() { return this.getComponent().getScrollContainerWidth(); } setHeight(height) { this.style.height = height + 'px'; } getHeight() { return this.getComponent().getScrollContainerHeight(); } onDidChangeScrollLeft(callback) { return this.emitter.on('did-change-scroll-left', callback); } onDidChangeScrollTop(callback) { return this.emitter.on('did-change-scroll-top', callback); } // Deprecated: get the width of an `x` character displayed in this element. // // Returns a {Number} of pixels. getDefaultCharacterWidth() { return this.getComponent().getBaseCharacterWidth(); } // Extended: get the width of an `x` character displayed in this element. // // Returns a {Number} of pixels. getBaseCharacterWidth() { return this.getComponent().getBaseCharacterWidth(); } getMaxScrollTop() { return this.getComponent().getMaxScrollTop(); } getScrollHeight() { return this.getComponent().getScrollHeight(); } getScrollWidth() { return this.getComponent().getScrollWidth(); } getVerticalScrollbarWidth() { return this.getComponent().getVerticalScrollbarWidth(); } getHorizontalScrollbarHeight() { return this.getComponent().getHorizontalScrollbarHeight(); } getScrollTop() { return this.getComponent().getScrollTop(); } setScrollTop(scrollTop) { const component = this.getComponent(); component.setScrollTop(scrollTop); component.scheduleUpdate(); } getScrollBottom() { return this.getComponent().getScrollBottom(); } setScrollBottom(scrollBottom) { return this.getComponent().setScrollBottom(scrollBottom); } getScrollLeft() { return this.getComponent().getScrollLeft(); } setScrollLeft(scrollLeft) { const component = this.getComponent(); component.setScrollLeft(scrollLeft); component.scheduleUpdate(); } getScrollRight() { return this.getComponent().getScrollRight(); } setScrollRight(scrollRight) { return this.getComponent().setScrollRight(scrollRight); } // Essential: Scrolls the editor to the top. scrollToTop() { this.setScrollTop(0); } // Essential: Scrolls the editor to the bottom. scrollToBottom() { this.setScrollTop(Infinity); } hasFocus() { return this.getComponent().focused; } // Extended: Converts a buffer position to a pixel position. // // * `bufferPosition` A {Point}-like object that represents a buffer position. // // Be aware that calling this method with a column that does not translate // to column 0 on screen could cause a synchronous DOM update in order to // measure the requested horizontal pixel position if it isn't already // cached. // // Returns an {Object} with two values: `top` and `left`, representing the // pixel position. pixelPositionForBufferPosition(bufferPosition) { const screenPosition = this.getModel().screenPositionForBufferPosition( bufferPosition ); return this.getComponent().pixelPositionForScreenPosition(screenPosition); } // Extended: Converts a screen position to a pixel position. // // * `screenPosition` A {Point}-like object that represents a buffer position. // // Be aware that calling this method with a non-zero column value could // cause a synchronous DOM update in order to measure the requested // horizontal pixel position if it isn't already cached. // // Returns an {Object} with two values: `top` and `left`, representing the // pixel position. pixelPositionForScreenPosition(screenPosition) { screenPosition = this.getModel().clipScreenPosition(screenPosition); return this.getComponent().pixelPositionForScreenPosition(screenPosition); } screenPositionForPixelPosition(pixelPosition) { return this.getComponent().screenPositionForPixelPosition(pixelPosition); } pixelRectForScreenRange(range) { range = Range.fromObject(range); const start = this.pixelPositionForScreenPosition(range.start); const end = this.pixelPositionForScreenPosition(range.end); const lineHeight = this.getComponent().getLineHeight(); return { top: start.top, left: start.left, height: end.top + lineHeight - start.top, width: end.left - start.left }; } pixelRangeForScreenRange(range) { range = Range.fromObject(range); return { start: this.pixelPositionForScreenPosition(range.start), end: this.pixelPositionForScreenPosition(range.end) }; } getComponent() { if (!this.component) { this.component = new TextEditorComponent({ element: this, mini: this.hasAttribute('mini'), updatedSynchronously: this.updatedSynchronously, readOnly: this.hasAttribute('readonly') }); this.updateModelFromAttributes(); } return this.component; } setUpdatedSynchronously(updatedSynchronously) { this.updatedSynchronously = updatedSynchronously; if (this.component) this.component.updatedSynchronously = updatedSynchronously; return updatedSynchronously; } isUpdatedSynchronously() { return this.component ? this.component.updatedSynchronously : this.updatedSynchronously; } // Experimental: Invalidate the passed block {Decoration}'s dimensions, // forcing them to be recalculated and the surrounding content to be adjusted // on the next animation frame. // // * {blockDecoration} A {Decoration} representing the block decoration you // want to update the dimensions of. invalidateBlockDecorationDimensions() { this.getComponent().invalidateBlockDecorationDimensions(...arguments); } setFirstVisibleScreenRow(row) { this.getModel().setFirstVisibleScreenRow(row); } getFirstVisibleScreenRow() { return this.getModel().getFirstVisibleScreenRow(); } getLastVisibleScreenRow() { return this.getModel().getLastVisibleScreenRow(); } getVisibleRowRange() { return this.getModel().getVisibleRowRange(); } intersectsVisibleRowRange(startRow, endRow) { return !( endRow <= this.getFirstVisibleScreenRow() || this.getLastVisibleScreenRow() <= startRow ); } selectionIntersectsVisibleRowRange(selection) { const { start, end } = selection.getScreenRange(); return this.intersectsVisibleRowRange(start.row, end.row + 1); } setFirstVisibleScreenColumn(column) { return this.getModel().setFirstVisibleScreenColumn(column); } getFirstVisibleScreenColumn() { return this.getModel().getFirstVisibleScreenColumn(); } static createTextEditorElement() { return document.createElement('atom-text-editor'); } } window.customElements.define('atom-text-editor', TextEditorElement); module.exports = TextEditorElement; ================================================ FILE: src/text-editor-registry.js ================================================ const _ = require('underscore-plus'); const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); const TextEditor = require('./text-editor'); const ScopeDescriptor = require('./scope-descriptor'); const EDITOR_PARAMS_BY_SETTING_KEY = [ ['core.fileEncoding', 'encoding'], ['editor.atomicSoftTabs', 'atomicSoftTabs'], ['editor.showInvisibles', 'showInvisibles'], ['editor.tabLength', 'tabLength'], ['editor.invisibles', 'invisibles'], ['editor.showCursorOnSelection', 'showCursorOnSelection'], ['editor.showIndentGuide', 'showIndentGuide'], ['editor.showLineNumbers', 'showLineNumbers'], ['editor.softWrap', 'softWrapped'], ['editor.softWrapHangingIndent', 'softWrapHangingIndentLength'], ['editor.softWrapAtPreferredLineLength', 'softWrapAtPreferredLineLength'], ['editor.preferredLineLength', 'preferredLineLength'], ['editor.maxScreenLineLength', 'maxScreenLineLength'], ['editor.autoIndent', 'autoIndent'], ['editor.autoIndentOnPaste', 'autoIndentOnPaste'], ['editor.scrollPastEnd', 'scrollPastEnd'], ['editor.undoGroupingInterval', 'undoGroupingInterval'], ['editor.scrollSensitivity', 'scrollSensitivity'] ]; // Experimental: This global registry tracks registered `TextEditors`. // // If you want to add functionality to a wider set of text editors than just // those appearing within workspace panes, use `atom.textEditors.observe` to // invoke a callback for all current and future registered text editors. // // If you want packages to be able to add functionality to your non-pane text // editors (such as a search field in a custom user interface element), register // them for observation via `atom.textEditors.add`. **Important:** When you're // done using your editor, be sure to call `dispose` on the returned disposable // to avoid leaking editors. module.exports = class TextEditorRegistry { constructor({ config, assert, packageManager }) { this.config = config; this.assert = assert; this.packageManager = packageManager; this.clear(); } deserialize(state) { this.editorGrammarOverrides = state.editorGrammarOverrides; } serialize() { return { editorGrammarOverrides: Object.assign({}, this.editorGrammarOverrides) }; } clear() { if (this.subscriptions) { this.subscriptions.dispose(); } this.subscriptions = new CompositeDisposable(); this.editors = new Set(); this.emitter = new Emitter(); this.scopesWithConfigSubscriptions = new Set(); this.editorsWithMaintainedConfig = new Set(); this.editorsWithMaintainedGrammar = new Set(); this.editorGrammarOverrides = {}; this.editorGrammarScores = new WeakMap(); } destroy() { this.subscriptions.dispose(); this.editorsWithMaintainedConfig = null; } // Register a `TextEditor`. // // * `editor` The editor to register. // // Returns a {Disposable} on which `.dispose()` can be called to remove the // added editor. To avoid any memory leaks this should be called when the // editor is destroyed. add(editor) { this.editors.add(editor); editor.registered = true; this.emitter.emit('did-add-editor', editor); return new Disposable(() => this.remove(editor)); } build(params) { params = Object.assign({ assert: this.assert }, params); let scope = null; if (params.buffer) { const { grammar } = params.buffer.getLanguageMode(); if (grammar) { scope = new ScopeDescriptor({ scopes: [grammar.scopeName] }); } } Object.assign(params, this.textEditorParamsForScope(scope)); return new TextEditor(params); } // Remove a `TextEditor`. // // * `editor` The editor to remove. // // Returns a {Boolean} indicating whether the editor was successfully removed. remove(editor) { const removed = this.editors.delete(editor); editor.registered = false; return removed; } // Gets the currently active text editor. // // Returns the currently active text editor, or `null` if there is none. getActiveTextEditor() { for (let ed of this.editors) { // fast path, works as long as there's a shadow DOM inside the text editor if (ed.getElement() === document.activeElement) { return ed; } else { let editorElement = ed.getElement(); let current = document.activeElement; while (current) { if (current === editorElement) { return ed; } current = current.parentNode; } } } return null; } // Invoke the given callback with all the current and future registered // `TextEditors`. // // * `callback` {Function} to be called with current and future text editors. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observe(callback) { this.editors.forEach(callback); return this.emitter.on('did-add-editor', callback); } // Keep a {TextEditor}'s configuration in sync with Atom's settings. // // * `editor` The editor whose configuration will be maintained. // // Returns a {Disposable} that can be used to stop updating the editor's // configuration. maintainConfig(editor) { if (this.editorsWithMaintainedConfig.has(editor)) { return new Disposable(noop); } this.editorsWithMaintainedConfig.add(editor); this.updateAndMonitorEditorSettings(editor); const languageChangeSubscription = editor.buffer.onDidChangeLanguageMode( (newLanguageMode, oldLanguageMode) => { this.updateAndMonitorEditorSettings(editor, oldLanguageMode); } ); this.subscriptions.add(languageChangeSubscription); const updateTabTypes = () => { const configOptions = { scope: editor.getRootScopeDescriptor() }; editor.setSoftTabs( shouldEditorUseSoftTabs( editor, this.config.get('editor.tabType', configOptions), this.config.get('editor.softTabs', configOptions) ) ); }; updateTabTypes(); const tokenizeSubscription = editor.onDidTokenize(updateTabTypes); this.subscriptions.add(tokenizeSubscription); return new Disposable(() => { this.editorsWithMaintainedConfig.delete(editor); tokenizeSubscription.dispose(); languageChangeSubscription.dispose(); this.subscriptions.remove(languageChangeSubscription); this.subscriptions.remove(tokenizeSubscription); }); } // Deprecated: set a {TextEditor}'s grammar based on its path and content, // and continue to update its grammar as grammars are added or updated, or // the editor's file path changes. // // * `editor` The editor whose grammar will be maintained. // // Returns a {Disposable} that can be used to stop updating the editor's // grammar. maintainGrammar(editor) { atom.grammars.maintainLanguageMode(editor.getBuffer()); } // Deprecated: Force a {TextEditor} to use a different grammar than the // one that would otherwise be selected for it. // // * `editor` The editor whose gramamr will be set. // * `languageId` The {String} language ID for the desired {Grammar}. setGrammarOverride(editor, languageId) { atom.grammars.assignLanguageMode(editor.getBuffer(), languageId); } // Deprecated: Retrieve the grammar scope name that has been set as a // grammar override for the given {TextEditor}. // // * `editor` The editor. // // Returns a {String} scope name, or `null` if no override has been set // for the given editor. getGrammarOverride(editor) { return atom.grammars.getAssignedLanguageId(editor.getBuffer()); } // Deprecated: Remove any grammar override that has been set for the given {TextEditor}. // // * `editor` The editor. clearGrammarOverride(editor) { atom.grammars.autoAssignLanguageMode(editor.getBuffer()); } async updateAndMonitorEditorSettings(editor, oldLanguageMode) { await this.packageManager.getActivatePromise(); this.updateEditorSettingsForLanguageMode(editor, oldLanguageMode); this.subscribeToSettingsForEditorScope(editor); } updateEditorSettingsForLanguageMode(editor, oldLanguageMode) { const newLanguageMode = editor.buffer.getLanguageMode(); if (oldLanguageMode) { const newSettings = this.textEditorParamsForScope( newLanguageMode.rootScopeDescriptor ); const oldSettings = this.textEditorParamsForScope( oldLanguageMode.rootScopeDescriptor ); const updatedSettings = {}; for (const [, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) { // Update the setting only if it has changed between the two language // modes. This prevents user-modified settings in an editor (like // 'softWrapped') from being reset when the language mode changes. if (!_.isEqual(newSettings[paramName], oldSettings[paramName])) { updatedSettings[paramName] = newSettings[paramName]; } } if (_.size(updatedSettings) > 0) { editor.update(updatedSettings); } } else { editor.update( this.textEditorParamsForScope(newLanguageMode.rootScopeDescriptor) ); } } subscribeToSettingsForEditorScope(editor) { if (!this.editorsWithMaintainedConfig) return; const scopeDescriptor = editor.getRootScopeDescriptor(); const scopeChain = scopeDescriptor.getScopeChain(); if (!this.scopesWithConfigSubscriptions.has(scopeChain)) { this.scopesWithConfigSubscriptions.add(scopeChain); const configOptions = { scope: scopeDescriptor }; for (const [settingKey, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) { this.subscriptions.add( this.config.onDidChange(settingKey, configOptions, ({ newValue }) => { this.editorsWithMaintainedConfig.forEach(editor => { if (editor.getRootScopeDescriptor().isEqual(scopeDescriptor)) { editor.update({ [paramName]: newValue }); } }); }) ); } const updateTabTypes = () => { const tabType = this.config.get('editor.tabType', configOptions); const softTabs = this.config.get('editor.softTabs', configOptions); this.editorsWithMaintainedConfig.forEach(editor => { if (editor.getRootScopeDescriptor().isEqual(scopeDescriptor)) { editor.setSoftTabs( shouldEditorUseSoftTabs(editor, tabType, softTabs) ); } }); }; this.subscriptions.add( this.config.onDidChange( 'editor.tabType', configOptions, updateTabTypes ), this.config.onDidChange( 'editor.softTabs', configOptions, updateTabTypes ) ); } } textEditorParamsForScope(scopeDescriptor) { const result = {}; const configOptions = { scope: scopeDescriptor }; for (const [settingKey, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) { result[paramName] = this.config.get(settingKey, configOptions); } return result; } }; function shouldEditorUseSoftTabs(editor, tabType, softTabs) { switch (tabType) { case 'hard': return false; case 'soft': return true; case 'auto': switch (editor.usesSoftTabs()) { case true: return true; case false: return false; default: return softTabs; } } } function noop() {} ================================================ FILE: src/text-editor.js ================================================ const _ = require('underscore-plus'); const path = require('path'); const fs = require('fs-plus'); const Grim = require('grim'); const dedent = require('dedent'); const { CompositeDisposable, Disposable, Emitter } = require('event-kit'); const TextBuffer = require('text-buffer'); const { Point, Range } = TextBuffer; const DecorationManager = require('./decoration-manager'); const Cursor = require('./cursor'); const Selection = require('./selection'); const NullGrammar = require('./null-grammar'); const TextMateLanguageMode = require('./text-mate-language-mode'); const ScopeDescriptor = require('./scope-descriptor'); const TextMateScopeSelector = require('first-mate').ScopeSelector; const GutterContainer = require('./gutter-container'); let TextEditorComponent = null; let TextEditorElement = null; const { isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary } = require('./text-utils'); const SERIALIZATION_VERSION = 1; const NON_WHITESPACE_REGEXP = /\S/; const ZERO_WIDTH_NBSP = '\ufeff'; let nextId = 0; const DEFAULT_NON_WORD_CHARACTERS = '/\\()"\':,.;<>~!@#$%^&*|+=[]{}`?-…'; // Essential: This class represents all essential editing state for a single // {TextBuffer}, including cursor and selection positions, folds, and soft wraps. // If you're manipulating the state of an editor, use this class. // // A single {TextBuffer} can belong to multiple editors. For example, if the // same file is open in two different panes, Atom creates a separate editor for // each pane. If the buffer is manipulated the changes are reflected in both // editors, but each maintains its own cursor position, folded lines, etc. // // ## Accessing TextEditor Instances // // The easiest way to get hold of `TextEditor` objects is by registering a callback // with `::observeTextEditors` on the `atom.workspace` global. Your callback will // then be called with all current editor instances and also when any editor is // created in the future. // // ```js // atom.workspace.observeTextEditors(editor => { // editor.insertText('Hello World') // }) // ``` // // ## Buffer vs. Screen Coordinates // // Because editors support folds and soft-wrapping, the lines on screen don't // always match the lines in the buffer. For example, a long line that soft wraps // twice renders as three lines on screen, but only represents one line in the // buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds // to row 11 in the buffer. // // Your choice of coordinates systems will depend on what you're trying to // achieve. For example, if you're writing a command that jumps the cursor up or // down by 10 lines, you'll want to use screen coordinates because the user // probably wants to skip lines *on screen*. However, if you're writing a package // that jumps between method definitions, you'll want to work in buffer // coordinates. // // **When in doubt, just default to buffer coordinates**, then experiment with // soft wraps and folds to ensure your code interacts with them correctly. module.exports = class TextEditor { static setClipboard(clipboard) { this.clipboard = clipboard; } static setScheduler(scheduler) { if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component'); } return TextEditorComponent.setScheduler(scheduler); } static didUpdateStyles() { if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component'); } return TextEditorComponent.didUpdateStyles(); } static didUpdateScrollbarStyles() { if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component'); } return TextEditorComponent.didUpdateScrollbarStyles(); } static viewForItem(item) { return item.element || item; } static deserialize(state, atomEnvironment) { if (state.version !== SERIALIZATION_VERSION) return null; let bufferId = state.tokenizedBuffer ? state.tokenizedBuffer.bufferId : state.bufferId; try { state.buffer = atomEnvironment.project.bufferForIdSync(bufferId); if (!state.buffer) return null; } catch (error) { if (error.syscall === 'read') { return; // Error reading the file, don't deserialize an editor for it } else { throw error; } } state.assert = atomEnvironment.assert.bind(atomEnvironment); // Semantics of the readOnly flag have changed since its introduction. // Only respect readOnly2, which has been set with the current readOnly semantics. delete state.readOnly; state.readOnly = state.readOnly2; delete state.readOnly2; const editor = new TextEditor(state); if (state.registered) { const disposable = atomEnvironment.textEditors.add(editor); editor.onDidDestroy(() => disposable.dispose()); } return editor; } constructor(params = {}) { if (this.constructor.clipboard == null) { throw new Error( 'Must call TextEditor.setClipboard at least once before creating TextEditor instances' ); } this.id = params.id != null ? params.id : nextId++; if (this.id >= nextId) { // Ensure that new editors get unique ids: nextId = this.id + 1; } this.initialScrollTopRow = params.initialScrollTopRow; this.initialScrollLeftColumn = params.initialScrollLeftColumn; this.decorationManager = params.decorationManager; this.selectionsMarkerLayer = params.selectionsMarkerLayer; this.mini = params.mini != null ? params.mini : false; this.keyboardInputEnabled = params.keyboardInputEnabled != null ? params.keyboardInputEnabled : true; this.readOnly = params.readOnly != null ? params.readOnly : false; this.placeholderText = params.placeholderText; this.showLineNumbers = params.showLineNumbers; this.assert = params.assert || (condition => condition); this.showInvisibles = params.showInvisibles != null ? params.showInvisibles : true; this.autoHeight = params.autoHeight; this.autoWidth = params.autoWidth; this.scrollPastEnd = params.scrollPastEnd != null ? params.scrollPastEnd : false; this.scrollSensitivity = params.scrollSensitivity != null ? params.scrollSensitivity : 40; this.editorWidthInChars = params.editorWidthInChars; this.invisibles = params.invisibles; this.showIndentGuide = params.showIndentGuide; this.softWrapped = params.softWrapped; this.softWrapAtPreferredLineLength = params.softWrapAtPreferredLineLength; this.preferredLineLength = params.preferredLineLength; this.showCursorOnSelection = params.showCursorOnSelection != null ? params.showCursorOnSelection : true; this.maxScreenLineLength = params.maxScreenLineLength; this.softTabs = params.softTabs != null ? params.softTabs : true; this.autoIndent = params.autoIndent != null ? params.autoIndent : true; this.autoIndentOnPaste = params.autoIndentOnPaste != null ? params.autoIndentOnPaste : true; this.undoGroupingInterval = params.undoGroupingInterval != null ? params.undoGroupingInterval : 300; this.softWrapped = params.softWrapped != null ? params.softWrapped : false; this.softWrapAtPreferredLineLength = params.softWrapAtPreferredLineLength != null ? params.softWrapAtPreferredLineLength : false; this.preferredLineLength = params.preferredLineLength != null ? params.preferredLineLength : 80; this.maxScreenLineLength = params.maxScreenLineLength != null ? params.maxScreenLineLength : 500; this.showLineNumbers = params.showLineNumbers != null ? params.showLineNumbers : true; const { tabLength = 2 } = params; this.alive = true; this.doBackgroundWork = this.doBackgroundWork.bind(this); this.serializationVersion = 1; this.suppressSelectionMerging = false; this.selectionFlashDuration = 500; this.gutterContainer = null; this.verticalScrollMargin = 2; this.horizontalScrollMargin = 6; this.lineHeightInPixels = null; this.defaultCharWidth = null; this.height = null; this.width = null; this.registered = false; this.atomicSoftTabs = true; this.emitter = new Emitter(); this.disposables = new CompositeDisposable(); this.cursors = []; this.cursorsByMarkerId = new Map(); this.selections = []; this.hasTerminatedPendingState = false; if (params.buffer) { this.buffer = params.buffer; } else { this.buffer = new TextBuffer({ shouldDestroyOnFileDelete() { return atom.config.get('core.closeDeletedFileTabs'); } }); this.buffer.setLanguageMode( new TextMateLanguageMode({ buffer: this.buffer, config: atom.config }) ); } const languageMode = this.buffer.getLanguageMode(); this.languageModeSubscription = languageMode.onDidTokenize && languageMode.onDidTokenize(() => { this.emitter.emit('did-tokenize'); }); if (this.languageModeSubscription) this.disposables.add(this.languageModeSubscription); if (params.displayLayer) { this.displayLayer = params.displayLayer; } else { const displayLayerParams = { invisibles: this.getInvisibles(), softWrapColumn: this.getSoftWrapColumn(), showIndentGuides: this.doesShowIndentGuide(), atomicSoftTabs: params.atomicSoftTabs != null ? params.atomicSoftTabs : true, tabLength, ratioForCharacter: this.ratioForCharacter.bind(this), isWrapBoundary, foldCharacter: ZERO_WIDTH_NBSP, softWrapHangingIndent: params.softWrapHangingIndentLength != null ? params.softWrapHangingIndentLength : 0 }; this.displayLayer = this.buffer.getDisplayLayer(params.displayLayerId); if (this.displayLayer) { this.displayLayer.reset(displayLayerParams); this.selectionsMarkerLayer = this.displayLayer.getMarkerLayer( params.selectionsMarkerLayerId ); } else { this.displayLayer = this.buffer.addDisplayLayer(displayLayerParams); } } this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork); this.disposables.add( new Disposable(() => { if (this.backgroundWorkHandle != null) return cancelIdleCallback(this.backgroundWorkHandle); }) ); this.defaultMarkerLayer = this.displayLayer.addMarkerLayer(); if (!this.selectionsMarkerLayer) { this.selectionsMarkerLayer = this.addMarkerLayer({ maintainHistory: true, persistent: true, role: 'selections' }); } this.decorationManager = new DecorationManager(this); this.decorateMarkerLayer(this.selectionsMarkerLayer, { type: 'cursor' }); if (!this.isMini()) this.decorateCursorLine(); this.decorateMarkerLayer(this.displayLayer.foldsMarkerLayer, { type: 'line-number', class: 'folded' }); for (let marker of this.selectionsMarkerLayer.getMarkers()) { this.addSelection(marker); } this.subscribeToBuffer(); this.subscribeToDisplayLayer(); if (this.cursors.length === 0 && !params.suppressCursorCreation) { const initialLine = Math.max(parseInt(params.initialLine) || 0, 0); const initialColumn = Math.max(parseInt(params.initialColumn) || 0, 0); this.addCursorAtBufferPosition([initialLine, initialColumn]); } this.gutterContainer = new GutterContainer(this); this.lineNumberGutter = this.gutterContainer.addGutter({ name: 'line-number', type: 'line-number', priority: 0, visible: params.lineNumberGutterVisible }); } get element() { return this.getElement(); } get editorElement() { Grim.deprecate(dedent`\ \`TextEditor.prototype.editorElement\` has always been private, but now it is gone. Reading the \`editorElement\` property still returns a reference to the editor element but this field will be removed in a later version of Atom, so we recommend using the \`element\` property instead.\ `); return this.getElement(); } get displayBuffer() { Grim.deprecate(dedent`\ \`TextEditor.prototype.displayBuffer\` has always been private, but now it is gone. Reading the \`displayBuffer\` property now returns a reference to the containing \`TextEditor\`, which now provides *some* of the API of the defunct \`DisplayBuffer\` class.\ `); return this; } get languageMode() { return this.buffer.getLanguageMode(); } get tokenizedBuffer() { return this.buffer.getLanguageMode(); } get rowsPerPage() { return this.getRowsPerPage(); } decorateCursorLine() { this.cursorLineDecorations = [ this.decorateMarkerLayer(this.selectionsMarkerLayer, { type: 'line', class: 'cursor-line', onlyEmpty: true }), this.decorateMarkerLayer(this.selectionsMarkerLayer, { type: 'line-number', class: 'cursor-line' }), this.decorateMarkerLayer(this.selectionsMarkerLayer, { type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true }) ]; } doBackgroundWork(deadline) { const previousLongestRow = this.getApproximateLongestScreenRow(); if (this.displayLayer.doBackgroundWork(deadline)) { this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork); } else { this.backgroundWorkHandle = null; } if ( this.component && this.getApproximateLongestScreenRow() !== previousLongestRow ) { this.component.scheduleUpdate(); } } update(params) { const displayLayerParams = {}; for (let param of Object.keys(params)) { const value = params[param]; switch (param) { case 'autoIndent': this.updateAutoIndent(value, false); break; case 'autoIndentOnPaste': this.updateAutoIndentOnPaste(value, false); break; case 'undoGroupingInterval': this.updateUndoGroupingInterval(value, false); break; case 'scrollSensitivity': this.updateScrollSensitivity(value, false); break; case 'encoding': this.updateEncoding(value, false); break; case 'softTabs': this.updateSoftTabs(value, false); break; case 'atomicSoftTabs': this.updateAtomicSoftTabs(value, false, displayLayerParams); break; case 'tabLength': this.updateTabLength(value, false, displayLayerParams); break; case 'softWrapped': this.updateSoftWrapped(value, false, displayLayerParams); break; case 'softWrapHangingIndentLength': this.updateSoftWrapHangingIndentLength( value, false, displayLayerParams ); break; case 'softWrapAtPreferredLineLength': this.updateSoftWrapAtPreferredLineLength( value, false, displayLayerParams ); break; case 'preferredLineLength': this.updatePreferredLineLength(value, false, displayLayerParams); break; case 'maxScreenLineLength': this.updateMaxScreenLineLength(value, false, displayLayerParams); break; case 'mini': this.updateMini(value, false, displayLayerParams); break; case 'readOnly': this.updateReadOnly(value, false); break; case 'keyboardInputEnabled': this.updateKeyboardInputEnabled(value, false); break; case 'placeholderText': this.updatePlaceholderText(value, false); break; case 'lineNumberGutterVisible': this.updateLineNumberGutterVisible(value, false); break; case 'showIndentGuide': this.updateShowIndentGuide(value, false, displayLayerParams); break; case 'showLineNumbers': this.updateShowLineNumbers(value, false); break; case 'showInvisibles': this.updateShowInvisibles(value, false, displayLayerParams); break; case 'invisibles': this.updateInvisibles(value, false, displayLayerParams); break; case 'editorWidthInChars': this.updateEditorWidthInChars(value, false, displayLayerParams); break; case 'width': this.updateWidth(value, false, displayLayerParams); break; case 'scrollPastEnd': this.updateScrollPastEnd(value, false); break; case 'autoHeight': this.updateAutoHight(value, false); break; case 'autoWidth': this.updateAutoWidth(value, false); break; case 'showCursorOnSelection': this.updateShowCursorOnSelection(value, false); break; default: if (param !== 'ref' && param !== 'key') { throw new TypeError(`Invalid TextEditor parameter: '${param}'`); } } } return this.finishUpdate(displayLayerParams); } finishUpdate(displayLayerParams = {}) { this.displayLayer.reset(displayLayerParams); if (this.component) { return this.component.getNextUpdatePromise(); } else { return Promise.resolve(); } } updateAutoIndent(value, finish) { this.autoIndent = value; if (finish) this.finishUpdate(); } updateAutoIndentOnPaste(value, finish) { this.autoIndentOnPaste = value; if (finish) this.finishUpdate(); } updateUndoGroupingInterval(value, finish) { this.undoGroupingInterval = value; if (finish) this.finishUpdate(); } updateScrollSensitivity(value, finish) { this.scrollSensitivity = value; if (finish) this.finishUpdate(); } updateEncoding(value, finish) { this.buffer.setEncoding(value); if (finish) this.finishUpdate(); } updateSoftTabs(value, finish) { if (value !== this.softTabs) { this.softTabs = value; } if (finish) this.finishUpdate(); } updateAtomicSoftTabs(value, finish, displayLayerParams = {}) { if (value !== this.displayLayer.atomicSoftTabs) { displayLayerParams.atomicSoftTabs = value; } if (finish) this.finishUpdate(displayLayerParams); } updateTabLength(value, finish, displayLayerParams = {}) { if (value > 0 && value !== this.displayLayer.tabLength) { displayLayerParams.tabLength = value; } if (finish) this.finishUpdate(displayLayerParams); } updateSoftWrapped(value, finish, displayLayerParams = {}) { if (value !== this.softWrapped) { this.softWrapped = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); this.emitter.emit('did-change-soft-wrapped', this.isSoftWrapped()); } if (finish) this.finishUpdate(displayLayerParams); } updateSoftWrapHangingIndentLength(value, finish, displayLayerParams = {}) { if (value !== this.displayLayer.softWrapHangingIndent) { displayLayerParams.softWrapHangingIndent = value; } if (finish) this.finishUpdate(displayLayerParams); } updateSoftWrapAtPreferredLineLength(value, finish, displayLayerParams = {}) { if (value !== this.softWrapAtPreferredLineLength) { this.softWrapAtPreferredLineLength = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updatePreferredLineLength(value, finish, displayLayerParams = {}) { if (value !== this.preferredLineLength) { this.preferredLineLength = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updateMaxScreenLineLength(value, finish, displayLayerParams = {}) { if (value !== this.maxScreenLineLength) { this.maxScreenLineLength = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updateMini(value, finish, displayLayerParams = {}) { if (value !== this.mini) { this.mini = value; this.emitter.emit('did-change-mini', value); displayLayerParams.invisibles = this.getInvisibles(); displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); displayLayerParams.showIndentGuides = this.doesShowIndentGuide(); if (this.mini) { for (let decoration of this.cursorLineDecorations) { decoration.destroy(); } this.cursorLineDecorations = null; } else { this.decorateCursorLine(); } if (this.component != null) { this.component.scheduleUpdate(); } } if (finish) this.finishUpdate(displayLayerParams); } updateReadOnly(value, finish) { if (value !== this.readOnly) { this.readOnly = value; if (this.component != null) { this.component.scheduleUpdate(); } } if (finish) this.finishUpdate(); } updateKeyboardInputEnabled(value, finish) { if (value !== this.keyboardInputEnabled) { this.keyboardInputEnabled = value; if (this.component != null) { this.component.scheduleUpdate(); } } if (finish) this.finishUpdate(); } updatePlaceholderText(value, finish) { if (value !== this.placeholderText) { this.placeholderText = value; this.emitter.emit('did-change-placeholder-text', value); } if (finish) this.finishUpdate(); } updateLineNumberGutterVisible(value, finish) { if (value !== this.lineNumberGutterVisible) { if (value) { this.lineNumberGutter.show(); } else { this.lineNumberGutter.hide(); } this.emitter.emit( 'did-change-line-number-gutter-visible', this.lineNumberGutter.isVisible() ); } if (finish) this.finishUpdate(); } updateShowIndentGuide(value, finish, displayLayerParams = {}) { if (value !== this.showIndentGuide) { this.showIndentGuide = value; displayLayerParams.showIndentGuides = this.doesShowIndentGuide(); } if (finish) this.finishUpdate(displayLayerParams); } updateShowLineNumbers(value, finish) { if (value !== this.showLineNumbers) { this.showLineNumbers = value; if (this.component != null) { this.component.scheduleUpdate(); } } if (finish) this.finishUpdate(); } updateShowInvisibles(value, finish, displayLayerParams = {}) { if (value !== this.showInvisibles) { this.showInvisibles = value; displayLayerParams.invisibles = this.getInvisibles(); } if (finish) this.finishUpdate(displayLayerParams); } updateInvisibles(value, finish, displayLayerParams = {}) { if (!_.isEqual(value, this.invisibles)) { this.invisibles = value; displayLayerParams.invisibles = this.getInvisibles(); } if (finish) this.finishUpdate(displayLayerParams); } updateEditorWidthInChars(value, finish, displayLayerParams = {}) { if (value > 0 && value !== this.editorWidthInChars) { this.editorWidthInChars = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updateWidth(value, finish, displayLayerParams = {}) { if (value !== this.width) { this.width = value; displayLayerParams.softWrapColumn = this.getSoftWrapColumn(); } if (finish) this.finishUpdate(displayLayerParams); } updateScrollPastEnd(value, finish) { if (value !== this.scrollPastEnd) { this.scrollPastEnd = value; if (this.component) this.component.scheduleUpdate(); } if (finish) this.finishUpdate(); } updateAutoHight(value, finish) { if (value !== this.autoHeight) { this.autoHeight = value; } if (finish) this.finishUpdate(); } updateAutoWidth(value, finish) { if (value !== this.autoWidth) { this.autoWidth = value; } if (finish) this.finishUpdate(); } updateShowCursorOnSelection(value, finish) { if (value !== this.showCursorOnSelection) { this.showCursorOnSelection = value; if (this.component) this.component.scheduleUpdate(); } if (finish) this.finishUpdate(); } scheduleComponentUpdate() { if (this.component) this.component.scheduleUpdate(); } serialize() { return { deserializer: 'TextEditor', version: SERIALIZATION_VERSION, displayLayerId: this.displayLayer.id, selectionsMarkerLayerId: this.selectionsMarkerLayer.id, initialScrollTopRow: this.getScrollTopRow(), initialScrollLeftColumn: this.getScrollLeftColumn(), tabLength: this.displayLayer.tabLength, atomicSoftTabs: this.displayLayer.atomicSoftTabs, softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent, id: this.id, bufferId: this.buffer.id, softTabs: this.softTabs, softWrapped: this.softWrapped, softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength, preferredLineLength: this.preferredLineLength, mini: this.mini, readOnly2: this.readOnly, // readOnly encompassed both readOnly and keyboardInputEnabled keyboardInputEnabled: this.keyboardInputEnabled, editorWidthInChars: this.editorWidthInChars, width: this.width, maxScreenLineLength: this.maxScreenLineLength, registered: this.registered, invisibles: this.invisibles, showInvisibles: this.showInvisibles, showIndentGuide: this.showIndentGuide, autoHeight: this.autoHeight, autoWidth: this.autoWidth }; } subscribeToBuffer() { this.buffer.retain(); this.disposables.add( this.buffer.onDidChangeLanguageMode( this.handleLanguageModeChange.bind(this) ) ); this.disposables.add( this.buffer.onDidChangePath(() => { this.emitter.emit('did-change-title', this.getTitle()); this.emitter.emit('did-change-path', this.getPath()); }) ); this.disposables.add( this.buffer.onDidChangeEncoding(() => { this.emitter.emit('did-change-encoding', this.getEncoding()); }) ); this.disposables.add(this.buffer.onDidDestroy(() => this.destroy())); this.disposables.add( this.buffer.onDidChangeModified(() => { if (!this.hasTerminatedPendingState && this.buffer.isModified()) this.terminatePendingState(); }) ); } terminatePendingState() { if (!this.hasTerminatedPendingState) this.emitter.emit('did-terminate-pending-state'); this.hasTerminatedPendingState = true; } onDidTerminatePendingState(callback) { return this.emitter.on('did-terminate-pending-state', callback); } subscribeToDisplayLayer() { this.disposables.add( this.displayLayer.onDidChange(changes => { this.mergeIntersectingSelections(); if (this.component) this.component.didChangeDisplayLayer(changes); this.emitter.emit( 'did-change', changes.map(change => new ChangeEvent(change)) ); }) ); this.disposables.add( this.displayLayer.onDidReset(() => { this.mergeIntersectingSelections(); if (this.component) this.component.didResetDisplayLayer(); this.emitter.emit('did-change', {}); }) ); this.disposables.add( this.selectionsMarkerLayer.onDidCreateMarker(this.addSelection.bind(this)) ); return this.disposables.add( this.selectionsMarkerLayer.onDidUpdate(() => this.component != null ? this.component.didUpdateSelections() : undefined ) ); } destroy() { if (!this.alive) return; this.alive = false; this.disposables.dispose(); this.displayLayer.destroy(); for (let selection of this.selections.slice()) { selection.destroy(); } this.buffer.release(); this.gutterContainer.destroy(); this.emitter.emit('did-destroy'); this.emitter.clear(); if (this.component) this.component.element.component = null; this.component = null; this.lineNumberGutter.element = null; } isAlive() { return this.alive; } isDestroyed() { return !this.alive; } /* Section: Event Subscription */ // Essential: Calls your `callback` when the buffer's title has changed. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeTitle(callback) { return this.emitter.on('did-change-title', callback); } // Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangePath(callback) { return this.emitter.on('did-change-path', callback); } // Essential: Invoke the given callback synchronously when the content of the // buffer changes. // // Because observers are invoked synchronously, it's important not to perform // any expensive operations via this method. Consider {::onDidStopChanging} to // delay expensive operations until after changes stop occurring. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChange(callback) { return this.emitter.on('did-change', callback); } // Essential: Invoke `callback` when the buffer's contents change. It is // emit asynchronously 300ms after the last buffer change. This is a good place // to handle changes to the buffer without compromising typing performance. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidStopChanging(callback) { return this.getBuffer().onDidStopChanging(callback); } // Essential: Calls your `callback` when a {Cursor} is moved. If there are // multiple cursors, your callback will be called for each cursor. // // * `callback` {Function} // * `event` {Object} // * `oldBufferPosition` {Point} // * `oldScreenPosition` {Point} // * `newBufferPosition` {Point} // * `newScreenPosition` {Point} // * `textChanged` {Boolean} // * `cursor` {Cursor} that triggered the event // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeCursorPosition(callback) { return this.emitter.on('did-change-cursor-position', callback); } // Essential: Calls your `callback` when a selection's screen range changes. // // * `callback` {Function} // * `event` {Object} // * `oldBufferRange` {Range} // * `oldScreenRange` {Range} // * `newBufferRange` {Range} // * `newScreenRange` {Range} // * `selection` {Selection} that triggered the event // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeSelectionRange(callback) { return this.emitter.on('did-change-selection-range', callback); } // Extended: Calls your `callback` when soft wrap was enabled or disabled. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeSoftWrapped(callback) { return this.emitter.on('did-change-soft-wrapped', callback); } // Extended: Calls your `callback` when the buffer's encoding has changed. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeEncoding(callback) { return this.emitter.on('did-change-encoding', callback); } // Extended: Calls your `callback` when the grammar that interprets and // colorizes the text has been changed. Immediately calls your callback with // the current grammar. // // * `callback` {Function} // * `grammar` {Grammar} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeGrammar(callback) { callback(this.getGrammar()); return this.onDidChangeGrammar(callback); } // Extended: Calls your `callback` when the grammar that interprets and // colorizes the text has been changed. // // * `callback` {Function} // * `grammar` {Grammar} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeGrammar(callback) { return this.buffer.onDidChangeLanguageMode(() => { callback(this.buffer.getLanguageMode().grammar); }); } // Extended: Calls your `callback` when the result of {::isModified} changes. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeModified(callback) { return this.getBuffer().onDidChangeModified(callback); } // Extended: Calls your `callback` when the buffer's underlying file changes on // disk at a moment when the result of {::isModified} is true. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidConflict(callback) { return this.getBuffer().onDidConflict(callback); } // Extended: Calls your `callback` before text has been inserted. // // * `callback` {Function} // * `event` event {Object} // * `text` {String} text to be inserted // * `cancel` {Function} Call to prevent the text from being inserted // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onWillInsertText(callback) { return this.emitter.on('will-insert-text', callback); } // Extended: Calls your `callback` after text has been inserted. // // * `callback` {Function} // * `event` event {Object} // * `text` {String} text to be inserted // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidInsertText(callback) { return this.emitter.on('did-insert-text', callback); } // Essential: Invoke the given callback after the buffer is saved to disk. // // * `callback` {Function} to be called after the buffer is saved. // * `event` {Object} with the following keys: // * `path` The path to which the buffer was saved. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidSave(callback) { return this.getBuffer().onDidSave(callback); } // Essential: Invoke the given callback when the editor is destroyed. // // * `callback` {Function} to be called when the editor is destroyed. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy(callback) { return this.emitter.once('did-destroy', callback); } // Extended: Calls your `callback` when a {Cursor} is added to the editor. // Immediately calls your callback for each existing cursor. // // * `callback` {Function} // * `cursor` {Cursor} that was added // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeCursors(callback) { this.getCursors().forEach(callback); return this.onDidAddCursor(callback); } // Extended: Calls your `callback` when a {Cursor} is added to the editor. // // * `callback` {Function} // * `cursor` {Cursor} that was added // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddCursor(callback) { return this.emitter.on('did-add-cursor', callback); } // Extended: Calls your `callback` when a {Cursor} is removed from the editor. // // * `callback` {Function} // * `cursor` {Cursor} that was removed // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveCursor(callback) { return this.emitter.on('did-remove-cursor', callback); } // Extended: Calls your `callback` when a {Selection} is added to the editor. // Immediately calls your callback for each existing selection. // // * `callback` {Function} // * `selection` {Selection} that was added // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeSelections(callback) { this.getSelections().forEach(callback); return this.onDidAddSelection(callback); } // Extended: Calls your `callback` when a {Selection} is added to the editor. // // * `callback` {Function} // * `selection` {Selection} that was added // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddSelection(callback) { return this.emitter.on('did-add-selection', callback); } // Extended: Calls your `callback` when a {Selection} is removed from the editor. // // * `callback` {Function} // * `selection` {Selection} that was removed // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveSelection(callback) { return this.emitter.on('did-remove-selection', callback); } // Extended: Calls your `callback` with each {Decoration} added to the editor. // Calls your `callback` immediately for any existing decorations. // // * `callback` {Function} // * `decoration` {Decoration} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeDecorations(callback) { return this.decorationManager.observeDecorations(callback); } // Extended: Calls your `callback` when a {Decoration} is added to the editor. // // * `callback` {Function} // * `decoration` {Decoration} that was added // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddDecoration(callback) { return this.decorationManager.onDidAddDecoration(callback); } // Extended: Calls your `callback` when a {Decoration} is removed from the editor. // // * `callback` {Function} // * `decoration` {Decoration} that was removed // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveDecoration(callback) { return this.decorationManager.onDidRemoveDecoration(callback); } // Called by DecorationManager when a decoration is added. didAddDecoration(decoration) { if (this.component && decoration.isType('block')) { this.component.addBlockDecoration(decoration); } } // Extended: Calls your `callback` when the placeholder text is changed. // // * `callback` {Function} // * `placeholderText` {String} new text // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangePlaceholderText(callback) { return this.emitter.on('did-change-placeholder-text', callback); } onDidChangeScrollTop(callback) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.' ); return this.getElement().onDidChangeScrollTop(callback); } onDidChangeScrollLeft(callback) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.' ); return this.getElement().onDidChangeScrollLeft(callback); } onDidRequestAutoscroll(callback) { return this.emitter.on('did-request-autoscroll', callback); } // TODO Remove once the tabs package no longer uses .on subscriptions onDidChangeIcon(callback) { return this.emitter.on('did-change-icon', callback); } onDidUpdateDecorations(callback) { return this.decorationManager.onDidUpdateDecorations(callback); } // Retrieves the current buffer's URI. getURI() { return this.buffer.getUri(); } // Create an {TextEditor} with its initial state based on this object copy() { const displayLayer = this.displayLayer.copy(); const selectionsMarkerLayer = displayLayer.getMarkerLayer( this.buffer.getMarkerLayer(this.selectionsMarkerLayer.id).copy().id ); const softTabs = this.getSoftTabs(); return new TextEditor({ buffer: this.buffer, selectionsMarkerLayer, softTabs, suppressCursorCreation: true, tabLength: this.getTabLength(), initialScrollTopRow: this.getScrollTopRow(), initialScrollLeftColumn: this.getScrollLeftColumn(), assert: this.assert, displayLayer, grammar: this.getGrammar(), autoWidth: this.autoWidth, autoHeight: this.autoHeight, showCursorOnSelection: this.showCursorOnSelection }); } // Controls visibility based on the given {Boolean}. setVisible(visible) { if (visible) { const languageMode = this.buffer.getLanguageMode(); if (languageMode.startTokenizing) languageMode.startTokenizing(); } } setMini(mini) { this.updateMini(mini, true); } isMini() { return this.mini; } setReadOnly(readOnly) { this.updateReadOnly(readOnly, true); } isReadOnly() { return this.readOnly; } enableKeyboardInput(enabled) { this.updateKeyboardInputEnabled(enabled, true); } isKeyboardInputEnabled() { return this.keyboardInputEnabled; } onDidChangeMini(callback) { return this.emitter.on('did-change-mini', callback); } setLineNumberGutterVisible(lineNumberGutterVisible) { this.updateLineNumberGutterVisible(lineNumberGutterVisible, true); } isLineNumberGutterVisible() { return this.lineNumberGutter.isVisible(); } anyLineNumberGutterVisible() { return this.getGutters().some( gutter => gutter.type === 'line-number' && gutter.visible ); } onDidChangeLineNumberGutterVisible(callback) { return this.emitter.on('did-change-line-number-gutter-visible', callback); } // Essential: Calls your `callback` when a {Gutter} is added to the editor. // Immediately calls your callback for each existing gutter. // // * `callback` {Function} // * `gutter` {Gutter} that currently exists/was added. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeGutters(callback) { return this.gutterContainer.observeGutters(callback); } // Essential: Calls your `callback` when a {Gutter} is added to the editor. // // * `callback` {Function} // * `gutter` {Gutter} that was added. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddGutter(callback) { return this.gutterContainer.onDidAddGutter(callback); } // Essential: Calls your `callback` when a {Gutter} is removed from the editor. // // * `callback` {Function} // * `name` The name of the {Gutter} that was removed. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveGutter(callback) { return this.gutterContainer.onDidRemoveGutter(callback); } // Set the number of characters that can be displayed horizontally in the // editor. // // * `editorWidthInChars` A {Number} representing the width of the // {TextEditorElement} in characters. setEditorWidthInChars(editorWidthInChars) { this.updateEditorWidthInChars(editorWidthInChars, true); } // Returns the editor width in characters. getEditorWidthInChars() { if (this.width != null && this.defaultCharWidth > 0) { return Math.max(0, Math.floor(this.width / this.defaultCharWidth)); } else { return this.editorWidthInChars; } } /* Section: Buffer */ // Essential: Retrieves the current {TextBuffer}. getBuffer() { return this.buffer; } /* Section: File Details */ // Essential: Get the editor's title for display in other parts of the // UI such as the tabs. // // If the editor's buffer is saved, its title is the file name. If it is // unsaved, its title is "untitled". // // Returns a {String}. getTitle() { return this.getFileName() || 'untitled'; } // Essential: Get unique title for display in other parts of the UI, such as // the window title. // // If the editor's buffer is unsaved, its title is "untitled" // If the editor's buffer is saved, its unique title is formatted as one // of the following, // * "" when it is the only editing buffer with this file name. // * "" when other buffers have this file name. // // Returns a {String} getLongTitle() { if (this.getPath()) { const fileName = this.getFileName(); let myPathSegments; const openEditorPathSegmentsWithSameFilename = []; for (const textEditor of atom.workspace.getTextEditors()) { if (textEditor.getFileName() === fileName) { const pathSegments = fs .tildify(textEditor.getDirectoryPath()) .split(path.sep); openEditorPathSegmentsWithSameFilename.push(pathSegments); if (textEditor === this) myPathSegments = pathSegments; } } if ( !myPathSegments || openEditorPathSegmentsWithSameFilename.length === 1 ) return fileName; let commonPathSegmentCount; for (let i = 0, { length } = myPathSegments; i < length; i++) { const myPathSegment = myPathSegments[i]; if ( openEditorPathSegmentsWithSameFilename.some( segments => segments.length === i + 1 || segments[i] !== myPathSegment ) ) { commonPathSegmentCount = i; break; } } return `${fileName} \u2014 ${path.join( ...myPathSegments.slice(commonPathSegmentCount) )}`; } else { return 'untitled'; } } // Essential: Returns the {String} path of this editor's text buffer. getPath() { return this.buffer.getPath(); } getFileName() { const fullPath = this.getPath(); if (fullPath) return path.basename(fullPath); } getDirectoryPath() { const fullPath = this.getPath(); if (fullPath) return path.dirname(fullPath); } // Extended: Returns the {String} character set encoding of this editor's text // buffer. getEncoding() { return this.buffer.getEncoding(); } // Extended: Set the character set encoding to use in this editor's text // buffer. // // * `encoding` The {String} character set encoding name such as 'utf8' setEncoding(encoding) { this.buffer.setEncoding(encoding); } // Essential: Returns {Boolean} `true` if this editor has been modified. isModified() { return this.buffer.isModified(); } // Essential: Returns {Boolean} `true` if this editor has no content. isEmpty() { return this.buffer.isEmpty(); } /* Section: File Operations */ // Essential: Saves the editor's text buffer. // // See {TextBuffer::save} for more details. save() { return this.buffer.save(); } // Essential: Saves the editor's text buffer as the given path. // // See {TextBuffer::saveAs} for more details. // // * `filePath` A {String} path. saveAs(filePath) { return this.buffer.saveAs(filePath); } // Determine whether the user should be prompted to save before closing // this editor. shouldPromptToSave({ windowCloseRequested, projectHasPaths } = {}) { if ( windowCloseRequested && projectHasPaths && atom.stateStore.isConnected() ) { return this.buffer.isInConflict(); } else { return this.isModified() && !this.buffer.hasMultipleEditors(); } } // Returns an {Object} to configure dialog shown when this editor is saved // via {Pane::saveItemAs}. getSaveDialogOptions() { return {}; } /* Section: Reading Text */ // Essential: Returns a {String} representing the entire contents of the editor. getText() { return this.buffer.getText(); } // Essential: Get the text in the given {Range} in buffer coordinates. // // * `range` A {Range} or range-compatible {Array}. // // Returns a {String}. getTextInBufferRange(range) { return this.buffer.getTextInRange(range); } // Essential: Returns a {Number} representing the number of lines in the buffer. getLineCount() { return this.buffer.getLineCount(); } // Essential: Returns a {Number} representing the number of screen lines in the // editor. This accounts for folds. getScreenLineCount() { return this.displayLayer.getScreenLineCount(); } getApproximateScreenLineCount() { return this.displayLayer.getApproximateScreenLineCount(); } // Essential: Returns a {Number} representing the last zero-indexed buffer row // number of the editor. getLastBufferRow() { return this.buffer.getLastRow(); } // Essential: Returns a {Number} representing the last zero-indexed screen row // number of the editor. getLastScreenRow() { return this.getScreenLineCount() - 1; } // Essential: Returns a {String} representing the contents of the line at the // given buffer row. // // * `bufferRow` A {Number} representing a zero-indexed buffer row. lineTextForBufferRow(bufferRow) { return this.buffer.lineForRow(bufferRow); } // Essential: Returns a {String} representing the contents of the line at the // given screen row. // // * `screenRow` A {Number} representing a zero-indexed screen row. lineTextForScreenRow(screenRow) { const screenLine = this.screenLineForScreenRow(screenRow); if (screenLine) return screenLine.lineText; } logScreenLines(start = 0, end = this.getLastScreenRow()) { for (let row = start; row <= end; row++) { const line = this.lineTextForScreenRow(row); console.log(row, this.bufferRowForScreenRow(row), line, line.length); } } tokensForScreenRow(screenRow) { const tokens = []; let lineTextIndex = 0; const currentTokenScopes = []; const { lineText, tags } = this.screenLineForScreenRow(screenRow); for (const tag of tags) { if (this.displayLayer.isOpenTag(tag)) { currentTokenScopes.push(this.displayLayer.classNameForTag(tag)); } else if (this.displayLayer.isCloseTag(tag)) { currentTokenScopes.pop(); } else { tokens.push({ text: lineText.substr(lineTextIndex, tag), scopes: currentTokenScopes.slice() }); lineTextIndex += tag; } } return tokens; } screenLineForScreenRow(screenRow) { return this.displayLayer.getScreenLine(screenRow); } bufferRowForScreenRow(screenRow) { return this.displayLayer.translateScreenPosition(Point(screenRow, 0)).row; } bufferRowsForScreenRows(startScreenRow, endScreenRow) { return this.displayLayer.bufferRowsForScreenRows( startScreenRow, endScreenRow + 1 ); } screenRowForBufferRow(row) { return this.displayLayer.translateBufferPosition(Point(row, 0)).row; } getRightmostScreenPosition() { return this.displayLayer.getRightmostScreenPosition(); } getApproximateRightmostScreenPosition() { return this.displayLayer.getApproximateRightmostScreenPosition(); } getMaxScreenLineLength() { return this.getRightmostScreenPosition().column; } getLongestScreenRow() { return this.getRightmostScreenPosition().row; } getApproximateLongestScreenRow() { return this.getApproximateRightmostScreenPosition().row; } lineLengthForScreenRow(screenRow) { return this.displayLayer.lineLengthForScreenRow(screenRow); } // Returns the range for the given buffer row. // // * `row` A row {Number}. // * `options` (optional) An options hash with an `includeNewline` key. // // Returns a {Range}. bufferRangeForBufferRow(row, options) { return this.buffer.rangeForRow(row, options && options.includeNewline); } // Get the text in the given {Range}. // // Returns a {String}. getTextInRange(range) { return this.buffer.getTextInRange(range); } // {Delegates to: TextBuffer.isRowBlank} isBufferRowBlank(bufferRow) { return this.buffer.isRowBlank(bufferRow); } // {Delegates to: TextBuffer.nextNonBlankRow} nextNonBlankBufferRow(bufferRow) { return this.buffer.nextNonBlankRow(bufferRow); } // {Delegates to: TextBuffer.getEndPosition} getEofBufferPosition() { return this.buffer.getEndPosition(); } // Essential: Get the {Range} of the paragraph surrounding the most recently added // cursor. // // Returns a {Range}. getCurrentParagraphBufferRange() { return this.getLastCursor().getCurrentParagraphBufferRange(); } /* Section: Mutating Text */ // Essential: Replaces the entire contents of the buffer with the given {String}. // // * `text` A {String} to replace with // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. setText(text, options = {}) { if (!this.ensureWritable('setText', options)) return; return this.buffer.setText(text); } // Essential: Set the text in the given {Range} in buffer coordinates. // // * `range` A {Range} or range-compatible {Array}. // * `text` A {String} // * `options` (optional) {Object} // * `normalizeLineEndings` (optional) {Boolean} (default: true) // * `undo` (optional) *Deprecated* {String} 'skip' will skip the undo system. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead. // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) // // Returns the {Range} of the newly-inserted text. setTextInBufferRange(range, text, options = {}) { if (!this.ensureWritable('setTextInBufferRange', options)) return; return this.getBuffer().setTextInRange(range, text, options); } // Essential: For each selection, replace the selected text with the given text. // // * `text` A {String} representing the text to insert. // * `options` (optional) See {Selection::insertText}. // // Returns a {Range} when the text has been inserted. Returns a {Boolean} `false` when the text has not been inserted. insertText(text, options = {}) { if (!this.ensureWritable('insertText', options)) return; if (!this.emitWillInsertTextEvent(text)) return false; let groupLastChanges = false; if (options.undo === 'skip') { options = Object.assign({}, options); delete options.undo; groupLastChanges = true; } const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0; if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent(); if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent(); const result = this.mutateSelectedText(selection => { const range = selection.insertText(text, options); const didInsertEvent = { text, range }; this.emitter.emit('did-insert-text', didInsertEvent); return range; }, groupingInterval); if (groupLastChanges) this.buffer.groupLastChanges(); return result; } // Essential: For each selection, replace the selected text with a newline. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) insertNewline(options = {}) { return this.insertText('\n', options); } // Essential: For each selection, if the selection is empty, delete the character // following the cursor. Otherwise delete the selected text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) delete(options = {}) { if (!this.ensureWritable('delete', options)) return; return this.mutateSelectedText(selection => selection.delete(options)); } // Essential: For each selection, if the selection is empty, delete the character // preceding the cursor. Otherwise delete the selected text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) backspace(options = {}) { if (!this.ensureWritable('backspace', options)) return; return this.mutateSelectedText(selection => selection.backspace(options)); } // Extended: Mutate the text of all the selections in a single transaction. // // All the changes made inside the given {Function} can be reverted with a // single call to {::undo}. // // * `fn` A {Function} that will be called once for each {Selection}. The first // argument will be a {Selection} and the second argument will be the // {Number} index of that selection. mutateSelectedText(fn, groupingInterval = 0) { return this.mergeIntersectingSelections(() => { return this.transact(groupingInterval, () => { return this.getSelectionsOrderedByBufferPosition().map( (selection, index) => fn(selection, index) ); }); }); } // Move lines intersecting the most recent selection or multiple selections // up by one row in screen coordinates. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveLineUp(options = {}) { if (!this.ensureWritable('moveLineUp', options)) return; const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b) ); if (selections[0].start.row === 0) return; if ( selections[selections.length - 1].start.row === this.getLastBufferRow() && this.buffer.getLastLine() === '' ) return; this.transact(() => { const newSelectionRanges = []; while (selections.length > 0) { // Find selections spanning a contiguous set of lines const selection = selections.shift(); const selectionsToMove = [selection]; while ( selection.end.row === (selections[0] != null ? selections[0].start.row : undefined) ) { selectionsToMove.push(selections[0]); selection.end.row = selections[0].end.row; selections.shift(); } // Compute the buffer range spanned by all these selections, expanding it // so that it includes any folded region that intersects them. let startRow = selection.start.row; let endRow = selection.end.row; if ( selection.end.row > selection.start.row && selection.end.column === 0 ) { // Don't move the last line of a multi-line selection if the selection ends at column 0 endRow--; } startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow); endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1); const linesRange = new Range(Point(startRow, 0), Point(endRow, 0)); // If selected line range is preceded by a fold, one line above on screen // could be multiple lines in the buffer. const precedingRow = this.displayLayer.findBoundaryPrecedingBufferRow( startRow - 1 ); const insertDelta = linesRange.start.row - precedingRow; // Any folds in the text that is moved will need to be re-created. // It includes the folds that were intersecting with the selection. const rangesToRefold = this.displayLayer .destroyFoldsIntersectingBufferRange(linesRange) .map(range => range.translate([-insertDelta, 0])); // Delete lines spanned by selection and insert them on the preceding buffer row let lines = this.buffer.getTextInRange(linesRange); if (lines[lines.length - 1] !== '\n') { lines += this.buffer.lineEndingForRow(linesRange.end.row - 2); } this.buffer.delete(linesRange); this.buffer.insert([precedingRow, 0], lines); // Restore folds that existed before the lines were moved for (let rangeToRefold of rangesToRefold) { this.displayLayer.foldBufferRange(rangeToRefold); } for (const selectionToMove of selectionsToMove) { newSelectionRanges.push(selectionToMove.translate([-insertDelta, 0])); } } this.setSelectedBufferRanges(newSelectionRanges, { autoscroll: false, preserveFolds: true }); if (this.shouldAutoIndent()) this.autoIndentSelectedRows(); this.scrollToBufferPosition([newSelectionRanges[0].start.row, 0]); }); } // Move lines intersecting the most recent selection or multiple selections // down by one row in screen coordinates. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveLineDown(options = {}) { if (!this.ensureWritable('moveLineDown', options)) return; const selections = this.getSelectedBufferRanges(); selections.sort((a, b) => b.compare(a)); this.transact(() => { this.consolidateSelections(); const newSelectionRanges = []; while (selections.length > 0) { // Find selections spanning a contiguous set of lines const selection = selections.shift(); const selectionsToMove = [selection]; // if the current selection start row matches the next selections' end row - make them one selection while ( selection.start.row === (selections[0] != null ? selections[0].end.row : undefined) ) { selectionsToMove.push(selections[0]); selection.start.row = selections[0].start.row; selections.shift(); } // Compute the buffer range spanned by all these selections, expanding it // so that it includes any folded region that intersects them. let startRow = selection.start.row; let endRow = selection.end.row; if ( selection.end.row > selection.start.row && selection.end.column === 0 ) { // Don't move the last line of a multi-line selection if the selection ends at column 0 endRow--; } startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow); endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1); const linesRange = new Range(Point(startRow, 0), Point(endRow, 0)); // If selected line range is followed by a fold, one line below on screen // could be multiple lines in the buffer. But at the same time, if the // next buffer row is wrapped, one line in the buffer can represent many // screen rows. const followingRow = Math.min( this.buffer.getLineCount(), this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1) ); const insertDelta = followingRow - linesRange.end.row; // Any folds in the text that is moved will need to be re-created. // It includes the folds that were intersecting with the selection. const rangesToRefold = this.displayLayer .destroyFoldsIntersectingBufferRange(linesRange) .map(range => range.translate([insertDelta, 0])); // Delete lines spanned by selection and insert them on the following correct buffer row let lines = this.buffer.getTextInRange(linesRange); if (followingRow - 1 === this.buffer.getLastRow()) { lines = `\n${lines}`; } this.buffer.insert([followingRow, 0], lines); this.buffer.delete(linesRange); // Restore folds that existed before the lines were moved for (let rangeToRefold of rangesToRefold) { this.displayLayer.foldBufferRange(rangeToRefold); } for (const selectionToMove of selectionsToMove) { newSelectionRanges.push(selectionToMove.translate([insertDelta, 0])); } } this.setSelectedBufferRanges(newSelectionRanges, { autoscroll: false, preserveFolds: true }); if (this.shouldAutoIndent()) this.autoIndentSelectedRows(); this.scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]); }); } // Move any active selections one column to the left. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveSelectionLeft(options = {}) { if (!this.ensureWritable('moveSelectionLeft', options)) return; const selections = this.getSelectedBufferRanges(); const noSelectionAtStartOfLine = selections.every( selection => selection.start.column !== 0 ); const translationDelta = [0, -1]; const translatedRanges = []; if (noSelectionAtStartOfLine) { this.transact(() => { for (let selection of selections) { const charToLeftOfSelection = new Range( selection.start.translate(translationDelta), selection.start ); const charTextToLeftOfSelection = this.buffer.getTextInRange( charToLeftOfSelection ); this.buffer.insert(selection.end, charTextToLeftOfSelection); this.buffer.delete(charToLeftOfSelection); translatedRanges.push(selection.translate(translationDelta)); } this.setSelectedBufferRanges(translatedRanges); }); } } // Move any active selections one column to the right. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveSelectionRight(options = {}) { if (!this.ensureWritable('moveSelectionRight', options)) return; const selections = this.getSelectedBufferRanges(); const noSelectionAtEndOfLine = selections.every(selection => { return ( selection.end.column !== this.buffer.lineLengthForRow(selection.end.row) ); }); const translationDelta = [0, 1]; const translatedRanges = []; if (noSelectionAtEndOfLine) { this.transact(() => { for (let selection of selections) { const charToRightOfSelection = new Range( selection.end, selection.end.translate(translationDelta) ); const charTextToRightOfSelection = this.buffer.getTextInRange( charToRightOfSelection ); this.buffer.delete(charToRightOfSelection); this.buffer.insert(selection.start, charTextToRightOfSelection); translatedRanges.push(selection.translate(translationDelta)); } this.setSelectedBufferRanges(translatedRanges); }); } } // Duplicate all lines containing active selections. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) duplicateLines(options = {}) { if (!this.ensureWritable('duplicateLines', options)) return; this.transact(() => { const selections = this.getSelectionsOrderedByBufferPosition(); const previousSelectionRanges = []; let i = selections.length - 1; while (i >= 0) { const j = i; previousSelectionRanges[i] = selections[i].getBufferRange(); if (selections[i].isEmpty()) { const { start } = selections[i].getScreenRange(); selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], { preserveFolds: true }); } let [startRow, endRow] = selections[i].getBufferRowRange(); endRow++; while (i > 0) { const [ previousSelectionStartRow, previousSelectionEndRow ] = selections[i - 1].getBufferRowRange(); if (previousSelectionEndRow === startRow) { startRow = previousSelectionStartRow; previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange(); i--; } else { break; } } const intersectingFolds = this.displayLayer.foldsIntersectingBufferRange( [[startRow, 0], [endRow, 0]] ); let textToDuplicate = this.getTextInBufferRange([ [startRow, 0], [endRow, 0] ]); if (endRow > this.getLastBufferRow()) textToDuplicate = `\n${textToDuplicate}`; this.buffer.insert([endRow, 0], textToDuplicate); const insertedRowCount = endRow - startRow; for (let k = i; k <= j; k++) { selections[k].setBufferRange( previousSelectionRanges[k].translate([insertedRowCount, 0]) ); } for (const fold of intersectingFolds) { const foldRange = this.displayLayer.bufferRangeForFold(fold); this.displayLayer.foldBufferRange( foldRange.translate([insertedRowCount, 0]) ); } i--; } }); } replaceSelectedText(options, fn) { this.mutateSelectedText(selection => { selection.getBufferRange(); if (options && options.selectWordIfEmpty && selection.isEmpty()) { selection.selectWord(); } const text = selection.getText(); selection.deleteSelectedText(); const range = selection.insertText(fn(text)); selection.setBufferRange(range); }); } // Split multi-line selections into one selection per line. // // Operates on all selections. This method breaks apart all multi-line // selections to create multiple single-line selections that cumulatively cover // the same original area. splitSelectionsIntoLines() { this.mergeIntersectingSelections(() => { for (const selection of this.getSelections()) { const range = selection.getBufferRange(); if (range.isSingleLine()) continue; const { start, end } = range; this.addSelectionForBufferRange([start, [start.row, Infinity]]); let { row } = start; while (++row < end.row) { this.addSelectionForBufferRange([[row, 0], [row, Infinity]]); } if (end.column !== 0) this.addSelectionForBufferRange([ [end.row, 0], [end.row, end.column] ]); selection.destroy(); } }); } // Extended: For each selection, transpose the selected text. // // If the selection is empty, the characters preceding and following the cursor // are swapped. Otherwise, the selected characters are reversed. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) transpose(options = {}) { if (!this.ensureWritable('transpose', options)) return; this.mutateSelectedText(selection => { if (selection.isEmpty()) { selection.selectRight(); const text = selection.getText(); selection.delete(); selection.cursor.moveLeft(); selection.insertText(text); } else { selection.insertText( selection .getText() .split('') .reverse() .join('') ); } }); } // Extended: Convert the selected text to upper case. // // For each selection, if the selection is empty, converts the containing word // to upper case. Otherwise convert the selected text to upper case. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) upperCase(options = {}) { if (!this.ensureWritable('upperCase', options)) return; this.replaceSelectedText({ selectWordIfEmpty: true }, text => text.toUpperCase(options) ); } // Extended: Convert the selected text to lower case. // // For each selection, if the selection is empty, converts the containing word // to upper case. Otherwise convert the selected text to upper case. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) lowerCase(options = {}) { if (!this.ensureWritable('lowerCase', options)) return; this.replaceSelectedText({ selectWordIfEmpty: true }, text => text.toLowerCase(options) ); } // Extended: Toggle line comments for rows intersecting selections. // // If the current grammar doesn't support comments, does nothing. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) toggleLineCommentsInSelection(options = {}) { if (!this.ensureWritable('toggleLineCommentsInSelection', options)) return; this.mutateSelectedText(selection => selection.toggleLineComments(options)); } // Convert multiple lines to a single line. // // Operates on all selections. If the selection is empty, joins the current // line with the next line. Otherwise it joins all lines that intersect the // selection. // // Joining a line means that multiple lines are converted to a single line with // the contents of each of the original non-empty lines separated by a space. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) joinLines(options = {}) { if (!this.ensureWritable('joinLines', options)) return; this.mutateSelectedText(selection => selection.joinLines()); } // Extended: For each cursor, insert a newline at beginning the following line. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) insertNewlineBelow(options = {}) { if (!this.ensureWritable('insertNewlineBelow', options)) return; this.transact(() => { this.moveToEndOfLine(); this.insertNewline(options); }); } // Extended: For each cursor, insert a newline at the end of the preceding line. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) insertNewlineAbove(options = {}) { if (!this.ensureWritable('insertNewlineAbove', options)) return; this.transact(() => { const bufferRow = this.getCursorBufferPosition().row; const indentLevel = this.indentationForBufferRow(bufferRow); const onFirstLine = bufferRow === 0; this.moveToBeginningOfLine(); this.moveLeft(); this.insertNewline(options); if ( this.shouldAutoIndent() && this.indentationForBufferRow(bufferRow) < indentLevel ) { this.setIndentationForBufferRow(bufferRow, indentLevel); } if (onFirstLine) { this.moveUp(); this.moveToEndOfLine(); } }); } // Extended: For each selection, if the selection is empty, delete all characters // of the containing word that precede the cursor. Otherwise delete the // selected text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfWord(options = {}) { if (!this.ensureWritable('deleteToBeginningOfWord', options)) return; this.mutateSelectedText(selection => selection.deleteToBeginningOfWord(options) ); } // Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the // previous word boundary. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToPreviousWordBoundary(options = {}) { if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return; this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary(options) ); } // Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the // next word boundary. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToNextWordBoundary(options = {}) { if (!this.ensureWritable('deleteToNextWordBoundary', options)) return; this.mutateSelectedText(selection => selection.deleteToNextWordBoundary(options) ); } // Extended: For each selection, if the selection is empty, delete all characters // of the containing subword following the cursor. Otherwise delete the selected // text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfSubword(options = {}) { if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return; this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword(options) ); } // Extended: For each selection, if the selection is empty, delete all characters // of the containing subword following the cursor. Otherwise delete the selected // text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfSubword(options = {}) { if (!this.ensureWritable('deleteToEndOfSubword', options)) return; this.mutateSelectedText(selection => selection.deleteToEndOfSubword(options) ); } // Extended: For each selection, if the selection is empty, delete all characters // of the containing line that precede the cursor. Otherwise delete the // selected text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfLine(options = {}) { if (!this.ensureWritable('deleteToBeginningOfLine', options)) return; this.mutateSelectedText(selection => selection.deleteToBeginningOfLine(options) ); } // Extended: For each selection, if the selection is not empty, deletes the // selection; otherwise, deletes all characters of the containing line // following the cursor. If the cursor is already at the end of the line, // deletes the following newline. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfLine(options = {}) { if (!this.ensureWritable('deleteToEndOfLine', options)) return; this.mutateSelectedText(selection => selection.deleteToEndOfLine(options)); } // Extended: For each selection, if the selection is empty, delete all characters // of the containing word following the cursor. Otherwise delete the selected // text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfWord(options = {}) { if (!this.ensureWritable('deleteToEndOfWord', options)) return; this.mutateSelectedText(selection => selection.deleteToEndOfWord(options)); } // Extended: Delete all lines intersecting selections. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteLine(options = {}) { if (!this.ensureWritable('deleteLine', options)) return; this.mergeSelectionsOnSameRows(); this.mutateSelectedText(selection => selection.deleteLine(options)); } // Private: Ensure that this editor is not marked read-only before allowing a buffer modification to occur. If // the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error. ensureWritable(methodName, opts) { if (!opts.bypassReadOnly && this.isReadOnly()) { if (atom.inDevMode() || atom.inSpecMode()) { const e = new Error('Attempt to mutate a read-only TextEditor'); e.detail = `Your package is attempting to call ${methodName} on an editor that has been marked read-only. ` + 'Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before attempting ' + 'modifications.'; throw e; } return false; } return true; } /* Section: History */ // Essential: Undo the last change. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) undo(options = {}) { if (!this.ensureWritable('undo', options)) return; this.avoidMergingSelections(() => this.buffer.undo({ selectionsMarkerLayer: this.selectionsMarkerLayer }) ); this.getLastSelection().autoscroll(); } // Essential: Redo the last change. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) redo(options = {}) { if (!this.ensureWritable('redo', options)) return; this.avoidMergingSelections(() => this.buffer.redo({ selectionsMarkerLayer: this.selectionsMarkerLayer }) ); this.getLastSelection().autoscroll(); } // Extended: Batch multiple operations as a single undo/redo step. // // Any group of operations that are logically grouped from the perspective of // undoing and redoing should be performed in a transaction. If you want to // abort the transaction, call {::abortTransaction} to terminate the function's // execution and revert any changes performed up to the abortion. // // * `groupingInterval` (optional) The {Number} of milliseconds for which this // transaction should be considered 'groupable' after it begins. If a transaction // with a positive `groupingInterval` is committed while the previous transaction is // still 'groupable', the two transactions are merged with respect to undo and redo. // * `fn` A {Function} to call inside the transaction. transact(groupingInterval, fn) { const options = { selectionsMarkerLayer: this.selectionsMarkerLayer }; if (typeof groupingInterval === 'function') { fn = groupingInterval; } else { options.groupingInterval = groupingInterval; } return this.buffer.transact(options, fn); } // Extended: Abort an open transaction, undoing any operations performed so far // within the transaction. abortTransaction() { return this.buffer.abortTransaction(); } // Extended: Create a pointer to the current state of the buffer for use // with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}. // // Returns a checkpoint value. createCheckpoint() { return this.buffer.createCheckpoint({ selectionsMarkerLayer: this.selectionsMarkerLayer }); } // Extended: Revert the buffer to the state it was in when the given // checkpoint was created. // // The redo stack will be empty following this operation, so changes since the // checkpoint will be lost. If the given checkpoint is no longer present in the // undo history, no changes will be made to the buffer and this method will // return `false`. // // * `checkpoint` The checkpoint to revert to. // // Returns a {Boolean} indicating whether the operation succeeded. revertToCheckpoint(checkpoint) { return this.buffer.revertToCheckpoint(checkpoint); } // Extended: Group all changes since the given checkpoint into a single // transaction for purposes of undo/redo. // // If the given checkpoint is no longer present in the undo history, no // grouping will be performed and this method will return `false`. // // * `checkpoint` The checkpoint from which to group changes. // // Returns a {Boolean} indicating whether the operation succeeded. groupChangesSinceCheckpoint(checkpoint) { return this.buffer.groupChangesSinceCheckpoint(checkpoint, { selectionsMarkerLayer: this.selectionsMarkerLayer }); } /* Section: TextEditor Coordinates */ // Essential: Convert a position in buffer-coordinates to screen-coordinates. // // The position is clipped via {::clipBufferPosition} prior to the conversion. // The position is also clipped via {::clipScreenPosition} following the // conversion, which only makes a difference when `options` are supplied. // // * `bufferPosition` A {Point} or {Array} of [row, column]. // * `options` (optional) An options hash for {::clipScreenPosition}. // // Returns a {Point}. screenPositionForBufferPosition(bufferPosition, options) { if (options && options.clip) { Grim.deprecate( 'The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.' ); if (options.clipDirection) options.clipDirection = options.clip; } if (options && options.wrapAtSoftNewlines != null) { Grim.deprecate( "The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead." ); if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward'; } if (options && options.wrapBeyondNewlines != null) { Grim.deprecate( "The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead." ); if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward'; } return this.displayLayer.translateBufferPosition(bufferPosition, options); } // Essential: Convert a position in screen-coordinates to buffer-coordinates. // // The position is clipped via {::clipScreenPosition} prior to the conversion. // // * `bufferPosition` A {Point} or {Array} of [row, column]. // * `options` (optional) An options hash for {::clipScreenPosition}. // // Returns a {Point}. bufferPositionForScreenPosition(screenPosition, options) { if (options && options.clip) { Grim.deprecate( 'The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.' ); if (options.clipDirection) options.clipDirection = options.clip; } if (options && options.wrapAtSoftNewlines != null) { Grim.deprecate( "The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead." ); if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward'; } if (options && options.wrapBeyondNewlines != null) { Grim.deprecate( "The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead." ); if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward'; } return this.displayLayer.translateScreenPosition(screenPosition, options); } // Essential: Convert a range in buffer-coordinates to screen-coordinates. // // * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. // // Returns a {Range}. screenRangeForBufferRange(bufferRange, options) { bufferRange = Range.fromObject(bufferRange); const start = this.screenPositionForBufferPosition( bufferRange.start, options ); const end = this.screenPositionForBufferPosition(bufferRange.end, options); return new Range(start, end); } // Essential: Convert a range in screen-coordinates to buffer-coordinates. // // * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. // // Returns a {Range}. bufferRangeForScreenRange(screenRange) { screenRange = Range.fromObject(screenRange); const start = this.bufferPositionForScreenPosition(screenRange.start); const end = this.bufferPositionForScreenPosition(screenRange.end); return new Range(start, end); } // Extended: Clip the given {Point} to a valid position in the buffer. // // If the given {Point} describes a position that is actually reachable by the // cursor based on the current contents of the buffer, it is returned // unchanged. If the {Point} does not describe a valid position, the closest // valid position is returned instead. // // ## Examples // // ```js // editor.clipBufferPosition([-1, -1]) // -> `[0, 0]` // // // When the line at buffer row 2 is 10 characters long // editor.clipBufferPosition([2, Infinity]) // -> `[2, 10]` // ``` // // * `bufferPosition` The {Point} representing the position to clip. // // Returns a {Point}. clipBufferPosition(bufferPosition) { return this.buffer.clipPosition(bufferPosition); } // Extended: Clip the start and end of the given range to valid positions in the // buffer. See {::clipBufferPosition} for more information. // // * `range` The {Range} to clip. // // Returns a {Range}. clipBufferRange(range) { return this.buffer.clipRange(range); } // Extended: Clip the given {Point} to a valid position on screen. // // If the given {Point} describes a position that is actually reachable by the // cursor based on the current contents of the screen, it is returned // unchanged. If the {Point} does not describe a valid position, the closest // valid position is returned instead. // // ## Examples // // ```js // editor.clipScreenPosition([-1, -1]) // -> `[0, 0]` // // // When the line at screen row 2 is 10 characters long // editor.clipScreenPosition([2, Infinity]) // -> `[2, 10]` // ``` // // * `screenPosition` The {Point} representing the position to clip. // * `options` (optional) {Object} // * `clipDirection` {String} If `'backward'`, returns the first valid // position preceding an invalid position. If `'forward'`, returns the // first valid position following an invalid position. If `'closest'`, // returns the first valid position closest to an invalid position. // Defaults to `'closest'`. // // Returns a {Point}. clipScreenPosition(screenPosition, options) { if (options && options.clip) { Grim.deprecate( 'The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.' ); if (options.clipDirection) options.clipDirection = options.clip; } if (options && options.wrapAtSoftNewlines != null) { Grim.deprecate( "The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead." ); if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward'; } if (options && options.wrapBeyondNewlines != null) { Grim.deprecate( "The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead." ); if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward'; } return this.displayLayer.clipScreenPosition(screenPosition, options); } // Extended: Clip the start and end of the given range to valid positions on screen. // See {::clipScreenPosition} for more information. // // * `range` The {Range} to clip. // * `options` (optional) See {::clipScreenPosition} `options`. // // Returns a {Range}. clipScreenRange(screenRange, options) { screenRange = Range.fromObject(screenRange); const start = this.displayLayer.clipScreenPosition( screenRange.start, options ); const end = this.displayLayer.clipScreenPosition(screenRange.end, options); return Range(start, end); } /* Section: Decorations */ // Essential: Add a decoration that tracks a {DisplayMarker}. When the // marker moves, is invalidated, or is destroyed, the decoration will be // updated to reflect the marker's state. // // The following are the supported decorations types: // // * __line__: Adds the given CSS `class` to the lines overlapping the rows // spanned by the marker. // * __line-number__: Adds the given CSS `class` to the line numbers overlapping // the rows spanned by the marker // * __text__: Injects spans into all text overlapping the marked range, then adds // the given `class` or `style` to these spans. Use this to manipulate the foreground // color or styling of text in a range. // * __highlight__: Creates an absolutely-positioned `.highlight` div to the editor // containing nested divs that cover the marked region. For example, when the user // selects text, the selection is implemented with a highlight decoration. The structure // of this highlight will be: // ```html //
          // //
          //
          // ``` // * __overlay__: Positions the view associated with the given item at the head // or tail of the given `DisplayMarker`, depending on the `position` property. // * __gutter__: Tracks a {DisplayMarker} in a {Gutter}. Gutter decorations are created // by calling {Gutter::decorateMarker} on the desired `Gutter` instance. // * __block__: Positions the view associated with the given item before or // after the row of the given {DisplayMarker}, depending on the `position` property. // Block decorations at the same screen row are ordered by their `order` property. // * __cursor__: Render a cursor at the head of the {DisplayMarker}. If multiple cursor decorations // are created for the same marker, their class strings and style objects are combined // into a single cursor. This decoration type may be used to style existing cursors // by passing in their markers or to render artificial cursors that don't actually // exist in the model by passing a marker that isn't associated with a real cursor. // // ## Arguments // // * `marker` A {DisplayMarker} you want this decoration to follow. // * `decorationParams` An {Object} representing the decoration e.g. // `{type: 'line-number', class: 'linter-error'}` // * `type` Determines the behavior and appearance of this {Decoration}. Supported decoration types // and their uses are listed above. // * `class` This CSS class will be applied to the decorated line number, // line, text spans, highlight regions, cursors, or overlay. // * `style` An {Object} containing CSS style properties to apply to the // relevant DOM node. Currently this only works with a `type` of `cursor` // or `text`. // * `item` (optional) An {HTMLElement} or a model {Object} with a // corresponding view registered. Only applicable to the `gutter`, // `overlay` and `block` decoration types. // * `onlyHead` (optional) If `true`, the decoration will only be applied to // the head of the `DisplayMarker`. Only applicable to the `line` and // `line-number` decoration types. // * `onlyEmpty` (optional) If `true`, the decoration will only be applied if // the associated `DisplayMarker` is empty. Only applicable to the `gutter`, // `line`, and `line-number` decoration types. // * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied // if the associated `DisplayMarker` is non-empty. Only applicable to the // `gutter`, `line`, and `line-number` decoration types. // * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied // to the last row of a non-empty range, even if it ends at column 0. // Defaults to `true`. Only applicable to the `gutter`, `line`, and // `line-number` decoration types. // * `position` (optional) Only applicable to decorations of type `overlay` and `block`. // Controls where the view is positioned relative to the `TextEditorMarker`. // Values can be `'head'` (the default) or `'tail'` for overlay decorations, and // `'before'` (the default) or `'after'` for block decorations. // * `order` (optional) Only applicable to decorations of type `block`. Controls // where the view is positioned relative to other block decorations at the // same screen row. If unspecified, block decorations render oldest to newest. // * `avoidOverflow` (optional) Only applicable to decorations of type // `overlay`. Determines whether the decoration adjusts its horizontal or // vertical position to remain fully visible when it would otherwise // overflow the editor. Defaults to `true`. // // Returns the created {Decoration} object. decorateMarker(marker, decorationParams) { return this.decorationManager.decorateMarker(marker, decorationParams); } // Essential: Add a decoration to every marker in the given marker layer. Can // be used to decorate a large number of markers without having to create and // manage many individual decorations. // // * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. // * `decorationParams` The same parameters that are passed to // {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`. // // Returns a {LayerDecoration}. decorateMarkerLayer(markerLayer, decorationParams) { return this.decorationManager.decorateMarkerLayer( markerLayer, decorationParams ); } // Deprecated: Get all the decorations within a screen row range on the default // layer. // // * `startScreenRow` the {Number} beginning screen row // * `endScreenRow` the {Number} end screen row (inclusive) // // Returns an {Object} of decorations in the form // `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}` // where the keys are {DisplayMarker} IDs, and the values are an array of decoration // params objects attached to the marker. // Returns an empty object when no decorations are found decorationsForScreenRowRange(startScreenRow, endScreenRow) { return this.decorationManager.decorationsForScreenRowRange( startScreenRow, endScreenRow ); } decorationsStateForScreenRowRange(startScreenRow, endScreenRow) { return this.decorationManager.decorationsStateForScreenRowRange( startScreenRow, endScreenRow ); } // Extended: Get all decorations. // // * `propertyFilter` (optional) An {Object} containing key value pairs that // the returned decorations' properties must match. // // Returns an {Array} of {Decoration}s. getDecorations(propertyFilter) { return this.decorationManager.getDecorations(propertyFilter); } // Extended: Get all decorations of type 'line'. // // * `propertyFilter` (optional) An {Object} containing key value pairs that // the returned decorations' properties must match. // // Returns an {Array} of {Decoration}s. getLineDecorations(propertyFilter) { return this.decorationManager.getLineDecorations(propertyFilter); } // Extended: Get all decorations of type 'line-number'. // // * `propertyFilter` (optional) An {Object} containing key value pairs that // the returned decorations' properties must match. // // Returns an {Array} of {Decoration}s. getLineNumberDecorations(propertyFilter) { return this.decorationManager.getLineNumberDecorations(propertyFilter); } // Extended: Get all decorations of type 'highlight'. // // * `propertyFilter` (optional) An {Object} containing key value pairs that // the returned decorations' properties must match. // // Returns an {Array} of {Decoration}s. getHighlightDecorations(propertyFilter) { return this.decorationManager.getHighlightDecorations(propertyFilter); } // Extended: Get all decorations of type 'overlay'. // // * `propertyFilter` (optional) An {Object} containing key value pairs that // the returned decorations' properties must match. // // Returns an {Array} of {Decoration}s. getOverlayDecorations(propertyFilter) { return this.decorationManager.getOverlayDecorations(propertyFilter); } /* Section: Markers */ // Essential: Create a marker on the default marker layer with the given range // in buffer coordinates. This marker will maintain its logical location as the // buffer is changed, so if you mark a particular word, the marker will remain // over that word even if the word's location in the buffer changes. // // * `range` A {Range} or range-compatible {Array} // * `properties` A hash of key-value pairs to associate with the marker. There // are also reserved property names that have marker-specific meaning. // * `maintainHistory` (optional) {Boolean} Whether to store this marker's // range before and after each change in the undo history. This allows the // marker's position to be restored more accurately for certain undo/redo // operations, but uses more time and memory. (default: false) // * `reversed` (optional) {Boolean} Creates the marker in a reversed // orientation. (default: false) // * `invalidate` (optional) {String} Determines the rules by which changes // to the buffer *invalidate* the marker. (default: 'overlap') It can be // any of the following strategies, in order of fragility: // * __never__: The marker is never marked as invalid. This is a good choice for // markers representing selections in an editor. // * __surround__: The marker is invalidated by changes that completely surround it. // * __overlap__: The marker is invalidated by changes that surround the // start or end of the marker. This is the default. // * __inside__: The marker is invalidated by changes that extend into the // inside of the marker. Changes that end at the marker's start or // start at the marker's end do not invalidate the marker. // * __touch__: The marker is invalidated by a change that touches the marked // region in any way, including changes that end at the marker's // start or start at the marker's end. This is the most fragile strategy. // // Returns a {DisplayMarker}. markBufferRange(bufferRange, options) { return this.defaultMarkerLayer.markBufferRange(bufferRange, options); } // Essential: Create a marker on the default marker layer with the given range // in screen coordinates. This marker will maintain its logical location as the // buffer is changed, so if you mark a particular word, the marker will remain // over that word even if the word's location in the buffer changes. // // * `range` A {Range} or range-compatible {Array} // * `properties` A hash of key-value pairs to associate with the marker. There // are also reserved property names that have marker-specific meaning. // * `maintainHistory` (optional) {Boolean} Whether to store this marker's // range before and after each change in the undo history. This allows the // marker's position to be restored more accurately for certain undo/redo // operations, but uses more time and memory. (default: false) // * `reversed` (optional) {Boolean} Creates the marker in a reversed // orientation. (default: false) // * `invalidate` (optional) {String} Determines the rules by which changes // to the buffer *invalidate* the marker. (default: 'overlap') It can be // any of the following strategies, in order of fragility: // * __never__: The marker is never marked as invalid. This is a good choice for // markers representing selections in an editor. // * __surround__: The marker is invalidated by changes that completely surround it. // * __overlap__: The marker is invalidated by changes that surround the // start or end of the marker. This is the default. // * __inside__: The marker is invalidated by changes that extend into the // inside of the marker. Changes that end at the marker's start or // start at the marker's end do not invalidate the marker. // * __touch__: The marker is invalidated by a change that touches the marked // region in any way, including changes that end at the marker's // start or start at the marker's end. This is the most fragile strategy. // // Returns a {DisplayMarker}. markScreenRange(screenRange, options) { return this.defaultMarkerLayer.markScreenRange(screenRange, options); } // Essential: Create a marker on the default marker layer with the given buffer // position and no tail. To group multiple markers together in their own // private layer, see {::addMarkerLayer}. // // * `bufferPosition` A {Point} or point-compatible {Array} // * `options` (optional) An {Object} with the following keys: // * `invalidate` (optional) {String} Determines the rules by which changes // to the buffer *invalidate* the marker. (default: 'overlap') It can be // any of the following strategies, in order of fragility: // * __never__: The marker is never marked as invalid. This is a good choice for // markers representing selections in an editor. // * __surround__: The marker is invalidated by changes that completely surround it. // * __overlap__: The marker is invalidated by changes that surround the // start or end of the marker. This is the default. // * __inside__: The marker is invalidated by changes that extend into the // inside of the marker. Changes that end at the marker's start or // start at the marker's end do not invalidate the marker. // * __touch__: The marker is invalidated by a change that touches the marked // region in any way, including changes that end at the marker's // start or start at the marker's end. This is the most fragile strategy. // // Returns a {DisplayMarker}. markBufferPosition(bufferPosition, options) { return this.defaultMarkerLayer.markBufferPosition(bufferPosition, options); } // Essential: Create a marker on the default marker layer with the given screen // position and no tail. To group multiple markers together in their own // private layer, see {::addMarkerLayer}. // // * `screenPosition` A {Point} or point-compatible {Array} // * `options` (optional) An {Object} with the following keys: // * `invalidate` (optional) {String} Determines the rules by which changes // to the buffer *invalidate* the marker. (default: 'overlap') It can be // any of the following strategies, in order of fragility: // * __never__: The marker is never marked as invalid. This is a good choice for // markers representing selections in an editor. // * __surround__: The marker is invalidated by changes that completely surround it. // * __overlap__: The marker is invalidated by changes that surround the // start or end of the marker. This is the default. // * __inside__: The marker is invalidated by changes that extend into the // inside of the marker. Changes that end at the marker's start or // start at the marker's end do not invalidate the marker. // * __touch__: The marker is invalidated by a change that touches the marked // region in any way, including changes that end at the marker's // start or start at the marker's end. This is the most fragile strategy. // * `clipDirection` {String} If `'backward'`, returns the first valid // position preceding an invalid position. If `'forward'`, returns the // first valid position following an invalid position. If `'closest'`, // returns the first valid position closest to an invalid position. // Defaults to `'closest'`. // // Returns a {DisplayMarker}. markScreenPosition(screenPosition, options) { return this.defaultMarkerLayer.markScreenPosition(screenPosition, options); } // Essential: Find all {DisplayMarker}s on the default marker layer that // match the given properties. // // This method finds markers based on the given properties. Markers can be // associated with custom properties that will be compared with basic equality. // In addition, there are several special properties that will be compared // with the range of the markers rather than their properties. // // * `properties` An {Object} containing properties that each returned marker // must satisfy. Markers can be associated with custom properties, which are // compared with basic equality. In addition, several reserved properties // can be used to filter markers based on their current range: // * `startBufferRow` Only include markers starting at this row in buffer // coordinates. // * `endBufferRow` Only include markers ending at this row in buffer // coordinates. // * `containsBufferRange` Only include markers containing this {Range} or // in range-compatible {Array} in buffer coordinates. // * `containsBufferPosition` Only include markers containing this {Point} // or {Array} of `[row, column]` in buffer coordinates. // // Returns an {Array} of {DisplayMarker}s findMarkers(params) { return this.defaultMarkerLayer.findMarkers(params); } // Extended: Get the {DisplayMarker} on the default layer for the given // marker id. // // * `id` {Number} id of the marker getMarker(id) { return this.defaultMarkerLayer.getMarker(id); } // Extended: Get all {DisplayMarker}s on the default marker layer. Consider // using {::findMarkers} getMarkers() { return this.defaultMarkerLayer.getMarkers(); } // Extended: Get the number of markers in the default marker layer. // // Returns a {Number}. getMarkerCount() { return this.defaultMarkerLayer.getMarkerCount(); } destroyMarker(id) { const marker = this.getMarker(id); if (marker) marker.destroy(); } // Essential: Create a marker layer to group related markers. // // * `options` An {Object} containing the following keys: // * `maintainHistory` A {Boolean} indicating whether marker state should be // restored on undo/redo. Defaults to `false`. // * `persistent` A {Boolean} indicating whether or not this marker layer // should be serialized and deserialized along with the rest of the // buffer. Defaults to `false`. If `true`, the marker layer's id will be // maintained across the serialization boundary, allowing you to retrieve // it via {::getMarkerLayer}. // // Returns a {DisplayMarkerLayer}. addMarkerLayer(options) { return this.displayLayer.addMarkerLayer(options); } // Essential: Get a {DisplayMarkerLayer} by id. // // * `id` The id of the marker layer to retrieve. // // Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the // given id. getMarkerLayer(id) { return this.displayLayer.getMarkerLayer(id); } // Essential: Get the default {DisplayMarkerLayer}. // // All marker APIs not tied to an explicit layer interact with this default // layer. // // Returns a {DisplayMarkerLayer}. getDefaultMarkerLayer() { return this.defaultMarkerLayer; } /* Section: Cursors */ // Essential: Get the position of the most recently added cursor in buffer // coordinates. // // Returns a {Point} getCursorBufferPosition() { return this.getLastCursor().getBufferPosition(); } // Essential: Get the position of all the cursor positions in buffer coordinates. // // Returns {Array} of {Point}s in the order they were added getCursorBufferPositions() { return this.getCursors().map(cursor => cursor.getBufferPosition()); } // Essential: Move the cursor to the given position in buffer coordinates. // // If there are multiple cursors, they will be consolidated to a single cursor. // // * `position` A {Point} or {Array} of `[row, column]` // * `options` (optional) An {Object} containing the following keys: // * `autoscroll` Determines whether the editor scrolls to the new cursor's // position. Defaults to true. setCursorBufferPosition(position, options) { return this.moveCursors(cursor => cursor.setBufferPosition(position, options) ); } // Essential: Get a {Cursor} at given screen coordinates {Point} // // * `position` A {Point} or {Array} of `[row, column]` // // Returns the first matched {Cursor} or undefined getCursorAtScreenPosition(position) { const selection = this.getSelectionAtScreenPosition(position); if (selection && selection.getHeadScreenPosition().isEqual(position)) { return selection.cursor; } } // Essential: Get the position of the most recently added cursor in screen // coordinates. // // Returns a {Point}. getCursorScreenPosition() { return this.getLastCursor().getScreenPosition(); } // Essential: Get the position of all the cursor positions in screen coordinates. // // Returns {Array} of {Point}s in the order the cursors were added getCursorScreenPositions() { return this.getCursors().map(cursor => cursor.getScreenPosition()); } // Essential: Move the cursor to the given position in screen coordinates. // // If there are multiple cursors, they will be consolidated to a single cursor. // // * `position` A {Point} or {Array} of `[row, column]` // * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: // * `autoscroll` Determines whether the editor scrolls to the new cursor's // position. Defaults to true. setCursorScreenPosition(position, options) { if (options && options.clip) { Grim.deprecate( 'The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.' ); if (options.clipDirection) options.clipDirection = options.clip; } if (options && options.wrapAtSoftNewlines != null) { Grim.deprecate( "The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead." ); if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward'; } if (options && options.wrapBeyondNewlines != null) { Grim.deprecate( "The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead." ); if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward'; } return this.moveCursors(cursor => cursor.setScreenPosition(position, options) ); } // Essential: Add a cursor at the given position in buffer coordinates. // // * `bufferPosition` A {Point} or {Array} of `[row, column]` // // Returns a {Cursor}. addCursorAtBufferPosition(bufferPosition, options) { this.selectionsMarkerLayer.markBufferPosition(bufferPosition, { invalidate: 'never' }); if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll(); return this.getLastSelection().cursor; } // Essential: Add a cursor at the position in screen coordinates. // // * `screenPosition` A {Point} or {Array} of `[row, column]` // // Returns a {Cursor}. addCursorAtScreenPosition(screenPosition, options) { this.selectionsMarkerLayer.markScreenPosition(screenPosition, { invalidate: 'never' }); if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll(); return this.getLastSelection().cursor; } // Essential: Returns {Boolean} indicating whether or not there are multiple cursors. hasMultipleCursors() { return this.getCursors().length > 1; } // Essential: Move every cursor up one row in screen coordinates. // // * `lineCount` (optional) {Number} number of lines to move moveUp(lineCount) { return this.moveCursors(cursor => cursor.moveUp(lineCount, { moveToEndOfSelection: true }) ); } // Essential: Move every cursor down one row in screen coordinates. // // * `lineCount` (optional) {Number} number of lines to move moveDown(lineCount) { return this.moveCursors(cursor => cursor.moveDown(lineCount, { moveToEndOfSelection: true }) ); } // Essential: Move every cursor left one column. // // * `columnCount` (optional) {Number} number of columns to move (default: 1) moveLeft(columnCount) { return this.moveCursors(cursor => cursor.moveLeft(columnCount, { moveToEndOfSelection: true }) ); } // Essential: Move every cursor right one column. // // * `columnCount` (optional) {Number} number of columns to move (default: 1) moveRight(columnCount) { return this.moveCursors(cursor => cursor.moveRight(columnCount, { moveToEndOfSelection: true }) ); } // Essential: Move every cursor to the beginning of its line in buffer coordinates. moveToBeginningOfLine() { return this.moveCursors(cursor => cursor.moveToBeginningOfLine()); } // Essential: Move every cursor to the beginning of its line in screen coordinates. moveToBeginningOfScreenLine() { return this.moveCursors(cursor => cursor.moveToBeginningOfScreenLine()); } // Essential: Move every cursor to the first non-whitespace character of its line. moveToFirstCharacterOfLine() { return this.moveCursors(cursor => cursor.moveToFirstCharacterOfLine()); } // Essential: Move every cursor to the end of its line in buffer coordinates. moveToEndOfLine() { return this.moveCursors(cursor => cursor.moveToEndOfLine()); } // Essential: Move every cursor to the end of its line in screen coordinates. moveToEndOfScreenLine() { return this.moveCursors(cursor => cursor.moveToEndOfScreenLine()); } // Essential: Move every cursor to the beginning of its surrounding word. moveToBeginningOfWord() { return this.moveCursors(cursor => cursor.moveToBeginningOfWord()); } // Essential: Move every cursor to the end of its surrounding word. moveToEndOfWord() { return this.moveCursors(cursor => cursor.moveToEndOfWord()); } // Cursor Extended // Extended: Move every cursor to the top of the buffer. // // If there are multiple cursors, they will be merged into a single cursor. moveToTop() { return this.moveCursors(cursor => cursor.moveToTop()); } // Extended: Move every cursor to the bottom of the buffer. // // If there are multiple cursors, they will be merged into a single cursor. moveToBottom() { return this.moveCursors(cursor => cursor.moveToBottom()); } // Extended: Move every cursor to the beginning of the next word. moveToBeginningOfNextWord() { return this.moveCursors(cursor => cursor.moveToBeginningOfNextWord()); } // Extended: Move every cursor to the previous word boundary. moveToPreviousWordBoundary() { return this.moveCursors(cursor => cursor.moveToPreviousWordBoundary()); } // Extended: Move every cursor to the next word boundary. moveToNextWordBoundary() { return this.moveCursors(cursor => cursor.moveToNextWordBoundary()); } // Extended: Move every cursor to the previous subword boundary. moveToPreviousSubwordBoundary() { return this.moveCursors(cursor => cursor.moveToPreviousSubwordBoundary()); } // Extended: Move every cursor to the next subword boundary. moveToNextSubwordBoundary() { return this.moveCursors(cursor => cursor.moveToNextSubwordBoundary()); } // Extended: Move every cursor to the beginning of the next paragraph. moveToBeginningOfNextParagraph() { return this.moveCursors(cursor => cursor.moveToBeginningOfNextParagraph()); } // Extended: Move every cursor to the beginning of the previous paragraph. moveToBeginningOfPreviousParagraph() { return this.moveCursors(cursor => cursor.moveToBeginningOfPreviousParagraph() ); } // Extended: Returns the most recently added {Cursor} getLastCursor() { this.createLastSelectionIfNeeded(); return _.last(this.cursors); } // Extended: Returns the word surrounding the most recently added cursor. // // * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. getWordUnderCursor(options) { return this.getTextInBufferRange( this.getLastCursor().getCurrentWordBufferRange(options) ); } // Extended: Get an Array of all {Cursor}s. getCursors() { this.createLastSelectionIfNeeded(); return this.cursors.slice(); } // Extended: Get all {Cursor}s, ordered by their position in the buffer // instead of the order in which they were added. // // Returns an {Array} of {Selection}s. getCursorsOrderedByBufferPosition() { return this.getCursors().sort((a, b) => a.compare(b)); } cursorsForScreenRowRange(startScreenRow, endScreenRow) { const cursors = []; for (let marker of this.selectionsMarkerLayer.findMarkers({ intersectsScreenRowRange: [startScreenRow, endScreenRow] })) { const cursor = this.cursorsByMarkerId.get(marker.id); if (cursor) cursors.push(cursor); } return cursors; } // Add a cursor based on the given {DisplayMarker}. addCursor(marker) { const cursor = new Cursor({ editor: this, marker, showCursorOnSelection: this.showCursorOnSelection }); this.cursors.push(cursor); this.cursorsByMarkerId.set(marker.id, cursor); return cursor; } moveCursors(fn) { return this.transact(() => { this.getCursors().forEach(fn); return this.mergeCursors(); }); } cursorMoved(event) { return this.emitter.emit('did-change-cursor-position', event); } // Merge cursors that have the same screen position mergeCursors() { const positions = {}; for (let cursor of this.getCursors()) { const position = cursor.getBufferPosition().toString(); if (positions.hasOwnProperty(position)) { cursor.destroy(); } else { positions[position] = true; } } } /* Section: Selections */ // Essential: Get the selected text of the most recently added selection. // // Returns a {String}. getSelectedText() { return this.getLastSelection().getText(); } // Essential: Get the {Range} of the most recently added selection in buffer // coordinates. // // Returns a {Range}. getSelectedBufferRange() { return this.getLastSelection().getBufferRange(); } // Essential: Get the {Range}s of all selections in buffer coordinates. // // The ranges are sorted by when the selections were added. Most recent at the end. // // Returns an {Array} of {Range}s. getSelectedBufferRanges() { return this.getSelections().map(selection => selection.getBufferRange()); } // Essential: Set the selected range in buffer coordinates. If there are multiple // selections, they are reduced to a single selection with the given range. // // * `bufferRange` A {Range} or range-compatible {Array}. // * `options` (optional) An options {Object}: // * `reversed` A {Boolean} indicating whether to create the selection in a // reversed orientation. // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the // selection is set. setSelectedBufferRange(bufferRange, options) { return this.setSelectedBufferRanges([bufferRange], options); } // Essential: Set the selected ranges in buffer coordinates. If there are multiple // selections, they are replaced by new selections with the given ranges. // // * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s. // * `options` (optional) An options {Object}: // * `reversed` A {Boolean} indicating whether to create the selection in a // reversed orientation. // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the // selection is set. setSelectedBufferRanges(bufferRanges, options = {}) { if (!bufferRanges.length) throw new Error('Passed an empty array to setSelectedBufferRanges'); const selections = this.getSelections(); for (let selection of selections.slice(bufferRanges.length)) { selection.destroy(); } this.mergeIntersectingSelections(options, () => { for (let i = 0; i < bufferRanges.length; i++) { let bufferRange = bufferRanges[i]; bufferRange = Range.fromObject(bufferRange); if (selections[i]) { selections[i].setBufferRange(bufferRange, options); } else { this.addSelectionForBufferRange(bufferRange, options); } } }); } // Essential: Get the {Range} of the most recently added selection in screen // coordinates. // // Returns a {Range}. getSelectedScreenRange() { return this.getLastSelection().getScreenRange(); } // Essential: Get the {Range}s of all selections in screen coordinates. // // The ranges are sorted by when the selections were added. Most recent at the end. // // Returns an {Array} of {Range}s. getSelectedScreenRanges() { return this.getSelections().map(selection => selection.getScreenRange()); } // Essential: Set the selected range in screen coordinates. If there are multiple // selections, they are reduced to a single selection with the given range. // // * `screenRange` A {Range} or range-compatible {Array}. // * `options` (optional) An options {Object}: // * `reversed` A {Boolean} indicating whether to create the selection in a // reversed orientation. setSelectedScreenRange(screenRange, options) { return this.setSelectedBufferRange( this.bufferRangeForScreenRange(screenRange, options), options ); } // Essential: Set the selected ranges in screen coordinates. If there are multiple // selections, they are replaced by new selections with the given ranges. // // * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s. // * `options` (optional) An options {Object}: // * `reversed` A {Boolean} indicating whether to create the selection in a // reversed orientation. setSelectedScreenRanges(screenRanges, options = {}) { if (!screenRanges.length) throw new Error('Passed an empty array to setSelectedScreenRanges'); const selections = this.getSelections(); for (let selection of selections.slice(screenRanges.length)) { selection.destroy(); } this.mergeIntersectingSelections(options, () => { for (let i = 0; i < screenRanges.length; i++) { let screenRange = screenRanges[i]; screenRange = Range.fromObject(screenRange); if (selections[i]) { selections[i].setScreenRange(screenRange, options); } else { this.addSelectionForScreenRange(screenRange, options); } } }); } // Essential: Add a selection for the given range in buffer coordinates. // // * `bufferRange` A {Range} // * `options` (optional) An options {Object}: // * `reversed` A {Boolean} indicating whether to create the selection in a // reversed orientation. // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the // selection is set. // // Returns the added {Selection}. addSelectionForBufferRange(bufferRange, options = {}) { bufferRange = Range.fromObject(bufferRange); if (!options.preserveFolds) { this.displayLayer.destroyFoldsContainingBufferPositions( [bufferRange.start, bufferRange.end], true ); } this.selectionsMarkerLayer.markBufferRange(bufferRange, { invalidate: 'never', reversed: options.reversed != null ? options.reversed : false }); if (options.autoscroll !== false) this.getLastSelection().autoscroll(); return this.getLastSelection(); } // Essential: Add a selection for the given range in screen coordinates. // // * `screenRange` A {Range} // * `options` (optional) An options {Object}: // * `reversed` A {Boolean} indicating whether to create the selection in a // reversed orientation. // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the // selection is set. // Returns the added {Selection}. addSelectionForScreenRange(screenRange, options = {}) { return this.addSelectionForBufferRange( this.bufferRangeForScreenRange(screenRange), options ); } // Essential: Select from the current cursor position to the given position in // buffer coordinates. // // This method may merge selections that end up intersecting. // // * `position` An instance of {Point}, with a given `row` and `column`. selectToBufferPosition(position) { const lastSelection = this.getLastSelection(); lastSelection.selectToBufferPosition(position); return this.mergeIntersectingSelections({ reversed: lastSelection.isReversed() }); } // Essential: Select from the current cursor position to the given position in // screen coordinates. // // This method may merge selections that end up intersecting. // // * `position` An instance of {Point}, with a given `row` and `column`. selectToScreenPosition(position, options) { const lastSelection = this.getLastSelection(); lastSelection.selectToScreenPosition(position, options); if (!options || !options.suppressSelectionMerge) { return this.mergeIntersectingSelections({ reversed: lastSelection.isReversed() }); } } // Essential: Move the cursor of each selection one character upward while // preserving the selection's tail position. // // * `rowCount` (optional) {Number} number of rows to select (default: 1) // // This method may merge selections that end up intersecting. selectUp(rowCount) { return this.expandSelectionsBackward(selection => selection.selectUp(rowCount) ); } // Essential: Move the cursor of each selection one character downward while // preserving the selection's tail position. // // * `rowCount` (optional) {Number} number of rows to select (default: 1) // // This method may merge selections that end up intersecting. selectDown(rowCount) { return this.expandSelectionsForward(selection => selection.selectDown(rowCount) ); } // Essential: Move the cursor of each selection one character leftward while // preserving the selection's tail position. // // * `columnCount` (optional) {Number} number of columns to select (default: 1) // // This method may merge selections that end up intersecting. selectLeft(columnCount) { return this.expandSelectionsBackward(selection => selection.selectLeft(columnCount) ); } // Essential: Move the cursor of each selection one character rightward while // preserving the selection's tail position. // // * `columnCount` (optional) {Number} number of columns to select (default: 1) // // This method may merge selections that end up intersecting. selectRight(columnCount) { return this.expandSelectionsForward(selection => selection.selectRight(columnCount) ); } // Essential: Select from the top of the buffer to the end of the last selection // in the buffer. // // This method merges multiple selections into a single selection. selectToTop() { return this.expandSelectionsBackward(selection => selection.selectToTop()); } // Essential: Selects from the top of the first selection in the buffer to the end // of the buffer. // // This method merges multiple selections into a single selection. selectToBottom() { return this.expandSelectionsForward(selection => selection.selectToBottom() ); } // Essential: Select all text in the buffer. // // This method merges multiple selections into a single selection. selectAll() { return this.expandSelectionsForward(selection => selection.selectAll()); } // Essential: Move the cursor of each selection to the beginning of its line // while preserving the selection's tail position. // // This method may merge selections that end up intersecting. selectToBeginningOfLine() { return this.expandSelectionsBackward(selection => selection.selectToBeginningOfLine() ); } // Essential: Move the cursor of each selection to the first non-whitespace // character of its line while preserving the selection's tail position. If the // cursor is already on the first character of the line, move it to the // beginning of the line. // // This method may merge selections that end up intersecting. selectToFirstCharacterOfLine() { return this.expandSelectionsBackward(selection => selection.selectToFirstCharacterOfLine() ); } // Essential: Move the cursor of each selection to the end of its line while // preserving the selection's tail position. // // This method may merge selections that end up intersecting. selectToEndOfLine() { return this.expandSelectionsForward(selection => selection.selectToEndOfLine() ); } // Essential: Expand selections to the beginning of their containing word. // // Operates on all selections. Moves the cursor to the beginning of the // containing word while preserving the selection's tail position. selectToBeginningOfWord() { return this.expandSelectionsBackward(selection => selection.selectToBeginningOfWord() ); } // Essential: Expand selections to the end of their containing word. // // Operates on all selections. Moves the cursor to the end of the containing // word while preserving the selection's tail position. selectToEndOfWord() { return this.expandSelectionsForward(selection => selection.selectToEndOfWord() ); } // Extended: For each selection, move its cursor to the preceding subword // boundary while maintaining the selection's tail position. // // This method may merge selections that end up intersecting. selectToPreviousSubwordBoundary() { return this.expandSelectionsBackward(selection => selection.selectToPreviousSubwordBoundary() ); } // Extended: For each selection, move its cursor to the next subword boundary // while maintaining the selection's tail position. // // This method may merge selections that end up intersecting. selectToNextSubwordBoundary() { return this.expandSelectionsForward(selection => selection.selectToNextSubwordBoundary() ); } // Essential: For each cursor, select the containing line. // // This method merges selections on successive lines. selectLinesContainingCursors() { return this.expandSelectionsForward(selection => selection.selectLine()); } // Essential: Select the word surrounding each cursor. selectWordsContainingCursors() { return this.expandSelectionsForward(selection => selection.selectWord()); } // Selection Extended // Extended: For each selection, move its cursor to the preceding word boundary // while maintaining the selection's tail position. // // This method may merge selections that end up intersecting. selectToPreviousWordBoundary() { return this.expandSelectionsBackward(selection => selection.selectToPreviousWordBoundary() ); } // Extended: For each selection, move its cursor to the next word boundary while // maintaining the selection's tail position. // // This method may merge selections that end up intersecting. selectToNextWordBoundary() { return this.expandSelectionsForward(selection => selection.selectToNextWordBoundary() ); } // Extended: Expand selections to the beginning of the next word. // // Operates on all selections. Moves the cursor to the beginning of the next // word while preserving the selection's tail position. selectToBeginningOfNextWord() { return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextWord() ); } // Extended: Expand selections to the beginning of the next paragraph. // // Operates on all selections. Moves the cursor to the beginning of the next // paragraph while preserving the selection's tail position. selectToBeginningOfNextParagraph() { return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextParagraph() ); } // Extended: Expand selections to the beginning of the next paragraph. // // Operates on all selections. Moves the cursor to the beginning of the next // paragraph while preserving the selection's tail position. selectToBeginningOfPreviousParagraph() { return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph() ); } // Extended: For each selection, select the syntax node that contains // that selection. selectLargerSyntaxNode() { const languageMode = this.buffer.getLanguageMode(); if (!languageMode.getRangeForSyntaxNodeContainingRange) return; this.expandSelectionsForward(selection => { const currentRange = selection.getBufferRange(); const newRange = languageMode.getRangeForSyntaxNodeContainingRange( currentRange ); if (newRange) { if (!selection._rangeStack) selection._rangeStack = []; selection._rangeStack.push(currentRange); selection.setBufferRange(newRange); } }); } // Extended: Undo the effect a preceding call to {::selectLargerSyntaxNode}. selectSmallerSyntaxNode() { this.expandSelectionsForward(selection => { if (selection._rangeStack) { const lastRange = selection._rangeStack[selection._rangeStack.length - 1]; if (lastRange && selection.getBufferRange().containsRange(lastRange)) { selection._rangeStack.length--; selection.setBufferRange(lastRange); } } }); } // Extended: Select the range of the given marker if it is valid. // // * `marker` A {DisplayMarker} // // Returns the selected {Range} or `undefined` if the marker is invalid. selectMarker(marker) { if (marker.isValid()) { const range = marker.getBufferRange(); this.setSelectedBufferRange(range); return range; } } // Extended: Get the most recently added {Selection}. // // Returns a {Selection}. getLastSelection() { this.createLastSelectionIfNeeded(); return _.last(this.selections); } getSelectionAtScreenPosition(position) { const markers = this.selectionsMarkerLayer.findMarkers({ containsScreenPosition: position }); if (markers.length > 0) return this.cursorsByMarkerId.get(markers[0].id).selection; } // Extended: Get current {Selection}s. // // Returns: An {Array} of {Selection}s. getSelections() { this.createLastSelectionIfNeeded(); return this.selections.slice(); } // Extended: Get all {Selection}s, ordered by their position in the buffer // instead of the order in which they were added. // // Returns an {Array} of {Selection}s. getSelectionsOrderedByBufferPosition() { return this.getSelections().sort((a, b) => a.compare(b)); } // Extended: Determine if a given range in buffer coordinates intersects a // selection. // // * `bufferRange` A {Range} or range-compatible {Array}. // // Returns a {Boolean}. selectionIntersectsBufferRange(bufferRange) { return this.getSelections().some(selection => selection.intersectsBufferRange(bufferRange) ); } // Selections Private // Add a similarly-shaped selection to the next eligible line below // each selection. // // Operates on all selections. If the selection is empty, adds an empty // selection to the next following non-empty line as close to the current // selection's column as possible. If the selection is non-empty, adds a // selection to the next line that is long enough for a non-empty selection // starting at the same column as the current selection to be added to it. addSelectionBelow() { return this.expandSelectionsForward(selection => selection.addSelectionBelow() ); } // Add a similarly-shaped selection to the next eligible line above // each selection. // // Operates on all selections. If the selection is empty, adds an empty // selection to the next preceding non-empty line as close to the current // selection's column as possible. If the selection is non-empty, adds a // selection to the next line that is long enough for a non-empty selection // starting at the same column as the current selection to be added to it. addSelectionAbove() { return this.expandSelectionsBackward(selection => selection.addSelectionAbove() ); } // Calls the given function with each selection, then merges selections expandSelectionsForward(fn) { this.mergeIntersectingSelections(() => this.getSelections().forEach(fn)); } // Calls the given function with each selection, then merges selections in the // reversed orientation expandSelectionsBackward(fn) { this.mergeIntersectingSelections({ reversed: true }, () => this.getSelections().forEach(fn) ); } finalizeSelections() { for (let selection of this.getSelections()) { selection.finalize(); } } selectionsForScreenRows(startRow, endRow) { return this.getSelections().filter(selection => selection.intersectsScreenRowRange(startRow, endRow) ); } // Merges intersecting selections. If passed a function, it executes // the function with merging suppressed, then merges intersecting selections // afterward. mergeIntersectingSelections(...args) { return this.mergeSelections( ...args, (previousSelection, currentSelection) => { const exclusive = !currentSelection.isEmpty() && !previousSelection.isEmpty(); return previousSelection.intersectsWith(currentSelection, exclusive); } ); } mergeSelectionsOnSameRows(...args) { return this.mergeSelections( ...args, (previousSelection, currentSelection) => { const screenRange = currentSelection.getScreenRange(); return previousSelection.intersectsScreenRowRange( screenRange.start.row, screenRange.end.row ); } ); } avoidMergingSelections(...args) { return this.mergeSelections(...args, () => false); } mergeSelections(...args) { const mergePredicate = args.pop(); let fn = args.pop(); let options = args.pop(); if (typeof fn !== 'function') { options = fn; fn = () => {}; } if (this.suppressSelectionMerging) return fn(); this.suppressSelectionMerging = true; const result = fn(); this.suppressSelectionMerging = false; const selections = this.getSelectionsOrderedByBufferPosition(); let lastSelection = selections.shift(); for (const selection of selections) { if (mergePredicate(lastSelection, selection)) { lastSelection.merge(selection, options); } else { lastSelection = selection; } } return result; } // Add a {Selection} based on the given {DisplayMarker}. // // * `marker` The {DisplayMarker} to highlight // * `options` (optional) An {Object} that pertains to the {Selection} constructor. // // Returns the new {Selection}. addSelection(marker, options = {}) { const cursor = this.addCursor(marker); let selection = new Selection( Object.assign({ editor: this, marker, cursor }, options) ); this.selections.push(selection); const selectionBufferRange = selection.getBufferRange(); this.mergeIntersectingSelections({ preserveFolds: options.preserveFolds }); if (selection.destroyed) { for (selection of this.getSelections()) { if (selection.intersectsBufferRange(selectionBufferRange)) return selection; } } else { this.emitter.emit('did-add-cursor', cursor); this.emitter.emit('did-add-selection', selection); return selection; } } // Remove the given selection. removeSelection(selection) { _.remove(this.cursors, selection.cursor); _.remove(this.selections, selection); this.cursorsByMarkerId.delete(selection.cursor.marker.id); this.emitter.emit('did-remove-cursor', selection.cursor); return this.emitter.emit('did-remove-selection', selection); } // Reduce one or more selections to a single empty selection based on the most // recently added cursor. clearSelections(options) { this.consolidateSelections(); this.getLastSelection().clear(options); } // Reduce multiple selections to the least recently added selection. consolidateSelections() { const selections = this.getSelections(); if (selections.length > 1) { for (let selection of selections.slice(1, selections.length)) { selection.destroy(); } selections[0].autoscroll({ center: true }); return true; } else { return false; } } // Called by the selection selectionRangeChanged(event) { if (this.component) this.component.didChangeSelectionRange(); this.emitter.emit('did-change-selection-range', event); } createLastSelectionIfNeeded() { if (this.selections.length === 0) { this.addSelectionForBufferRange([[0, 0], [0, 0]], { autoscroll: false, preserveFolds: true }); } } /* Section: Searching and Replacing */ // Essential: Scan regular expression matches in the entire buffer, calling the // given iterator function on each match. // // `::scan` functions as the replace method as well via the `replace` // // If you're programmatically modifying the results, you may want to try // {::backwardsScanInBufferRange} to avoid tripping over your own changes. // // * `regex` A {RegExp} to search for. // * `options` (optional) {Object} // * `leadingContextLineCount` {Number} default `0`; The number of lines // before the matched line to include in the results object. // * `trailingContextLineCount` {Number} default `0`; The number of lines // after the matched line to include in the results object. // * `iterator` A {Function} that's called on each match // * `object` {Object} // * `match` The current regular expression match. // * `matchText` A {String} with the text of the match. // * `range` The {Range} of the match. // * `stop` Call this {Function} to terminate the scan. // * `replace` Call this {Function} with a {String} to replace the match. scan(regex, options = {}, iterator) { if (_.isFunction(options)) { iterator = options; options = {}; } return this.buffer.scan(regex, options, iterator); } // Essential: Scan regular expression matches in a given range, calling the given // iterator function on each match. // // * `regex` A {RegExp} to search for. // * `range` A {Range} in which to search. // * `iterator` A {Function} that's called on each match with an {Object} // containing the following keys: // * `match` The current regular expression match. // * `matchText` A {String} with the text of the match. // * `range` The {Range} of the match. // * `stop` Call this {Function} to terminate the scan. // * `replace` Call this {Function} with a {String} to replace the match. scanInBufferRange(regex, range, iterator) { return this.buffer.scanInRange(regex, range, iterator); } // Essential: Scan regular expression matches in a given range in reverse order, // calling the given iterator function on each match. // // * `regex` A {RegExp} to search for. // * `range` A {Range} in which to search. // * `iterator` A {Function} that's called on each match with an {Object} // containing the following keys: // * `match` The current regular expression match. // * `matchText` A {String} with the text of the match. // * `range` The {Range} of the match. // * `stop` Call this {Function} to terminate the scan. // * `replace` Call this {Function} with a {String} to replace the match. backwardsScanInBufferRange(regex, range, iterator) { return this.buffer.backwardsScanInRange(regex, range, iterator); } /* Section: Tab Behavior */ // Essential: Returns a {Boolean} indicating whether softTabs are enabled for this // editor. getSoftTabs() { return this.softTabs; } // Essential: Enable or disable soft tabs for this editor. // // * `softTabs` A {Boolean} setSoftTabs(softTabs) { this.softTabs = softTabs; this.updateSoftTabs(this.softTabs, true); } // Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. hasAtomicSoftTabs() { return this.displayLayer.atomicSoftTabs; } // Essential: Toggle soft tabs for this editor toggleSoftTabs() { this.setSoftTabs(!this.getSoftTabs()); } // Essential: Get the on-screen length of tab characters. // // Returns a {Number}. getTabLength() { return this.displayLayer.tabLength; } // Essential: Set the on-screen length of tab characters. Setting this to a // {Number} This will override the `editor.tabLength` setting. // // * `tabLength` {Number} length of a single tab. Setting to `null` will // fallback to using the `editor.tabLength` config setting setTabLength(tabLength) { this.updateTabLength(tabLength, true); } // Returns an {Object} representing the current invisible character // substitutions for this editor, whose keys are names of invisible characters // and whose values are 1-character {Strings}s that are displayed in place of // those invisible characters getInvisibles() { if (!this.mini && this.showInvisibles && this.invisibles != null) { return this.invisibles; } else { return {}; } } doesShowIndentGuide() { return this.showIndentGuide && !this.mini; } getSoftWrapHangingIndentLength() { return this.displayLayer.softWrapHangingIndent; } // Extended: Determine if the buffer uses hard or soft tabs. // // Returns `true` if the first non-comment line with leading whitespace starts // with a space character. Returns `false` if it starts with a hard tab (`\t`). // // Returns a {Boolean} or undefined if no non-comment lines had leading // whitespace. usesSoftTabs() { const languageMode = this.buffer.getLanguageMode(); const hasIsRowCommented = languageMode.isRowCommented; for ( let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow()); bufferRow <= end; bufferRow++ ) { if (hasIsRowCommented && languageMode.isRowCommented(bufferRow)) continue; const line = this.buffer.lineForRow(bufferRow); if (line[0] === ' ') return true; if (line[0] === '\t') return false; } } // Extended: Get the text representing a single level of indent. // // If soft tabs are enabled, the text is composed of N spaces, where N is the // tab length. Otherwise the text is a tab character (`\t`). // // Returns a {String}. getTabText() { return this.buildIndentString(1); } // If soft tabs are enabled, convert all hard tabs to soft tabs in the given // {Range}. normalizeTabsInBufferRange(bufferRange) { if (!this.getSoftTabs()) { return; } return this.scanInBufferRange(/\t/g, bufferRange, ({ replace }) => replace(this.getTabText()) ); } /* Section: Soft Wrap Behavior */ // Essential: Determine whether lines in this editor are soft-wrapped. // // Returns a {Boolean}. isSoftWrapped() { return this.softWrapped; } // Essential: Enable or disable soft wrapping for this editor. // // * `softWrapped` A {Boolean} // // Returns a {Boolean}. setSoftWrapped(softWrapped) { this.updateSoftWrapped(softWrapped, true); return this.isSoftWrapped(); } getPreferredLineLength() { return this.preferredLineLength; } // Essential: Toggle soft wrapping for this editor // // Returns a {Boolean}. toggleSoftWrapped() { return this.setSoftWrapped(!this.isSoftWrapped()); } // Essential: Gets the column at which column will soft wrap getSoftWrapColumn() { if (this.isSoftWrapped() && !this.mini) { if (this.softWrapAtPreferredLineLength) { return Math.min(this.getEditorWidthInChars(), this.preferredLineLength); } else { return this.getEditorWidthInChars(); } } else { return this.maxScreenLineLength; } } /* Section: Indentation */ // Essential: Get the indentation level of the given buffer row. // // Determines how deeply the given row is indented based on the soft tabs and // tab length settings of this editor. Note that if soft tabs are enabled and // the tab length is 2, a row with 4 leading spaces would have an indentation // level of 2. // // * `bufferRow` A {Number} indicating the buffer row. // // Returns a {Number}. indentationForBufferRow(bufferRow) { return this.indentLevelForLine(this.lineTextForBufferRow(bufferRow)); } // Essential: Set the indentation level for the given buffer row. // // Inserts or removes hard tabs or spaces based on the soft tabs and tab length // settings of this editor in order to bring it to the given indentation level. // Note that if soft tabs are enabled and the tab length is 2, a row with 4 // leading spaces would have an indentation level of 2. // // * `bufferRow` A {Number} indicating the buffer row. // * `newLevel` A {Number} indicating the new indentation level. // * `options` (optional) An {Object} with the following keys: // * `preserveLeadingWhitespace` `true` to preserve any whitespace already at // the beginning of the line (default: false). setIndentationForBufferRow( bufferRow, newLevel, { preserveLeadingWhitespace } = {} ) { let endColumn; if (preserveLeadingWhitespace) { endColumn = 0; } else { endColumn = this.lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length; } const newIndentString = this.buildIndentString(newLevel); return this.buffer.setTextInRange( [[bufferRow, 0], [bufferRow, endColumn]], newIndentString ); } // Extended: Indent rows intersecting selections by one level. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. indentSelectedRows(options = {}) { if (!this.ensureWritable('indentSelectedRows', options)) return; return this.mutateSelectedText(selection => selection.indentSelectedRows(options) ); } // Extended: Outdent rows intersecting selections by one level. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. outdentSelectedRows(options = {}) { if (!this.ensureWritable('outdentSelectedRows', options)) return; return this.mutateSelectedText(selection => selection.outdentSelectedRows(options) ); } // Extended: Get the indentation level of the given line of text. // // Determines how deeply the given line is indented based on the soft tabs and // tab length settings of this editor. Note that if soft tabs are enabled and // the tab length is 2, a row with 4 leading spaces would have an indentation // level of 2. // // * `line` A {String} representing a line of text. // // Returns a {Number}. indentLevelForLine(line) { const tabLength = this.getTabLength(); 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; } // Extended: Indent rows intersecting selections based on the grammar's suggested // indent level. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. autoIndentSelectedRows(options = {}) { if (!this.ensureWritable('autoIndentSelectedRows', options)) return; return this.mutateSelectedText(selection => selection.autoIndentSelectedRows(options) ); } // Indent all lines intersecting selections. See {Selection::indent} for more // information. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. indent(options = {}) { if (!this.ensureWritable('indent', options)) return; if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent(); this.mutateSelectedText(selection => selection.indent(options)); } // Constructs the string used for indents. buildIndentString(level, column = 0) { if (this.getSoftTabs()) { const tabStopViolation = column % this.getTabLength(); return _.multiplyString( ' ', Math.floor(level * this.getTabLength()) - tabStopViolation ); } else { const excessWhitespace = _.multiplyString( ' ', Math.round((level - Math.floor(level)) * this.getTabLength()) ); return _.multiplyString('\t', Math.floor(level)) + excessWhitespace; } } /* Section: Grammars */ // Essential: Get the current {Grammar} of this editor. getGrammar() { const languageMode = this.buffer.getLanguageMode(); return ( (languageMode.getGrammar && languageMode.getGrammar()) || NullGrammar ); } // Deprecated: Set the current {Grammar} of this editor. // // Assigning a grammar will cause the editor to re-tokenize based on the new // grammar. // // * `grammar` {Grammar} setGrammar(grammar) { const buffer = this.getBuffer(); buffer.setLanguageMode( atom.grammars.languageModeForGrammarAndBuffer(grammar, buffer) ); } // Experimental: Get a notification when async tokenization is completed. onDidTokenize(callback) { return this.emitter.on('did-tokenize', callback); } /* Section: Managing Syntax Scopes */ // Essential: Returns a {ScopeDescriptor} that includes this editor's language. // e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with // {Config::get} to get language specific config values. getRootScopeDescriptor() { return this.buffer.getLanguageMode().rootScopeDescriptor; } // Essential: Get the syntactic {ScopeDescriptor} for the given position in buffer // coordinates. Useful with {Config::get}. // // For example, if called with a position inside the parameter list of an // anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with // the following scopes array: // `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]` // // * `bufferPosition` A {Point} or {Array} of `[row, column]`. // // Returns a {ScopeDescriptor}. scopeDescriptorForBufferPosition(bufferPosition) { const languageMode = this.buffer.getLanguageMode(); return languageMode.scopeDescriptorForPosition ? languageMode.scopeDescriptorForPosition(bufferPosition) : new ScopeDescriptor({ scopes: ['text'] }); } // Essential: Get the syntactic tree {ScopeDescriptor} for the given position in buffer // coordinates or the syntactic {ScopeDescriptor} for TextMate language mode // // For example, if called with a position inside the parameter list of a // JavaScript class function, this method returns a {ScopeDescriptor} with // the following syntax nodes array: // `["source.js", "program", "expression_statement", "assignment_expression", "class", "class_body", "method_definition", "formal_parameters", "identifier"]` // if tree-sitter is used // and the following scopes array: // `["source.js"]` // if textmate is used // // * `bufferPosition` A {Point} or {Array} of `[row, column]`. // // Returns a {ScopeDescriptor}. syntaxTreeScopeDescriptorForBufferPosition(bufferPosition) { const languageMode = this.buffer.getLanguageMode(); return languageMode.syntaxTreeScopeDescriptorForPosition ? languageMode.syntaxTreeScopeDescriptorForPosition(bufferPosition) : this.scopeDescriptorForBufferPosition(bufferPosition); } // Extended: Get the range in buffer coordinates of all tokens surrounding the // cursor that match the given scope selector. // // For example, if you wanted to find the string surrounding the cursor, you // could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. // // * `scopeSelector` {String} selector. e.g. `'.source.ruby'` // // Returns a {Range}. bufferRangeForScopeAtCursor(scopeSelector) { return this.bufferRangeForScopeAtPosition( scopeSelector, this.getCursorBufferPosition() ); } // Extended: Get the range in buffer coordinates of all tokens surrounding the // given position in buffer coordinates that match the given scope selector. // // For example, if you wanted to find the string surrounding the cursor, you // could call `editor.bufferRangeForScopeAtPosition(".string.quoted", this.getCursorBufferPosition())`. // // * `scopeSelector` {String} selector. e.g. `'.source.ruby'` // * `bufferPosition` A {Point} or {Array} of [row, column] // // Returns a {Range}. bufferRangeForScopeAtPosition(scopeSelector, bufferPosition) { return this.buffer .getLanguageMode() .bufferRangeForScopeAtPosition(scopeSelector, bufferPosition); } // Extended: Determine if the given row is entirely a comment isBufferRowCommented(bufferRow) { const match = this.lineTextForBufferRow(bufferRow).match(/\S/); if (match) { if (!this.commentScopeSelector) this.commentScopeSelector = new TextMateScopeSelector('comment.*'); return this.commentScopeSelector.matches( this.scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes ); } } // Get the scope descriptor at the cursor. getCursorScope() { return this.getLastCursor().getScopeDescriptor(); } // Get the syntax nodes at the cursor. getCursorSyntaxTreeScope() { return this.getLastCursor().getSyntaxTreeScopeDescriptor(); } tokenForBufferPosition(bufferPosition) { return this.buffer.getLanguageMode().tokenForPosition(bufferPosition); } /* Section: Clipboard Operations */ // Essential: For each selection, copy the selected text. copySelectedText() { let maintainClipboard = false; for (let selection of this.getSelectionsOrderedByBufferPosition()) { if (selection.isEmpty()) { const previousRange = selection.getBufferRange(); selection.selectLine(); selection.copy(maintainClipboard, true); selection.setBufferRange(previousRange); } else { selection.copy(maintainClipboard, false); } maintainClipboard = true; } } // Private: For each selection, only copy highlighted text. copyOnlySelectedText() { let maintainClipboard = false; for (let selection of this.getSelectionsOrderedByBufferPosition()) { if (!selection.isEmpty()) { selection.copy(maintainClipboard, false); maintainClipboard = true; } } } // Essential: For each selection, cut the selected text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutSelectedText(options = {}) { if (!this.ensureWritable('cutSelectedText', options)) return; let maintainClipboard = false; this.mutateSelectedText(selection => { if (selection.isEmpty()) { selection.selectLine(); selection.cut(maintainClipboard, true, options.bypassReadOnly); } else { selection.cut(maintainClipboard, false, options.bypassReadOnly); } maintainClipboard = true; }); } // Essential: For each selection, replace the selected text with the contents of // the clipboard. // // If the clipboard contains the same number of selections as the current // editor, each selection will be replaced with the content of the // corresponding clipboard selection text. // // * `options` (optional) See {Selection::insertText}. pasteText(options = {}) { if (!this.ensureWritable('parseText', options)) return; options = Object.assign({}, options); let { text: clipboardText, metadata } = this.constructor.clipboard.readWithMetadata(); if (!this.emitWillInsertTextEvent(clipboardText)) return false; if (!metadata) metadata = {}; if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndentOnPaste(); this.mutateSelectedText((selection, index) => { let fullLine, indentBasis, text; if ( metadata.selections && metadata.selections.length === this.getSelections().length ) { ({ text, indentBasis, fullLine } = metadata.selections[index]); } else { ({ indentBasis, fullLine } = metadata); text = clipboardText; } if ( indentBasis != null && (text.includes('\n') || !selection.cursor.hasPrecedingCharactersOnLine()) ) { options.indentBasis = indentBasis; } else { options.indentBasis = null; } let range; if (fullLine && selection.isEmpty()) { const oldPosition = selection.getBufferRange().start; selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]); range = selection.insertText(text, options); const newPosition = oldPosition.translate([1, 0]); selection.setBufferRange([newPosition, newPosition]); } else { range = selection.insertText(text, options); } this.emitter.emit('did-insert-text', { text, range }); }); } // Essential: For each selection, if the selection is empty, cut all characters // of the containing screen line following the cursor. Otherwise cut the selected // text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutToEndOfLine(options = {}) { if (!this.ensureWritable('cutToEndOfLine', options)) return; let maintainClipboard = false; this.mutateSelectedText(selection => { selection.cutToEndOfLine(maintainClipboard, options); maintainClipboard = true; }); } // Essential: For each selection, if the selection is empty, cut all characters // of the containing buffer line following the cursor. Otherwise cut the // selected text. // // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutToEndOfBufferLine(options = {}) { if (!this.ensureWritable('cutToEndOfBufferLine', options)) return; let maintainClipboard = false; this.mutateSelectedText(selection => { selection.cutToEndOfBufferLine(maintainClipboard, options); maintainClipboard = true; }); } /* Section: Folds */ // Essential: Fold the most recent cursor's row based on its indentation level. // // The fold will extend from the nearest preceding line with a lower // indentation level up to the nearest following row with a lower indentation // level. foldCurrentRow() { const { row } = this.getCursorBufferPosition(); const languageMode = this.buffer.getLanguageMode(); const range = languageMode.getFoldableRangeContainingPoint && languageMode.getFoldableRangeContainingPoint( Point(row, Infinity), this.getTabLength() ); if (range) return this.displayLayer.foldBufferRange(range); } // Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow() { const { row } = this.getCursorBufferPosition(); return this.displayLayer.destroyFoldsContainingBufferPositions( [Point(row, Infinity)], false ); } // Essential: Fold the given row in buffer coordinates based on its indentation // level. // // If the given row is foldable, the fold will begin there. Otherwise, it will // begin at the first foldable row preceding the given row. // // * `bufferRow` A {Number}. foldBufferRow(bufferRow) { let position = Point(bufferRow, Infinity); const languageMode = this.buffer.getLanguageMode(); while (true) { const foldableRange = languageMode.getFoldableRangeContainingPoint && languageMode.getFoldableRangeContainingPoint( position, this.getTabLength() ); if (foldableRange) { const existingFolds = this.displayLayer.foldsIntersectingBufferRange( Range(foldableRange.start, foldableRange.start) ); if (existingFolds.length === 0) { this.displayLayer.foldBufferRange(foldableRange); } else { const firstExistingFoldRange = this.displayLayer.bufferRangeForFold( existingFolds[0] ); if (firstExistingFoldRange.start.isLessThan(position)) { position = Point(firstExistingFoldRange.start.row, 0); continue; } } } break; } } // Essential: Unfold all folds containing the given row in buffer coordinates. // // * `bufferRow` A {Number} unfoldBufferRow(bufferRow) { const position = Point(bufferRow, Infinity); return this.displayLayer.destroyFoldsContainingBufferPositions([position]); } // Extended: For each selection, fold the rows it intersects. foldSelectedLines() { for (let selection of this.selections) { selection.fold(); } } // Extended: Fold all foldable lines. foldAll() { const languageMode = this.buffer.getLanguageMode(); const foldableRanges = languageMode.getFoldableRanges && languageMode.getFoldableRanges(this.getTabLength()); this.displayLayer.destroyAllFolds(); for (let range of foldableRanges || []) { this.displayLayer.foldBufferRange(range); } } // Extended: Unfold all existing folds. unfoldAll() { const result = this.displayLayer.destroyAllFolds(); if (result.length > 0) this.scrollToCursorPosition(); return result; } // Extended: Fold all foldable lines at the given indent level. // // * `level` A {Number} starting at 0. foldAllAtIndentLevel(level) { const languageMode = this.buffer.getLanguageMode(); const foldableRanges = languageMode.getFoldableRangesAtIndentLevel && languageMode.getFoldableRangesAtIndentLevel(level, this.getTabLength()); this.displayLayer.destroyAllFolds(); for (let range of foldableRanges || []) { this.displayLayer.foldBufferRange(range); } } // Extended: Determine whether the given row in buffer coordinates is foldable. // // A *foldable* row is a row that *starts* a row range that can be folded. // // * `bufferRow` A {Number} // // Returns a {Boolean}. isFoldableAtBufferRow(bufferRow) { const languageMode = this.buffer.getLanguageMode(); return ( languageMode.isFoldableAtRow && languageMode.isFoldableAtRow(bufferRow) ); } // Extended: Determine whether the given row in screen coordinates is foldable. // // A *foldable* row is a row that *starts* a row range that can be folded. // // * `bufferRow` A {Number} // // Returns a {Boolean}. isFoldableAtScreenRow(screenRow) { return this.isFoldableAtBufferRow(this.bufferRowForScreenRow(screenRow)); } // Extended: Fold the given buffer row if it isn't currently folded, and unfold // it otherwise. toggleFoldAtBufferRow(bufferRow) { if (this.isFoldedAtBufferRow(bufferRow)) { return this.unfoldBufferRow(bufferRow); } else { return this.foldBufferRow(bufferRow); } } // Extended: Determine whether the most recently added cursor's row is folded. // // Returns a {Boolean}. isFoldedAtCursorRow() { return this.isFoldedAtBufferRow(this.getCursorBufferPosition().row); } // Extended: Determine whether the given row in buffer coordinates is folded. // // * `bufferRow` A {Number} // // Returns a {Boolean}. isFoldedAtBufferRow(bufferRow) { const range = Range( Point(bufferRow, 0), Point(bufferRow, this.buffer.lineLengthForRow(bufferRow)) ); return this.displayLayer.foldsIntersectingBufferRange(range).length > 0; } // Extended: Determine whether the given row in screen coordinates is folded. // // * `screenRow` A {Number} // // Returns a {Boolean}. isFoldedAtScreenRow(screenRow) { return this.isFoldedAtBufferRow(this.bufferRowForScreenRow(screenRow)); } // Creates a new fold between two row numbers. // // startRow - The row {Number} to start folding at // endRow - The row {Number} to end the fold // // Returns the new {Fold}. foldBufferRowRange(startRow, endRow) { return this.foldBufferRange( Range(Point(startRow, Infinity), Point(endRow, Infinity)) ); } foldBufferRange(range) { return this.displayLayer.foldBufferRange(range); } // Remove any {Fold}s found that intersect the given buffer range. destroyFoldsIntersectingBufferRange(bufferRange) { return this.displayLayer.destroyFoldsIntersectingBufferRange(bufferRange); } // Remove any {Fold}s found that contain the given array of buffer positions. destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) { return this.displayLayer.destroyFoldsContainingBufferPositions( bufferPositions, excludeEndpoints ); } /* Section: Gutters */ // Essential: Add a custom {Gutter}. // // * `options` An {Object} with the following fields: // * `name` (required) A unique {String} to identify this gutter. // * `priority` (optional) A {Number} that determines stacking order between // gutters. Lower priority items are forced closer to the edges of the // window. (default: -100) // * `visible` (optional) {Boolean} specifying whether the gutter is visible // initially after being created. (default: true) // * `type` (optional) {String} specifying the type of gutter to create. `'decorated'` // gutters are useful as a destination for decorations created with {Gutter::decorateMarker}. // `'line-number'` gutters. // * `class` (optional) {String} added to the CSS classnames of the gutter's root DOM element. // * `labelFn` (optional) {Function} called by a `'line-number'` gutter to generate the label for each line number // element. Should return a {String} that will be used to label the corresponding line. // * `lineData` an {Object} containing information about each line to label. // * `bufferRow` {Number} indicating the zero-indexed buffer index of this line. // * `screenRow` {Number} indicating the zero-indexed screen index. // * `foldable` {Boolean} that is `true` if a fold may be created here. // * `softWrapped` {Boolean} if this screen row is the soft-wrapped continuation of the same buffer row. // * `maxDigits` {Number} the maximum number of digits necessary to represent any known screen row. // * `onMouseDown` (optional) {Function} to be called when a mousedown event is received by a line-number // element within this `type: 'line-number'` {Gutter}. If unspecified, the default behavior is to select the // clicked buffer row. // * `lineData` an {Object} containing information about the line that's being clicked. // * `bufferRow` {Number} of the originating line element // * `screenRow` {Number} // * `onMouseMove` (optional) {Function} to be called when a mousemove event occurs on a line-number element within // within this `type: 'line-number'` {Gutter}. // * `lineData` an {Object} containing information about the line that's being clicked. // * `bufferRow` {Number} of the originating line element // * `screenRow` {Number} // // Returns the newly-created {Gutter}. addGutter(options) { return this.gutterContainer.addGutter(options); } // Essential: Get this editor's gutters. // // Returns an {Array} of {Gutter}s. getGutters() { return this.gutterContainer.getGutters(); } getLineNumberGutter() { return this.lineNumberGutter; } // Essential: Get the gutter with the given name. // // Returns a {Gutter}, or `null` if no gutter exists for the given name. gutterWithName(name) { return this.gutterContainer.gutterWithName(name); } /* Section: Scrolling the TextEditor */ // Essential: Scroll the editor to reveal the most recently added cursor if it is // off-screen. // // * `options` (optional) {Object} // * `center` Center the editor around the cursor if possible. (default: true) scrollToCursorPosition(options) { this.getLastCursor().autoscroll({ center: options && options.center !== false }); } // Essential: Scrolls the editor to the given buffer position. // // * `bufferPosition` An object that represents a buffer position. It can be either // an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} // * `options` (optional) {Object} // * `center` Center the editor around the position if possible. (default: false) scrollToBufferPosition(bufferPosition, options) { return this.scrollToScreenPosition( this.screenPositionForBufferPosition(bufferPosition), options ); } // Essential: Scrolls the editor to the given screen position. // // * `screenPosition` An object that represents a screen position. It can be either // an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} // * `options` (optional) {Object} // * `center` Center the editor around the position if possible. (default: false) scrollToScreenPosition(screenPosition, options) { this.scrollToScreenRange( new Range(screenPosition, screenPosition), options ); } scrollToTop() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::scrollToTop instead.' ); this.getElement().scrollToTop(); } scrollToBottom() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::scrollToTop instead.' ); this.getElement().scrollToBottom(); } scrollToScreenRange(screenRange, options = {}) { if (options.clip !== false) screenRange = this.clipScreenRange(screenRange); const scrollEvent = { screenRange, options }; if (this.component) this.component.didRequestAutoscroll(scrollEvent); this.emitter.emit('did-request-autoscroll', scrollEvent); } getHorizontalScrollbarHeight() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.' ); return this.getElement().getHorizontalScrollbarHeight(); } getVerticalScrollbarWidth() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.' ); return this.getElement().getVerticalScrollbarWidth(); } pageUp() { this.moveUp(this.getRowsPerPage()); } pageDown() { this.moveDown(this.getRowsPerPage()); } selectPageUp() { this.selectUp(this.getRowsPerPage()); } selectPageDown() { this.selectDown(this.getRowsPerPage()); } // Returns the number of rows per page getRowsPerPage() { if (this.component) { const clientHeight = this.component.getScrollContainerClientHeight(); const lineHeight = this.component.getLineHeight(); return Math.max(1, Math.ceil(clientHeight / lineHeight)); } else { return 1; } } /* Section: Config */ // Experimental: Is auto-indentation enabled for this editor? // // Returns a {Boolean}. shouldAutoIndent() { return this.autoIndent; } // Experimental: Is auto-indentation on paste enabled for this editor? // // Returns a {Boolean}. shouldAutoIndentOnPaste() { return this.autoIndentOnPaste; } // Experimental: Does this editor allow scrolling past the last line? // // Returns a {Boolean}. getScrollPastEnd() { if (this.getAutoHeight()) { return false; } else { return this.scrollPastEnd; } } // Experimental: How fast does the editor scroll in response to mouse wheel // movements? // // Returns a positive {Number}. getScrollSensitivity() { return this.scrollSensitivity; } // Experimental: Does this editor show cursors while there is a selection? // // Returns a positive {Boolean}. getShowCursorOnSelection() { return this.showCursorOnSelection; } // Experimental: Are line numbers enabled for this editor? // // Returns a {Boolean} doesShowLineNumbers() { return this.showLineNumbers; } // Experimental: Get the time interval within which text editing operations // are grouped together in the editor's undo history. // // Returns the time interval {Number} in milliseconds. getUndoGroupingInterval() { return this.undoGroupingInterval; } // Experimental: Get the characters that are *not* considered part of words, // for the purpose of word-based cursor movements. // // Returns a {String} containing the non-word characters. getNonWordCharacters(position) { const languageMode = this.buffer.getLanguageMode(); return ( (languageMode.getNonWordCharacters && languageMode.getNonWordCharacters(position || Point(0, 0))) || DEFAULT_NON_WORD_CHARACTERS ); } /* Section: Event Handlers */ handleLanguageModeChange() { this.unfoldAll(); if (this.languageModeSubscription) { this.languageModeSubscription.dispose(); this.disposables.remove(this.languageModeSubscription); } const languageMode = this.buffer.getLanguageMode(); if ( this.component && this.component.visible && languageMode.startTokenizing ) { languageMode.startTokenizing(); } this.languageModeSubscription = languageMode.onDidTokenize && languageMode.onDidTokenize(() => { this.emitter.emit('did-tokenize'); }); if (this.languageModeSubscription) this.disposables.add(this.languageModeSubscription); this.emitter.emit('did-change-grammar', languageMode.grammar); } /* Section: TextEditor Rendering */ // Get the Element for the editor. getElement() { if (!this.component) { if (!TextEditorComponent) TextEditorComponent = require('./text-editor-component'); if (!TextEditorElement) TextEditorElement = require('./text-editor-element'); this.component = new TextEditorComponent({ model: this, updatedSynchronously: TextEditorElement.prototype.updatedSynchronously, initialScrollTopRow: this.initialScrollTopRow, initialScrollLeftColumn: this.initialScrollLeftColumn }); } return this.component.element; } getAllowedLocations() { return ['center']; } // Essential: Retrieves the greyed out placeholder of a mini editor. // // Returns a {String}. getPlaceholderText() { return this.placeholderText; } // Essential: Set the greyed out placeholder of a mini editor. Placeholder text // will be displayed when the editor has no content. // // * `placeholderText` {String} text that is displayed when the editor has no content. setPlaceholderText(placeholderText) { this.updatePlaceholderText(placeholderText, true); } pixelPositionForBufferPosition(bufferPosition) { Grim.deprecate( 'This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead' ); return this.getElement().pixelPositionForBufferPosition(bufferPosition); } pixelPositionForScreenPosition(screenPosition) { Grim.deprecate( 'This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead' ); return this.getElement().pixelPositionForScreenPosition(screenPosition); } getVerticalScrollMargin() { const maxScrollMargin = Math.floor( (this.height / this.getLineHeightInPixels() - 1) / 2 ); return Math.min(this.verticalScrollMargin, maxScrollMargin); } setVerticalScrollMargin(verticalScrollMargin) { this.verticalScrollMargin = verticalScrollMargin; return this.verticalScrollMargin; } getHorizontalScrollMargin() { return Math.min( this.horizontalScrollMargin, Math.floor((this.width / this.getDefaultCharWidth() - 1) / 2) ); } setHorizontalScrollMargin(horizontalScrollMargin) { this.horizontalScrollMargin = horizontalScrollMargin; return this.horizontalScrollMargin; } getLineHeightInPixels() { return this.lineHeightInPixels; } setLineHeightInPixels(lineHeightInPixels) { this.lineHeightInPixels = lineHeightInPixels; return this.lineHeightInPixels; } getKoreanCharWidth() { return this.koreanCharWidth; } getHalfWidthCharWidth() { return this.halfWidthCharWidth; } getDoubleWidthCharWidth() { return this.doubleWidthCharWidth; } getDefaultCharWidth() { return this.defaultCharWidth; } ratioForCharacter(character) { if (isKoreanCharacter(character)) { return this.getKoreanCharWidth() / this.getDefaultCharWidth(); } else if (isHalfWidthCharacter(character)) { return this.getHalfWidthCharWidth() / this.getDefaultCharWidth(); } else if (isDoubleWidthCharacter(character)) { return this.getDoubleWidthCharWidth() / this.getDefaultCharWidth(); } else { return 1; } } setDefaultCharWidth( defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth ) { if (doubleWidthCharWidth == null) { doubleWidthCharWidth = defaultCharWidth; } if (halfWidthCharWidth == null) { halfWidthCharWidth = defaultCharWidth; } if (koreanCharWidth == null) { koreanCharWidth = defaultCharWidth; } if ( defaultCharWidth !== this.defaultCharWidth || (doubleWidthCharWidth !== this.doubleWidthCharWidth && halfWidthCharWidth !== this.halfWidthCharWidth && koreanCharWidth !== this.koreanCharWidth) ) { this.defaultCharWidth = defaultCharWidth; this.doubleWidthCharWidth = doubleWidthCharWidth; this.halfWidthCharWidth = halfWidthCharWidth; this.koreanCharWidth = koreanCharWidth; if (this.isSoftWrapped()) { this.displayLayer.reset({ softWrapColumn: this.getSoftWrapColumn() }); } } return defaultCharWidth; } setHeight(height) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setHeight instead.' ); this.getElement().setHeight(height); } getHeight() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getHeight instead.' ); return this.getElement().getHeight(); } getAutoHeight() { return this.autoHeight != null ? this.autoHeight : true; } getAutoWidth() { return this.autoWidth != null ? this.autoWidth : false; } setWidth(width) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setWidth instead.' ); this.getElement().setWidth(width); } getWidth() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getWidth instead.' ); return this.getElement().getWidth(); } // Use setScrollTopRow instead of this method setFirstVisibleScreenRow(screenRow) { this.setScrollTopRow(screenRow); } getFirstVisibleScreenRow() { return this.getElement().component.getFirstVisibleRow(); } getLastVisibleScreenRow() { return this.getElement().component.getLastVisibleRow(); } getVisibleRowRange() { return [this.getFirstVisibleScreenRow(), this.getLastVisibleScreenRow()]; } // Use setScrollLeftColumn instead of this method setFirstVisibleScreenColumn(column) { return this.setScrollLeftColumn(column); } getFirstVisibleScreenColumn() { return this.getElement().component.getFirstVisibleColumn(); } getScrollTop() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollTop instead.' ); return this.getElement().getScrollTop(); } setScrollTop(scrollTop) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setScrollTop instead.' ); this.getElement().setScrollTop(scrollTop); } getScrollBottom() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollBottom instead.' ); return this.getElement().getScrollBottom(); } setScrollBottom(scrollBottom) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setScrollBottom instead.' ); this.getElement().setScrollBottom(scrollBottom); } getScrollLeft() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollLeft instead.' ); return this.getElement().getScrollLeft(); } setScrollLeft(scrollLeft) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setScrollLeft instead.' ); this.getElement().setScrollLeft(scrollLeft); } getScrollRight() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollRight instead.' ); return this.getElement().getScrollRight(); } setScrollRight(scrollRight) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::setScrollRight instead.' ); this.getElement().setScrollRight(scrollRight); } getScrollHeight() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollHeight instead.' ); return this.getElement().getScrollHeight(); } getScrollWidth() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getScrollWidth instead.' ); return this.getElement().getScrollWidth(); } getMaxScrollTop() { Grim.deprecate( 'This is now a view method. Call TextEditorElement::getMaxScrollTop instead.' ); return this.getElement().getMaxScrollTop(); } getScrollTopRow() { return this.getElement().component.getScrollTopRow(); } setScrollTopRow(scrollTopRow) { this.getElement().component.setScrollTopRow(scrollTopRow); } getScrollLeftColumn() { return this.getElement().component.getScrollLeftColumn(); } setScrollLeftColumn(scrollLeftColumn) { this.getElement().component.setScrollLeftColumn(scrollLeftColumn); } intersectsVisibleRowRange(startRow, endRow) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.' ); return this.getElement().intersectsVisibleRowRange(startRow, endRow); } selectionIntersectsVisibleRowRange(selection) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.' ); return this.getElement().selectionIntersectsVisibleRowRange(selection); } screenPositionForPixelPosition(pixelPosition) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.' ); return this.getElement().screenPositionForPixelPosition(pixelPosition); } pixelRectForScreenRange(screenRange) { Grim.deprecate( 'This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.' ); return this.getElement().pixelRectForScreenRange(screenRange); } /* Section: Utility */ inspect() { return ``; } emitWillInsertTextEvent(text) { let result = true; const cancel = () => { result = false; }; this.emitter.emit('will-insert-text', { cancel, text }); return result; } /* Section: Language Mode Delegated Methods */ suggestedIndentForBufferRow(bufferRow, options) { const languageMode = this.buffer.getLanguageMode(); return ( languageMode.suggestedIndentForBufferRow && languageMode.suggestedIndentForBufferRow( bufferRow, this.getTabLength(), options ) ); } // Given a buffer row, indent it. // // * bufferRow - The row {Number}. // * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. autoIndentBufferRow(bufferRow, options) { const indentLevel = this.suggestedIndentForBufferRow(bufferRow, options); return this.setIndentationForBufferRow(bufferRow, indentLevel, options); } // Indents all the rows between two buffer row numbers. // // * startRow - The row {Number} to start at // * endRow - The row {Number} to end at autoIndentBufferRows(startRow, endRow) { let row = startRow; while (row <= endRow) { this.autoIndentBufferRow(row); row++; } } autoDecreaseIndentForBufferRow(bufferRow) { const languageMode = this.buffer.getLanguageMode(); const indentLevel = languageMode.suggestedIndentForEditedBufferRow && languageMode.suggestedIndentForEditedBufferRow( bufferRow, this.getTabLength() ); if (indentLevel != null) this.setIndentationForBufferRow(bufferRow, indentLevel); } toggleLineCommentForBufferRow(row) { this.toggleLineCommentsForBufferRows(row, row); } toggleLineCommentsForBufferRows(start, end, options = {}) { const languageMode = this.buffer.getLanguageMode(); let { commentStartString, commentEndString } = (languageMode.commentStringsForPosition && languageMode.commentStringsForPosition(new 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 ); // Prevent the cursor from selecting / passing the delimiters // See https://github.com/atom/atom/pull/17519 if (options.correctSelection && options.selection) { const endLineLength = this.buffer.lineLengthForRow(end); const oldRange = options.selection.getBufferRange(); if (oldRange.isEmpty()) { if (oldRange.start.column === endLineLength) { const endCol = endLineLength - commentEndString.length - 1; options.selection.setBufferRange( [[end, endCol], [end, endCol]], { autoscroll: false } ); } } else { const startDelta = oldRange.start.column === indentLength ? [0, commentStartString.length + 1] : [0, 0]; const endDelta = oldRange.end.column === endLineLength ? [0, -commentEndString.length - 1] : [0, 0]; options.selection.setBufferRange( oldRange.translate(startDelta, endDelta), { autoscroll: false } ); } } }); } } else { let hasCommentedLines = false; let hasUncommentedLines = false; for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row); if (NON_WHITESPACE_REGEXP.test(line)) { if (columnRangeForStartDelimiter(line, commentStartString)) { hasCommentedLines = true; } else { hasUncommentedLines = true; } } } const shouldUncomment = hasCommentedLines && !hasUncommentedLines; if (shouldUncomment) { for (let row = start; row <= end; row++) { const columnRange = columnRangeForStartDelimiter( this.buffer.lineForRow(row), commentStartString ); if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]); } } else { let minIndentLevel = Infinity; let minBlankIndentLevel = Infinity; for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row); const indentLevel = this.indentLevelForLine(line); if (NON_WHITESPACE_REGEXP.test(line)) { if (indentLevel < minIndentLevel) minIndentLevel = indentLevel; } else { if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel; } } minIndentLevel = Number.isFinite(minIndentLevel) ? minIndentLevel : Number.isFinite(minBlankIndentLevel) ? minBlankIndentLevel : 0; const indentString = this.buildIndentString(minIndentLevel); for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row); if (NON_WHITESPACE_REGEXP.test(line)) { const indentColumn = columnForIndentLevel( line, minIndentLevel, this.getTabLength() ); this.buffer.insert( Point(row, indentColumn), commentStartString + ' ' ); } else { this.buffer.setTextInRange( new Range(new Point(row, 0), new Point(row, Infinity)), indentString + commentStartString + ' ' ); } } } } } rowRangeForParagraphAtBufferRow(bufferRow) { if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow))) return; const languageMode = this.buffer.getLanguageMode(); const isCommented = languageMode.isRowCommented(bufferRow); let startRow = bufferRow; while (startRow > 0) { if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(startRow - 1))) break; if (languageMode.isRowCommented(startRow - 1) !== isCommented) break; startRow--; } let endRow = bufferRow; const rowCount = this.getLineCount(); while (endRow + 1 < rowCount) { if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break; if (languageMode.isRowCommented(endRow + 1) !== isCommented) break; endRow++; } return new Range( new Point(startRow, 0), new Point(endRow, this.buffer.lineLengthForRow(endRow)) ); } }; function columnForIndentLevel(line, indentLevel, tabLength) { let column = 0; let indentLength = 0; const goalIndentLength = indentLevel * tabLength; while (indentLength < goalIndentLength) { const char = line[column]; if (char === '\t') { indentLength += tabLength - (indentLength % tabLength); } else if (char === ' ') { indentLength++; } else { break; } column++; } return column; } function columnRangeForStartDelimiter(line, delimiter) { const startColumn = line.search(NON_WHITESPACE_REGEXP); if (startColumn === -1) return null; if (!line.startsWith(delimiter, startColumn)) return null; let endColumn = startColumn + delimiter.length; if (line[endColumn] === ' ') endColumn++; return [startColumn, endColumn]; } function columnRangeForEndDelimiter(line, delimiter) { let startColumn = line.lastIndexOf(delimiter); if (startColumn === -1) return null; const endColumn = startColumn + delimiter.length; if (NON_WHITESPACE_REGEXP.test(line.slice(endColumn))) return null; if (line[startColumn - 1] === ' ') startColumn--; return [startColumn, endColumn]; } class ChangeEvent { constructor({ oldRange, newRange }) { this.oldRange = oldRange; this.newRange = newRange; } get start() { return this.newRange.start; } get oldExtent() { return this.oldRange.getExtent(); } get newExtent() { return this.newRange.getExtent(); } } ================================================ FILE: src/text-mate-language-mode.js ================================================ 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 NullGrammar = require('./null-grammar'); const { OnigRegExp } = require('oniguruma'); const { toFirstMateScopeId, fromFirstMateScopeId } = require('./first-mate-helpers'); const { selectorMatchesAnyScope } = require('./selectors'); const NON_WHITESPACE_REGEX = /\S/; let nextId = 0; const prefixedScopes = new Map(); class TextMateLanguageMode { constructor(params) { this.emitter = new Emitter(); this.disposables = new CompositeDisposable(); this.tokenIterator = new TokenIterator(this); this.regexesByPattern = {}; this.alive = true; this.tokenizationStarted = false; this.id = params.id != null ? params.id : nextId++; this.buffer = params.buffer; this.largeFileMode = params.largeFileMode; this.config = params.config; this.largeFileMode = params.largeFileMode != null ? params.largeFileMode : this.buffer.buffer.getLength() >= 2 * 1024 * 1024; this.grammar = params.grammar || NullGrammar; this.rootScopeDescriptor = new ScopeDescriptor({ scopes: [this.grammar.scopeName] }); this.disposables.add( this.grammar.onDidUpdate(() => this.retokenizeLines()) ); this.retokenizeLines(); } destroy() { if (!this.alive) return; this.alive = false; this.disposables.dispose(); this.tokenizedLines.length = 0; } isAlive() { return this.alive; } isDestroyed() { return !this.alive; } getGrammar() { return this.grammar; } getLanguageId() { return this.grammar.scopeName; } getNonWordCharacters(position) { const scope = this.scopeDescriptorForPosition(position); return this.config.get('editor.nonWordCharacters', { scope }); } /* 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, tabLength, options) { const line = this.buffer.lineForRow(bufferRow); const tokenizedLine = this.tokenizedLineForRow(bufferRow); const iterator = tokenizedLine.getTokenIterator(); iterator.next(); const scopeDescriptor = new ScopeDescriptor({ scopes: iterator.getScopes() }); return this._suggestedIndentForLineWithScopeAtBufferRow( bufferRow, line, scopeDescriptor, tabLength, 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, tabLength) { const tokenizedLine = this.buildTokenizedLineForRowWithText( bufferRow, line ); const iterator = tokenizedLine.getTokenIterator(); iterator.next(); const scopeDescriptor = new ScopeDescriptor({ scopes: iterator.getScopes() }); return this._suggestedIndentForLineWithScopeAtBufferRow( bufferRow, line, scopeDescriptor, tabLength ); } // 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, tabLength) { const line = this.buffer.lineForRow(bufferRow); const currentIndentLevel = this.indentLevelForLine(line, tabLength); if (currentIndentLevel === 0) return; const scopeDescriptor = this.scopeDescriptorForPosition( new Point(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, tabLength); 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; } _suggestedIndentForLineWithScopeAtBufferRow( bufferRow, line, scopeDescriptor, tabLength, options ) { 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, tabLength); 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) { const scope = this.scopeDescriptorForPosition(position); const commentStartEntries = this.config.getAll('editor.commentStart', { scope }); const commentEndEntries = this.config.getAll('editor.commentEnd', { scope }); const commentStartEntry = commentStartEntries[0]; const commentEndEntry = commentEndEntries.find(entry => { return entry.scopeSelector === commentStartEntry.scopeSelector; }); return { commentStartString: commentStartEntry && commentStartEntry.value, commentEndString: commentEndEntry && commentEndEntry.value }; } /* Section - Syntax Highlighting */ buildHighlightIterator() { return new TextMateHighlightIterator(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 []; } onDidChangeHighlighting(fn) { return this.emitter.on('did-change-highlighting', fn); } onDidTokenize(callback) { return this.emitter.on('did-tokenize', callback); } getGrammarSelectionContent() { return this.buffer.getTextInRange([[0, 0], [10, 0]]); } updateForInjection(grammar) { if (!grammar.injectionSelector) return; for (const tokenizedLine of this.tokenizedLines) { if (tokenizedLine) { for (let token of tokenizedLine.tokens) { if (grammar.injectionSelector.matches(token.scopes)) { this.retokenizeLines(); return; } } } } } 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); } } startTokenizing() { this.tokenizationStarted = true; if (this.grammar.name !== 'Null Grammar' && !this.largeFileMode) { this.tokenizeInBackground(); } } tokenizeInBackground() { if (!this.tokenizationStarted || 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) { let 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-change-highlighting', 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); } } } bufferDidFinishTransaction() {} 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) { break; } } } } } return scopes; } indentLevelForLine(line, 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(scope) { return this.regexForPattern( this.config.get('editor.increaseIndentPattern', { scope }) ); } decreaseIndentRegexForScopeDescriptor(scope) { return this.regexForPattern( this.config.get('editor.decreaseIndentPattern', { scope }) ); } decreaseNextIndentRegexForScopeDescriptor(scope) { return this.regexForPattern( this.config.get('editor.decreaseNextIndentPattern', { scope }) ); } foldEndRegexForScopeDescriptor(scope) { return this.regexForPattern( this.config.get('editor.foldEndPattern', { scope }) ); } 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); } } } TextMateLanguageMode.prototype.chunkSize = 50; class TextMateHighlightIterator { constructor(languageMode) { this.languageMode = languageMode; this.openScopeIds = null; this.closeScopeIds = null; } seek(position) { this.openScopeIds = []; this.closeScopeIds = []; this.tagIndex = null; const currentLine = this.languageMode.tokenizedLineForRow(position.row); this.currentLineTags = currentLine.tags; this.currentLineLength = currentLine.text.length; const containingScopeIds = currentLine.openScopes.map(id => fromFirstMateScopeId(id) ); let currentColumn = 0; for (let index = 0; index < this.currentLineTags.length; index++) { const tag = this.currentLineTags[index]; if (tag >= 0) { if (currentColumn >= position.column) { this.tagIndex = index; break; } else { currentColumn += tag; while (this.closeScopeIds.length > 0) { this.closeScopeIds.shift(); containingScopeIds.pop(); } while (this.openScopeIds.length > 0) { const openTag = this.openScopeIds.shift(); containingScopeIds.push(openTag); } } } else { const scopeId = fromFirstMateScopeId(tag); if ((tag & 1) === 0) { if (this.openScopeIds.length > 0) { if (currentColumn >= position.column) { this.tagIndex = index; break; } else { while (this.closeScopeIds.length > 0) { this.closeScopeIds.shift(); containingScopeIds.pop(); } while (this.openScopeIds.length > 0) { const openTag = this.openScopeIds.shift(); containingScopeIds.push(openTag); } } } this.closeScopeIds.push(scopeId); } else { this.openScopeIds.push(scopeId); } } } if (this.tagIndex == null) { this.tagIndex = this.currentLineTags.length; } this.position = Point( position.row, Math.min(this.currentLineLength, currentColumn) ); return containingScopeIds; } moveToSuccessor() { this.openScopeIds = []; this.closeScopeIds = []; while (true) { if (this.tagIndex === this.currentLineTags.length) { if (this.isAtTagBoundary()) { break; } else if (!this.moveToNextLine()) { return false; } } else { const tag = this.currentLineTags[this.tagIndex]; if (tag >= 0) { if (this.isAtTagBoundary()) { break; } else { this.position = Point( this.position.row, Math.min( this.currentLineLength, this.position.column + this.currentLineTags[this.tagIndex] ) ); } } else { const scopeId = fromFirstMateScopeId(tag); if ((tag & 1) === 0) { if (this.openScopeIds.length > 0) { break; } else { this.closeScopeIds.push(scopeId); } } else { this.openScopeIds.push(scopeId); } } this.tagIndex++; } } return true; } getPosition() { return this.position; } getCloseScopeIds() { return this.closeScopeIds.slice(); } getOpenScopeIds() { return this.openScopeIds.slice(); } moveToNextLine() { this.position = Point(this.position.row + 1, 0); const tokenizedLine = this.languageMode.tokenizedLineForRow( this.position.row ); if (tokenizedLine == null) { return false; } else { this.currentLineTags = tokenizedLine.tags; this.currentLineLength = tokenizedLine.text.length; this.tagIndex = 0; return true; } } isAtTagBoundary() { return this.closeScopeIds.length > 0 || this.openScopeIds.length > 0; } } TextMateLanguageMode.TextMateHighlightIterator = TextMateHighlightIterator; module.exports = TextMateLanguageMode; ================================================ FILE: src/text-utils.js ================================================ const isHighSurrogate = charCode => charCode >= 0xd800 && charCode <= 0xdbff; const isLowSurrogate = charCode => charCode >= 0xdc00 && charCode <= 0xdfff; const isVariationSelector = charCode => charCode >= 0xfe00 && charCode <= 0xfe0f; const isCombiningCharacter = charCode => (charCode >= 0x0300 && charCode <= 0x036f) || (charCode >= 0x1ab0 && charCode <= 0x1aff) || (charCode >= 0x1dc0 && charCode <= 0x1dff) || (charCode >= 0x20d0 && charCode <= 0x20ff) || (charCode >= 0xfe20 && charCode <= 0xfe2f); // Are the given character codes a high/low surrogate pair? // // * `charCodeA` The first character code {Number}. // * `charCode2` The second character code {Number}. // // Return a {Boolean}. const isSurrogatePair = (charCodeA, charCodeB) => isHighSurrogate(charCodeA) && isLowSurrogate(charCodeB); // Are the given character codes a variation sequence? // // * `charCodeA` The first character code {Number}. // * `charCode2` The second character code {Number}. // // Return a {Boolean}. const isVariationSequence = (charCodeA, charCodeB) => !isVariationSelector(charCodeA) && isVariationSelector(charCodeB); // Are the given character codes a combined character pair? // // * `charCodeA` The first character code {Number}. // * `charCode2` The second character code {Number}. // // Return a {Boolean}. const isCombinedCharacter = (charCodeA, charCodeB) => !isCombiningCharacter(charCodeA) && isCombiningCharacter(charCodeB); // Is the character at the given index the start of high/low surrogate pair // a variation sequence, or a combined character? // // * `string` The {String} to check for a surrogate pair, variation sequence, // or combined character. // * `index` The {Number} index to look for a surrogate pair, variation // sequence, or combined character. // // Return a {Boolean}. const isPairedCharacter = (string, index = 0) => { const charCodeA = string.charCodeAt(index); const charCodeB = string.charCodeAt(index + 1); return ( isSurrogatePair(charCodeA, charCodeB) || isVariationSequence(charCodeA, charCodeB) || isCombinedCharacter(charCodeA, charCodeB) ); }; const IsJapaneseKanaCharacter = charCode => charCode >= 0x3000 && charCode <= 0x30ff; const isCJKUnifiedIdeograph = charCode => charCode >= 0x4e00 && charCode <= 0x9fff; const isFullWidthForm = charCode => (charCode >= 0xff01 && charCode <= 0xff5e) || (charCode >= 0xffe0 && charCode <= 0xffe6); const isDoubleWidthCharacter = character => { const charCode = character.charCodeAt(0); return ( IsJapaneseKanaCharacter(charCode) || isCJKUnifiedIdeograph(charCode) || isFullWidthForm(charCode) ); }; const isHalfWidthCharacter = character => { const charCode = character.charCodeAt(0); return ( (charCode >= 0xff65 && charCode <= 0xffdc) || (charCode >= 0xffe8 && charCode <= 0xffee) ); }; const isKoreanCharacter = character => { const charCode = character.charCodeAt(0); return ( (charCode >= 0xac00 && charCode <= 0xd7a3) || (charCode >= 0x1100 && charCode <= 0x11ff) || (charCode >= 0x3130 && charCode <= 0x318f) || (charCode >= 0xa960 && charCode <= 0xa97f) || (charCode >= 0xd7b0 && charCode <= 0xd7ff) ); }; const isCJKCharacter = character => isDoubleWidthCharacter(character) || isHalfWidthCharacter(character) || isKoreanCharacter(character); const isWordStart = (previousCharacter, character) => (previousCharacter === ' ' || previousCharacter === '\t' || previousCharacter === '-' || previousCharacter === '/') && (character !== ' ' && character !== '\t'); const isWrapBoundary = (previousCharacter, character) => isWordStart(previousCharacter, character) || isCJKCharacter(character); // Does the given string contain at least surrogate pair, variation sequence, // or combined character? // // * `string` The {String} to check for the presence of paired characters. // // Returns a {Boolean}. const hasPairedCharacter = string => { let index = 0; while (index < string.length) { if (isPairedCharacter(string, index)) { return true; } index++; } return false; }; module.exports = { isPairedCharacter, hasPairedCharacter, isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary }; ================================================ FILE: src/theme-manager.js ================================================ /* global snapshotAuxiliaryData */ const path = require('path'); const _ = require('underscore-plus'); const { Emitter, CompositeDisposable } = require('event-kit'); const { File } = require('pathwatcher'); const fs = require('fs-plus'); const LessCompileCache = require('./less-compile-cache'); // Extended: Handles loading and activating available themes. // // An instance of this class is always available as the `atom.themes` global. module.exports = class ThemeManager { constructor({ packageManager, config, styleManager, notificationManager, viewRegistry }) { this.packageManager = packageManager; this.config = config; this.styleManager = styleManager; this.notificationManager = notificationManager; this.viewRegistry = viewRegistry; this.emitter = new Emitter(); this.styleSheetDisposablesBySourcePath = {}; this.lessCache = null; this.initialLoadComplete = false; this.packageManager.registerPackageActivator(this, ['theme']); this.packageManager.onDidActivateInitialPackages(() => { this.onDidChangeActiveThemes(() => this.packageManager.reloadActivePackageStyleSheets() ); }); } initialize({ resourcePath, configDirPath, safeMode, devMode }) { this.resourcePath = resourcePath; this.configDirPath = configDirPath; this.safeMode = safeMode; this.lessSourcesByRelativeFilePath = null; if (devMode || typeof snapshotAuxiliaryData === 'undefined') { this.lessSourcesByRelativeFilePath = {}; this.importedFilePathsByRelativeImportPath = {}; } else { this.lessSourcesByRelativeFilePath = snapshotAuxiliaryData.lessSourcesByRelativeFilePath; this.importedFilePathsByRelativeImportPath = snapshotAuxiliaryData.importedFilePathsByRelativeImportPath; } } /* Section: Event Subscription */ // Essential: Invoke `callback` when style sheet changes associated with // updating the list of active themes have completed. // // * `callback` {Function} // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActiveThemes(callback) { return this.emitter.on('did-change-active-themes', callback); } /* Section: Accessing Available Themes */ getAvailableNames() { // TODO: Maybe should change to list all the available themes out there? return this.getLoadedNames(); } /* Section: Accessing Loaded Themes */ // Public: Returns an {Array} of {String}s of all the loaded theme names. getLoadedThemeNames() { return this.getLoadedThemes().map(theme => theme.name); } // Public: Returns an {Array} of all the loaded themes. getLoadedThemes() { return this.packageManager .getLoadedPackages() .filter(pack => pack.isTheme()); } /* Section: Accessing Active Themes */ // Public: Returns an {Array} of {String}s of all the active theme names. getActiveThemeNames() { return this.getActiveThemes().map(theme => theme.name); } // Public: Returns an {Array} of all the active themes. getActiveThemes() { return this.packageManager .getActivePackages() .filter(pack => pack.isTheme()); } activatePackages() { return this.activateThemes(); } /* Section: Managing Enabled Themes */ warnForNonExistentThemes() { let themeNames = this.config.get('core.themes') || []; if (!Array.isArray(themeNames)) { themeNames = [themeNames]; } for (let themeName of themeNames) { if ( !themeName || typeof themeName !== 'string' || !this.packageManager.resolvePackagePath(themeName) ) { console.warn(`Enabled theme '${themeName}' is not installed.`); } } } // Public: Get the enabled theme names from the config. // // Returns an array of theme names in the order that they should be activated. getEnabledThemeNames() { let themeNames = this.config.get('core.themes') || []; if (!Array.isArray(themeNames)) { themeNames = [themeNames]; } themeNames = themeNames.filter( themeName => typeof themeName === 'string' && this.packageManager.resolvePackagePath(themeName) ); // Use a built-in syntax and UI theme any time the configured themes are not // available. if (themeNames.length < 2) { const builtInThemeNames = [ 'atom-dark-syntax', 'atom-dark-ui', 'atom-light-syntax', 'atom-light-ui', 'base16-tomorrow-dark-theme', 'base16-tomorrow-light-theme', 'solarized-dark-syntax', 'solarized-light-syntax' ]; themeNames = _.intersection(themeNames, builtInThemeNames); if (themeNames.length === 0) { themeNames = ['one-dark-syntax', 'one-dark-ui']; } else if (themeNames.length === 1) { if (themeNames[0].endsWith('-ui')) { themeNames.unshift('one-dark-syntax'); } else { themeNames.push('one-dark-ui'); } } } // Reverse so the first (top) theme is loaded after the others. We want // the first/top theme to override later themes in the stack. return themeNames.reverse(); } /* Section: Private */ // Resolve and apply the stylesheet specified by the path. // // This supports both CSS and Less stylesheets. // // * `stylesheetPath` A {String} path to the stylesheet that can be an absolute // path or a relative path that will be resolved against the load path. // // Returns a {Disposable} on which `.dispose()` can be called to remove the // required stylesheet. requireStylesheet( stylesheetPath, priority, skipDeprecatedSelectorsTransformation ) { let fullPath = this.resolveStylesheet(stylesheetPath); if (fullPath) { const content = this.loadStylesheet(fullPath); return this.applyStylesheet( fullPath, content, priority, skipDeprecatedSelectorsTransformation ); } else { throw new Error(`Could not find a file at path '${stylesheetPath}'`); } } unwatchUserStylesheet() { if (this.userStylesheetSubscriptions != null) this.userStylesheetSubscriptions.dispose(); this.userStylesheetSubscriptions = null; this.userStylesheetFile = null; if (this.userStyleSheetDisposable != null) this.userStyleSheetDisposable.dispose(); this.userStyleSheetDisposable = null; } loadUserStylesheet() { this.unwatchUserStylesheet(); const userStylesheetPath = this.styleManager.getUserStyleSheetPath(); if (!fs.isFileSync(userStylesheetPath)) { return; } try { this.userStylesheetFile = new File(userStylesheetPath); this.userStylesheetSubscriptions = new CompositeDisposable(); const reloadStylesheet = () => this.loadUserStylesheet(); this.userStylesheetSubscriptions.add( this.userStylesheetFile.onDidChange(reloadStylesheet) ); this.userStylesheetSubscriptions.add( this.userStylesheetFile.onDidRename(reloadStylesheet) ); this.userStylesheetSubscriptions.add( this.userStylesheetFile.onDidDelete(reloadStylesheet) ); } catch (error) { const message = `\ Unable to watch path: \`${path.basename(userStylesheetPath)}\`. Make sure you have permissions to \`${userStylesheetPath}\`. On linux there are currently problems with watch sizes. See [this document][watches] for more info. [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\ `; this.notificationManager.addError(message, { dismissable: true }); } let userStylesheetContents; try { userStylesheetContents = this.loadStylesheet(userStylesheetPath, true); } catch (error) { return; } this.userStyleSheetDisposable = this.styleManager.addStyleSheet( userStylesheetContents, { sourcePath: userStylesheetPath, priority: 2 } ); } loadBaseStylesheets() { this.reloadBaseStylesheets(); } reloadBaseStylesheets() { this.requireStylesheet('../static/atom', -2, true); } stylesheetElementForId(id) { const escapedId = id.replace(/\\/g, '\\\\'); return document.head.querySelector( `atom-styles style[source-path="${escapedId}"]` ); } resolveStylesheet(stylesheetPath) { if (path.extname(stylesheetPath).length > 0) { return fs.resolveOnLoadPath(stylesheetPath); } else { return fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']); } } loadStylesheet(stylesheetPath, importFallbackVariables) { if (path.extname(stylesheetPath) === '.less') { return this.loadLessStylesheet(stylesheetPath, importFallbackVariables); } else { return fs.readFileSync(stylesheetPath, 'utf8'); } } loadLessStylesheet(lessStylesheetPath, importFallbackVariables = false) { if (this.lessCache == null) { this.lessCache = new LessCompileCache({ resourcePath: this.resourcePath, lessSourcesByRelativeFilePath: this.lessSourcesByRelativeFilePath, importedFilePathsByRelativeImportPath: this .importedFilePathsByRelativeImportPath, importPaths: this.getImportPaths() }); } try { if (importFallbackVariables) { const baseVarImports = `\ @import "variables/ui-variables"; @import "variables/syntax-variables";\ `; const relativeFilePath = path.relative( this.resourcePath, lessStylesheetPath ); const lessSource = this.lessSourcesByRelativeFilePath[relativeFilePath]; let content, digest; if (lessSource != null) { ({ content } = lessSource); ({ digest } = lessSource); } else { content = baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8'); digest = null; } return this.lessCache.cssForFile(lessStylesheetPath, content, digest); } else { return this.lessCache.read(lessStylesheetPath); } } catch (error) { let detail, message; error.less = true; if (error.line != null) { // Adjust line numbers for import fallbacks if (importFallbackVariables) { error.line -= 2; } message = `Error compiling Less stylesheet: \`${lessStylesheetPath}\``; detail = `Line number: ${error.line}\n${error.message}`; } else { message = `Error loading Less stylesheet: \`${lessStylesheetPath}\``; detail = error.message; } this.notificationManager.addError(message, { detail, dismissable: true }); throw error; } } removeStylesheet(stylesheetPath) { if (this.styleSheetDisposablesBySourcePath[stylesheetPath] != null) { this.styleSheetDisposablesBySourcePath[stylesheetPath].dispose(); } } applyStylesheet(path, text, priority, skipDeprecatedSelectorsTransformation) { this.styleSheetDisposablesBySourcePath[ path ] = this.styleManager.addStyleSheet(text, { priority, skipDeprecatedSelectorsTransformation, sourcePath: path }); return this.styleSheetDisposablesBySourcePath[path]; } activateThemes() { return new Promise(resolve => { // @config.observe runs the callback once, then on subsequent changes. this.config.observe('core.themes', () => { this.deactivateThemes().then(() => { this.warnForNonExistentThemes(); this.refreshLessCache(); // Update cache for packages in core.themes config const promises = []; for (const themeName of this.getEnabledThemeNames()) { if (this.packageManager.resolvePackagePath(themeName)) { promises.push(this.packageManager.activatePackage(themeName)); } else { console.warn( `Failed to activate theme '${themeName}' because it isn't installed.` ); } } return Promise.all(promises).then(() => { this.addActiveThemeClasses(); this.refreshLessCache(); // Update cache again now that @getActiveThemes() is populated this.loadUserStylesheet(); this.reloadBaseStylesheets(); this.initialLoadComplete = true; this.emitter.emit('did-change-active-themes'); resolve(); }); }); }); }); } deactivateThemes() { this.removeActiveThemeClasses(); this.unwatchUserStylesheet(); const results = this.getActiveThemes().map(pack => this.packageManager.deactivatePackage(pack.name) ); return Promise.all( results.filter(r => r != null && typeof r.then === 'function') ); } isInitialLoadComplete() { return this.initialLoadComplete; } addActiveThemeClasses() { const workspaceElement = this.viewRegistry.getView(this.workspace); if (workspaceElement) { for (const pack of this.getActiveThemes()) { workspaceElement.classList.add(`theme-${pack.name}`); } } } removeActiveThemeClasses() { const workspaceElement = this.viewRegistry.getView(this.workspace); for (const pack of this.getActiveThemes()) { workspaceElement.classList.remove(`theme-${pack.name}`); } } refreshLessCache() { if (this.lessCache) this.lessCache.setImportPaths(this.getImportPaths()); } getImportPaths() { let themePaths; const activeThemes = this.getActiveThemes(); if (activeThemes.length > 0) { themePaths = activeThemes .filter(theme => theme) .map(theme => theme.getStylesheetsPath()); } else { themePaths = []; for (const themeName of this.getEnabledThemeNames()) { const themePath = this.packageManager.resolvePackagePath(themeName); if (themePath) { const deprecatedPath = path.join(themePath, 'stylesheets'); if (fs.isDirectorySync(deprecatedPath)) { themePaths.push(deprecatedPath); } else { themePaths.push(path.join(themePath, 'styles')); } } } } return themePaths.filter(themePath => fs.isDirectorySync(themePath)); } }; ================================================ FILE: src/theme-package.js ================================================ const path = require('path'); const Package = require('./package'); module.exports = class ThemePackage extends Package { getType() { return 'theme'; } getStyleSheetPriority() { return 1; } enable() { this.config.unshiftAtKeyPath('core.themes', this.name); } disable() { this.config.removeAtKeyPath('core.themes', this.name); } preload() { this.loadTime = 0; this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata(); } finishLoading() { this.path = path.join(this.packageManager.resourcePath, this.path); } load() { this.loadTime = 0; this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata(); return this; } activate() { if (this.activationPromise == null) { this.activationPromise = new Promise((resolve, reject) => { this.resolveActivationPromise = resolve; this.rejectActivationPromise = reject; this.measure('activateTime', () => { try { this.loadStylesheets(); this.activateNow(); } catch (error) { this.handleError( `Failed to activate the ${this.name} theme`, error ); } }); }); } return this.activationPromise; } }; ================================================ FILE: src/title-bar.js ================================================ module.exports = class TitleBar { constructor({ workspace, themes, applicationDelegate }) { this.dblclickHandler = this.dblclickHandler.bind(this); this.workspace = workspace; this.themes = themes; this.applicationDelegate = applicationDelegate; this.element = document.createElement('div'); this.element.classList.add('title-bar'); this.titleElement = document.createElement('div'); this.titleElement.classList.add('title'); this.element.appendChild(this.titleElement); this.element.addEventListener('dblclick', this.dblclickHandler); this.workspace.onDidChangeWindowTitle(() => this.updateTitle()); this.themes.onDidChangeActiveThemes(() => this.updateWindowSheetOffset()); this.updateTitle(); this.updateWindowSheetOffset(); } dblclickHandler() { // User preference deciding which action to take on a title bar double-click switch ( this.applicationDelegate.getUserDefault( 'AppleActionOnDoubleClick', 'string' ) ) { case 'Minimize': this.applicationDelegate.minimizeWindow(); break; case 'Maximize': if (this.applicationDelegate.isWindowMaximized()) { this.applicationDelegate.unmaximizeWindow(); } else { this.applicationDelegate.maximizeWindow(); } break; } } updateTitle() { this.titleElement.textContent = document.title; } updateWindowSheetOffset() { this.applicationDelegate .getCurrentWindow() .setSheetOffset(this.element.offsetHeight); } }; ================================================ FILE: src/token-iterator.js ================================================ module.exports = class TokenIterator { constructor(languageMode) { this.languageMode = languageMode; } reset(line) { this.line = line; this.index = null; this.startColumn = 0; this.endColumn = 0; this.scopes = this.line.openScopes.map(id => this.languageMode.grammar.scopeForId(id) ); this.scopeStarts = this.scopes.slice(); this.scopeEnds = []; return this; } next() { const { tags } = this.line; if (this.index != null) { this.startColumn = this.endColumn; this.scopeEnds.length = 0; this.scopeStarts.length = 0; this.index++; } else { this.index = 0; } while (this.index < tags.length) { const tag = tags[this.index]; if (tag < 0) { const scope = this.languageMode.grammar.scopeForId(tag); if (tag % 2 === 0) { if (this.scopeStarts[this.scopeStarts.length - 1] === scope) { this.scopeStarts.pop(); } else { this.scopeEnds.push(scope); } this.scopes.pop(); } else { this.scopeStarts.push(scope); this.scopes.push(scope); } this.index++; } else { this.endColumn += tag; this.text = this.line.text.substring(this.startColumn, this.endColumn); return true; } } return false; } getScopes() { return this.scopes; } getScopeStarts() { return this.scopeStarts; } getScopeEnds() { return this.scopeEnds; } getText() { return this.text; } getBufferStart() { return this.startColumn; } getBufferEnd() { return this.endColumn; } }; ================================================ FILE: src/token.coffee ================================================ _ = require 'underscore-plus' StartDotRegex = /^\.?/ # Represents a single unit of text as selected by a grammar. module.exports = class Token value: null scopes: null constructor: (properties) -> {@value, @scopes} = properties isEqual: (other) -> # TODO: scopes is deprecated. This is here for the sake of lang package tests @value is other.value and _.isEqual(@scopes, other.scopes) isBracket: -> /^meta\.brace\b/.test(_.last(@scopes)) matchesScopeSelector: (selector) -> targetClasses = selector.replace(StartDotRegex, '').split('.') _.any @scopes, (scope) -> scopeClasses = scope.split('.') _.isSubset(targetClasses, scopeClasses) ================================================ FILE: src/tokenized-line.coffee ================================================ Token = require './token' CommentScopeRegex = /(\b|\.)comment/ idCounter = 1 module.exports = class TokenizedLine constructor: (properties) -> @id = idCounter++ return unless properties? {@openScopes, @text, @tags, @ruleStack, @tokenIterator, @grammar, tokens} = properties @cachedTokens = tokens getTokenIterator: -> @tokenIterator.reset(this) Object.defineProperty @prototype, 'tokens', get: -> if @cachedTokens @cachedTokens else iterator = @getTokenIterator() tokens = [] while iterator.next() tokens.push(new Token({ value: iterator.getText() scopes: iterator.getScopes().slice() })) tokens tokenAtBufferColumn: (bufferColumn) -> @tokens[@tokenIndexAtBufferColumn(bufferColumn)] tokenIndexAtBufferColumn: (bufferColumn) -> column = 0 for token, index in @tokens column += token.value.length return index if column > bufferColumn index - 1 tokenStartColumnForBufferColumn: (bufferColumn) -> delta = 0 for token in @tokens nextDelta = delta + token.bufferDelta break if nextDelta > bufferColumn delta = nextDelta delta isComment: -> return @isCommentLine if @isCommentLine? @isCommentLine = false for tag in @openScopes if @isCommentOpenTag(tag) @isCommentLine = true return @isCommentLine startIndex = 0 for tag in @tags # If we haven't encountered any comment scope when reading the first # non-whitespace chunk of text, then we consider this as not being a # comment line. if tag > 0 break unless isWhitespaceOnly(@text.substr(startIndex, tag)) startIndex += tag if @isCommentOpenTag(tag) @isCommentLine = true return @isCommentLine @isCommentLine isCommentOpenTag: (tag) -> if tag < 0 and (tag & 1) is 1 scope = @grammar.scopeForId(tag) if CommentScopeRegex.test(scope) return true false tokenAtIndex: (index) -> @tokens[index] getTokenCount: -> count = 0 count++ for tag in @tags when tag >= 0 count isWhitespaceOnly = (text) -> for char in text if char isnt '\t' and char isnt ' ' return false return true ================================================ FILE: src/tooltip-manager.js ================================================ 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 // // ```js // // 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 // // ```js // 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. // // ```js // 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 (let i = 0; i < target.length; i++) { disposable.add(this.add(target[i], 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(); }; // note: adding a listener here adds a new listener for every tooltip element that's registered. Adding unnecessary listeners is bad for performance. It would be better to add/remove listeners when tooltips are actually created in the dom. 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 )}`; } } ================================================ FILE: src/tooltip.js ================================================ 'use strict'; const EventKit = require('event-kit'); const tooltipComponentsByElement = new WeakMap(); const listen = require('./delegated-listener'); // This tooltip class is derived from Bootstrap 3, but modified to not require // jQuery, which is an expensive dependency we want to eliminate. let followThroughTimer = null; const Tooltip = function(element, options, viewRegistry) { this.options = null; this.enabled = null; this.timeout = null; this.hoverState = null; this.element = null; this.inState = null; this.viewRegistry = viewRegistry; this.init(element, options); }; Tooltip.VERSION = '3.3.5'; Tooltip.FOLLOW_THROUGH_DURATION = 300; Tooltip.DEFAULTS = { animation: true, placement: 'top', selector: false, template: '', trigger: 'hover focus', title: '', delay: 0, html: false, container: false, viewport: { selector: 'body', padding: 0 } }; Tooltip.prototype.init = function(element, options) { this.enabled = true; this.element = element; this.options = this.getOptions(options); this.disposables = new EventKit.CompositeDisposable(); this.mutationObserver = new MutationObserver(this.handleMutations.bind(this)); if (this.options.viewport) { if (typeof this.options.viewport === 'function') { this.viewport = this.options.viewport.call(this, this.element); } else { this.viewport = document.querySelector( this.options.viewport.selector || this.options.viewport ); } } this.inState = { click: false, hover: false, focus: false }; if (this.element instanceof document.constructor && !this.options.selector) { throw new Error( '`selector` option must be specified when initializing tooltip on the window.document object!' ); } const triggers = this.options.trigger.split(' '); for (let i = triggers.length; i--; ) { var trigger = triggers[i]; if (trigger === 'click') { this.disposables.add( listen( this.element, 'click', this.options.selector, this.toggle.bind(this) ) ); this.hideOnClickOutsideOfTooltip = event => { const tooltipElement = this.getTooltipElement(); if (tooltipElement === event.target) return; if (tooltipElement.contains(event.target)) return; if (this.element === event.target) return; if (this.element.contains(event.target)) return; this.hide(); }; } else if (trigger === 'manual') { this.show(); } else { let eventIn, eventOut; if (trigger === 'hover') { this.hideOnKeydownOutsideOfTooltip = () => this.hide(); if (this.options.selector) { eventIn = 'mouseover'; eventOut = 'mouseout'; } else { eventIn = 'mouseenter'; eventOut = 'mouseleave'; } } else { eventIn = 'focusin'; eventOut = 'focusout'; } this.disposables.add( listen( this.element, eventIn, this.options.selector, this.enter.bind(this) ) ); this.disposables.add( listen( this.element, eventOut, this.options.selector, this.leave.bind(this) ) ); } } this.options.selector ? (this._options = extend({}, this.options, { trigger: 'manual', selector: '' })) : this.fixTitle(); }; Tooltip.prototype.startObservingMutations = function() { this.mutationObserver.observe(this.getTooltipElement(), { attributes: true, childList: true, characterData: true, subtree: true }); }; Tooltip.prototype.stopObservingMutations = function() { this.mutationObserver.disconnect(); }; Tooltip.prototype.handleMutations = function() { window.requestAnimationFrame( function() { this.stopObservingMutations(); this.recalculatePosition(); this.startObservingMutations(); }.bind(this) ); }; Tooltip.prototype.getDefaults = function() { return Tooltip.DEFAULTS; }; Tooltip.prototype.getOptions = function(options) { options = extend({}, this.getDefaults(), options); if (options.delay && typeof options.delay === 'number') { options.delay = { show: options.delay, hide: options.delay }; } return options; }; Tooltip.prototype.getDelegateOptions = function() { const options = {}; const defaults = this.getDefaults(); if (this._options) { for (const key of Object.getOwnPropertyNames(this._options)) { const value = this._options[key]; if (defaults[key] !== value) options[key] = value; } } return options; }; Tooltip.prototype.enter = function(event) { if (event) { if (event.currentTarget !== this.element) { this.getDelegateComponent(event.currentTarget).enter(event); return; } this.inState[event.type === 'focusin' ? 'focus' : 'hover'] = true; } if ( this.getTooltipElement().classList.contains('in') || this.hoverState === 'in' ) { this.hoverState = 'in'; return; } clearTimeout(this.timeout); this.hoverState = 'in'; if (!this.options.delay || !this.options.delay.show || followThroughTimer) { return this.show(); } this.timeout = setTimeout( function() { if (this.hoverState === 'in') this.show(); }.bind(this), this.options.delay.show ); }; Tooltip.prototype.isInStateTrue = function() { for (const key in this.inState) { if (this.inState[key]) return true; } return false; }; Tooltip.prototype.leave = function(event) { if (event) { if (event.currentTarget !== this.element) { this.getDelegateComponent(event.currentTarget).leave(event); return; } this.inState[event.type === 'focusout' ? 'focus' : 'hover'] = false; } if (this.isInStateTrue()) return; clearTimeout(this.timeout); this.hoverState = 'out'; if (!this.options.delay || !this.options.delay.hide) return this.hide(); this.timeout = setTimeout( function() { if (this.hoverState === 'out') this.hide(); }.bind(this), this.options.delay.hide ); }; Tooltip.prototype.show = function() { if (this.hasContent() && this.enabled) { if (this.hideOnClickOutsideOfTooltip) { window.addEventListener('click', this.hideOnClickOutsideOfTooltip, { capture: true }); } if (this.hideOnKeydownOutsideOfTooltip) { window.addEventListener( 'keydown', this.hideOnKeydownOutsideOfTooltip, true ); } const tip = this.getTooltipElement(); this.startObservingMutations(); const tipId = this.getUID('tooltip'); this.setContent(); tip.setAttribute('id', tipId); this.element.setAttribute('aria-describedby', tipId); if (this.options.animation) tip.classList.add('fade'); let placement = typeof this.options.placement === 'function' ? this.options.placement.call(this, tip, this.element) : this.options.placement; const autoToken = /\s?auto?\s?/i; const autoPlace = autoToken.test(placement); if (autoPlace) placement = placement.replace(autoToken, '') || 'top'; tip.remove(); tip.style.top = '0px'; tip.style.left = '0px'; tip.style.display = 'block'; tip.classList.add(placement); document.body.appendChild(tip); const pos = this.element.getBoundingClientRect(); const actualWidth = tip.offsetWidth; const actualHeight = tip.offsetHeight; if (autoPlace) { const orgPlacement = placement; const viewportDim = this.viewport.getBoundingClientRect(); placement = placement === 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : placement === 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : placement === 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : placement === 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : placement; tip.classList.remove(orgPlacement); tip.classList.add(placement); } const calculatedOffset = this.getCalculatedOffset( placement, pos, actualWidth, actualHeight ); this.applyPlacement(calculatedOffset, placement); const prevHoverState = this.hoverState; this.hoverState = null; if (prevHoverState === 'out') this.leave(); } }; Tooltip.prototype.applyPlacement = function(offset, placement) { const tip = this.getTooltipElement(); const width = tip.offsetWidth; const height = tip.offsetHeight; // manually read margins because getBoundingClientRect includes difference const computedStyle = window.getComputedStyle(tip); const marginTop = parseInt(computedStyle.marginTop, 10); const marginLeft = parseInt(computedStyle.marginLeft, 10); offset.top += marginTop; offset.left += marginLeft; tip.style.top = offset.top + 'px'; tip.style.left = offset.left + 'px'; tip.classList.add('in'); // check to see if placing tip in new offset caused the tip to resize itself const actualWidth = tip.offsetWidth; const actualHeight = tip.offsetHeight; if (placement === 'top' && actualHeight !== height) { offset.top = offset.top + height - actualHeight; } const delta = this.getViewportAdjustedDelta( placement, offset, actualWidth, actualHeight ); if (delta.left) offset.left += delta.left; else offset.top += delta.top; const isVertical = /top|bottom/.test(placement); const arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight; const arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'; tip.style.top = offset.top + 'px'; tip.style.left = offset.left + 'px'; this.replaceArrow(arrowDelta, tip[arrowOffsetPosition], isVertical); }; Tooltip.prototype.replaceArrow = function(delta, dimension, isVertical) { const arrow = this.getArrowElement(); const amount = 50 * (1 - delta / dimension) + '%'; if (isVertical) { arrow.style.left = amount; arrow.style.top = ''; } else { arrow.style.top = amount; arrow.style.left = ''; } }; Tooltip.prototype.setContent = function() { const tip = this.getTooltipElement(); if (this.options.class) { tip.classList.add(this.options.class); } const inner = tip.querySelector('.tooltip-inner'); if (this.options.item) { inner.appendChild(this.viewRegistry.getView(this.options.item)); } else { const title = this.getTitle(); if (this.options.html) { inner.innerHTML = title; } else { inner.textContent = title; } } tip.classList.remove('fade', 'in', 'top', 'bottom', 'left', 'right'); }; Tooltip.prototype.hide = function(callback) { this.inState = {}; if (this.hideOnClickOutsideOfTooltip) { window.removeEventListener('click', this.hideOnClickOutsideOfTooltip, true); } if (this.hideOnKeydownOutsideOfTooltip) { window.removeEventListener( 'keydown', this.hideOnKeydownOutsideOfTooltip, true ); } this.tip && this.tip.classList.remove('in'); this.stopObservingMutations(); if (this.hoverState !== 'in') this.tip && this.tip.remove(); this.element.removeAttribute('aria-describedby'); callback && callback(); this.hoverState = null; clearTimeout(followThroughTimer); followThroughTimer = setTimeout(function() { followThroughTimer = null; }, Tooltip.FOLLOW_THROUGH_DURATION); return this; }; Tooltip.prototype.fixTitle = function() { if ( this.element.getAttribute('title') || typeof this.element.getAttribute('data-original-title') !== 'string' ) { this.element.setAttribute( 'data-original-title', this.element.getAttribute('title') || '' ); this.element.setAttribute('title', ''); } }; Tooltip.prototype.hasContent = function() { return this.getTitle() || this.options.item; }; Tooltip.prototype.getCalculatedOffset = function( placement, pos, actualWidth, actualHeight ) { return placement === 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement === 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement === 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : /* placement === 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }; }; Tooltip.prototype.getViewportAdjustedDelta = function( placement, pos, actualWidth, actualHeight ) { const delta = { top: 0, left: 0 }; if (!this.viewport) return delta; const viewportPadding = (this.options.viewport && this.options.viewport.padding) || 0; const viewportDimensions = this.viewport.getBoundingClientRect(); if (/right|left/.test(placement)) { const topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll; const bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight; if (topEdgeOffset < viewportDimensions.top) { // top overflow delta.top = viewportDimensions.top - topEdgeOffset; } else if ( bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height ) { // bottom overflow delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset; } } else { const leftEdgeOffset = pos.left - viewportPadding; const rightEdgeOffset = pos.left + viewportPadding + actualWidth; if (leftEdgeOffset < viewportDimensions.left) { // left overflow delta.left = viewportDimensions.left - leftEdgeOffset; } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset; } } return delta; }; Tooltip.prototype.getTitle = function() { const title = this.element.getAttribute('data-original-title'); if (title) { return title; } else { return typeof this.options.title === 'function' ? this.options.title.call(this.element) : this.options.title; } }; Tooltip.prototype.getUID = function(prefix) { do prefix += ~~(Math.random() * 1000000); while (document.getElementById(prefix)); return prefix; }; Tooltip.prototype.getTooltipElement = function() { if (!this.tip) { let div = document.createElement('div'); div.innerHTML = this.options.template; if (div.children.length !== 1) { throw new Error( 'Tooltip `template` option must consist of exactly 1 top-level element!' ); } this.tip = div.firstChild; } return this.tip; }; Tooltip.prototype.getArrowElement = function() { this.arrow = this.arrow || this.getTooltipElement().querySelector('.tooltip-arrow'); return this.arrow; }; Tooltip.prototype.enable = function() { this.enabled = true; }; Tooltip.prototype.disable = function() { this.enabled = false; }; Tooltip.prototype.toggleEnabled = function() { this.enabled = !this.enabled; }; Tooltip.prototype.toggle = function(event) { if (event) { if (event.currentTarget !== this.element) { this.getDelegateComponent(event.currentTarget).toggle(event); return; } this.inState.click = !this.inState.click; if (this.isInStateTrue()) this.enter(); else this.leave(); } else { this.getTooltipElement().classList.contains('in') ? this.leave() : this.enter(); } }; Tooltip.prototype.destroy = function() { clearTimeout(this.timeout); this.tip && this.tip.remove(); this.disposables.dispose(); }; Tooltip.prototype.getDelegateComponent = function(element) { let component = tooltipComponentsByElement.get(element); if (!component) { component = new Tooltip( element, this.getDelegateOptions(), this.viewRegistry ); tooltipComponentsByElement.set(element, component); } return component; }; Tooltip.prototype.recalculatePosition = function() { const tip = this.getTooltipElement(); let placement = typeof this.options.placement === 'function' ? this.options.placement.call(this, tip, this.element) : this.options.placement; const autoToken = /\s?auto?\s?/i; const autoPlace = autoToken.test(placement); if (autoPlace) placement = placement.replace(autoToken, '') || 'top'; tip.classList.add(placement); const pos = this.element.getBoundingClientRect(); const actualWidth = tip.offsetWidth; const actualHeight = tip.offsetHeight; if (autoPlace) { const orgPlacement = placement; const viewportDim = this.viewport.getBoundingClientRect(); placement = placement === 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : placement === 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : placement === 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : placement === 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : placement; tip.classList.remove(orgPlacement); tip.classList.add(placement); } const calculatedOffset = this.getCalculatedOffset( placement, pos, actualWidth, actualHeight ); this.applyPlacement(calculatedOffset, placement); }; function extend() { const args = Array.prototype.slice.apply(arguments); const target = args.shift(); let source = args.shift(); while (source) { for (const key of Object.getOwnPropertyNames(source)) { target[key] = source[key]; } source = args.shift(); } return target; } module.exports = Tooltip; ================================================ FILE: src/tree-indenter.js ================================================ // const log = console.debug // in dev const log = () => {}; // in production module.exports = class TreeIndenter { constructor(languageMode, scopes = undefined) { this.languageMode = languageMode; this.scopes = scopes || languageMode.config.get('editor.scopes', { scope: this.languageMode.rootScopeDescriptor }); log('[TreeIndenter] constructor', this.scopes); } /** tree indenter is configured for this language */ get isConfigured() { return !!this.scopes; } // Given a position, walk up the syntax tree, to find the highest level // node that still starts here. This is to identify the column where this // node (e.g., an HTML closing tag) ends. _getHighestSyntaxNodeAtPosition(row, column = null) { if (column == null) { // Find the first character on the row that is not whitespace + 1 column = this.languageMode.buffer.lineForRow(row).search(/\S/); } let syntaxNode; if (column >= 0) { syntaxNode = this.languageMode.getSyntaxNodeAtPosition({ row, column }); while ( syntaxNode && syntaxNode.parent && syntaxNode.parent.startPosition.row === syntaxNode.startPosition.row && syntaxNode.parent.endPosition.row === syntaxNode.startPosition.row && syntaxNode.parent.startPosition.column === syntaxNode.startPosition.column ) { syntaxNode = syntaxNode.parent; } return syntaxNode; } } /** Walk up the tree. Everytime we meet a scope type, check whether we are coming from the first (resp. last) child. If so, we are opening (resp. closing) that scope, i.e., do not count it. Otherwise, add 1. This is the core function. It might make more sense to reverse the direction of this walk, i.e., go from root to leaf instead. */ _treeWalk(node, lastScope = null) { if (node == null || node.parent == null) { return 0; } else { let increment = 0; const notFirstOrLastSibling = node.previousSibling != null && node.nextSibling != null; const isScope = this.scopes.indent[node.parent.type]; notFirstOrLastSibling && isScope && increment++; const isScope2 = this.scopes.indentExceptFirst[node.parent.type]; !increment && isScope2 && node.previousSibling != null && increment++; const isScope3 = this.scopes.indentExceptFirstOrBlock[node.parent.type]; !increment && isScope3 && node.previousSibling != null && increment++; // apply current row, single line, type-based rules, e.g., 'else' or 'private:' let typeDent = 0; this.scopes.types.indent[node.type] && typeDent++; this.scopes.types.outdent[node.type] && increment && typeDent--; increment += typeDent; // check whether the last (lower) indentation happened due to a scope that // started on the same row and ends directly before this. if ( lastScope && increment > 0 && // previous (lower) scope was a two-sided scope, reduce if starts on // same row and ends right before // TODO: this currently only works for scopes that have a single-character // closing delimiter (like statement_blocks, but not HTML, for instance). ((node.parent.startPosition.row === lastScope.node.startPosition.row && node.parent.endIndex <= lastScope.node.endIndex + 1) || // or this is a special scope (like if, while) and it's ends coincide (isScope3 && (lastScope.node.endIndex === node.endIndex || node.parent.endIndex === node.endIndex))) ) { log('ignoring repeat', node.parent.type, lastScope); increment = 0; } else { lastScope && log( node.parent.startPosition.row, lastScope.node.startPosition.row, node.parent.endIndex, lastScope.node.endIndex, isScope3, node.endIndex ); } log('treewalk', { node, notFirstOrLastSibling, type: node.parent.type, increment }); const newLastScope = isScope || isScope2 ? { node: node.parent } : lastScope; return this._treeWalk(node.parent, newLastScope) + increment; } } suggestedIndentForBufferRow(row, tabLength, options) { // get current indentation for row const line = this.languageMode.buffer.lineForRow(row); const currentIndentation = this.languageMode.indentLevelForLine( line, tabLength ); const syntaxNode = this._getHighestSyntaxNodeAtPosition(row); if (!syntaxNode) { const previousRow = Math.max(row - 1, 0); const previousIndentation = this.languageMode.indentLevelForLine( this.languageMode.indentLevelForLine(previousRow), tabLength ); return previousIndentation; } let indentation = this._treeWalk(syntaxNode); // Special case for comments if ( (syntaxNode.type === 'comment' || syntaxNode.type === 'description') && syntaxNode.startPosition.row < row && syntaxNode.endPosition.row > row ) { indentation += 1; } if (options && options.preserveLeadingWhitespace) { indentation -= currentIndentation; } return indentation; } }; ================================================ FILE: src/tree-sitter-grammar.js ================================================ const path = require('path'); const SyntaxScopeMap = require('./syntax-scope-map'); const Module = require('module'); module.exports = class TreeSitterGrammar { constructor(registry, filePath, params) { this.registry = registry; this.name = params.name; this.scopeName = params.scopeName; // TODO - Remove the `RegExp` spelling and only support `Regex`, once all of the existing // Tree-sitter grammars are updated to spell it `Regex`. this.contentRegex = buildRegex(params.contentRegex || params.contentRegExp); this.injectionRegex = buildRegex( params.injectionRegex || params.injectionRegExp ); this.firstLineRegex = buildRegex(params.firstLineRegex); this.folds = params.folds || []; this.folds.forEach(normalizeFoldSpecification); this.commentStrings = { commentStartString: params.comments && params.comments.start, commentEndString: params.comments && params.comments.end }; const scopeSelectors = {}; for (const key in params.scopes || {}) { const classes = preprocessScopes(params.scopes[key]); const selectors = key.split(/,\s+/); for (let selector of selectors) { selector = selector.trim(); if (!selector) continue; if (scopeSelectors[selector]) { scopeSelectors[selector] = [].concat( scopeSelectors[selector], classes ); } else { scopeSelectors[selector] = classes; } } } this.scopeMap = new SyntaxScopeMap(scopeSelectors); this.fileTypes = params.fileTypes || []; this.injectionPointsByType = {}; for (const injectionPoint of params.injectionPoints || []) { this.addInjectionPoint(injectionPoint); } // TODO - When we upgrade to a new enough version of node, use `require.resolve` // with the new `paths` option instead of this private API. const languageModulePath = Module._resolveFilename(params.parser, { id: filePath, filename: filePath, paths: Module._nodeModulePaths(path.dirname(filePath)) }); this.languageModule = require(languageModulePath); this.classNamesById = new Map(); this.scopeNamesById = new Map(); this.idsByScope = Object.create(null); this.nextScopeId = 256 + 1; this.registration = null; } inspect() { return `TreeSitterGrammar {scopeName: ${this.scopeName}}`; } idForScope(scopeName) { if (!scopeName) { return undefined; } let id = this.idsByScope[scopeName]; if (!id) { id = this.nextScopeId += 2; const className = scopeName .split('.') .map(s => `syntax--${s}`) .join(' '); this.idsByScope[scopeName] = id; this.classNamesById.set(id, className); this.scopeNamesById.set(id, scopeName); } return id; } classNameForScopeId(id) { return this.classNamesById.get(id); } scopeNameForScopeId(id) { return this.scopeNamesById.get(id); } activate() { this.registration = this.registry.addGrammar(this); } deactivate() { if (this.registration) this.registration.dispose(); } addInjectionPoint(injectionPoint) { let injectionPoints = this.injectionPointsByType[injectionPoint.type]; if (!injectionPoints) { injectionPoints = this.injectionPointsByType[injectionPoint.type] = []; } injectionPoints.push(injectionPoint); } removeInjectionPoint(injectionPoint) { const injectionPoints = this.injectionPointsByType[injectionPoint.type]; if (injectionPoints) { const index = injectionPoints.indexOf(injectionPoint); if (index !== -1) injectionPoints.splice(index, 1); if (injectionPoints.length === 0) { delete this.injectionPointsByType[injectionPoint.type]; } } } /* Section - Backward compatibility shims */ onDidUpdate(callback) { // do nothing } tokenizeLines(text, compatibilityMode = true) { return text.split('\n').map(line => this.tokenizeLine(line, null, false)); } tokenizeLine(line, ruleStack, firstLine) { return { value: line, scopes: [this.scopeName] }; } }; const preprocessScopes = value => typeof value === 'string' ? value : Array.isArray(value) ? value.map(preprocessScopes) : value.match ? { match: new RegExp(value.match), scopes: preprocessScopes(value.scopes) } : Object.assign({}, value, { scopes: preprocessScopes(value.scopes) }); const NODE_NAME_REGEX = /[\w_]+/; function matcherForSpec(spec) { if (typeof spec === 'string') { if (spec[0] === '"' && spec[spec.length - 1] === '"') { return { type: spec.substr(1, spec.length - 2), named: false }; } if (!NODE_NAME_REGEX.test(spec)) { return { type: spec, named: false }; } return { type: spec, named: true }; } return spec; } function normalizeFoldSpecification(spec) { if (spec.type) { if (Array.isArray(spec.type)) { spec.matchers = spec.type.map(matcherForSpec); } else { spec.matchers = [matcherForSpec(spec.type)]; } } if (spec.start) normalizeFoldSpecification(spec.start); if (spec.end) normalizeFoldSpecification(spec.end); } function buildRegex(value) { // Allow multiple alternatives to be specified via an array, for // readability of the grammar file if (Array.isArray(value)) value = value.map(_ => `(${_})`).join('|'); if (typeof value === 'string') return new RegExp(value); return null; } ================================================ FILE: src/tree-sitter-language-mode.js ================================================ const Parser = require('tree-sitter'); const { Point, Range, spliceArray } = require('text-buffer'); const { Patch } = require('superstring'); const { Emitter } = require('event-kit'); const ScopeDescriptor = require('./scope-descriptor'); const Token = require('./token'); const TokenizedLine = require('./tokenized-line'); const TextMateLanguageMode = require('./text-mate-language-mode'); const { matcherForSelector } = require('./selectors'); const TreeIndenter = require('./tree-indenter'); let nextId = 0; const MAX_RANGE = new Range(Point.ZERO, Point.INFINITY).freeze(); const PARSER_POOL = []; const WORD_REGEX = /\w/; class TreeSitterLanguageMode { static _patchSyntaxNode() { if (!Parser.SyntaxNode.prototype.hasOwnProperty('range')) { Object.defineProperty(Parser.SyntaxNode.prototype, 'range', { get() { return rangeForNode(this); } }); } } constructor({ buffer, grammar, config, grammars, syncTimeoutMicros }) { TreeSitterLanguageMode._patchSyntaxNode(); this.id = nextId++; this.buffer = buffer; this.grammar = grammar; this.config = config; this.grammarRegistry = grammars; this.rootLanguageLayer = new LanguageLayer(null, this, grammar, 0); this.injectionsMarkerLayer = buffer.addMarkerLayer(); if (syncTimeoutMicros != null) { this.syncTimeoutMicros = syncTimeoutMicros; } this.rootScopeDescriptor = new ScopeDescriptor({ scopes: [this.grammar.scopeName] }); this.emitter = new Emitter(); this.isFoldableCache = []; this.hasQueuedParse = false; this.grammarForLanguageString = this.grammarForLanguageString.bind(this); this.rootLanguageLayer .update(null) .then(() => this.emitter.emit('did-tokenize')); // TODO: Remove this once TreeSitterLanguageMode implements its own auto-indentation system. This // is temporarily needed in order to delegate to the TextMateLanguageMode's auto-indent system. this.regexesByPattern = {}; } async parseCompletePromise() { let done = false; while (!done) { if (this.rootLanguageLayer.currentParsePromise) { await this.rootLanguageLayer.currentParsePromises; } else { done = true; for (const marker of this.injectionsMarkerLayer.getMarkers()) { if (marker.languageLayer.currentParsePromise) { done = false; await marker.languageLayer.currentParsePromise; break; } } } await new Promise(resolve => setTimeout(resolve, 0)); } } destroy() { this.injectionsMarkerLayer.destroy(); this.rootLanguageLayer = null; } getLanguageId() { return this.grammar.scopeName; } bufferDidChange({ oldRange, newRange, oldText, newText }) { const edit = this.rootLanguageLayer._treeEditForBufferChange( oldRange.start, oldRange.end, newRange.end, oldText, newText ); this.rootLanguageLayer.handleTextChange(edit, oldText, newText); for (const marker of this.injectionsMarkerLayer.getMarkers()) { marker.languageLayer.handleTextChange(edit, oldText, newText); } } bufferDidFinishTransaction({ changes }) { for (let i = 0, { length } = changes; i < length; i++) { const { oldRange, newRange } = changes[i]; spliceArray( this.isFoldableCache, newRange.start.row, oldRange.end.row - oldRange.start.row, { length: newRange.end.row - newRange.start.row } ); } this.rootLanguageLayer.update(null); } parse(language, oldTree, ranges) { const parser = PARSER_POOL.pop() || new Parser(); parser.setLanguage(language); const result = parser.parseTextBuffer(this.buffer.buffer, oldTree, { syncTimeoutMicros: this.syncTimeoutMicros, includedRanges: ranges }); if (result.then) { return result.then(tree => { PARSER_POOL.push(parser); return tree; }); } else { PARSER_POOL.push(parser); return result; } } get tree() { return this.rootLanguageLayer.tree; } updateForInjection(grammar) { this.rootLanguageLayer.updateInjections(grammar); } /* Section - Highlighting */ buildHighlightIterator() { if (!this.rootLanguageLayer) return new NullLanguageModeHighlightIterator(); return new HighlightIterator(this); } onDidTokenize(callback) { return this.emitter.on('did-tokenize', callback); } onDidChangeHighlighting(callback) { return this.emitter.on('did-change-highlighting', callback); } classNameForScopeId(scopeId) { return this.grammar.classNameForScopeId(scopeId); } /* Section - Commenting */ commentStringsForPosition(position) { const range = this.firstNonWhitespaceRange(position.row) || new Range(position, position); const { grammar } = this.getSyntaxNodeAndGrammarContainingRange(range); return grammar.commentStrings; } isRowCommented(row) { const range = this.firstNonWhitespaceRange(row); if (range) { const firstNode = this.getSyntaxNodeContainingRange(range); if (firstNode) return firstNode.type.includes('comment'); } return false; } /* Section - Indentation */ suggestedIndentForLineAtBufferRow(row, line, tabLength) { return this._suggestedIndentForLineWithScopeAtBufferRow( row, line, this.rootScopeDescriptor, tabLength ); } suggestedIndentForBufferRow(row, tabLength, options) { if (!this.treeIndenter) { this.treeIndenter = new TreeIndenter(this); } if (this.treeIndenter.isConfigured) { const indent = this.treeIndenter.suggestedIndentForBufferRow( row, tabLength, options ); return indent; } else { return this._suggestedIndentForLineWithScopeAtBufferRow( row, this.buffer.lineForRow(row), this.rootScopeDescriptor, tabLength, options ); } } indentLevelForLine(line, 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; } /* Section - Folding */ isFoldableAtRow(row) { if (this.isFoldableCache[row] != null) return this.isFoldableCache[row]; const result = this.getFoldableRangeContainingPoint(Point(row, Infinity), 0, true) != null; this.isFoldableCache[row] = result; return result; } getFoldableRanges() { return this.getFoldableRangesAtIndentLevel(null); } /** * TODO: Make this method generate folds for nested languages (currently, * folds are only generated for the root language layer). */ getFoldableRangesAtIndentLevel(goalLevel) { let result = []; let stack = [{ node: this.tree.rootNode, level: 0 }]; while (stack.length > 0) { const { node, level } = stack.pop(); const range = this.getFoldableRangeForNode(node, this.grammar); if (range) { if (goalLevel == null || level === goalLevel) { let updatedExistingRange = false; for (let i = 0, { length } = result; i < length; i++) { if ( result[i].start.row === range.start.row && result[i].end.row === range.end.row ) { result[i] = range; updatedExistingRange = true; break; } } if (!updatedExistingRange) result.push(range); } } const parentStartRow = node.startPosition.row; const parentEndRow = node.endPosition.row; for ( let children = node.namedChildren, i = 0, { length } = children; i < length; i++ ) { const child = children[i]; const { startPosition: childStart, endPosition: childEnd } = child; if (childEnd.row > childStart.row) { if ( childStart.row === parentStartRow && childEnd.row === parentEndRow ) { stack.push({ node: child, level: level }); } else { const childLevel = range && range.containsPoint(childStart) && range.containsPoint(childEnd) ? level + 1 : level; if (childLevel <= goalLevel || goalLevel == null) { stack.push({ node: child, level: childLevel }); } } } } } return result.sort((a, b) => a.start.row - b.start.row); } getFoldableRangeContainingPoint(point, tabLength, existenceOnly = false) { if (!this.tree) return null; let smallestRange; this._forEachTreeWithRange(new Range(point, point), (tree, grammar) => { let node = tree.rootNode.descendantForPosition( this.buffer.clipPosition(point) ); while (node) { if (existenceOnly && node.startPosition.row < point.row) return; if (node.endPosition.row > point.row) { const range = this.getFoldableRangeForNode(node, grammar); if (range && rangeIsSmaller(range, smallestRange)) { smallestRange = range; return; } } node = node.parent; } }); return existenceOnly ? smallestRange && smallestRange.start.row === point.row : smallestRange; } _forEachTreeWithRange(range, callback) { if (this.rootLanguageLayer.tree) { callback(this.rootLanguageLayer.tree, this.rootLanguageLayer.grammar); } const injectionMarkers = this.injectionsMarkerLayer.findMarkers({ intersectsRange: range }); for (const injectionMarker of injectionMarkers) { const { tree, grammar } = injectionMarker.languageLayer; if (tree) callback(tree, grammar); } } getFoldableRangeForNode(node, grammar, existenceOnly) { const { children } = node; const childCount = children.length; for (var i = 0, { length } = grammar.folds; i < length; i++) { const foldSpec = grammar.folds[i]; if (foldSpec.matchers && !hasMatchingFoldSpec(foldSpec.matchers, node)) continue; let foldStart; const startEntry = foldSpec.start; if (startEntry) { let foldStartNode; if (startEntry.index != null) { foldStartNode = children[startEntry.index]; if ( !foldStartNode || (startEntry.matchers && !hasMatchingFoldSpec(startEntry.matchers, foldStartNode)) ) continue; } else { foldStartNode = children.find(child => hasMatchingFoldSpec(startEntry.matchers, child) ); if (!foldStartNode) continue; } foldStart = new Point(foldStartNode.endPosition.row, Infinity); } else { foldStart = new Point(node.startPosition.row, Infinity); } let foldEnd; const endEntry = foldSpec.end; if (endEntry) { let foldEndNode; if (endEntry.index != null) { const index = endEntry.index < 0 ? childCount + endEntry.index : endEntry.index; foldEndNode = children[index]; if ( !foldEndNode || (endEntry.type && endEntry.type !== foldEndNode.type) ) continue; } else { foldEndNode = children.find(child => hasMatchingFoldSpec(endEntry.matchers, child) ); if (!foldEndNode) continue; } if (foldEndNode.startPosition.row <= foldStart.row) continue; foldEnd = foldEndNode.startPosition; if ( this.buffer.findInRangeSync( WORD_REGEX, new Range(foldEnd, new Point(foldEnd.row, Infinity)) ) ) { foldEnd = new Point(foldEnd.row - 1, Infinity); } } else { const { endPosition } = node; if (endPosition.column === 0) { foldEnd = Point(endPosition.row - 1, Infinity); } else if (childCount > 0) { foldEnd = endPosition; } else { foldEnd = Point(endPosition.row, 0); } } return existenceOnly ? true : new Range(foldStart, foldEnd); } } /* Section - Syntax Tree APIs */ getSyntaxNodeContainingRange(range, where = _ => true) { return this.getSyntaxNodeAndGrammarContainingRange(range, where).node; } getSyntaxNodeAndGrammarContainingRange(range, where = _ => true) { const startIndex = this.buffer.characterIndexForPosition(range.start); const endIndex = this.buffer.characterIndexForPosition(range.end); const searchEndIndex = Math.max(0, endIndex - 1); let smallestNode = null; let smallestNodeGrammar = this.grammar; this._forEachTreeWithRange(range, (tree, grammar) => { let node = tree.rootNode.descendantForIndex(startIndex, searchEndIndex); while (node) { if ( nodeContainsIndices(node, startIndex, endIndex) && where(node, grammar) ) { if (nodeIsSmaller(node, smallestNode)) { smallestNode = node; smallestNodeGrammar = grammar; } break; } node = node.parent; } }); return { node: smallestNode, grammar: smallestNodeGrammar }; } getRangeForSyntaxNodeContainingRange(range, where) { const node = this.getSyntaxNodeContainingRange(range, where); return node && node.range; } getSyntaxNodeAtPosition(position, where) { return this.getSyntaxNodeContainingRange( new Range(position, position), where ); } bufferRangeForScopeAtPosition(selector, position) { const nodeCursorAdapter = new NodeCursorAdaptor(); if (typeof selector === 'string') { const match = matcherForSelector(selector); selector = (node, grammar) => { const rules = grammar.scopeMap.get([node.type], [0], node.named); nodeCursorAdapter.node = node; const scopeName = applyLeafRules(rules, nodeCursorAdapter); if (scopeName != null) { return match(scopeName); } }; } if (selector === null) selector = undefined; const node = this.getSyntaxNodeAtPosition(position, selector); return node && node.range; } /* Section - Backward compatibility shims */ tokenizedLineForRow(row) { const lineText = this.buffer.lineForRow(row); const tokens = []; const iterator = this.buildHighlightIterator(); let start = { row, column: 0 }; const scopes = iterator.seek(start, row); while (true) { const end = iterator.getPosition(); if (end.row > row) { end.row = row; end.column = lineText.length; } if (end.column > start.column) { tokens.push( new Token({ value: lineText.substring(start.column, end.column), scopes: scopes.map(s => this.grammar.scopeNameForScopeId(s)) }) ); } if (end.column < lineText.length) { const closeScopeCount = iterator.getCloseScopeIds().length; for (let i = 0; i < closeScopeCount; i++) { scopes.pop(); } scopes.push(...iterator.getOpenScopeIds()); start = end; iterator.moveToSuccessor(); } else { break; } } return new TokenizedLine({ openScopes: [], text: lineText, tokens, tags: [], ruleStack: [], lineEnding: this.buffer.lineEndingForRow(row), tokenIterator: null, grammar: this.grammar }); } syntaxTreeScopeDescriptorForPosition(point) { const nodes = []; point = this.buffer.clipPosition(Point.fromObject(point)); // If the position is the end of a line, get node of left character instead of newline // This is to match TextMate behaviour, see https://github.com/atom/atom/issues/18463 if ( point.column > 0 && point.column === this.buffer.lineLengthForRow(point.row) ) { point = point.copy(); point.column--; } this._forEachTreeWithRange(new Range(point, point), tree => { let node = tree.rootNode.descendantForPosition(point); while (node) { nodes.push(node); node = node.parent; } }); // The nodes are mostly already sorted from smallest to largest, // but for files with multiple syntax trees (e.g. ERB), each tree's // nodes are separate. Sort the nodes from largest to smallest. nodes.reverse(); nodes.sort( (a, b) => a.startIndex - b.startIndex || b.endIndex - a.endIndex ); const nodeTypes = nodes.map(node => node.type); nodeTypes.unshift(this.grammar.scopeName); return new ScopeDescriptor({ scopes: nodeTypes }); } scopeDescriptorForPosition(point) { point = this.buffer.clipPosition(Point.fromObject(point)); // If the position is the end of a line, get scope of left character instead of newline // This is to match TextMate behaviour, see https://github.com/atom/atom/issues/18463 if ( point.column > 0 && point.column === this.buffer.lineLengthForRow(point.row) ) { point = point.copy(); point.column--; } const iterator = this.buildHighlightIterator(); const scopes = []; for (const scope of iterator.seek(point, point.row + 1)) { scopes.push(this.grammar.scopeNameForScopeId(scope)); } if (point.isEqual(iterator.getPosition())) { for (const scope of iterator.getOpenScopeIds()) { scopes.push(this.grammar.scopeNameForScopeId(scope)); } } if (scopes.length === 0 || scopes[0] !== this.grammar.scopeName) { scopes.unshift(this.grammar.scopeName); } return new ScopeDescriptor({ scopes }); } tokenForPosition(point) { const node = this.getSyntaxNodeAtPosition(point); const scopes = this.scopeDescriptorForPosition(point).getScopesArray(); return new Token({ value: node.text, scopes }); } getGrammar() { return this.grammar; } /* Section - Private */ firstNonWhitespaceRange(row) { return this.buffer.findInRangeSync( /\S/, new Range(new Point(row, 0), new Point(row, Infinity)) ); } grammarForLanguageString(languageString) { return this.grammarRegistry.treeSitterGrammarForLanguageString( languageString ); } emitRangeUpdate(range) { const startRow = range.start.row; const endRow = range.end.row; for (let row = startRow; row < endRow; row++) { this.isFoldableCache[row] = undefined; } this.emitter.emit('did-change-highlighting', range); } } class LanguageLayer { constructor(marker, languageMode, grammar, depth) { this.marker = marker; this.languageMode = languageMode; this.grammar = grammar; this.tree = null; this.currentParsePromise = null; this.patchSinceCurrentParseStarted = null; this.depth = depth; } buildHighlightIterator() { if (this.tree) { return new LayerHighlightIterator(this, this.tree.walk()); } else { return new NullLayerHighlightIterator(); } } handleTextChange(edit, oldText, newText) { const { startPosition, oldEndPosition, newEndPosition } = edit; if (this.tree) { this.tree.edit(edit); if (this.editedRange) { if (startPosition.isLessThan(this.editedRange.start)) { this.editedRange.start = startPosition; } if (oldEndPosition.isLessThan(this.editedRange.end)) { this.editedRange.end = newEndPosition.traverse( this.editedRange.end.traversalFrom(oldEndPosition) ); } else { this.editedRange.end = newEndPosition; } } else { this.editedRange = new Range(startPosition, newEndPosition); } } if (this.patchSinceCurrentParseStarted) { this.patchSinceCurrentParseStarted.splice( startPosition, oldEndPosition.traversalFrom(startPosition), newEndPosition.traversalFrom(startPosition), oldText, newText ); } } destroy() { this.tree = null; this.destroyed = true; this.marker.destroy(); for (const marker of this.languageMode.injectionsMarkerLayer.getMarkers()) { if (marker.parentLanguageLayer === this) { marker.languageLayer.destroy(); } } } async update(nodeRangeSet) { if (!this.currentParsePromise) { while ( !this.destroyed && (!this.tree || this.tree.rootNode.hasChanges()) ) { const params = { async: false }; this.currentParsePromise = this._performUpdate(nodeRangeSet, params); if (!params.async) break; await this.currentParsePromise; } this.currentParsePromise = null; } } updateInjections(grammar) { if (grammar.injectionRegex) { if (!this.currentParsePromise) this.currentParsePromise = Promise.resolve(); this.currentParsePromise = this.currentParsePromise.then(async () => { await this._populateInjections(MAX_RANGE, null); this.currentParsePromise = null; }); } } async _performUpdate(nodeRangeSet, params) { let includedRanges = null; if (nodeRangeSet) { includedRanges = nodeRangeSet.getRanges(this.languageMode.buffer); if (includedRanges.length === 0) { const range = this.marker.getRange(); this.destroy(); this.languageMode.emitRangeUpdate(range); return; } } let affectedRange = this.editedRange; this.editedRange = null; this.patchSinceCurrentParseStarted = new Patch(); let tree = this.languageMode.parse( this.grammar.languageModule, this.tree, includedRanges ); if (tree.then) { params.async = true; tree = await tree; } const changes = this.patchSinceCurrentParseStarted.getChanges(); this.patchSinceCurrentParseStarted = null; for (const { oldStart, newStart, oldEnd, newEnd, oldText, newText } of changes) { const newExtent = Point.fromObject(newEnd).traversalFrom(newStart); tree.edit( this._treeEditForBufferChange( newStart, oldEnd, Point.fromObject(oldStart).traverse(newExtent), oldText, newText ) ); } if (this.tree) { const rangesWithSyntaxChanges = this.tree.getChangedRanges(tree); this.tree = tree; if (rangesWithSyntaxChanges.length > 0) { for (const range of rangesWithSyntaxChanges) { this.languageMode.emitRangeUpdate(rangeForNode(range)); } const combinedRangeWithSyntaxChange = new Range( rangesWithSyntaxChanges[0].startPosition, last(rangesWithSyntaxChanges).endPosition ); if (affectedRange) { this.languageMode.emitRangeUpdate(affectedRange); affectedRange = affectedRange.union(combinedRangeWithSyntaxChange); } else { affectedRange = combinedRangeWithSyntaxChange; } } } else { this.tree = tree; this.languageMode.emitRangeUpdate(rangeForNode(tree.rootNode)); if (includedRanges) { affectedRange = new Range( includedRanges[0].startPosition, last(includedRanges).endPosition ); } else { affectedRange = MAX_RANGE; } } if (affectedRange) { const injectionPromise = this._populateInjections( affectedRange, nodeRangeSet ); if (injectionPromise) { params.async = true; return injectionPromise; } } } _populateInjections(range, nodeRangeSet) { const existingInjectionMarkers = this.languageMode.injectionsMarkerLayer .findMarkers({ intersectsRange: range }) .filter(marker => marker.parentLanguageLayer === this); if (existingInjectionMarkers.length > 0) { range = range.union( new Range( existingInjectionMarkers[0].getRange().start, last(existingInjectionMarkers).getRange().end ) ); } const markersToUpdate = new Map(); const nodes = this.tree.rootNode.descendantsOfType( Object.keys(this.grammar.injectionPointsByType), range.start, range.end ); let existingInjectionMarkerIndex = 0; for (const node of nodes) { for (const injectionPoint of this.grammar.injectionPointsByType[ node.type ]) { const languageName = injectionPoint.language(node); if (!languageName) continue; const grammar = this.languageMode.grammarForLanguageString( languageName ); if (!grammar) continue; const contentNodes = injectionPoint.content(node); if (!contentNodes) continue; const injectionNodes = [].concat(contentNodes); if (!injectionNodes.length) continue; const injectionRange = rangeForNode(node); let marker; for ( let i = existingInjectionMarkerIndex, n = existingInjectionMarkers.length; i < n; i++ ) { const existingMarker = existingInjectionMarkers[i]; const comparison = existingMarker.getRange().compare(injectionRange); if (comparison > 0) { break; } else if (comparison === 0) { existingInjectionMarkerIndex = i; if (existingMarker.languageLayer.grammar === grammar) { marker = existingMarker; break; } } else { existingInjectionMarkerIndex = i; } } if (!marker) { marker = this.languageMode.injectionsMarkerLayer.markRange( injectionRange ); marker.languageLayer = new LanguageLayer( marker, this.languageMode, grammar, this.depth + 1 ); marker.parentLanguageLayer = this; } markersToUpdate.set( marker, new NodeRangeSet( nodeRangeSet, injectionNodes, injectionPoint.newlinesBetween, injectionPoint.includeChildren ) ); } } for (const marker of existingInjectionMarkers) { if (!markersToUpdate.has(marker)) { this.languageMode.emitRangeUpdate(marker.getRange()); marker.languageLayer.destroy(); } } if (markersToUpdate.size > 0) { const promises = []; for (const [marker, nodeRangeSet] of markersToUpdate) { promises.push(marker.languageLayer.update(nodeRangeSet)); } return Promise.all(promises); } } _treeEditForBufferChange(start, oldEnd, newEnd, oldText, newText) { const startIndex = this.languageMode.buffer.characterIndexForPosition( start ); return { startIndex, oldEndIndex: startIndex + oldText.length, newEndIndex: startIndex + newText.length, startPosition: start, oldEndPosition: oldEnd, newEndPosition: newEnd }; } } class HighlightIterator { constructor(languageMode) { this.languageMode = languageMode; this.iterators = null; } seek(targetPosition, endRow) { const injectionMarkers = this.languageMode.injectionsMarkerLayer.findMarkers( { intersectsRange: new Range(targetPosition, new Point(endRow + 1, 0)) } ); const containingTags = []; const containingTagStartIndices = []; const targetIndex = this.languageMode.buffer.characterIndexForPosition( targetPosition ); this.iterators = []; const iterator = this.languageMode.rootLanguageLayer.buildHighlightIterator(); if (iterator.seek(targetIndex, containingTags, containingTagStartIndices)) { this.iterators.push(iterator); } // Populate the iterators array with all of the iterators whose syntax // trees span the given position. for (const marker of injectionMarkers) { const iterator = marker.languageLayer.buildHighlightIterator(); if ( iterator.seek(targetIndex, containingTags, containingTagStartIndices) ) { this.iterators.push(iterator); } } // Sort the iterators so that the last one in the array is the earliest // in the document, and represents the current position. this.iterators.sort((a, b) => b.compare(a)); this.detectCoveredScope(); return containingTags; } moveToSuccessor() { // Advance the earliest layer iterator to its next scope boundary. let leader = last(this.iterators); // Maintain the sorting of the iterators by their position in the document. if (leader.moveToSuccessor()) { const leaderIndex = this.iterators.length - 1; let i = leaderIndex; while (i > 0 && this.iterators[i - 1].compare(leader) < 0) i--; if (i < leaderIndex) { this.iterators.splice(i, 0, this.iterators.pop()); } } else { // If the layer iterator was at the end of its syntax tree, then remove // it from the array. this.iterators.pop(); } this.detectCoveredScope(); } // Detect whether or not another more deeply-nested language layer has a // scope boundary at this same position. If so, the current language layer's // scope boundary should not be reported. detectCoveredScope() { const layerCount = this.iterators.length; if (layerCount > 1) { const first = this.iterators[layerCount - 1]; const next = this.iterators[layerCount - 2]; if ( next.offset === first.offset && next.atEnd === first.atEnd && next.depth > first.depth && !next.isAtInjectionBoundary() ) { this.currentScopeIsCovered = true; return; } } this.currentScopeIsCovered = false; } getPosition() { const iterator = last(this.iterators); if (iterator) { return iterator.getPosition(); } else { return Point.INFINITY; } } getCloseScopeIds() { const iterator = last(this.iterators); if (iterator && !this.currentScopeIsCovered) { return iterator.getCloseScopeIds(); } return []; } getOpenScopeIds() { const iterator = last(this.iterators); if (iterator && !this.currentScopeIsCovered) { return iterator.getOpenScopeIds(); } return []; } logState() { const iterator = last(this.iterators); if (iterator && iterator.treeCursor) { console.log( iterator.getPosition(), iterator.treeCursor.nodeType, `depth=${iterator.languageLayer.depth}`, new Range( iterator.languageLayer.tree.rootNode.startPosition, iterator.languageLayer.tree.rootNode.endPosition ).toString() ); if (this.currentScopeIsCovered) { console.log('covered'); } else { console.log( 'close', iterator.closeTags.map(id => this.languageMode.grammar.scopeNameForScopeId(id) ) ); console.log( 'open', iterator.openTags.map(id => this.languageMode.grammar.scopeNameForScopeId(id) ) ); } } } } class LayerHighlightIterator { constructor(languageLayer, treeCursor) { this.languageLayer = languageLayer; this.depth = this.languageLayer.depth; // The iterator is always positioned at either the start or the end of some node // in the syntax tree. this.atEnd = false; this.treeCursor = treeCursor; this.offset = 0; // In order to determine which selectors match its current node, the iterator maintains // a list of the current node's ancestors. Because the selectors can use the `:nth-child` // pseudo-class, each node's child index is also stored. this.containingNodeTypes = []; this.containingNodeChildIndices = []; this.containingNodeEndIndices = []; // At any given position, the iterator exposes the list of class names that should be // *ended* at its current position and the list of class names that should be *started* // at its current position. this.closeTags = []; this.openTags = []; } seek(targetIndex, containingTags, containingTagStartIndices) { while (this.treeCursor.gotoParent()) {} this.atEnd = true; this.closeTags.length = 0; this.openTags.length = 0; this.containingNodeTypes.length = 0; this.containingNodeChildIndices.length = 0; this.containingNodeEndIndices.length = 0; const containingTagEndIndices = []; if (targetIndex >= this.treeCursor.endIndex) { return false; } let childIndex = -1; for (;;) { this.containingNodeTypes.push(this.treeCursor.nodeType); this.containingNodeChildIndices.push(childIndex); this.containingNodeEndIndices.push(this.treeCursor.endIndex); const scopeId = this._currentScopeId(); if (scopeId) { if (this.treeCursor.startIndex < targetIndex) { insertContainingTag( scopeId, this.treeCursor.startIndex, containingTags, containingTagStartIndices ); containingTagEndIndices.push(this.treeCursor.endIndex); } else { this.atEnd = false; this.openTags.push(scopeId); this._moveDown(); break; } } childIndex = this.treeCursor.gotoFirstChildForIndex(targetIndex); if (childIndex === null) break; if (this.treeCursor.startIndex >= targetIndex) this.atEnd = false; } if (this.atEnd) { this.offset = this.treeCursor.endIndex; for (let i = 0, { length } = containingTags; i < length; i++) { if (containingTagEndIndices[i] === this.offset) { this.closeTags.push(containingTags[i]); } } } else { this.offset = this.treeCursor.startIndex; } return true; } moveToSuccessor() { this.closeTags.length = 0; this.openTags.length = 0; while (!this.closeTags.length && !this.openTags.length) { if (this.atEnd) { if (this._moveRight()) { const scopeId = this._currentScopeId(); if (scopeId) this.openTags.push(scopeId); this.atEnd = false; this._moveDown(); } else if (this._moveUp(true)) { this.atEnd = true; } else { return false; } } else if (!this._moveDown()) { const scopeId = this._currentScopeId(); if (scopeId) this.closeTags.push(scopeId); this.atEnd = true; this._moveUp(false); } } if (this.atEnd) { this.offset = this.treeCursor.endIndex; } else { this.offset = this.treeCursor.startIndex; } return true; } getPosition() { if (this.atEnd) { return this.treeCursor.endPosition; } else { return this.treeCursor.startPosition; } } compare(other) { const result = this.offset - other.offset; if (result !== 0) return result; if (this.atEnd && !other.atEnd) return -1; if (other.atEnd && !this.atEnd) return 1; return this.languageLayer.depth - other.languageLayer.depth; } getCloseScopeIds() { return this.closeTags.slice(); } getOpenScopeIds() { return this.openTags.slice(); } isAtInjectionBoundary() { return this.containingNodeTypes.length === 1; } // Private methods _moveUp(atLastChild) { let result = false; const { endIndex } = this.treeCursor; let depth = this.containingNodeEndIndices.length; // The iterator should not move up until it has visited all of the children of this node. while ( depth > 1 && (atLastChild || this.containingNodeEndIndices[depth - 2] === endIndex) ) { atLastChild = false; result = true; this.treeCursor.gotoParent(); this.containingNodeTypes.pop(); this.containingNodeChildIndices.pop(); this.containingNodeEndIndices.pop(); --depth; const scopeId = this._currentScopeId(); if (scopeId) this.closeTags.push(scopeId); } return result; } _moveDown() { let result = false; const { startIndex } = this.treeCursor; // Once the iterator has found a scope boundary, it needs to stay at the same // position, so it should not move down if the first child node starts later than the // current node. while (this.treeCursor.gotoFirstChild()) { if ( (this.closeTags.length || this.openTags.length) && this.treeCursor.startIndex > startIndex ) { this.treeCursor.gotoParent(); break; } result = true; this.containingNodeTypes.push(this.treeCursor.nodeType); this.containingNodeChildIndices.push(0); this.containingNodeEndIndices.push(this.treeCursor.endIndex); const scopeId = this._currentScopeId(); if (scopeId) this.openTags.push(scopeId); } return result; } _moveRight() { if (this.treeCursor.gotoNextSibling()) { const depth = this.containingNodeTypes.length; this.containingNodeTypes[depth - 1] = this.treeCursor.nodeType; this.containingNodeChildIndices[depth - 1]++; this.containingNodeEndIndices[depth - 1] = this.treeCursor.endIndex; return true; } } _currentScopeId() { const value = this.languageLayer.grammar.scopeMap.get( this.containingNodeTypes, this.containingNodeChildIndices, this.treeCursor.nodeIsNamed ); const scopeName = applyLeafRules(value, this.treeCursor); const node = this.treeCursor.currentNode; if (!node.childCount) { return this.languageLayer.languageMode.grammar.idForScope( scopeName, node.text ); } else if (scopeName) { return this.languageLayer.languageMode.grammar.idForScope(scopeName); } } } const applyLeafRules = (rules, cursor) => { if (!rules || typeof rules === 'string') return rules; if (Array.isArray(rules)) { for (let i = 0, { length } = rules; i !== length; ++i) { const result = applyLeafRules(rules[i], cursor); if (result) return result; } return undefined; } if (typeof rules === 'object') { if (rules.exact) { return cursor.nodeText === rules.exact ? applyLeafRules(rules.scopes, cursor) : undefined; } if (rules.match) { return rules.match.test(cursor.nodeText) ? applyLeafRules(rules.scopes, cursor) : undefined; } } }; class NodeCursorAdaptor { get nodeText() { return this.node.text; } } class NullLanguageModeHighlightIterator { seek() { return []; } compare() { return 1; } moveToSuccessor() {} getPosition() { return Point.INFINITY; } getOpenScopeIds() { return []; } getCloseScopeIds() { return []; } } class NullLayerHighlightIterator { seek() { return null; } compare() { return 1; } moveToSuccessor() {} getPosition() { return Point.INFINITY; } getOpenScopeIds() { return []; } getCloseScopeIds() { return []; } } class NodeRangeSet { constructor(previous, nodes, newlinesBetween, includeChildren) { this.previous = previous; this.nodes = nodes; this.newlinesBetween = newlinesBetween; this.includeChildren = includeChildren; } getRanges(buffer) { const previousRanges = this.previous && this.previous.getRanges(buffer); const result = []; for (const node of this.nodes) { let position = node.startPosition; let index = node.startIndex; if (!this.includeChildren) { for (const child of node.children) { const nextIndex = child.startIndex; if (nextIndex > index) { this._pushRange(buffer, previousRanges, result, { startIndex: index, endIndex: nextIndex, startPosition: position, endPosition: child.startPosition }); } position = child.endPosition; index = child.endIndex; } } if (node.endIndex > index) { this._pushRange(buffer, previousRanges, result, { startIndex: index, endIndex: node.endIndex, startPosition: position, endPosition: node.endPosition }); } } return result; } _pushRange(buffer, previousRanges, newRanges, newRange) { if (!previousRanges) { if (this.newlinesBetween) { const { startIndex, startPosition } = newRange; this._ensureNewline(buffer, newRanges, startIndex, startPosition); } newRanges.push(newRange); return; } for (const previousRange of previousRanges) { if (previousRange.endIndex <= newRange.startIndex) continue; if (previousRange.startIndex >= newRange.endIndex) break; const startIndex = Math.max( previousRange.startIndex, newRange.startIndex ); const endIndex = Math.min(previousRange.endIndex, newRange.endIndex); const startPosition = Point.max( previousRange.startPosition, newRange.startPosition ); const endPosition = Point.min( previousRange.endPosition, newRange.endPosition ); if (this.newlinesBetween) { this._ensureNewline(buffer, newRanges, startIndex, startPosition); } newRanges.push({ startIndex, endIndex, startPosition, endPosition }); } } // For injection points with `newlinesBetween` enabled, ensure that a // newline is included between each disjoint range. _ensureNewline(buffer, newRanges, startIndex, startPosition) { const lastRange = newRanges[newRanges.length - 1]; if (lastRange && lastRange.endPosition.row < startPosition.row) { newRanges.push({ startPosition: new Point( startPosition.row - 1, buffer.lineLengthForRow(startPosition.row - 1) ), endPosition: new Point(startPosition.row, 0), startIndex: startIndex - startPosition.column - 1, endIndex: startIndex - startPosition.column }); } } } function insertContainingTag(tag, index, tags, indices) { const i = indices.findIndex(existingIndex => existingIndex > index); if (i === -1) { tags.push(tag); indices.push(index); } else { tags.splice(i, 0, tag); indices.splice(i, 0, index); } } // Return true iff `mouse` is smaller than `house`. Only correct if // mouse and house overlap. // // * `mouse` {Range} // * `house` {Range} function rangeIsSmaller(mouse, house) { if (!house) return true; const mvec = vecFromRange(mouse); const hvec = vecFromRange(house); return Point.min(mvec, hvec) === mvec; } function vecFromRange({ start, end }) { return end.translate(start.negate()); } function rangeForNode(node) { return new Range(node.startPosition, node.endPosition); } function nodeContainsIndices(node, start, end) { if (node.startIndex < start) return node.endIndex >= end; if (node.startIndex === start) return node.endIndex > end; return false; } function nodeIsSmaller(left, right) { if (!left) return false; if (!right) return true; return left.endIndex - left.startIndex < right.endIndex - right.startIndex; } function last(array) { return array[array.length - 1]; } function hasMatchingFoldSpec(specs, node) { return specs.some( ({ type, named }) => type === node.type && named === node.isNamed ); } // TODO: Remove this once TreeSitterLanguageMode implements its own auto-indent system. [ '_suggestedIndentForLineWithScopeAtBufferRow', 'suggestedIndentForEditedBufferRow', 'increaseIndentRegexForScopeDescriptor', 'decreaseIndentRegexForScopeDescriptor', 'decreaseNextIndentRegexForScopeDescriptor', 'regexForPattern', 'getNonWordCharacters' ].forEach(methodName => { TreeSitterLanguageMode.prototype[methodName] = TextMateLanguageMode.prototype[methodName]; }); TreeSitterLanguageMode.LanguageLayer = LanguageLayer; TreeSitterLanguageMode.prototype.syncTimeoutMicros = 1000; module.exports = TreeSitterLanguageMode; ================================================ FILE: src/typescript.js ================================================ 'use strict'; const _ = require('underscore-plus'); const crypto = require('crypto'); const path = require('path'); const defaultOptions = { target: 1, module: 'commonjs', sourceMap: true }; let TypeScriptSimple = null; let typescriptVersionDir = null; exports.shouldCompile = function() { return true; }; exports.getCachePath = function(sourceCode) { if (typescriptVersionDir == null) { const version = require('typescript-simple/package.json').version; typescriptVersionDir = path.join( 'ts', createVersionAndOptionsDigest(version, defaultOptions) ); } return path.join( typescriptVersionDir, crypto .createHash('sha1') .update(sourceCode, 'utf8') .digest('hex') + '.js' ); }; exports.compile = function(sourceCode, filePath) { if (!TypeScriptSimple) { TypeScriptSimple = require('typescript-simple').TypeScriptSimple; } if (process.platform === 'win32') { filePath = 'file:///' + path.resolve(filePath).replace(/\\/g, '/'); } const options = _.defaults({ filename: filePath }, defaultOptions); return new TypeScriptSimple(options, false).compile(sourceCode, filePath); }; function createVersionAndOptionsDigest(version, options) { return crypto .createHash('sha1') .update('typescript', 'utf8') .update('\0', 'utf8') .update(version, 'utf8') .update('\0', 'utf8') .update(JSON.stringify(options), 'utf8') .digest('hex'); } ================================================ FILE: src/update-process-env.js ================================================ const fs = require('fs'); const childProcess = require('child_process'); const ENVIRONMENT_VARIABLES_TO_PRESERVE = new Set([ 'NODE_ENV', 'NODE_PATH', 'ATOM_HOME', 'ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT' ]); const PLATFORMS_KNOWN_TO_WORK = new Set(['darwin', 'linux']); // Shell command that returns env var=value lines separated by \0s so that // newlines are handled properly. Note: need to use %c to inject the \0s // to work with some non GNU awks. const ENV_COMMAND = 'command awk \'BEGIN{for(v in ENVIRON) printf("%s=%s%c", v, ENVIRON[v], 0)}\''; async function updateProcessEnv(launchEnv) { let envToAssign; if (launchEnv) { if (shouldGetEnvFromShell(launchEnv)) { envToAssign = await getEnvFromShell(launchEnv); } else if (launchEnv.PWD || launchEnv.PROMPT || launchEnv.PSModulePath) { envToAssign = launchEnv; } } if (envToAssign) { for (let key in process.env) { if (!ENVIRONMENT_VARIABLES_TO_PRESERVE.has(key)) { delete process.env[key]; } } for (let key in envToAssign) { if ( !ENVIRONMENT_VARIABLES_TO_PRESERVE.has(key) || (!process.env[key] && envToAssign[key]) ) { process.env[key] = envToAssign[key]; } } if (envToAssign.ATOM_HOME && fs.existsSync(envToAssign.ATOM_HOME)) { process.env.ATOM_HOME = envToAssign.ATOM_HOME; } } } function shouldGetEnvFromShell(env) { if (!PLATFORMS_KNOWN_TO_WORK.has(process.platform)) { return false; } if (!env || !env.SHELL || env.SHELL.trim() === '') { return false; } const disableSellingOut = env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT || process.env.ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT; if (disableSellingOut === 'true') { return false; } return true; } async function getEnvFromShell(env) { let { stdout, error } = await new Promise(resolve => { let child; let error; let stdout = ''; let done = false; const cleanup = () => { if (!done && child) { child.kill(); done = true; } }; process.once('exit', cleanup); setTimeout(() => { cleanup(); }, 5000); child = childProcess.spawn(env.SHELL, ['-ilc', ENV_COMMAND], { encoding: 'utf8', detached: true, stdio: ['ignore', 'pipe', process.stderr] }); const buffers = []; child.on('error', e => { done = true; error = e; }); child.stdout.on('data', data => { buffers.push(data); }); child.on('close', (code, signal) => { done = true; process.removeListener('exit', cleanup); if (buffers.length) { stdout = Buffer.concat(buffers).toString('utf8'); } resolve({ stdout, error }); }); }); if (error) { if (error.handle) { error.handle(); } console.log( 'warning: ' + env.SHELL + ' -ilc "' + ENV_COMMAND + '" failed with signal (' + error.signal + ')' ); console.log(error); } if (!stdout || stdout.trim() === '') { return null; } let result = {}; for (let line of stdout.split('\0')) { if (line.includes('=')) { let components = line.split('='); let key = components.shift(); let value = components.join('='); result[key] = value; } } return result; } module.exports = { updateProcessEnv, shouldGetEnvFromShell }; ================================================ FILE: src/uri-handler-registry.js ================================================ 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 package 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); }); } async 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; await 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; } }; ================================================ FILE: src/view-registry.js ================================================ 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 = this.documentWriters.shift(); while (writer) { writer(); writer = this.documentWriters.shift(); } let reader = this.documentReaders.shift(); this.documentReadInProgress = true; while (reader) { reader(); reader = this.documentReaders.shift(); } this.documentReadInProgress = false; // process updates requested as a result of reads writer = this.documentWriters.shift(); while (writer) { writer(); writer = this.documentWriters.shift(); } if (resolveNextUpdatePromise) { resolveNextUpdatePromise(); } } }; ================================================ FILE: src/window-event-handler.js ================================================ const { Disposable, CompositeDisposable } = require('event-kit'); const listen = require('./delegated-listener'); const { debounce } = require('underscore-plus'); // 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.handleWindowResize = this.handleWindowResize.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.window, 'resize', debounce(this.handleWindowResize, 500) ); 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(); } handleWindowResize() { 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] !== '#') { if (/^https?:\/\//.test(uri)) { this.applicationDelegate.openExternal(uri); } else if (uri.startsWith('atom://')) { this.atomEnvironment.uriHandlerRegistry.handleURI(uri); } } } handleFormSubmit(event) { // Prevent form submits from changing the current window's URL event.preventDefault(); } handleDocumentContextmenu(event) { event.preventDefault(); this.atomEnvironment.contextMenu.showForEvent(event); } }; ================================================ FILE: src/window.js ================================================ // Public: Measure how long a function takes to run. // // description - A {String} description that will be logged to the console when // the function completes. // fn - A {Function} to measure the duration of. // // Returns the value returned by the given function. window.measure = function(description, fn) { let start = Date.now(); let value = fn(); let result = Date.now() - start; console.log(description, result); return value; }; // Public: Create a dev tools profile for a function. // // description - A {String} description that will be available in the Profiles // tab of the dev tools. // fn - A {Function} to profile. // // Returns the value returned by the given function. window.profile = function(description, fn) { window.measure(description, function() { console.profile(description); let value = fn(); console.profileEnd(description); return value; }); }; ================================================ FILE: src/workspace-center.js ================================================ 'use strict'; const TextEditor = require('./text-editor'); const PaneContainer = require('./pane-container'); // Essential: Represents the workspace at the center of the entire window. module.exports = class WorkspaceCenter { constructor(params) { params.location = 'center'; this.paneContainer = new PaneContainer(params); this.didActivate = params.didActivate; this.paneContainer.onDidActivatePane(() => this.didActivate(this)); this.paneContainer.onDidChangeActivePane(pane => { params.didChangeActivePane(this, pane); }); this.paneContainer.onDidChangeActivePaneItem(item => { params.didChangeActivePaneItem(this, item); }); this.paneContainer.onDidDestroyPaneItem(item => params.didDestroyPaneItem(item) ); } destroy() { this.paneContainer.destroy(); } serialize() { return this.paneContainer.serialize(); } deserialize(state, deserializerManager) { this.paneContainer.deserialize(state, deserializerManager); } activate() { this.getActivePane().activate(); } getLocation() { return 'center'; } setDraggingItem() { // No-op } /* Section: Event Subscription */ // Essential: Invoke the given callback with all current and future text // editors in the workspace center. // // * `callback` {Function} to be called with current and future text editors. // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time // of subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeTextEditors(callback) { for (let textEditor of this.getTextEditors()) { callback(textEditor); } return this.onDidAddTextEditor(({ textEditor }) => callback(textEditor)); } // Essential: Invoke the given callback with all current and future panes items // in the workspace center. // // * `callback` {Function} to be called with current and future pane items. // * `item` An item that is present in {::getPaneItems} at the time of // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePaneItems(callback) { return this.paneContainer.observePaneItems(callback); } // Essential: Invoke the given callback when the active pane item changes. // // Because observers are invoked synchronously, it's important not to perform // any expensive operations via this method. Consider // {::onDidStopChangingActivePaneItem} to delay operations until after changes // stop occurring. // // * `callback` {Function} to be called when the active pane item changes. // * `item` The active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActivePaneItem(callback) { return this.paneContainer.onDidChangeActivePaneItem(callback); } // Essential: Invoke the given callback when the active pane item stops // changing. // // Observers are called asynchronously 100ms after the last active pane item // change. Handling changes here rather than in the synchronous // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly // changing or closing tabs and ensures critical UI feedback, like changing the // highlighted tab, gets priority over work that can be done asynchronously. // // * `callback` {Function} to be called when the active pane item stopts // changing. // * `item` The active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidStopChangingActivePaneItem(callback) { return this.paneContainer.onDidStopChangingActivePaneItem(callback); } // Essential: Invoke the given callback with the current active pane item and // with all future active pane items in the workspace center. // // * `callback` {Function} to be called when the active pane item changes. // * `item` The current active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeActivePaneItem(callback) { return this.paneContainer.observeActivePaneItem(callback); } // Extended: Invoke the given callback when a pane is added to the workspace // center. // // * `callback` {Function} to be called panes are added. // * `event` {Object} with the following keys: // * `pane` The added pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPane(callback) { return this.paneContainer.onDidAddPane(callback); } // Extended: Invoke the given callback before a pane is destroyed in the // workspace center. // // * `callback` {Function} to be called before panes are destroyed. // * `event` {Object} with the following keys: // * `pane` The pane to be destroyed. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onWillDestroyPane(callback) { return this.paneContainer.onWillDestroyPane(callback); } // Extended: Invoke the given callback when a pane is destroyed in the // workspace center. // // * `callback` {Function} to be called panes are destroyed. // * `event` {Object} with the following keys: // * `pane` The destroyed pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroyPane(callback) { return this.paneContainer.onDidDestroyPane(callback); } // Extended: Invoke the given callback with all current and future panes in the // workspace center. // // * `callback` {Function} to be called with current and future panes. // * `pane` A {Pane} that is present in {::getPanes} at the time of // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePanes(callback) { return this.paneContainer.observePanes(callback); } // Extended: Invoke the given callback when the active pane changes. // // * `callback` {Function} to be called when the active pane changes. // * `pane` A {Pane} that is the current return value of {::getActivePane}. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActivePane(callback) { return this.paneContainer.onDidChangeActivePane(callback); } // Extended: Invoke the given callback with the current active pane and when // the active pane changes. // // * `callback` {Function} to be called with the current and future active# // panes. // * `pane` A {Pane} that is the current return value of {::getActivePane}. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeActivePane(callback) { return this.paneContainer.observeActivePane(callback); } // Extended: Invoke the given callback when a pane item is added to the // workspace center. // // * `callback` {Function} to be called when pane items are added. // * `event` {Object} with the following keys: // * `item` The added pane item. // * `pane` {Pane} containing the added item. // * `index` {Number} indicating the index of the added item in its pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPaneItem(callback) { return this.paneContainer.onDidAddPaneItem(callback); } // 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. // * `event` {Object} with the following keys: // * `item` The item to be destroyed. // * `pane` {Pane} containing the item to be destroyed. // * `index` {Number} indicating the index of the item to be destroyed in // its pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onWillDestroyPaneItem(callback) { return this.paneContainer.onWillDestroyPaneItem(callback); } // Extended: Invoke the given callback when a pane item is destroyed. // // * `callback` {Function} to be called when pane items are destroyed. // * `event` {Object} with the following keys: // * `item` The destroyed item. // * `pane` {Pane} containing the destroyed item. // * `index` {Number} indicating the index of the destroyed item in its // pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onDidDestroyPaneItem(callback) { return this.paneContainer.onDidDestroyPaneItem(callback); } // Extended: Invoke the given callback when a text editor is added to the // workspace center. // // * `callback` {Function} to be called when panes are added. // * `event` {Object} with the following keys: // * `textEditor` {TextEditor} that was added. // * `pane` {Pane} containing the added text editor. // * `index` {Number} indicating the index of the added text editor in its // pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddTextEditor(callback) { return this.onDidAddPaneItem(({ item, pane, index }) => { if (item instanceof TextEditor) { callback({ textEditor: item, pane, index }); } }); } /* Section: Pane Items */ // Essential: Get all pane items in the workspace center. // // Returns an {Array} of items. getPaneItems() { return this.paneContainer.getPaneItems(); } // Essential: Get the active {Pane}'s active item. // // Returns an pane item {Object}. getActivePaneItem() { return this.paneContainer.getActivePaneItem(); } // Essential: Get all text editors in the workspace center. // // Returns an {Array} of {TextEditor}s. getTextEditors() { return this.getPaneItems().filter(item => item instanceof TextEditor); } // Essential: Get the active item if it is an {TextEditor}. // // Returns an {TextEditor} or `undefined` if the current active item is not an // {TextEditor}. getActiveTextEditor() { const activeItem = this.getActivePaneItem(); if (activeItem instanceof TextEditor) { return activeItem; } } // Save all pane items. saveAll() { this.paneContainer.saveAll(); } confirmClose(options) { return this.paneContainer.confirmClose(options); } /* Section: Panes */ // Extended: Get all panes in the workspace center. // // Returns an {Array} of {Pane}s. getPanes() { return this.paneContainer.getPanes(); } // Extended: Get the active {Pane}. // // Returns a {Pane}. getActivePane() { return this.paneContainer.getActivePane(); } // Extended: Make the next pane active. activateNextPane() { return this.paneContainer.activateNextPane(); } // Extended: Make the previous pane active. activatePreviousPane() { return this.paneContainer.activatePreviousPane(); } paneForURI(uri) { return this.paneContainer.paneForURI(uri); } paneForItem(item) { return this.paneContainer.paneForItem(item); } // Destroy (close) the active pane. destroyActivePane() { const activePane = this.getActivePane(); if (activePane != null) { activePane.destroy(); } } }; ================================================ FILE: src/workspace-element.js ================================================ 'use strict'; const { ipcRenderer } = require('electron'); const path = require('path'); const fs = require('fs-plus'); const { CompositeDisposable, Disposable } = require('event-kit'); const scrollbarStyle = require('scrollbar-style'); const _ = require('underscore-plus'); class WorkspaceElement extends HTMLElement { connectedCallback() { this.focus(); this.htmlElement = document.querySelector('html'); this.htmlElement.addEventListener('mouseleave', this.handleCenterLeave); } disconnectedCallback() { this.subscriptions.dispose(); this.htmlElement.removeEventListener('mouseleave', this.handleCenterLeave); } initializeContent() { this.classList.add('workspace'); this.setAttribute('tabindex', -1); this.verticalAxis = document.createElement('atom-workspace-axis'); this.verticalAxis.classList.add('vertical'); this.horizontalAxis = document.createElement('atom-workspace-axis'); this.horizontalAxis.classList.add('horizontal'); this.horizontalAxis.appendChild(this.verticalAxis); this.appendChild(this.horizontalAxis); } observeScrollbarStyle() { this.subscriptions.add( scrollbarStyle.observePreferredScrollbarStyle(style => { switch (style) { case 'legacy': this.classList.remove('scrollbars-visible-when-scrolling'); this.classList.add('scrollbars-visible-always'); break; case 'overlay': this.classList.remove('scrollbars-visible-always'); this.classList.add('scrollbars-visible-when-scrolling'); break; } }) ); } observeTextEditorFontConfig() { this.updateGlobalTextEditorStyleSheet(); this.subscriptions.add( this.config.onDidChange( 'editor.fontSize', this.updateGlobalTextEditorStyleSheet.bind(this) ) ); this.subscriptions.add( this.config.onDidChange( 'editor.fontFamily', this.updateGlobalTextEditorStyleSheet.bind(this) ) ); this.subscriptions.add( this.config.onDidChange( 'editor.lineHeight', this.updateGlobalTextEditorStyleSheet.bind(this) ) ); } updateGlobalTextEditorStyleSheet() { const styleSheetSource = `atom-workspace { --editor-font-size: ${this.config.get('editor.fontSize')}px; --editor-font-family: ${this.config.get('editor.fontFamily')}; --editor-line-height: ${this.config.get('editor.lineHeight')}; }`; this.styleManager.addStyleSheet(styleSheetSource, { sourcePath: 'global-text-editor-styles', priority: -1 }); } initialize(model, { config, project, styleManager, viewRegistry }) { this.handleCenterEnter = this.handleCenterEnter.bind(this); this.handleCenterLeave = this.handleCenterLeave.bind(this); this.handleEdgesMouseMove = _.throttle( this.handleEdgesMouseMove.bind(this), 100 ); this.handleDockDragEnd = this.handleDockDragEnd.bind(this); this.handleDragStart = this.handleDragStart.bind(this); this.handleDragEnd = this.handleDragEnd.bind(this); this.handleDrop = this.handleDrop.bind(this); this.model = model; this.viewRegistry = viewRegistry; this.project = project; this.config = config; this.styleManager = styleManager; 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(() => { this.paneContainer.removeEventListener( 'mouseenter', this.handleCenterEnter ); this.paneContainer.removeEventListener( 'mouseleave', this.handleCenterLeave ); window.removeEventListener('mousemove', this.handleEdgesMouseMove); window.removeEventListener('dragend', this.handleDockDragEnd); window.removeEventListener('dragstart', this.handleDragStart); window.removeEventListener('dragend', this.handleDragEnd, true); window.removeEventListener('drop', this.handleDrop, true); }), ...[ this.model.getLeftDock(), this.model.getRightDock(), this.model.getBottomDock() ].map(dock => dock.onDidChangeHovered(hovered => { if (hovered) this.hoveredDock = dock; else if (dock === this.hoveredDock) this.hoveredDock = null; this.checkCleanupDockHoverEvents(); }) ) ); this.initializeContent(); this.observeScrollbarStyle(); this.observeTextEditorFontConfig(); this.paneContainer = this.model.getCenter().paneContainer.getElement(); this.verticalAxis.appendChild(this.paneContainer); this.addEventListener('focus', this.handleFocus.bind(this)); this.addEventListener('mousewheel', this.handleMousewheel.bind(this), { capture: true }); window.addEventListener('dragstart', this.handleDragStart); window.addEventListener('mousemove', this.handleEdgesMouseMove); this.panelContainers = { top: this.model.panelContainers.top.getElement(), left: this.model.panelContainers.left.getElement(), right: this.model.panelContainers.right.getElement(), bottom: this.model.panelContainers.bottom.getElement(), header: this.model.panelContainers.header.getElement(), footer: this.model.panelContainers.footer.getElement(), modal: this.model.panelContainers.modal.getElement() }; this.horizontalAxis.insertBefore( this.panelContainers.left, this.verticalAxis ); this.horizontalAxis.appendChild(this.panelContainers.right); this.verticalAxis.insertBefore( this.panelContainers.top, this.paneContainer ); this.verticalAxis.appendChild(this.panelContainers.bottom); this.insertBefore(this.panelContainers.header, this.horizontalAxis); this.appendChild(this.panelContainers.footer); this.appendChild(this.panelContainers.modal); this.paneContainer.addEventListener('mouseenter', this.handleCenterEnter); this.paneContainer.addEventListener('mouseleave', this.handleCenterLeave); return this; } destroy() { this.subscriptions.dispose(); } getModel() { return this.model; } handleDragStart(event) { if (!isTab(event.target)) return; const { item } = event.target; if (!item) return; this.model.setDraggingItem(item); window.addEventListener('dragend', this.handleDragEnd, { capture: true }); window.addEventListener('drop', this.handleDrop, { capture: true }); } handleDragEnd(event) { this.dragEnded(); } handleDrop(event) { this.dragEnded(); } dragEnded() { this.model.setDraggingItem(null); window.removeEventListener('dragend', this.handleDragEnd, true); window.removeEventListener('drop', this.handleDrop, true); } handleCenterEnter(event) { // Just re-entering the center isn't enough to hide the dock toggle buttons, since they poke // into the center and we want to give an affordance. this.cursorInCenter = true; this.checkCleanupDockHoverEvents(); } handleCenterLeave(event) { // If the cursor leaves the center, we start listening to determine whether one of the docs is // being hovered. this.cursorInCenter = false; this.updateHoveredDock({ x: event.pageX, y: event.pageY }); window.addEventListener('dragend', this.handleDockDragEnd); } handleEdgesMouseMove(event) { this.updateHoveredDock({ x: event.pageX, y: event.pageY }); } handleDockDragEnd(event) { this.updateHoveredDock({ x: event.pageX, y: event.pageY }); } updateHoveredDock(mousePosition) { // If we haven't left the currently hovered dock, don't change anything. if ( this.hoveredDock && this.hoveredDock.pointWithinHoverArea(mousePosition, true) ) return; const docks = [ this.model.getLeftDock(), this.model.getRightDock(), this.model.getBottomDock() ]; const nextHoveredDock = docks.find( dock => dock !== this.hoveredDock && dock.pointWithinHoverArea(mousePosition) ); docks.forEach(dock => { dock.setHovered(dock === nextHoveredDock); }); } checkCleanupDockHoverEvents() { if (this.cursorInCenter && !this.hoveredDock) { window.removeEventListener('dragend', this.handleDockDragEnd); } } handleMousewheel(event) { if ( event.ctrlKey && this.config.get('editor.zoomFontWhenCtrlScrolling') && event.target.closest('atom-text-editor') != null ) { if (event.wheelDeltaY > 0) { this.model.increaseFontSize(); } else if (event.wheelDeltaY < 0) { this.model.decreaseFontSize(); } event.preventDefault(); event.stopPropagation(); } } handleFocus(event) { this.model.getActivePane().activate(); } focusPaneViewAbove() { this.focusPaneViewInDirection('above'); } focusPaneViewBelow() { this.focusPaneViewInDirection('below'); } focusPaneViewOnLeft() { this.focusPaneViewInDirection('left'); } focusPaneViewOnRight() { this.focusPaneViewInDirection('right'); } focusPaneViewInDirection(direction, pane) { const activePane = this.model.getActivePane(); const paneToFocus = this.nearestVisiblePaneInDirection( direction, activePane ); paneToFocus && paneToFocus.focus(); } moveActiveItemToPaneAbove(params) { this.moveActiveItemToNearestPaneInDirection('above', params); } moveActiveItemToPaneBelow(params) { this.moveActiveItemToNearestPaneInDirection('below', params); } moveActiveItemToPaneOnLeft(params) { this.moveActiveItemToNearestPaneInDirection('left', params); } moveActiveItemToPaneOnRight(params) { this.moveActiveItemToNearestPaneInDirection('right', params); } moveActiveItemToNearestPaneInDirection(direction, params) { const activePane = this.model.getActivePane(); const nearestPaneView = this.nearestVisiblePaneInDirection( direction, activePane ); if (nearestPaneView == null) { return; } if (params && params.keepOriginal) { activePane .getContainer() .copyActiveItemToPane(nearestPaneView.getModel()); } else { activePane .getContainer() .moveActiveItemToPane(nearestPaneView.getModel()); } nearestPaneView.focus(); } nearestVisiblePaneInDirection(direction, pane) { const distance = function(pointA, pointB) { const x = pointB.x - pointA.x; const y = pointB.y - pointA.y; return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); }; const paneView = pane.getElement(); const box = this.boundingBoxForPaneView(paneView); const paneViews = atom.workspace .getVisiblePanes() .map(otherPane => otherPane.getElement()) .filter(otherPaneView => { const otherBox = this.boundingBoxForPaneView(otherPaneView); switch (direction) { case 'left': return otherBox.right.x <= box.left.x; case 'right': return otherBox.left.x >= box.right.x; case 'above': return otherBox.bottom.y <= box.top.y; case 'below': return otherBox.top.y >= box.bottom.y; } }) .sort((paneViewA, paneViewB) => { const boxA = this.boundingBoxForPaneView(paneViewA); const boxB = this.boundingBoxForPaneView(paneViewB); switch (direction) { case 'left': return ( distance(box.left, boxA.right) - distance(box.left, boxB.right) ); case 'right': return ( distance(box.right, boxA.left) - distance(box.right, boxB.left) ); case 'above': return ( distance(box.top, boxA.bottom) - distance(box.top, boxB.bottom) ); case 'below': return ( distance(box.bottom, boxA.top) - distance(box.bottom, boxB.top) ); } }); return paneViews[0]; } boundingBoxForPaneView(paneView) { const boundingBox = paneView.getBoundingClientRect(); return { left: { x: boundingBox.left, y: boundingBox.top }, right: { x: boundingBox.right, y: boundingBox.top }, top: { x: boundingBox.left, y: boundingBox.top }, bottom: { x: boundingBox.left, y: boundingBox.bottom } }; } runPackageSpecs(options = {}) { const activePaneItem = this.model.getActivePaneItem(); const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null; let projectPath; if (activePath != null) { [projectPath] = this.project.relativizePath(activePath); } else { [projectPath] = this.project.getPaths(); } if (projectPath) { let specPath = path.join(projectPath, 'spec'); const testPath = path.join(projectPath, 'test'); if (!fs.existsSync(specPath) && fs.existsSync(testPath)) { specPath = testPath; } ipcRenderer.send('run-package-specs', specPath, options); } } runBenchmarks() { const activePaneItem = this.model.getActivePaneItem(); const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null; let projectPath; if (activePath) { [projectPath] = this.project.relativizePath(activePath); } else { [projectPath] = this.project.getPaths(); } if (projectPath) { ipcRenderer.send('run-benchmarks', path.join(projectPath, 'benchmarks')); } } } function isTab(element) { let el = element; while (el != null) { if (el.getAttribute && el.getAttribute('is') === 'tabs-tab') return true; el = el.parentElement; } return false; } window.customElements.define('atom-workspace', WorkspaceElement); function createWorkspaceElement() { return document.createElement('atom-workspace'); } module.exports = { createWorkspaceElement }; ================================================ FILE: src/workspace.js ================================================ const _ = require('underscore-plus'); const url = require('url'); const path = require('path'); const { Emitter, Disposable, CompositeDisposable } = require('event-kit'); const fs = require('fs-plus'); const { Directory } = require('pathwatcher'); const Grim = require('grim'); const DefaultDirectorySearcher = require('./default-directory-searcher'); const RipgrepDirectorySearcher = require('./ripgrep-directory-searcher'); const Dock = require('./dock'); const Model = require('./model'); const StateStore = require('./state-store'); const TextEditor = require('./text-editor'); const Panel = require('./panel'); const PanelContainer = require('./panel-container'); const Task = require('./task'); const WorkspaceCenter = require('./workspace-center'); const { createWorkspaceElement } = require('./workspace-element'); const STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY = 100; const ALL_LOCATIONS = ['center', 'left', 'right', 'bottom']; // Essential: Represents the state of the user interface for the entire window. // An instance of this class is available via the `atom.workspace` global. // // Interact with this object to open files, be notified of current and future // editors, and manipulate panes. To add panels, use {Workspace::addTopPanel} // and friends. // // ## Workspace Items // // The term "item" refers to anything that can be displayed // in a pane within the workspace, either in the {WorkspaceCenter} or in one // of the three {Dock}s. The workspace expects items to conform to the // following interface: // // ### Required Methods // // #### `getTitle()` // // Returns a {String} containing the title of the item to display on its // associated tab. // // ### Optional Methods // // #### `getElement()` // // If your item already *is* a DOM element, you do not need to implement this // method. Otherwise it should return the element you want to display to // represent this item. // // #### `destroy()` // // Destroys the item. This will be called when the item is removed from its // parent pane. // // #### `onDidDestroy(callback)` // // Called by the workspace so it can be notified when the item is destroyed. // Must return a {Disposable}. // // #### `serialize()` // // Serialize the state of the item. Must return an object that can be passed to // `JSON.stringify`. The state should include a field called `deserializer`, // which names a deserializer declared in your `package.json`. This method is // invoked on items when serializing the workspace so they can be restored to // the same location later. // // #### `getURI()` // // Returns the URI associated with the item. // // #### `getLongTitle()` // // Returns a {String} containing a longer version of the title to display in // places like the window title or on tabs their short titles are ambiguous. // // #### `onDidChangeTitle(callback)` // // Called by the workspace so it can be notified when the item's title changes. // Must return a {Disposable}. // // #### `getIconName()` // // Return a {String} with the name of an icon. If this method is defined and // returns a string, the item's tab element will be rendered with the `icon` and // `icon-${iconName}` CSS classes. // // ### `onDidChangeIcon(callback)` // // Called by the workspace so it can be notified when the item's icon changes. // Must return a {Disposable}. // // #### `getDefaultLocation()` // // Tells the workspace where your item should be opened in absence of a user // override. Items can appear in the center or in a dock on the left, right, or // bottom of the workspace. // // Returns a {String} with one of the following values: `'center'`, `'left'`, // `'right'`, `'bottom'`. If this method is not defined, `'center'` is the // default. // // #### `getAllowedLocations()` // // Tells the workspace where this item can be moved. Returns an {Array} of one // or more of the following values: `'center'`, `'left'`, `'right'`, or // `'bottom'`. // // #### `isPermanentDockItem()` // // Tells the workspace whether or not this item can be closed by the user by // clicking an `x` on its tab. Use of this feature is discouraged unless there's // a very good reason not to allow users to close your item. Items can be made // permanent *only* when they are contained in docks. Center pane items can // always be removed. Note that it is currently still possible to close dock // items via the `Close Pane` option in the context menu and via Atom APIs, so // you should still be prepared to handle your dock items being destroyed by the // user even if you implement this method. // // #### `save()` // // Saves the item. // // #### `saveAs(path)` // // Saves the item to the specified path. // // #### `getPath()` // // Returns the local path associated with this item. This is only used to set // the initial location of the "save as" dialog. // // #### `isModified()` // // Returns whether or not the item is modified to reflect modification in the // UI. // // #### `onDidChangeModified()` // // Called by the workspace so it can be notified when item's modified status // changes. Must return a {Disposable}. // // #### `copy()` // // Create a copy of the item. If defined, the workspace will call this method to // duplicate the item when splitting panes via certain split commands. // // #### `getPreferredHeight()` // // If this item is displayed in the bottom {Dock}, called by the workspace when // initially displaying the dock to set its height. Once the dock has been // resized by the user, their height will override this value. // // Returns a {Number}. // // #### `getPreferredWidth()` // // If this item is displayed in the left or right {Dock}, called by the // workspace when initially displaying the dock to set its width. Once the dock // has been resized by the user, their width will override this value. // // Returns a {Number}. // // #### `onDidTerminatePendingState(callback)` // // If the workspace is configured to use *pending pane items*, the workspace // will subscribe to this method to terminate the pending state of the item. // Must return a {Disposable}. // // #### `shouldPromptToSave()` // // This method indicates whether Atom should prompt the user to save this item // when the user closes or reloads the window. Returns a boolean. module.exports = class Workspace extends Model { constructor(params) { super(...arguments); this.updateWindowTitle = this.updateWindowTitle.bind(this); this.updateDocumentEdited = this.updateDocumentEdited.bind(this); this.didDestroyPaneItem = this.didDestroyPaneItem.bind(this); this.didChangeActivePaneOnPaneContainer = this.didChangeActivePaneOnPaneContainer.bind( this ); this.didChangeActivePaneItemOnPaneContainer = this.didChangeActivePaneItemOnPaneContainer.bind( this ); this.didActivatePaneContainer = this.didActivatePaneContainer.bind(this); this.enablePersistence = params.enablePersistence; this.packageManager = params.packageManager; this.config = params.config; this.project = params.project; this.notificationManager = params.notificationManager; this.viewRegistry = params.viewRegistry; this.grammarRegistry = params.grammarRegistry; this.applicationDelegate = params.applicationDelegate; this.assert = params.assert; this.deserializerManager = params.deserializerManager; this.textEditorRegistry = params.textEditorRegistry; this.styleManager = params.styleManager; this.draggingItem = false; this.itemLocationStore = new StateStore('AtomPreviousItemLocations', 1); this.emitter = new Emitter(); this.openers = []; this.destroyedItemURIs = []; this.stoppedChangingActivePaneItemTimeout = null; this.scandalDirectorySearcher = new DefaultDirectorySearcher(); this.ripgrepDirectorySearcher = new RipgrepDirectorySearcher(); this.consumeServices(this.packageManager); this.paneContainers = { center: this.createCenter(), left: this.createDock('left'), right: this.createDock('right'), bottom: this.createDock('bottom') }; this.activePaneContainer = this.paneContainers.center; this.hasActiveTextEditor = false; this.panelContainers = { top: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'top' }), left: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'left', dock: this.paneContainers.left }), right: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'right', dock: this.paneContainers.right }), bottom: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'bottom', dock: this.paneContainers.bottom }), header: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'header' }), footer: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'footer' }), modal: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'modal' }) }; this.incoming = new Map(); } get paneContainer() { Grim.deprecate( '`atom.workspace.paneContainer` has always been private, but it is now gone. Please use `atom.workspace.getCenter()` instead and consult the workspace API docs for public methods.' ); return this.paneContainers.center.paneContainer; } getElement() { if (!this.element) { this.element = createWorkspaceElement().initialize(this, { config: this.config, project: this.project, viewRegistry: this.viewRegistry, styleManager: this.styleManager }); } return this.element; } createCenter() { return new WorkspaceCenter({ config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry, didActivate: this.didActivatePaneContainer, didChangeActivePane: this.didChangeActivePaneOnPaneContainer, didChangeActivePaneItem: this.didChangeActivePaneItemOnPaneContainer, didDestroyPaneItem: this.didDestroyPaneItem }); } createDock(location) { return new Dock({ location, config: this.config, applicationDelegate: this.applicationDelegate, deserializerManager: this.deserializerManager, notificationManager: this.notificationManager, viewRegistry: this.viewRegistry, didActivate: this.didActivatePaneContainer, didChangeActivePane: this.didChangeActivePaneOnPaneContainer, didChangeActivePaneItem: this.didChangeActivePaneItemOnPaneContainer, didDestroyPaneItem: this.didDestroyPaneItem }); } reset(packageManager) { this.packageManager = packageManager; this.emitter.dispose(); this.emitter = new Emitter(); this.paneContainers.center.destroy(); this.paneContainers.left.destroy(); this.paneContainers.right.destroy(); this.paneContainers.bottom.destroy(); _.values(this.panelContainers).forEach(panelContainer => { panelContainer.destroy(); }); this.paneContainers = { center: this.createCenter(), left: this.createDock('left'), right: this.createDock('right'), bottom: this.createDock('bottom') }; this.activePaneContainer = this.paneContainers.center; this.hasActiveTextEditor = false; this.panelContainers = { top: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'top' }), left: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'left', dock: this.paneContainers.left }), right: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'right', dock: this.paneContainers.right }), bottom: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'bottom', dock: this.paneContainers.bottom }), header: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'header' }), footer: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'footer' }), modal: new PanelContainer({ viewRegistry: this.viewRegistry, location: 'modal' }) }; this.openers = []; this.destroyedItemURIs = []; if (this.element) { this.element.destroy(); this.element = null; } this.consumeServices(this.packageManager); } initialize() { // we set originalFontSize to avoid breaking packages that might have relied on it this.originalFontSize = this.config.get('defaultFontSize'); this.project.onDidChangePaths(this.updateWindowTitle); this.subscribeToAddedItems(); this.subscribeToMovedItems(); this.subscribeToDockToggling(); } consumeServices({ serviceHub }) { this.directorySearchers = []; serviceHub.consume('atom.directory-searcher', '^0.1.0', provider => this.directorySearchers.unshift(provider) ); } // Called by the Serializable mixin during serialization. serialize() { return { deserializer: 'Workspace', packagesWithActiveGrammars: this.getPackageNamesWithActiveGrammars(), destroyedItemURIs: this.destroyedItemURIs.slice(), // Ensure deserializing 1.17 state with pre 1.17 Atom does not error // TODO: Remove after 1.17 has been on stable for a while paneContainer: { version: 2 }, paneContainers: { center: this.paneContainers.center.serialize(), left: this.paneContainers.left.serialize(), right: this.paneContainers.right.serialize(), bottom: this.paneContainers.bottom.serialize() } }; } deserialize(state, deserializerManager) { const packagesWithActiveGrammars = state.packagesWithActiveGrammars != null ? state.packagesWithActiveGrammars : []; for (let packageName of packagesWithActiveGrammars) { const pkg = this.packageManager.getLoadedPackage(packageName); if (pkg != null) { pkg.loadGrammarsSync(); } } if (state.destroyedItemURIs != null) { this.destroyedItemURIs = state.destroyedItemURIs; } if (state.paneContainers) { this.paneContainers.center.deserialize( state.paneContainers.center, deserializerManager ); this.paneContainers.left.deserialize( state.paneContainers.left, deserializerManager ); this.paneContainers.right.deserialize( state.paneContainers.right, deserializerManager ); this.paneContainers.bottom.deserialize( state.paneContainers.bottom, deserializerManager ); } else if (state.paneContainer) { // TODO: Remove this fallback once a lot of time has passed since 1.17 was released this.paneContainers.center.deserialize( state.paneContainer, deserializerManager ); } this.hasActiveTextEditor = this.getActiveTextEditor() != null; this.updateWindowTitle(); } getPackageNamesWithActiveGrammars() { const packageNames = []; const addGrammar = ({ includedGrammarScopes, packageName } = {}) => { if (!packageName) { return; } // Prevent cycles if (packageNames.indexOf(packageName) !== -1) { return; } packageNames.push(packageName); for (let scopeName of includedGrammarScopes != null ? includedGrammarScopes : []) { addGrammar(this.grammarRegistry.grammarForScopeName(scopeName)); } }; const editors = this.getTextEditors(); for (let editor of editors) { addGrammar(editor.getGrammar()); } if (editors.length > 0) { for (let grammar of this.grammarRegistry.getGrammars()) { if (grammar.injectionSelector) { addGrammar(grammar); } } } return _.uniq(packageNames); } didActivatePaneContainer(paneContainer) { if (paneContainer !== this.getActivePaneContainer()) { this.activePaneContainer = paneContainer; this.didChangeActivePaneItem( this.activePaneContainer.getActivePaneItem() ); this.emitter.emit( 'did-change-active-pane-container', this.activePaneContainer ); this.emitter.emit( 'did-change-active-pane', this.activePaneContainer.getActivePane() ); this.emitter.emit( 'did-change-active-pane-item', this.activePaneContainer.getActivePaneItem() ); } } didChangeActivePaneOnPaneContainer(paneContainer, pane) { if (paneContainer === this.getActivePaneContainer()) { this.emitter.emit('did-change-active-pane', pane); } } didChangeActivePaneItemOnPaneContainer(paneContainer, item) { if (paneContainer === this.getActivePaneContainer()) { this.didChangeActivePaneItem(item); this.emitter.emit('did-change-active-pane-item', item); } if (paneContainer === this.getCenter()) { const hadActiveTextEditor = this.hasActiveTextEditor; this.hasActiveTextEditor = item instanceof TextEditor; if (this.hasActiveTextEditor || hadActiveTextEditor) { const itemValue = this.hasActiveTextEditor ? item : undefined; this.emitter.emit('did-change-active-text-editor', itemValue); } } } didChangeActivePaneItem(item) { this.updateWindowTitle(); this.updateDocumentEdited(); if (this.activeItemSubscriptions) this.activeItemSubscriptions.dispose(); this.activeItemSubscriptions = new CompositeDisposable(); let modifiedSubscription, titleSubscription; if (item != null && typeof item.onDidChangeTitle === 'function') { titleSubscription = item.onDidChangeTitle(this.updateWindowTitle); } else if (item != null && typeof item.on === 'function') { titleSubscription = item.on('title-changed', this.updateWindowTitle); if ( titleSubscription == null || typeof titleSubscription.dispose !== 'function' ) { titleSubscription = new Disposable(() => { item.off('title-changed', this.updateWindowTitle); }); } } if (item != null && typeof item.onDidChangeModified === 'function') { modifiedSubscription = item.onDidChangeModified( this.updateDocumentEdited ); } else if (item != null && typeof item.on === 'function') { modifiedSubscription = item.on( 'modified-status-changed', this.updateDocumentEdited ); if ( modifiedSubscription == null || typeof modifiedSubscription.dispose !== 'function' ) { modifiedSubscription = new Disposable(() => { item.off('modified-status-changed', this.updateDocumentEdited); }); } } if (titleSubscription != null) { this.activeItemSubscriptions.add(titleSubscription); } if (modifiedSubscription != null) { this.activeItemSubscriptions.add(modifiedSubscription); } this.cancelStoppedChangingActivePaneItemTimeout(); this.stoppedChangingActivePaneItemTimeout = setTimeout(() => { this.stoppedChangingActivePaneItemTimeout = null; this.emitter.emit('did-stop-changing-active-pane-item', item); }, STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY); } cancelStoppedChangingActivePaneItemTimeout() { if (this.stoppedChangingActivePaneItemTimeout != null) { clearTimeout(this.stoppedChangingActivePaneItemTimeout); } } setDraggingItem(draggingItem) { _.values(this.paneContainers).forEach(dock => { dock.setDraggingItem(draggingItem); }); } subscribeToAddedItems() { this.onDidAddPaneItem(({ item, pane, index }) => { if (item instanceof TextEditor) { const subscriptions = new CompositeDisposable( this.textEditorRegistry.add(item), this.textEditorRegistry.maintainConfig(item) ); if (!this.project.findBufferForId(item.buffer.id)) { this.project.addBuffer(item.buffer); } item.onDidDestroy(() => { subscriptions.dispose(); }); this.emitter.emit('did-add-text-editor', { textEditor: item, pane, index }); // It's important to call handleGrammarUsed after emitting the did-add event: // if we activate a package between adding the editor to the registry and emitting // the package may receive the editor twice from `observeTextEditors`. // (Note that the item can be destroyed by an `observeTextEditors` handler.) if (!item.isDestroyed()) { subscriptions.add( item.observeGrammar(this.handleGrammarUsed.bind(this)) ); } } }); } subscribeToDockToggling() { const docks = [ this.getLeftDock(), this.getRightDock(), this.getBottomDock() ]; docks.forEach(dock => { dock.onDidChangeVisible(visible => { if (visible) return; const { activeElement } = document; const dockElement = dock.getElement(); if ( dockElement === activeElement || dockElement.contains(activeElement) ) { this.getCenter().activate(); } }); }); } subscribeToMovedItems() { for (const paneContainer of this.getPaneContainers()) { paneContainer.observePanes(pane => { pane.onDidAddItem(({ item }) => { if (typeof item.getURI === 'function' && this.enablePersistence) { const uri = item.getURI(); if (uri) { const location = paneContainer.getLocation(); let defaultLocation; if (typeof item.getDefaultLocation === 'function') { defaultLocation = item.getDefaultLocation(); } defaultLocation = defaultLocation || 'center'; if (location === defaultLocation) { this.itemLocationStore.delete(item.getURI()); } else { this.itemLocationStore.save(item.getURI(), location); } } } }); }); } } // Updates the application's title and proxy icon based on whichever file is // open. updateWindowTitle() { let itemPath, itemTitle, projectPath, representedPath; const appName = atom.getAppName(); const left = this.project.getPaths(); const projectPaths = left != null ? left : []; const item = this.getActivePaneItem(); if (item) { itemPath = typeof item.getPath === 'function' ? item.getPath() : undefined; const longTitle = typeof item.getLongTitle === 'function' ? item.getLongTitle() : undefined; itemTitle = longTitle == null ? typeof item.getTitle === 'function' ? item.getTitle() : undefined : longTitle; projectPath = _.find( projectPaths, projectPath => itemPath === projectPath || (itemPath != null ? itemPath.startsWith(projectPath + path.sep) : undefined) ); } if (itemTitle == null) { itemTitle = 'untitled'; } if (projectPath == null) { projectPath = itemPath ? path.dirname(itemPath) : projectPaths[0]; } if (projectPath != null) { projectPath = fs.tildify(projectPath); } const titleParts = []; if (item != null && projectPath != null) { titleParts.push(itemTitle, projectPath); representedPath = itemPath != null ? itemPath : projectPath; } else if (projectPath != null) { titleParts.push(projectPath); representedPath = projectPath; } else { titleParts.push(itemTitle); representedPath = ''; } if (process.platform !== 'darwin') { titleParts.push(appName); } document.title = titleParts.join(' \u2014 '); this.applicationDelegate.setRepresentedFilename(representedPath); this.emitter.emit('did-change-window-title'); } // On macOS, fades the application window's proxy icon when the current file // has been modified. updateDocumentEdited() { const activePaneItem = this.getActivePaneItem(); const modified = activePaneItem != null && typeof activePaneItem.isModified === 'function' ? activePaneItem.isModified() || false : false; this.applicationDelegate.setWindowDocumentEdited(modified); } /* Section: Event Subscription */ onDidChangeActivePaneContainer(callback) { return this.emitter.on('did-change-active-pane-container', callback); } // Essential: Invoke the given callback with all current and future text // editors in the workspace. // // * `callback` {Function} to be called with current and future text editors. // * `editor` A {TextEditor} that is present in {::getTextEditors} at the time // of subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeTextEditors(callback) { for (let textEditor of this.getTextEditors()) { callback(textEditor); } return this.onDidAddTextEditor(({ textEditor }) => callback(textEditor)); } // Essential: Invoke the given callback with all current and future panes items // in the workspace. // // * `callback` {Function} to be called with current and future pane items. // * `item` An item that is present in {::getPaneItems} at the time of // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePaneItems(callback) { return new CompositeDisposable( ...this.getPaneContainers().map(container => container.observePaneItems(callback) ) ); } // Essential: Invoke the given callback when the active pane item changes. // // Because observers are invoked synchronously, it's important not to perform // any expensive operations via this method. Consider // {::onDidStopChangingActivePaneItem} to delay operations until after changes // stop occurring. // // * `callback` {Function} to be called when the active pane item changes. // * `item` The active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActivePaneItem(callback) { return this.emitter.on('did-change-active-pane-item', callback); } // Essential: Invoke the given callback when the active pane item stops // changing. // // Observers are called asynchronously 100ms after the last active pane item // change. Handling changes here rather than in the synchronous // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly // changing or closing tabs and ensures critical UI feedback, like changing the // highlighted tab, gets priority over work that can be done asynchronously. // // * `callback` {Function} to be called when the active pane item stops // changing. // * `item` The active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidStopChangingActivePaneItem(callback) { return this.emitter.on('did-stop-changing-active-pane-item', callback); } // Essential: Invoke the given callback when a text editor becomes the active // text editor and when there is no longer an active text editor. // // * `callback` {Function} to be called when the active text editor changes. // * `editor` The active {TextEditor} or undefined if there is no longer an // active text editor. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActiveTextEditor(callback) { return this.emitter.on('did-change-active-text-editor', callback); } // Essential: Invoke the given callback with the current active pane item and // with all future active pane items in the workspace. // // * `callback` {Function} to be called when the active pane item changes. // * `item` The current active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeActivePaneItem(callback) { callback(this.getActivePaneItem()); return this.onDidChangeActivePaneItem(callback); } // Essential: Invoke the given callback with the current active text editor // (if any), with all future active text editors, and when there is no longer // an active text editor. // // * `callback` {Function} to be called when the active text editor changes. // * `editor` The active {TextEditor} or undefined if there is not an // active text editor. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeActiveTextEditor(callback) { callback(this.getActiveTextEditor()); return this.onDidChangeActiveTextEditor(callback); } // Essential: Invoke the given callback whenever an item is opened. Unlike // {::onDidAddPaneItem}, observers will be notified for items that are already // present in the workspace when they are reopened. // // * `callback` {Function} to be called whenever an item is opened. // * `event` {Object} with the following keys: // * `uri` {String} representing the opened URI. Could be `undefined`. // * `item` The opened item. // * `pane` The pane in which the item was opened. // * `index` The index of the opened item on its pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidOpen(callback) { return this.emitter.on('did-open', callback); } // Extended: Invoke the given callback when a pane is added to the workspace. // // * `callback` {Function} to be called panes are added. // * `event` {Object} with the following keys: // * `pane` The added pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPane(callback) { return new CompositeDisposable( ...this.getPaneContainers().map(container => container.onDidAddPane(callback) ) ); } // Extended: Invoke the given callback before a pane is destroyed in the // workspace. // // * `callback` {Function} to be called before panes are destroyed. // * `event` {Object} with the following keys: // * `pane` The pane to be destroyed. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onWillDestroyPane(callback) { return new CompositeDisposable( ...this.getPaneContainers().map(container => container.onWillDestroyPane(callback) ) ); } // Extended: Invoke the given callback when a pane is destroyed in the // workspace. // // * `callback` {Function} to be called panes are destroyed. // * `event` {Object} with the following keys: // * `pane` The destroyed pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroyPane(callback) { return new CompositeDisposable( ...this.getPaneContainers().map(container => container.onDidDestroyPane(callback) ) ); } // Extended: Invoke the given callback with all current and future panes in the // workspace. // // * `callback` {Function} to be called with current and future panes. // * `pane` A {Pane} that is present in {::getPanes} at the time of // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observePanes(callback) { return new CompositeDisposable( ...this.getPaneContainers().map(container => container.observePanes(callback) ) ); } // Extended: Invoke the given callback when the active pane changes. // // * `callback` {Function} to be called when the active pane changes. // * `pane` A {Pane} that is the current return value of {::getActivePane}. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeActivePane(callback) { return this.emitter.on('did-change-active-pane', callback); } // Extended: Invoke the given callback with the current active pane and when // the active pane changes. // // * `callback` {Function} to be called with the current and future active# // panes. // * `pane` A {Pane} that is the current return value of {::getActivePane}. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeActivePane(callback) { callback(this.getActivePane()); return this.onDidChangeActivePane(callback); } // Extended: Invoke the given callback when a pane item is added to the // workspace. // // * `callback` {Function} to be called when pane items are added. // * `event` {Object} with the following keys: // * `item` The added pane item. // * `pane` {Pane} containing the added item. // * `index` {Number} indicating the index of the added item in its pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddPaneItem(callback) { return new CompositeDisposable( ...this.getPaneContainers().map(container => container.onDidAddPaneItem(callback) ) ); } // 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. 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. // * `index` {Number} indicating the index of the item to be destroyed in // its pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onWillDestroyPaneItem(callback) { return new CompositeDisposable( ...this.getPaneContainers().map(container => container.onWillDestroyPaneItem(callback) ) ); } // Extended: Invoke the given callback when a pane item is destroyed. // // * `callback` {Function} to be called when pane items are destroyed. // * `event` {Object} with the following keys: // * `item` The destroyed item. // * `pane` {Pane} containing the destroyed item. // * `index` {Number} indicating the index of the destroyed item in its // pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. onDidDestroyPaneItem(callback) { return new CompositeDisposable( ...this.getPaneContainers().map(container => container.onDidDestroyPaneItem(callback) ) ); } // Extended: Invoke the given callback when a text editor is added to the // workspace. // // * `callback` {Function} to be called panes are added. // * `event` {Object} with the following keys: // * `textEditor` {TextEditor} that was added. // * `pane` {Pane} containing the added text editor. // * `index` {Number} indicating the index of the added text editor in its // pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddTextEditor(callback) { return this.emitter.on('did-add-text-editor', callback); } onDidChangeWindowTitle(callback) { return this.emitter.on('did-change-window-title', callback); } /* Section: Opening */ // Essential: Opens the given URI in Atom asynchronously. // If the URI is already open, the existing item for that URI will be // activated. If no URI is given, or no registered opener can open // the URI, a new empty {TextEditor} will be created. // // * `uri` (optional) A {String} containing a URI. // * `options` (optional) {Object} // * `initialLine` A {Number} indicating which row to move the cursor to // initially. Defaults to `0`. // * `initialColumn` A {Number} indicating which column to move the cursor to // initially. Defaults to `0`. // * `split` Either 'left', 'right', 'up' or 'down'. // If 'left', the item will be opened in leftmost pane of the current active pane's row. // If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created. // If 'up', the item will be opened in topmost pane of the current active pane's column. // If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created. // * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on // containing pane. Defaults to `true`. // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} // on containing pane. Defaults to `true`. // * `pending` A {Boolean} indicating whether or not the item should be opened // in a pending state. Existing pending items in a pane are replaced with // new pending items when they are opened. // * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to // activate an existing item for the given URI on any pane. // If `false`, only the active pane will be searched for // an existing item for the same URI. Defaults to `false`. // * `location` (optional) A {String} containing the name of the location // in which this item should be opened (one of "left", "right", "bottom", // or "center"). If omitted, Atom will fall back to the last location in // which a user has placed an item with the same URI or, if this is a new // URI, the default location specified by the item. NOTE: This option // should almost always be omitted to honor user preference. // // Returns a {Promise} that resolves to the {TextEditor} for the file URI. async open(itemOrURI, options = {}) { let uri, item; if (typeof itemOrURI === 'string') { uri = this.project.resolvePath(itemOrURI); } else if (itemOrURI) { item = itemOrURI; if (typeof item.getURI === 'function') uri = item.getURI(); } let resolveItem = () => {}; if (uri) { const incomingItem = this.incoming.get(uri); if (!incomingItem) { this.incoming.set( uri, new Promise(resolve => { resolveItem = resolve; }) ); } else { await incomingItem; } } try { if (!atom.config.get('core.allowPendingPaneItems')) { options.pending = false; } // Avoid adding URLs as recent documents to work-around this Spotlight crash: // https://github.com/atom/atom/issues/10071 if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) { this.applicationDelegate.addRecentDocument(uri); } let pane, itemExistsInWorkspace; // Try to find an existing item in the workspace. if (item || uri) { if (options.pane) { pane = options.pane; } else if (options.searchAllPanes) { pane = item ? this.paneForItem(item) : this.paneForURI(uri); } else { // If an item with the given URI is already in the workspace, assume // that item's pane container is the preferred location for that URI. let container; if (uri) container = this.paneContainerForURI(uri); if (!container) container = this.getActivePaneContainer(); // The `split` option affects where we search for the item. pane = container.getActivePane(); switch (options.split) { case 'left': pane = pane.findLeftmostSibling(); break; case 'right': pane = pane.findRightmostSibling(); break; case 'up': pane = pane.findTopmostSibling(); break; case 'down': pane = pane.findBottommostSibling(); break; } } if (pane) { if (item) { itemExistsInWorkspace = pane.getItems().includes(item); } else { item = pane.itemForURI(uri); itemExistsInWorkspace = item != null; } } } // If we already have an item at this stage, we won't need to do an async // lookup of the URI, so we yield the event loop to ensure this method // is consistently asynchronous. if (item) await Promise.resolve(); if (!itemExistsInWorkspace) { item = item || (await this.createItemForURI(uri, options)); if (!item) return; if (options.pane) { pane = options.pane; } else { let location = options.location; if (!location && !options.split && uri && this.enablePersistence) { location = await this.itemLocationStore.load(uri); } if (!location && typeof item.getDefaultLocation === 'function') { location = item.getDefaultLocation(); } const allowedLocations = typeof item.getAllowedLocations === 'function' ? item.getAllowedLocations() : ALL_LOCATIONS; location = allowedLocations.includes(location) ? location : allowedLocations[0]; const container = this.paneContainers[location] || this.getCenter(); pane = container.getActivePane(); switch (options.split) { case 'left': pane = pane.findLeftmostSibling(); break; case 'right': pane = pane.findOrCreateRightmostSibling(); break; case 'up': pane = pane.findTopmostSibling(); break; case 'down': pane = pane.findOrCreateBottommostSibling(); break; } } } if (!options.pending && pane.getPendingItem() === item) { pane.clearPendingItem(); } this.itemOpened(item); if (options.activateItem === false) { pane.addItem(item, { pending: options.pending }); } else { pane.activateItem(item, { pending: options.pending }); } if (options.activatePane !== false) { pane.activate(); } let initialColumn = 0; let initialLine = 0; if (!Number.isNaN(options.initialLine)) { initialLine = options.initialLine; } if (!Number.isNaN(options.initialColumn)) { initialColumn = options.initialColumn; } if (initialLine >= 0 || initialColumn >= 0) { if (typeof item.setCursorBufferPosition === 'function') { item.setCursorBufferPosition([initialLine, initialColumn]); } if (typeof item.unfoldBufferRow === 'function') { item.unfoldBufferRow(initialLine); } if (typeof item.scrollToBufferPosition === 'function') { item.scrollToBufferPosition([initialLine, initialColumn], { center: true }); } } const index = pane.getActiveItemIndex(); this.emitter.emit('did-open', { uri, pane, item, index }); if (uri) { this.incoming.delete(uri); } } finally { resolveItem(); } return item; } // Essential: Search the workspace for items matching the given URI and hide them. // // * `itemOrURI` The item to hide or a {String} containing the URI // of the item to hide. // // Returns a {Boolean} indicating whether any items were found (and hidden). hide(itemOrURI) { let foundItems = false; // If any visible item has the given URI, hide it for (const container of this.getPaneContainers()) { const isCenter = container === this.getCenter(); if (isCenter || container.isVisible()) { for (const pane of container.getPanes()) { const activeItem = pane.getActiveItem(); const foundItem = activeItem != null && (activeItem === itemOrURI || (typeof activeItem.getURI === 'function' && activeItem.getURI() === itemOrURI)); if (foundItem) { foundItems = true; // We can't really hide the center so we just destroy the item. if (isCenter) { pane.destroyItem(activeItem); } else { container.hide(); } } } } } return foundItems; } // Essential: Search the workspace for items matching the given URI. If any are found, hide them. // Otherwise, open the URL. // // * `itemOrURI` (optional) The item to toggle or a {String} containing the URI // of the item to toggle. // // Returns a Promise that resolves when the item is shown or hidden. toggle(itemOrURI) { if (this.hide(itemOrURI)) { return Promise.resolve(); } else { return this.open(itemOrURI, { searchAllPanes: true }); } } // Open Atom's license in the active pane. openLicense() { return this.open(path.join(process.resourcesPath, 'LICENSE.md')); } // Synchronously open the given URI in the active pane. **Only use this method // in specs. Calling this in production code will block the UI thread and // everyone will be mad at you.** // // * `uri` A {String} containing a URI. // * `options` An optional options {Object} // * `initialLine` A {Number} indicating which row to move the cursor to // initially. Defaults to `0`. // * `initialColumn` A {Number} indicating which column to move the cursor to // initially. Defaults to `0`. // * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on // the containing pane. Defaults to `true`. // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} // on containing pane. Defaults to `true`. openSync(uri_ = '', options = {}) { const { initialLine, initialColumn } = options; const activatePane = options.activatePane != null ? options.activatePane : true; const activateItem = options.activateItem != null ? options.activateItem : true; const uri = this.project.resolvePath(uri_); let item = this.getActivePane().itemForURI(uri); if (uri && item == null) { for (const opener of this.getOpeners()) { item = opener(uri, options); if (item) break; } } if (item == null) { item = this.project.openSync(uri, { initialLine, initialColumn }); } if (activateItem) { this.getActivePane().activateItem(item); } this.itemOpened(item); if (activatePane) { this.getActivePane().activate(); } return item; } openURIInPane(uri, pane) { return this.open(uri, { pane }); } // Public: Creates a new item that corresponds to the provided URI. // // If no URI is given, or no registered opener can open the URI, a new empty // {TextEditor} will be created. // // * `uri` A {String} containing a URI. // // Returns a {Promise} that resolves to the {TextEditor} (or other item) for the given URI. async createItemForURI(uri, options) { if (uri != null) { for (const opener of this.getOpeners()) { const item = opener(uri, options); if (item != null) return item; } } try { const item = await this.openTextFile(uri, options); return item; } catch (error) { switch (error.code) { case 'CANCELLED': return Promise.resolve(); case 'EACCES': this.notificationManager.addWarning( `Permission denied '${error.path}'` ); return Promise.resolve(); case 'EPERM': case 'EBUSY': case 'ENXIO': case 'EIO': case 'ENOTCONN': case 'UNKNOWN': case 'ECONNRESET': case 'EINVAL': case 'EMFILE': case 'ENOTDIR': case 'EAGAIN': this.notificationManager.addWarning( `Unable to open '${error.path != null ? error.path : uri}'`, { detail: error.message } ); return Promise.resolve(); default: throw error; } } } async openTextFile(uri, options) { const filePath = this.project.resolvePath(uri); if (filePath != null) { try { fs.closeSync(fs.openSync(filePath, 'r')); } catch (error) { // allow ENOENT errors to create an editor for paths that dont exist if (error.code !== 'ENOENT') { throw error; } } } const fileSize = fs.getSizeSync(filePath); if (fileSize >= this.config.get('core.warnOnLargeFileLimit') * 1048576) { // 40MB by default await new Promise((resolve, reject) => { this.applicationDelegate.confirm( { message: 'Atom will be unresponsive during the loading of very large files.', detail: 'Do you still want to load this file?', buttons: ['Proceed', 'Cancel'] }, response => { if (response === 1) { const error = new Error(); error.code = 'CANCELLED'; reject(error); } else { resolve(); } } ); }); } const buffer = await this.project.bufferForPath(filePath, options); return this.textEditorRegistry.build( Object.assign({ buffer, autoHeight: false }, options) ); } handleGrammarUsed(grammar) { if (grammar == null) { return; } this.packageManager.triggerActivationHook( `${grammar.scopeName}:root-scope-used` ); this.packageManager.triggerActivationHook( `${grammar.packageName}:grammar-used` ); } // Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. // // * `object` An {Object} you want to perform the check against. isTextEditor(object) { return object instanceof TextEditor; } // Extended: Create a new text editor. // // Returns a {TextEditor}. buildTextEditor(params) { const editor = this.textEditorRegistry.build(params); const subscription = this.textEditorRegistry.maintainConfig(editor); editor.onDidDestroy(() => subscription.dispose()); return editor; } // Public: Asynchronously reopens the last-closed item's URI if it hasn't already been // reopened. // // Returns a {Promise} that is resolved when the item is opened reopenItem() { const uri = this.destroyedItemURIs.pop(); if (uri) { return this.open(uri); } else { return Promise.resolve(); } } // Public: Register an opener for a uri. // // When a URI is opened via {Workspace::open}, Atom loops through its registered // opener functions until one returns a value for the given uri. // Openers are expected to return an object that inherits from HTMLElement or // a model which has an associated view in the {ViewRegistry}. // A {TextEditor} will be used if no opener returns a value. // // ## Examples // // ```coffee // atom.workspace.addOpener (uri) -> // if path.extname(uri) is '.toml' // return new TomlEditor(uri) // ``` // // * `opener` A {Function} to be called when a path is being opened. // // Returns a {Disposable} on which `.dispose()` can be called to remove the // opener. // // Note that the opener will be called if and only if the URI is not already open // in the current pane. The searchAllPanes flag expands the search from the // current pane to all panes. If you wish to open a view of a different type for // a file that is already open, consider changing the protocol of the URI. For // example, perhaps you wish to preview a rendered version of the file `/foo/bar/baz.quux` // that is already open in a text editor view. You could signal this by calling // {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener // can check the protocol for quux-preview and only handle those URIs that match. // // To defer your package's activation until a specific URL is opened, add a // `workspaceOpeners` field to your `package.json` containing an array of URL // strings. addOpener(opener) { this.openers.push(opener); return new Disposable(() => { _.remove(this.openers, opener); }); } getOpeners() { return this.openers; } /* Section: Pane Items */ // Essential: Get all pane items in the workspace. // // Returns an {Array} of items. getPaneItems() { return _.flatten( this.getPaneContainers().map(container => container.getPaneItems()) ); } // Essential: Get the active {Pane}'s active item. // // Returns a pane item {Object}. getActivePaneItem() { return this.getActivePaneContainer().getActivePaneItem(); } // Essential: Get all text editors in the workspace, if they are pane items. // // Returns an {Array} of {TextEditor}s. getTextEditors() { return this.getPaneItems().filter(item => item instanceof TextEditor); } // Essential: Get the workspace center's active item if it is a {TextEditor}. // // Returns a {TextEditor} or `undefined` if the workspace center's current // active item is not a {TextEditor}. getActiveTextEditor() { const activeItem = this.getCenter().getActivePaneItem(); if (activeItem instanceof TextEditor) { return activeItem; } } // Save all pane items. saveAll() { this.getPaneContainers().forEach(container => { container.saveAll(); }); } confirmClose(options) { return Promise.all( this.getPaneContainers().map(container => container.confirmClose(options)) ).then(results => !results.includes(false)); } // Save the active pane item. // // If the active pane item currently has a URI according to the item's // `.getURI` method, calls `.save` on the item. Otherwise // {::saveActivePaneItemAs} # will be called instead. This method does nothing // if the active item does not implement a `.save` method. saveActivePaneItem() { return this.getCenter() .getActivePane() .saveActiveItem(); } // Prompt the user for a path and save the active pane item to it. // // Opens a native dialog where the user selects a path on disk, then calls // `.saveAs` on the item with the selected path. This method does nothing if // the active item does not implement a `.saveAs` method. saveActivePaneItemAs() { this.getCenter() .getActivePane() .saveActiveItemAs(); } // Destroy (close) the active pane item. // // Removes the active pane item and calls the `.destroy` method on it if one is // defined. destroyActivePaneItem() { return this.getActivePane().destroyActiveItem(); } /* Section: Panes */ // Extended: Get the most recently focused pane container. // // Returns a {Dock} or the {WorkspaceCenter}. getActivePaneContainer() { return this.activePaneContainer; } // Extended: Get all panes in the workspace. // // Returns an {Array} of {Pane}s. getPanes() { return _.flatten( this.getPaneContainers().map(container => container.getPanes()) ); } getVisiblePanes() { return _.flatten( this.getVisiblePaneContainers().map(container => container.getPanes()) ); } // Extended: Get the active {Pane}. // // Returns a {Pane}. getActivePane() { return this.getActivePaneContainer().getActivePane(); } // Extended: Make the next pane active. activateNextPane() { return this.getActivePaneContainer().activateNextPane(); } // Extended: Make the previous pane active. activatePreviousPane() { return this.getActivePaneContainer().activatePreviousPane(); } // Extended: Get the first pane container that contains an item with the given // URI. // // * `uri` {String} uri // // Returns a {Dock}, the {WorkspaceCenter}, or `undefined` if no item exists // with the given URI. paneContainerForURI(uri) { return this.getPaneContainers().find(container => container.paneForURI(uri) ); } // Extended: Get the first pane container that contains the given item. // // * `item` the Item that the returned pane container must contain. // // Returns a {Dock}, the {WorkspaceCenter}, or `undefined` if no item exists // with the given URI. paneContainerForItem(uri) { return this.getPaneContainers().find(container => container.paneForItem(uri) ); } // Extended: Get the first {Pane} that contains an item with the given URI. // // * `uri` {String} uri // // Returns a {Pane} or `undefined` if no item exists with the given URI. paneForURI(uri) { for (let location of this.getPaneContainers()) { const pane = location.paneForURI(uri); if (pane != null) { return pane; } } } // Extended: Get the {Pane} containing the given item. // // * `item` the Item that the returned pane must contain. // // Returns a {Pane} or `undefined` if no pane exists for the given item. paneForItem(item) { for (let location of this.getPaneContainers()) { const pane = location.paneForItem(item); if (pane != null) { return pane; } } } // Destroy (close) the active pane. destroyActivePane() { const activePane = this.getActivePane(); if (activePane != null) { activePane.destroy(); } } // Close the active center pane item, or the active center pane if it is // empty, or the current window if there is only the empty root pane. closeActivePaneItemOrEmptyPaneOrWindow() { if (this.getCenter().getActivePaneItem() != null) { this.getCenter() .getActivePane() .destroyActiveItem(); } else if (this.getCenter().getPanes().length > 1) { this.getCenter().destroyActivePane(); } else if (this.config.get('core.closeEmptyWindows')) { atom.close(); } } // Increase the editor font size by 1px. increaseFontSize() { this.config.set('editor.fontSize', this.config.get('editor.fontSize') + 1); } // Decrease the editor font size by 1px. decreaseFontSize() { const fontSize = this.config.get('editor.fontSize'); if (fontSize > 1) { this.config.set('editor.fontSize', fontSize - 1); } } // Restore to the window's default editor font size. resetFontSize() { this.config.set( 'editor.fontSize', this.config.get('editor.defaultFontSize') ); } // Removes the item's uri from the list of potential items to reopen. itemOpened(item) { let uri; if (typeof item.getURI === 'function') { uri = item.getURI(); } else if (typeof item.getUri === 'function') { uri = item.getUri(); } if (uri != null) { _.remove(this.destroyedItemURIs, uri); } } // Adds the destroyed item's uri to the list of items to reopen. didDestroyPaneItem({ item }) { let uri; if (typeof item.getURI === 'function') { uri = item.getURI(); } else if (typeof item.getUri === 'function') { uri = item.getUri(); } if (uri != null) { this.destroyedItemURIs.push(uri); } } // Called by Model superclass when destroyed destroyed() { this.paneContainers.center.destroy(); this.paneContainers.left.destroy(); this.paneContainers.right.destroy(); this.paneContainers.bottom.destroy(); this.cancelStoppedChangingActivePaneItemTimeout(); if (this.activeItemSubscriptions != null) { this.activeItemSubscriptions.dispose(); } if (this.element) this.element.destroy(); } /* Section: Pane Locations */ // Essential: Get the {WorkspaceCenter} at the center of the editor window. getCenter() { return this.paneContainers.center; } // Essential: Get the {Dock} to the left of the editor window. getLeftDock() { return this.paneContainers.left; } // Essential: Get the {Dock} to the right of the editor window. getRightDock() { return this.paneContainers.right; } // Essential: Get the {Dock} below the editor window. getBottomDock() { return this.paneContainers.bottom; } getPaneContainers() { return [ this.paneContainers.center, this.paneContainers.left, this.paneContainers.right, this.paneContainers.bottom ]; } getVisiblePaneContainers() { const center = this.getCenter(); return atom.workspace .getPaneContainers() .filter(container => container === center || container.isVisible()); } /* Section: Panels Panels are used to display UI related to an editor window. They are placed at one of the four edges of the window: left, right, top or bottom. If there are multiple panels on the same window edge they are stacked in order of priority: higher priority is closer to the center, lower priority towards the edge. *Note:* If your panel changes its size throughout its lifetime, consider giving it a higher priority, allowing fixed size panels to be closer to the edge. This allows control targets to remain more static for easier targeting by users that employ mice or trackpads. (See [atom/atom#4834](https://github.com/atom/atom/issues/4834) for discussion.) */ // Essential: Get an {Array} of all the panel items at the bottom of the editor window. getBottomPanels() { return this.getPanels('bottom'); } // Essential: Adds a panel item to the bottom of the editor window. // // * `options` {Object} // * `item` Your panel content. It can be DOM element, a jQuery element, or // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the // latter. See {ViewRegistry::addViewProvider} for more information. // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden // (default: true) // * `priority` (optional) {Number} Determines stacking order. Lower priority items are // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} addBottomPanel(options) { return this.addPanel('bottom', options); } // Essential: Get an {Array} of all the panel items to the left of the editor window. getLeftPanels() { return this.getPanels('left'); } // Essential: Adds a panel item to the left of the editor window. // // * `options` {Object} // * `item` Your panel content. It can be DOM element, a jQuery element, or // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the // latter. See {ViewRegistry::addViewProvider} for more information. // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden // (default: true) // * `priority` (optional) {Number} Determines stacking order. Lower priority items are // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} addLeftPanel(options) { return this.addPanel('left', options); } // Essential: Get an {Array} of all the panel items to the right of the editor window. getRightPanels() { return this.getPanels('right'); } // Essential: Adds a panel item to the right of the editor window. // // * `options` {Object} // * `item` Your panel content. It can be DOM element, a jQuery element, or // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the // latter. See {ViewRegistry::addViewProvider} for more information. // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden // (default: true) // * `priority` (optional) {Number} Determines stacking order. Lower priority items are // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} addRightPanel(options) { return this.addPanel('right', options); } // Essential: Get an {Array} of all the panel items at the top of the editor window. getTopPanels() { return this.getPanels('top'); } // Essential: Adds a panel item to the top of the editor window above the tabs. // // * `options` {Object} // * `item` Your panel content. It can be DOM element, a jQuery element, or // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the // latter. See {ViewRegistry::addViewProvider} for more information. // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden // (default: true) // * `priority` (optional) {Number} Determines stacking order. Lower priority items are // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} addTopPanel(options) { return this.addPanel('top', options); } // Essential: Get an {Array} of all the panel items in the header. getHeaderPanels() { return this.getPanels('header'); } // Essential: Adds a panel item to the header. // // * `options` {Object} // * `item` Your panel content. It can be DOM element, a jQuery element, or // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the // latter. See {ViewRegistry::addViewProvider} for more information. // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden // (default: true) // * `priority` (optional) {Number} Determines stacking order. Lower priority items are // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} addHeaderPanel(options) { return this.addPanel('header', options); } // Essential: Get an {Array} of all the panel items in the footer. getFooterPanels() { return this.getPanels('footer'); } // Essential: Adds a panel item to the footer. // // * `options` {Object} // * `item` Your panel content. It can be DOM element, a jQuery element, or // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the // latter. See {ViewRegistry::addViewProvider} for more information. // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden // (default: true) // * `priority` (optional) {Number} Determines stacking order. Lower priority items are // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} addFooterPanel(options) { return this.addPanel('footer', options); } // Essential: Get an {Array} of all the modal panel items getModalPanels() { return this.getPanels('modal'); } // Essential: Adds a panel item as a modal dialog. // // * `options` {Object} // * `item` Your panel content. It can be a DOM element, a jQuery element, or // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the // model option. See {ViewRegistry::addViewProvider} for more information. // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden // (default: true) // * `priority` (optional) {Number} Determines stacking order. Lower priority items are // forced closer to the edges of the window. (default: 100) // * `autoFocus` (optional) {Boolean|Element} true if you want modal focus managed for you by Atom. // Atom will automatically focus on this element or your modal panel's first tabbable element when the modal // opens and will restore the previously selected element when the modal closes. Atom will // also automatically restrict user tab focus within your modal while it is open. // (default: false) // // Returns a {Panel} addModalPanel(options = {}) { return this.addPanel('modal', options); } // Essential: Returns the {Panel} associated with the given item. Returns // `null` when the item has no panel. // // * `item` Item the panel contains panelForItem(item) { for (let location in this.panelContainers) { const container = this.panelContainers[location]; const panel = container.panelForItem(item); if (panel != null) { return panel; } } return null; } getPanels(location) { return this.panelContainers[location].getPanels(); } addPanel(location, options) { if (options == null) { options = {}; } return this.panelContainers[location].addPanel( new Panel(options, this.viewRegistry) ); } /* Section: Searching and Replacing */ // Public: Performs a search across all files in the workspace. // // * `regex` {RegExp} to search with. // * `options` (optional) {Object} // * `paths` An {Array} of glob patterns to search within. // * `onPathsSearched` (optional) {Function} to be periodically called // with number of paths searched. // * `leadingContextLineCount` {Number} default `0`; The number of lines // before the matched line to include in the results object. // * `trailingContextLineCount` {Number} default `0`; The number of lines // after the matched line to include in the results object. // * `iterator` {Function} callback on each file found. // // Returns a {Promise} with a `cancel()` method that will cancel all // of the underlying searches that were started as part of this scan. scan(regex, options = {}, iterator) { if (_.isFunction(options)) { iterator = options; options = {}; } // Find a searcher for every Directory in the project. Each searcher that is matched // will be associated with an Array of Directory objects in the Map. const directoriesForSearcher = new Map(); for (const directory of this.project.getDirectories()) { let searcher = options.ripgrep ? this.ripgrepDirectorySearcher : this.scandalDirectorySearcher; for (const directorySearcher of this.directorySearchers) { if (directorySearcher.canSearchDirectory(directory)) { searcher = directorySearcher; break; } } let directories = directoriesForSearcher.get(searcher); if (!directories) { directories = []; directoriesForSearcher.set(searcher, directories); } directories.push(directory); } // Define the onPathsSearched callback. let onPathsSearched; if (_.isFunction(options.onPathsSearched)) { // Maintain a map of directories to the number of search results. When notified of a new count, // replace the entry in the map and update the total. const onPathsSearchedOption = options.onPathsSearched; let totalNumberOfPathsSearched = 0; const numberOfPathsSearchedForSearcher = new Map(); onPathsSearched = function(searcher, numberOfPathsSearched) { const oldValue = numberOfPathsSearchedForSearcher.get(searcher); if (oldValue) { totalNumberOfPathsSearched -= oldValue; } numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched); totalNumberOfPathsSearched += numberOfPathsSearched; return onPathsSearchedOption(totalNumberOfPathsSearched); }; } else { onPathsSearched = function() {}; } // Kick off all of the searches and unify them into one Promise. const allSearches = []; directoriesForSearcher.forEach((directories, searcher) => { const searchOptions = { inclusions: options.paths || [], includeHidden: true, excludeVcsIgnores: this.config.get('core.excludeVcsIgnoredPaths'), exclusions: this.config.get('core.ignoredNames'), follow: this.config.get('core.followSymlinks'), leadingContextLineCount: options.leadingContextLineCount || 0, trailingContextLineCount: options.trailingContextLineCount || 0, PCRE2: options.PCRE2, didMatch: result => { if (!this.project.isPathModified(result.filePath)) { return iterator(result); } }, didError(error) { return iterator(null, error); }, didSearchPaths(count) { return onPathsSearched(searcher, count); } }; const directorySearcher = searcher.search( directories, regex, searchOptions ); allSearches.push(directorySearcher); }); const searchPromise = Promise.all(allSearches); for (let buffer of this.project.getBuffers()) { if (buffer.isModified()) { const filePath = buffer.getPath(); if (!this.project.contains(filePath)) { continue; } var matches = []; buffer.scan(regex, match => matches.push(match)); if (matches.length > 0) { iterator({ filePath, matches }); } } } // Make sure the Promise that is returned to the client is cancelable. To be consistent // with the existing behavior, instead of cancel() rejecting the promise, it should // resolve it with the special value 'cancelled'. At least the built-in find-and-replace // package relies on this behavior. let isCancelled = false; const cancellablePromise = new Promise((resolve, reject) => { const onSuccess = function() { if (isCancelled) { resolve('cancelled'); } else { resolve(null); } }; const onFailure = function(error) { for (let promise of allSearches) { promise.cancel(); } reject(error); }; searchPromise.then(onSuccess, onFailure); }); cancellablePromise.cancel = () => { isCancelled = true; // Note that cancelling all of the members of allSearches will cause all of the searches // to resolve, which causes searchPromise to resolve, which is ultimately what causes // cancellablePromise to resolve. allSearches.map(promise => promise.cancel()); }; return cancellablePromise; } // Public: Performs a replace across all the specified files in the project. // // * `regex` A {RegExp} to search with. // * `replacementText` {String} to replace all matches of regex with. // * `filePaths` An {Array} of file path strings to run the replace on. // * `iterator` A {Function} callback on each file with replacements: // * `options` {Object} with keys `filePath` and `replacements`. // // Returns a {Promise}. replace(regex, replacementText, filePaths, iterator) { return new Promise((resolve, reject) => { let buffer; const openPaths = this.project .getBuffers() .map(buffer => buffer.getPath()); const outOfProcessPaths = _.difference(filePaths, openPaths); let inProcessFinished = !openPaths.length; let outOfProcessFinished = !outOfProcessPaths.length; const checkFinished = () => { if (outOfProcessFinished && inProcessFinished) { resolve(); } }; if (!outOfProcessFinished.length) { let flags = 'g'; if (regex.multiline) { flags += 'm'; } if (regex.ignoreCase) { flags += 'i'; } const task = Task.once( require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, () => { outOfProcessFinished = true; checkFinished(); } ); task.on('replace:path-replaced', iterator); task.on('replace:file-error', error => { iterator(null, error); }); } for (buffer of this.project.getBuffers()) { if (!filePaths.includes(buffer.getPath())) { continue; } const replacements = buffer.replace(regex, replacementText, iterator); if (replacements) { iterator({ filePath: buffer.getPath(), replacements }); } } inProcessFinished = true; checkFinished(); }); } checkoutHeadRevision(editor) { if (editor.getPath()) { const checkoutHead = async () => { const repository = await this.project.repositoryForDirectory( new Directory(editor.getDirectoryPath()) ); if (repository) repository.checkoutHeadForEditor(editor); }; if (this.config.get('editor.confirmCheckoutHeadRevision')) { this.applicationDelegate.confirm( { message: 'Confirm Checkout HEAD Revision', detail: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`, buttons: ['OK', 'Cancel'] }, response => { if (response === 0) checkoutHead(); } ); } else { checkoutHead(); } } } }; ================================================ FILE: static/atom-ui/README.md ================================================ # :sparkles: Atom UI :sparkles: This is Atom's UI library. Originally forked from Bootstrap `3.3.6`, then merged with some core styles and now tweaked to Atom's needy needs. ## Components Here a list of [all components](atom-ui.less). Open the [Styleguide](https://github.com/atom/styleguide) package (`cmd-ctrl-shift-g`) to see them in action and how to use them. ![Styleguide](https://cloud.githubusercontent.com/assets/378023/15767543/ccecf9bc-2983-11e6-9c5e-d228d39f52b0.png) ## Feature requests If you need something, feel free to open an issue and it might can be added. :v: ================================================ FILE: static/atom-ui/_index.less ================================================ // Atom UI // Private! Don't use these in packages. // If you need something, feel free to open an issue and it might can be made public @import "styles/private/scaffolding.less"; @import "styles/private/alerts.less"; @import "styles/private/close.less"; @import "styles/private/code.less"; @import "styles/private/forms.less"; @import "styles/private/links.less"; @import "styles/private/navs.less"; @import "styles/private/sections.less"; @import "styles/private/tables.less"; @import "styles/private/utilities.less"; // Public components // Open the Styleguide to see them in action @import "styles/badges.less"; @import "styles/button-groups.less"; @import "styles/buttons.less"; @import "styles/git-status.less"; @import "styles/icons.less"; @import "styles/inputs.less"; @import "styles/layout.less"; @import "styles/lists.less"; @import "styles/loading.less"; @import "styles/messages.less"; @import "styles/modals.less"; @import "styles/panels.less"; @import "styles/select-list.less"; @import "styles/site-colors.less"; @import "styles/text.less"; @import "styles/tooltip.less"; ================================================ FILE: static/atom-ui/styles/badges.less ================================================ @import "ui-variables"; .badge { display: inline-block; line-height: 1; vertical-align: middle; font-weight: normal; text-align: center; white-space: nowrap; border-radius: 1em; &:empty { display: none; // Hide when un-used } // Color ---------------------- .badge-color( @fg: @text-color-selected; @bg: @background-color-selected; ) { color: @fg; background-color: @bg; } .badge-color(); &.badge-info { .badge-color(white, @background-color-info); } &.badge-success { .badge-color(white, @background-color-success); } &.badge-warning { .badge-color(white, @background-color-warning); } &.badge-error { .badge-color(white, @background-color-error); } // Size ---------------------- .badge-size( @size: @font-size; ) { @padding: round(@size/4); font-size: @size; min-width: @size + @padding*2; padding: @padding round(@padding*1.5); } .badge-size(); // default // Fixed size &.badge-large { .badge-size(18px); } &.badge-medium { .badge-size(14px); } &.badge-small { .badge-size(10px); } // Flexible size // The size changes depending on the parent element // Best used for larger sizes, since em's can cause rounding errors &.badge-flexible { @size: .8em; @padding: @size/2; font-size: @size; min-width: @size + @padding*2; padding: @padding @padding*1.5; } // Icon ---------------------- &.icon { font-size: round(@component-icon-size*0.8); padding: @component-icon-padding @component-icon-padding*2; } } ================================================ FILE: static/atom-ui/styles/button-groups.less ================================================ @import "variables/variables"; @import "ui-variables"; @import "mixins/mixins"; // // Button groups // -------------------------------------------------- // Make the div behave like a button .btn-group, .btn-group-vertical { position: relative; display: inline-block; vertical-align: middle; // match .btn alignment given font-size hack above > .btn { position: relative; float: left; // Bring the "active" button to the front &:hover, &:focus, &:active, &.active { z-index: 2; } } } // Borders // --------------------------------------------------------- .btn-group > .btn { border-left: 1px solid @button-border-color; border-right: 1px solid @button-border-color; } .btn-group > .btn:first-child { border-left: none; border-top-left-radius: @component-border-radius; border-bottom-left-radius: @component-border-radius; } .btn-group > .btn:last-child, .btn-group > .btn.selected:last-child, .btn-group > .dropdown-toggle { border-right: none; border-top-right-radius: @component-border-radius; border-bottom-right-radius: @component-border-radius; } // Prevent double borders when buttons are next to each other .btn-group { .btn + .btn, .btn + .btn-group, .btn-group + .btn, .btn-group + .btn-group { margin-left: -1px; } } // Optional: Group multiple button groups together for a toolbar .btn-toolbar { margin-left: -5px; // Offset the first child's margin &:extend(.clearfix all); .btn, .btn-group, .input-group { float: left; } > .btn, > .btn-group, > .input-group { margin-left: 5px; } } .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { border-radius: 0; } // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match .btn-group > .btn:first-child { margin-left: 0; &:not(:last-child):not(.dropdown-toggle) { .border-right-radius(0); } } // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it .btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) { .border-left-radius(0); } // Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) .btn-group > .btn-group { float: left; } .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { border-radius: 0; } .btn-group > .btn-group:first-child:not(:last-child) { > .btn:last-child, > .dropdown-toggle { .border-right-radius(0); } } .btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { .border-left-radius(0); } // On active and open, don't show outline .btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { outline: 0; } // Sizing // // Remix the default button sizing classes into new ones for easier manipulation. .btn-group-xs > .btn { &:extend(.btn-xs); } .btn-group-sm > .btn { &:extend(.btn-sm); } .btn-group-lg > .btn { &:extend(.btn-lg); } // Split button dropdowns // ---------------------- // Give the line between buttons some depth .btn-group > .btn + .dropdown-toggle { padding-left: 8px; padding-right: 8px; } .btn-group > .btn-lg + .dropdown-toggle { padding-left: 12px; padding-right: 12px; } // The clickable button for toggling the menu // Remove the gradient and set the same inset shadow as the :active state .btn-group.open .dropdown-toggle { box-shadow: inset 0 3px 5px rgba(0,0,0,.125); // Show no shadow for `.btn-link` since it has no other button styles. &.btn-link { box-shadow: none; } } // Reposition the caret .btn .caret { margin-left: 0; } // Carets in other button sizes .btn-lg .caret { border-width: @caret-width-large @caret-width-large 0; border-bottom-width: 0; } // Upside down carets for .dropup .dropup .btn-lg .caret { border-width: 0 @caret-width-large @caret-width-large; } // Justified button groups // ---------------------- .btn-group-justified { display: table; width: 100%; table-layout: fixed; border-collapse: separate; > .btn, > .btn-group { float: none; display: table-cell; width: 1%; } > .btn-group .btn { width: 100%; } > .btn-group .dropdown-menu { left: auto; } } ================================================ FILE: static/atom-ui/styles/buttons.less ================================================ @import "variables/variables"; @import "ui-variables"; @import "mixins/mixins"; // // Buttons // -------------------------------------------------- // Base styles // -------------------------------------------------- .btn { display: inline-block; margin-bottom: 0; // For input.btn height: @component-line-height + 2px; padding: 0 @component-padding; font-size: @font-size; font-weight: normal; line-height: @component-line-height; text-align: center; vertical-align: middle; border: none; border-radius: @component-border-radius; background-color: @btn-default-bg; white-space: nowrap; cursor: pointer; z-index: 0; -webkit-user-select: none; &, &:active, &.active { &:focus, &.focus { .tab-focus(); } } &:hover, &:focus, &.focus { color: @btn-default-color; text-decoration: none; background-color: @button-background-color-hover; } &:active, &.active { outline: 0; background-image: none; box-shadow: inset 0 3px 5px rgba(0,0,0,.125); } &.selected, &.selected:hover { // we want the selected button to behave like the :hover button; it's on top of the other buttons. z-index: 1; color: @text-color-selected; background-color: @button-background-color-selected; } &.disabled, &[disabled], fieldset[disabled] & { cursor: @cursor-disabled; opacity: .65; box-shadow: none; } a& { &.disabled, fieldset[disabled] & { pointer-events: none; // Future-proof disabling of clicks on `` elements } } } // Button variants // -------------------------------------------------- .button-variant(@color; @background;) { color: @color; background-color: @background; &:focus, &.focus { color: @color; background-color: darken(@background, 10%); } &:hover { color: @color; background-color: darken(@background, 10%); } &:active, &.active { color: @color; background-color: darken(@background, 10%); &:hover, &:focus, &.focus { color: @color; background-color: darken(@background, 17%); } } &.selected, &.selected:hover { // we want the selected button to behave like the :hover button; it's on top of the other buttons. z-index: 1; background-color: darken(@background, 10%); } &.disabled, &[disabled], fieldset[disabled] & { &:hover, &:focus, &.focus { background-color: @background; } } .badge { color: @background; background-color: @color; } } .btn-primary { .button-variant(@btn-primary-color; @btn-primary-bg;); } // Success appears as green .btn-success { .button-variant(@btn-success-color; @btn-success-bg;); } // Info appears as blue-green .btn-info { .button-variant(@btn-info-color; @btn-info-bg;); } // Warning appears as orange .btn-warning { .button-variant(@btn-warning-color; @btn-warning-bg;); } // Danger and error appear as red .btn-error { .button-variant(@btn-error-color; @btn-error-bg;); } // Button Sizes // -------------------------------------------------- .btn-xs, .btn-group-xs > .btn { padding: @component-padding/4 @component-padding/2; font-size: @font-size - 2px; height: auto; line-height: 1.3em; &.icon:before { font-size: @font-size - 2px; } } .btn-sm, .btn-group-sm > .btn { padding: @component-padding/4 @component-padding/2; height: auto; line-height: 1.3em; &.icon:before { font-size: @font-size + 1px; } } .btn-lg, .btn-group-lg > .btn { font-size: @font-size + 2px; padding: @component-padding - 2px @component-padding + 2px; height: auto; line-height: 1.3em; &.icon:before { font-size: @font-size + 6px; } } // Link button // ------------------------- // Make a button look and behave like a link .btn-link { color: @link-color; font-weight: normal; border-radius: 0; &, &:active, &.active, &[disabled], fieldset[disabled] & { background-color: transparent; box-shadow: none; } &:hover, &:focus { color: @link-hover-color; text-decoration: @link-hover-decoration; background-color: transparent; } &[disabled], fieldset[disabled] & { &:hover, &:focus { color: @btn-link-disabled-color; text-decoration: none; } } } // Block button // -------------------------------------------------- .btn-block { display: block; width: 100%; } // Vertically space out multiple block buttons .btn-block + .btn-block { margin-top: 5px; } // Specificity overrides input[type="submit"], input[type="reset"], input[type="button"] { &.btn-block { width: 100%; } } // Icon buttons // -------------------------------------------------- .btn.icon { &:before { width: initial; height: initial; margin-right: .3125em; } &:empty:before { margin-right: 0; } } // Button Toolbar // -------------------------------------------------- .btn-toolbar { > .btn-group + .btn-group, > .btn-group + .btn, > .btn + .btn { float: none; display: inline-block; margin-left: 0; } > * { margin-right: @component-padding / 2; } > *:last-child { margin-right: 0; } } ================================================ FILE: static/atom-ui/styles/git-status.less ================================================ @import "ui-variables"; // // Git Status // -------------------------------------------------- .status { &-ignored { color: @text-color-subtle; } &-added { color: @text-color-success; } &-modified { color: @text-color-warning; } &-removed { color: @text-color-error; } &-renamed { color: @text-color-info; } } ================================================ FILE: static/atom-ui/styles/icons.less ================================================ @import "ui-variables"; .icon::before { margin-right: @component-icon-padding; } a.icon { text-decoration: none; color: @text-color; &:hover{ color: @text-color-highlight; } } ================================================ FILE: static/atom-ui/styles/inputs.less ================================================ @import "./variables/ui-variables"; // Fallback for @use-custom-controls @import "ui-variables"; @component-size: @component-icon-size; // use for text-less controls like radio, checkboxes etc. @component-margin-side: .3em; @text-component-height: 2em; @component-background-color: mix(@text-color, @base-background-color, 20%); // // Overrides // ------------------------- input.input-radio, input.input-checkbox, input.input-toggle { margin-top: 0; // Override Bootstrap's 4px } .input-label { margin-bottom: 0; } // // Mixins // ------------------------- .input-field-mixin() { padding: .25em .4em; line-height: 1.5; // line-height + padding = @text-component-height border-radius: @component-border-radius; border: 1px solid @input-border-color; background-color: @input-background-color; &::-webkit-input-placeholder { color: @text-color-subtle; } &:invalid { color: @text-color-error; border-color: @background-color-error; } } .input-block-mixin() { display: block; width: 100%; } // // Checkbox // ------------------------- .input-checkbox { vertical-align: middle; & when (@use-custom-controls) { -webkit-appearance: none; display: inline-block; position: relative; width: @component-size; height: @component-size; font-size: inherit; border-radius: @component-border-radius; background-color: @component-background-color; transition: background-color .16s cubic-bezier(0.5, 0.15, 0.2, 1); &&:focus { outline: 0; // TODO: Add it back } &:active { background-color: @background-color-info; } &:before, &:after { content: ""; position: absolute; top: @component-size * .75; left: @component-size * .4; height: 2px; border-radius: 1px; background-color: @base-background-color; transform-origin: 0 0; opacity: 0; transition: transform .1s cubic-bezier(0.5, 0.15, 0.2, 1), opacity .1s cubic-bezier(0.5, 0.15, 0.2, 1); } &:before { width: @component-size * .33; transform: translate3d(0,0,0) rotate(225deg) scale(0); } &:after { width: @component-size * .66; margin: -1px; transform: translate3d(0,0,0) rotate(-45deg) scale(0); transition-delay: .05s; } &:checked { background-color: @background-color-info; &:active { background-color: @component-background-color; } &:before { opacity: 1; transform: translate3d(0,0,0) rotate(225deg) scale(1); transition-delay: .05s; } &:after { opacity: 1; transform: translate3d(0, 0, 0) rotate(-45deg) scale(1); transition-delay: 0; } } &:indeterminate { background-color: @background-color-info; &:active { background-color: @component-background-color; } &:after { opacity: 1; transform: translate3d(@component-size * -.14, @component-size * -.25, 0) rotate(0deg) scale(1); transition-delay: 0; } } } } // // Color // ------------------------- .input-color { vertical-align: middle; & when (@use-custom-controls) { -webkit-appearance: none; padding: 0; width: @component-size * 2.5; height: @component-size * 2.5; border-radius: 50%; border: 2px solid @input-border-color; background-color: @input-background-color; &::-webkit-color-swatch-wrapper { padding: 0; } &::-webkit-color-swatch { border: 1px solid hsla(0,0%,0%,.1); border-radius: 50%; transition: transform .16s cubic-bezier(0.5, 0.15, 0.2, 1); &:active { transition-duration: 0s; transform: scale(.9); } } } } // // Label // ------------------------- .input-label { .input-radio, .input-checkbox, .input-toggle { margin-top: -.25em; // Vertical center (visually) - since most labels are upper case. margin-right: @component-margin-side; } } // // Number // ------------------------- .input-number { vertical-align: middle; & when (@use-custom-controls) { .input-field-mixin(); position: relative; width: auto; .platform-darwin & { padding-right: 1.2em; // space for the spin button &::-webkit-inner-spin-button { -webkit-appearance: menulist-button; position: absolute; top: 1px; bottom: 1px; right: 1px; width: calc(.6em ~'+' 9px); // magic numbers, OMG! outline: 1px solid @input-background-color; outline-offset: -1px; // reduces border radius (that can't be changed) border-right: .2em solid @background-color-highlight; // a bit more padding background-color: @background-color-highlight; transition: transform .16s cubic-bezier(0.5, 0.15, 0.2, 1); &:active { transform: scale(.9); transition-duration: 0s; } } } } } // // Radio // ------------------------- .input-radio { vertical-align: middle; & when (@use-custom-controls) { -webkit-appearance: none; display: inline-block; position: relative; width: @component-size; height: @component-size; font-size: inherit; border-radius: 50%; background-color: @component-background-color; transition: background-color .16s cubic-bezier(0.5, 0.15, 0.2, 1); &:before { content: ""; position: absolute; width: inherit; height: inherit; border-radius: inherit; border: @component-size/3 solid transparent; background-clip: content-box; background-color: @base-background-color; transform: scale(0); transition: transform .1s cubic-bezier(0.5, 0.15, 0.2, 1); } &&:focus { outline: none; } &:active { background-color: @background-color-info; } &:checked { background-color: @background-color-info; &:before { transform: scale(1); } } } } // // Range (Slider) // ------------------------- .input-range { & when (@use-custom-controls) { -webkit-appearance: none; margin: @component-padding 0; height: 4px; border-radius: @component-border-radius; background-color: @component-background-color; &::-webkit-slider-thumb { -webkit-appearance: none; width: @component-size; height: @component-size; border-radius: 50%; background-color: @background-color-info; transition: transform .16s; &:active { transition-duration: 0s; transform: scale(.9); } } } } // // Search // ------------------------- .input-search { .input-block-mixin(); &&::-webkit-search-cancel-button { -webkit-appearance: searchfield-cancel-button; } & when (@use-custom-controls) { .input-field-mixin(); } } // // Select // ------------------------- .input-select { vertical-align: middle; & when (@use-custom-controls) { height: calc(@text-component-height ~'+' 2px); // + 2px? Magic! border-radius: @component-border-radius; border: 1px solid @button-border-color; background-color: @button-background-color; } } // // Text // ------------------------- .input-text { .input-block-mixin(); & when (@use-custom-controls) { .input-field-mixin(); } } // // Text Area // ------------------------- .input-textarea { .input-block-mixin(); & when (@use-custom-controls) { .input-field-mixin(); } } // // Toggle // ------------------------- .input-toggle { & when (@use-custom-controls) { -webkit-appearance: none; display: inline-block; position: relative; font-size: inherit; width: @component-size * 2; height: @component-size; vertical-align: middle; border-radius: 2em; background-color: @component-background-color; transition: background-color .2s cubic-bezier(0.5, 0.15, 0.2, 1); &&:focus { outline: 0; } &:checked { background-color: @background-color-info; } // Thumb &:before { content: ""; position: absolute; width: @component-size; height: @component-size; border-radius: inherit; border: @component-size/4 solid transparent; background-clip: content-box; background-color: @base-background-color; transition: transform .2s cubic-bezier(0.5, 0.15, 0.2, 1); } &:active:before { opacity: .5; } &:checked:before { transform: translate3d(100%, 0, 0); } } } ================================================ FILE: static/atom-ui/styles/layout.less ================================================ @import "ui-variables"; @import "mixins/mixins"; .padded { padding: @component-padding; } // Blocks .center-block { display: block; margin-left: auto; margin-right: auto; } // Must be div.block so as not to affect syntax highlighting. ul.block, div.block { margin-bottom: @component-padding; } div > ul.block:last-child, div > div.block:last-child { margin-bottom: 0; } // Inline Blocks .inline-block, .inline-block-tight { display: inline-block; vertical-align: middle; } .inline-block { margin-right: @component-padding; } .inline-block-tight { margin-right: @component-padding/2; } div > .inline-block:last-child, div > .inline-block-tight:last-child { margin-right: 0; } .inline-block .inline-block { vertical-align: top; } // Centering // ------------------------- .pull-center { margin-left: auto; margin-right: auto; } // Floats // ------------------------- // Use left margin when it's in a float: right element. // Sets the margin correctly when inline blocks are hidden and shown. .pull-right { float: right !important; .inline-block { margin-right: 0; margin-left: @component-padding; } .inline-block-tight { margin-right: 0; margin-left: @component-padding/2; } > .inline-block:first-child, > .inline-block-tight:first-child { margin-left: 0; } } .pull-left { float: left !important; } .clearfix { .clearfix(); } ================================================ FILE: static/atom-ui/styles/lists.less ================================================ @import "variables/variables"; @import "ui-variables"; @import "mixins/mixins"; @import "octicon-mixins"; // // List options // -------------------------------------------------- // Unstyled keeps list items block level, just removes default browser padding and list-style .list-unstyled { padding-left: 0; list-style: none; } // Inline turns list items into inline-block .list-inline { .list-unstyled(); margin-left: -5px; > li { display: inline-block; padding-left: 5px; padding-right: 5px; } } // // List groups // -------------------------------------------------- // Mixins .list-group-item-variant(@state; @background; @color) { .list-group-item-@{state} { color: @color; background-color: @background; a&, button& { color: @color; .list-group-item-heading { color: inherit; } &:hover, &:focus { color: @color; background-color: darken(@background, 5%); } &.active, &.active:hover, &.active:focus { color: #fff; background-color: @color; border-color: @color; } } } } // Individual list items // // Use on `li`s or `div`s within the `.list-group` parent. .list-group-item { position: relative; display: block; padding: 10px 15px; // Place the border on the list items and negative margin up for better styling margin-bottom: -1px; background-color: @list-group-bg; border: 1px solid @list-group-border; // Round the first and last items &:first-child { .border-top-radius(@list-group-border-radius); } &:last-child { margin-bottom: 0; .border-bottom-radius(@list-group-border-radius); } } // Interactive list items // // Use anchor or button elements instead of `li`s or `div`s to create interactive items. // Includes an extra `.active` modifier class for showing selected items. a.list-group-item, button.list-group-item { color: @list-group-link-color; .list-group-item-heading { color: @list-group-link-heading-color; } // Hover state &:hover, &:focus { text-decoration: none; color: @list-group-link-hover-color; background-color: @list-group-hover-bg; } } button.list-group-item { width: 100%; text-align: left; } .list-group-item { // Disabled state &.disabled, &.disabled:hover, &.disabled:focus { background-color: @list-group-disabled-bg; color: @list-group-disabled-color; cursor: @cursor-disabled; // Force color to inherit for custom content .list-group-item-heading { color: inherit; } .list-group-item-text { color: @list-group-disabled-text-color; } } // Active class on item itself, not parent &.active, &.active:hover, &.active:focus { z-index: 2; // Place active items above their siblings for proper border styling color: @list-group-active-color; background-color: @list-group-active-bg; border-color: @list-group-active-border; // Force color to inherit for custom content .list-group-item-heading, .list-group-item-heading > small, .list-group-item-heading > .small { color: inherit; } .list-group-item-text { color: @list-group-active-text-color; } } } // Contextual variants // // Add modifier classes to change text and background color on individual items. // Organizationally, this must come after the `:hover` states. .list-group-item-variant(success; @state-success-bg; @state-success-text); .list-group-item-variant(info; @state-info-bg; @state-info-text); .list-group-item-variant(warning; @state-warning-bg; @state-warning-text); .list-group-item-variant(danger; @state-danger-bg; @state-danger-text); // Custom content options // // Extra classes for creating well-formatted content within `.list-group-item`s. .list-group-item-heading { margin-top: 0; margin-bottom: 5px; } .list-group-item-text { margin-bottom: 0; line-height: 1.3; } // This is a bootstrap override // --------------------------------------------- .list-group, .list-group .list-group-item { background-color: transparent; border: none; padding: 0; margin: 0; position: static; } .list-group, .list-tree { margin: 0; padding: 0; list-style: none; cursor: default; li:not(.list-nested-item), li.list-nested-item > .list-item { line-height: @component-line-height; text-wrap: none; white-space: nowrap; } // The background highlight uses ::before rather than the item background so // it can span the entire width of the parent container rather than the size // of the list item. .selected::before { content: ''; background-color: @background-color-selected; position: absolute; left: 0; right: 0; height: @component-line-height; } // Make sure the background highlight is below the content. .selected > * { position: relative; } .icon::before { margin-right: @component-icon-padding; position: relative; top: 1px; } .no-icon { padding-left: @component-icon-padding + @component-icon-size; } } // // List Tree // -------------------------------------------------- // Handle indentation of the tree. Assume disclosure arrows. .list-tree { .list-nested-item > .list-tree > li, .list-nested-item > .list-group > li { padding-left: @component-icon-size + @component-icon-padding; } &.has-collapsable-children { @disclosure-arrow-padding: @disclosure-arrow-size + @component-icon-padding; li.list-item { margin-left: @disclosure-arrow-padding; } .list-nested-item.collapsed > .list-group, .list-nested-item.collapsed > .list-tree { display: none; } // Nested items always get disclosure arrows .list-nested-item > .list-item { .octicon(chevron-down, @disclosure-arrow-size); &::before{ position: relative; top: -1px; margin-right: @component-icon-padding; } } .list-nested-item.collapsed > .list-item { .octicon(chevron-right, @disclosure-arrow-size); &::before{ left: 1px; } } .list-nested-item > .list-tree > li, .list-nested-item > .list-group > li { padding-left: @disclosure-arrow-padding; } // You want a subtree to be flat -- no collapsable children .has-flat-children, &.has-flat-children { li.list-item { margin-left: 0; } } } } ================================================ FILE: static/atom-ui/styles/loading.less ================================================ // // Loading // -------------------------------------------------- .loading-spinner(@size) { display: block; width: @size; height: @size; background-image: url(images/octocat-spinner-128.gif); background-repeat: no-repeat; background-size: cover; &.inline-block { display: inline-block; } } .loading-spinner-tiny { .loading-spinner(16px); } .loading-spinner-small { .loading-spinner(32px); } .loading-spinner-medium { .loading-spinner(48px); } .loading-spinner-large { .loading-spinner(64px); } ================================================ FILE: static/atom-ui/styles/messages.less ================================================ @import "ui-variables"; .info-messages, .error-messages { margin: 0; padding: 0; list-style: none; } .error-messages { color: @text-color-error; } ul.background-message { font-size: @font-size * 3; margin: 0; padding: 0; li { margin: 0; padding: 0; list-style: none; } &.centered { display: flex; position: absolute; top: 0; left: 0; right: 0; bottom: 0; align-items: center; text-align: center; li { width: 100%; } } } ================================================ FILE: static/atom-ui/styles/mixins/mixins.less ================================================ @import "ui-variables"; // Core mixins // ---------------------------------------- // Focus // .tab-focus() { outline: 2px auto @text-color-info; outline-offset: -2px; } // Border-radius // .border-top-radius(@radius) { border-top-right-radius: @radius; border-top-left-radius: @radius; } .border-right-radius(@radius) { border-bottom-right-radius: @radius; border-top-right-radius: @radius; } .border-bottom-radius(@radius) { border-bottom-right-radius: @radius; border-bottom-left-radius: @radius; } .border-left-radius(@radius) { border-bottom-left-radius: @radius; border-top-left-radius: @radius; } // Clearfix // // For modern browsers // 1. The space content is one way to avoid an Opera bug when the // contenteditable attribute is included anywhere else in the document. // Otherwise it causes space to appear at the top and bottom of elements // that are clearfixed. // 2. The use of `table` rather than `block` is only necessary if using // `:before` to contain the top-margins of child elements. // // Source: http://nicolasgallagher.com/micro-clearfix-hack/ .clearfix() { &:before, &:after { content: " "; // 1 display: table; // 2 } &:after { clear: both; } } // CSS image replacement // // Heads up! v3 launched with only `.hide-text()`, but per our pattern for // mixins being reused as classes with the same name, this doesn't hold up. As // of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. // // Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 // Deprecated as of v3.0.1 (has been removed in v4) .hide-text() { font: ~"0/0" a; color: transparent; text-shadow: none; background-color: transparent; border: 0; } // New mixin to use as of v3.0.1 .text-hide() { .hide-text(); } // Text overflow // Requires inline-block or block for proper styling .text-overflow() { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } ================================================ FILE: static/atom-ui/styles/modals.less ================================================ @import "ui-variables"; // // Modals // -------------------------------------------------- .overlay, // deprecated .overlay atom-panel.modal { position: absolute; display: block; top: 0; left: 50%; width: 500px; margin-left: -250px; z-index: 9999; box-sizing: border-box; border-top: none; border-top-left-radius: 0; border-top-right-radius: 0; color: @text-color; background-color: @overlay-background-color; padding: 10px; // shrink modals when window gets narrow @media (max-width: 500px) { & { width: 100%; left: 0; margin-left: 0; } } h1 { margin-top: 0; color: @text-color-highlight; font-size: 1.6em; font-weight: bold; } h2 { font-size: 1.3em; } atom-text-editor[mini] { margin-bottom: 10px; } .message { padding-top: 5px; font-size: 11px; } &.mini { width: 200px; margin-left: -100px; font-size: 12px; } } // Deprecated: overlay, from-top, from-bottom, floating // -------------------------------------------------- // TODO: Remove these! .overlay.from-top { top: 0; border-top: none; border-top-left-radius: 0; border-top-right-radius: 0; } .overlay.from-bottom { bottom: 0; border-bottom: none; border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .overlay.floating { left: auto; } ================================================ FILE: static/atom-ui/styles/panels.less ================================================ @import "ui-variables"; // // Panels // -------------------------------------------------- .tool-panel, // deprecated: .tool-panel .panel, // deprecated: .panel atom-panel { background-color: @tool-panel-background-color; } .inset-panel { border-radius: @component-border-radius; background-color: @inset-panel-background-color; } .panel-heading { margin: 0; padding: @component-padding; border-radius: 0; font-size: @font-size; line-height: 1; background-color: @panel-heading-background-color; .inset-panel & { border-radius: @component-border-radius @component-border-radius 0 0; } .btn { @btn-height: @component-line-height - 5px; height: @btn-height; line-height: @btn-height; font-size: @font-size - 2px; position: relative; top: -5px; } } ================================================ FILE: static/atom-ui/styles/private/README.md ================================================ # Private components > Private! Don't use these in packages. If you need something, feel free to open an issue and it might can be made public. ================================================ FILE: static/atom-ui/styles/private/alerts.less ================================================ @import "../variables/variables"; @import "ui-variables"; // // Alerts // -------------------------------------------------- //## Define alert colors, border radius, and padding. @alert-padding: 15px; @alert-border-radius: @border-radius-base; @alert-link-font-weight: bold; @alert-success-bg: @state-success-bg; @alert-success-text: @state-success-text; @alert-success-border: @state-success-border; @alert-info-bg: @state-info-bg; @alert-info-text: @state-info-text; @alert-info-border: @state-info-border; @alert-warning-bg: @state-warning-bg; @alert-warning-text: @state-warning-text; @alert-warning-border: @state-warning-border; @alert-danger-bg: @state-danger-bg; @alert-danger-text: @state-danger-text; @alert-danger-border: @state-danger-border; //## variant mixin .alert-variant(@background; @border; @text-color) { background-color: @background; border-color: @border; color: @text-color; hr { border-top-color: darken(@border, 5%); } .alert-link { color: darken(@text-color, 10%); } } // Base styles // ------------------------- .alert { padding: @alert-padding; margin-bottom: @line-height-computed; border: 1px solid transparent; border-radius: @alert-border-radius; // Headings for larger alerts h4 { margin-top: 0; // Specified for the h4 to prevent conflicts of changing @headings-color color: inherit; } // Provide class for links that match alerts .alert-link { font-weight: @alert-link-font-weight; } // Improve alignment and spacing of inner content > p, > ul { margin-bottom: 0; } > p + p { margin-top: 5px; } } // Dismissible alerts // // Expand the right padding and account for the close button's positioning. .alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. .alert-dismissible { padding-right: (@alert-padding + 20); // Adjust close link position .close { position: relative; top: -2px; right: -21px; color: inherit; } } // Alternate styles // // Generate contextual modifier classes for colorizing the alert. .alert-success { .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); } .alert-info { .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); } .alert-warning { .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); } .alert-danger { .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); } ================================================ FILE: static/atom-ui/styles/private/close.less ================================================ // // Close icon (deprecated) // -------------------------------------------------- .close { @font-size-base: 14px; @close-font-weight: bold; @close-color: #000; @close-text-shadow: 0 1px 0 #fff; float: right; font-size: (@font-size-base * 1.5); font-weight: @close-font-weight; line-height: 1; color: @close-color; text-shadow: @close-text-shadow; opacity: .2; &:hover, &:focus { color: @close-color; text-decoration: none; cursor: pointer; opacity: .5; } // Additional properties for button version // iOS requires the button element instead of an anchor tag. // If you want the anchor version, it requires `href="#"`. // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile button& { padding: 0; cursor: pointer; background: transparent; border: 0; -webkit-appearance: none; } } ================================================ FILE: static/atom-ui/styles/private/code.less ================================================ @import "../variables/variables"; @import "ui-variables"; // // Code (inline and block) // -------------------------------------------------- @code-color: @text-color-highlight; @code-bg: @background-color-highlight; @pre-color: @code-color; @pre-bg: @code-bg; @pre-border-color: @base-border-color; @pre-scrollable-max-height: 340px; // Inline and block code styles code, kbd, pre, samp { font-family: @font-family-monospace; } // Inline code code { padding: 2px 4px; font-size: 90%; color: @code-color; background-color: @code-bg; border-radius: @border-radius-base; } // User input typically entered via keyboard kbd { padding: 2px 4px; font-size: 90%; color: @code-color; background-color: @code-bg; border-radius: @border-radius-small; kbd { padding: 0; font-size: 100%; font-weight: bold; } } // Blocks of code pre { display: block; padding: ((@line-height-computed - 1) / 2); margin: 0 0 (@line-height-computed / 2); font-size: (@font-size-base - 1); // 14px to 13px line-height: @line-height-base; word-break: break-all; word-wrap: break-word; color: @pre-color; background-color: @pre-bg; border: 1px solid @pre-border-color; border-radius: @border-radius-base; // Account for some code outputs that place code tags in pre tags code { padding: 0; font-size: inherit; color: inherit; white-space: pre-wrap; background-color: transparent; border-radius: 0; } } // Enable scrollable blocks of code .pre-scrollable { max-height: @pre-scrollable-max-height; overflow-y: scroll; } ================================================ FILE: static/atom-ui/styles/private/forms.less ================================================ @import "../variables/variables"; @import "ui-variables"; @import "../mixins/mixins"; // // Forms // -------------------------------------------------- @input-bg: #fff; //** `` background color @input-bg-disabled: @gray-lighter; //** `` background color @input-color: @gray; //** Text color for ``s @input-border: #ccc; //** `` border color // TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4 //** Default `.form-control` border radius // This has no effect on ``s in CSS. @input-border-radius: @border-radius-base; //** Large `.form-control` border radius @input-border-radius-large: @border-radius-large; //** Small `.form-control` border radius @input-border-radius-small: @border-radius-small; @input-border-focus: #66afe9; //** Border color for inputs on focus @input-color-placeholder: #999; //** Placeholder text color @input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2); //** Default `.form-control` height @input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2); //** Large `.form-control` height @input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2); //** Small `.form-control` height @form-group-margin-bottom: 15px; //** `.form-group` margin @legend-color: @gray-dark; @legend-border-color: #e5e5e5; @input-group-addon-bg: @gray-lighter; //** Background color for textual input addons @input-group-addon-border-color: @input-border; //** Border color for textual input addons @cursor-disabled: not-allowed; //** Disabled cursor for form controls and buttons. @grid-gutter-width: 30px; //** Padding between columns. Gets divided in half for the left and right. // Form validation states // // Used in forms.less to generate the form validation CSS for warnings, errors, // and successes. .form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) { // Color the label and help text .help-block, .control-label, .radio, .checkbox, .radio-inline, .checkbox-inline, &.radio label, &.checkbox label, &.radio-inline label, &.checkbox-inline label { color: @text-color; } // Set the border and box shadow on specific inputs to match .form-control { border-color: @border-color; box-shadow: inset 0 1px 1px rgba(0,0,0,.075); // Redeclare so transitions work &:focus { border-color: darken(@border-color, 10%); box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); } } // Set validation states also for addons .input-group-addon { color: @text-color; border-color: @border-color; background-color: @background-color; } // Optional feedback icon .form-control-feedback { color: @text-color; } } // Form control focus state // // Generate a customized focus state and for any input with the specified color, // which defaults to the `@input-border-focus` variable. // // We highly encourage you to not customize the default value, but instead use // this to tweak colors on an as-needed basis. This aesthetic change is based on // WebKit's default styles, but applicable to a wider range of browsers. Its // usability and accessibility should be taken into account with any change. // // Example usage: change the default blue border and shadow to white for better // contrast against a dark gray background. .form-control-focus(@color: @input-border-focus) { @color-rgba: rgba(red(@color), green(@color), blue(@color), .6); &:focus { border-color: @color; outline: 0; box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @color-rgba; } } // Form control sizing // // Relative text size, padding, and border-radii changes for form controls. For // horizontal sizing, wrap controls in the predefined grid classes. `s in some browsers, due to the limited stylability of `s in IE10+. &::-ms-expand { border: 0; background-color: transparent; } // Disabled and read-only inputs // // HTML5 says that controls under a fieldset > legend:first-child won't be // disabled if the fieldset is disabled. Due to implementation difficulty, we // don't honor that edge case; we style them as disabled anyway. &[disabled], &[readonly], fieldset[disabled] & { background-color: @input-bg-disabled; opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655 } &[disabled], fieldset[disabled] & { cursor: @cursor-disabled; } // Reset height for `textarea`s textarea& { height: auto; } } // Form groups // // Designed to help with the organization and spacing of vertical forms. For // horizontal forms, use the predefined grid classes. .form-group { margin-bottom: @form-group-margin-bottom; } // Checkboxes and radios // // Indent the labels to position radios/checkboxes as hanging controls. .radio, .checkbox { position: relative; display: block; margin-top: 10px; margin-bottom: 10px; label { min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text padding-left: 20px; margin-bottom: 0; font-weight: normal; cursor: pointer; } } .radio input[type="radio"], .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] { position: absolute; margin-left: -20px; margin-top: 4px \9; } .radio + .radio, .checkbox + .checkbox { margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing } // Radios and checkboxes on same line .radio-inline, .checkbox-inline { position: relative; display: inline-block; padding-left: 20px; margin-bottom: 0; vertical-align: middle; font-weight: normal; cursor: pointer; } .radio-inline + .radio-inline, .checkbox-inline + .checkbox-inline { margin-top: 0; margin-left: 10px; // space out consecutive inline controls } // Apply same disabled cursor tweak as for inputs // Some special care is needed because