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).

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
[](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.


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

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) | [](https://github.visualstudio.com/Atom/_build/latest?definitionId=32&branch=master) | | | [](https://david-dm.org/atom/atom) |
| [APM](https://github.com/atom/apm) | [](https://dev.azure.com/github/Atom/_build/latest?definitionId=32&branchName=master) | |
| [Electron](https://github.com/electron/electron) | | [](https://circleci.com/gh/electron/electron/tree/master) | [](https://ci.appveyor.com/project/electron-bot/electron-ljo26/branch/master) | [](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) | | [](https://david-dm.org/atom/about) |
| [Archive View](https://github.com/atom/archive-view) | [](https://github.com/atom/archive-view/actions) | [](https://david-dm.org/atom/archive-view) |
| [AutoComplete Atom API](https://github.com/atom/autocomplete-atom-api) | [](https://github.com/atom/autocomplete-atom-api/actions) | [](https://david-dm.org/atom/autocomplete-atom-api) |
| [AutoComplete CSS](https://github.com/atom/autocomplete-css) | [](https://github.com/atom/autocomplete-css/actions) | [](https://david-dm.org/atom/autocomplete-css) |
| [AutoComplete HTML](https://github.com/atom/autocomplete-html) | [](https://github.com/atom/autocomplete-html/actions) | [](https://david-dm.org/atom/autocomplete-html) |
| [AutoComplete+](https://github.com/atom/autocomplete-plus) | [](https://github.com/atom/autocomplete-plus/actions) | [](https://david-dm.org/atom/autocomplete-plus) |
| [AutoComplete Snippets](https://github.com/atom/autocomplete-snippets) | [](https://github.com/atom/autocomplete-snippets/actions) | [](https://david-dm.org/atom/autocomplete-snippets) |
| [AutoFlow](https://github.com/atom/atom/tree/master/packages/autoflow) | | [](https://david-dm.org/atom/autoflow) |
| [AutoSave](https://github.com/atom/autosave) | [](https://github.com/atom/autosave/actions) | [](https://david-dm.org/atom/autosave) |
| [Background Tips](https://github.com/atom/background-tips) | [](https://github.com/atom/background-tips/actions) | [](https://david-dm.org/atom/background-tips) |
| [Bookmarks](https://github.com/atom/bookmarks) | [](https://github.com/atom/bookmarks/actions) | [](https://david-dm.org/atom/bookmarks) |
| [Bracket Matcher](https://github.com/atom/bracket-matcher) | [](https://github.com/atom/bracket-matcher/actions) | [](https://david-dm.org/atom/bracket-matcher) |
| [Command Palette](https://github.com/atom/command-palette) | [](https://github.com/atom/command-palette/actions) | [](https://david-dm.org/atom/command-palette) |
| [Deprecation Cop](https://github.com/atom/atom/tree/master/packages/deprecation-cop) | | [](https://david-dm.org/atom/deprecation-cop) |
| [Dev Live Reload](https://github.com/atom/atom/tree/master/packages/dev-live-reload) | | [](https://david-dm.org/atom/dev-live-reload) |
| [Encoding Selector](https://github.com/atom/encoding-selector) | [](https://github.com/atom/encoding-selector/actions) | [](https://david-dm.org/atom/encoding-selector) |
| [Exception Reporting](https://github.com/atom/atom/tree/master/packages/exception-reporting) | | [](https://david-dm.org/atom/exception-reporting) |
| [Find and Replace](https://github.com/atom/find-and-replace) | [](https://github.com/atom/find-and-replace/actions) | [](https://david-dm.org/atom/find-and-replace) |
| [Fuzzy Finder](https://github.com/atom/fuzzy-finder) | [](https://github.com/atom/fuzzy-finder/actions) | [](https://david-dm.org/atom/fuzzy-finder) |
| [GitHub](https://github.com/atom/github) | [](https://github.com/atom/github/actions?query=workflow%3Aci+branch%3Amaster) | [](https://david-dm.org/atom/github) |
| [Git Diff](https://github.com/atom/atom/tree/master/packages/) | | [](https://david-dm.org/atom/git-diff) |
| [Go to Line](https://github.com/atom/atom/tree/master/packages/) | | [](https://david-dm.org/atom/go-to-line) |
| [Grammar Selector](https://github.com/atom/atom/tree/master/packages/grammar-selector) | | [](https://david-dm.org/atom/grammar-selector) |
| [Image View](https://github.com/atom/image-view) | [](https://github.com/atom/image-view/actions) | [](https://david-dm.org/atom/image-view) |
| [Incompatible Packages](https://github.com/atom/incompatible-packages) | | [](https://david-dm.org/atom/incompatible-packages) |
| [Keybinding Resolver](https://github.com/atom/keybinding-resolver) | [](https://github.com/atom/keybinding-resolver/actions) | [](https://david-dm.org/atom/keybinding-resolver) |
| [Line Ending Selector](https://github.com/atom/atom/tree/master/packages/line-ending-selector) | | [](https://david-dm.org/atom/line-ending-selector) |
| [Link](https://github.com/atom/atom/tree/master/packages/link) | | [](https://david-dm.org/atom/link) |
| [Markdown Preview](https://github.com/atom/markdown-preview) | [](https://github.com/atom/markdown-preview/actions) | [](https://david-dm.org/atom/markdown-preview) |
| [Metrics](https://github.com/atom/metrics) | [](https://github.com/atom/metrics/actions) | [](https://david-dm.org/atom/metrics) |
| [Notifications](https://github.com/atom/notifications) | [](https://github.com/atom/notifications/actions) | [](https://david-dm.org/atom/notifications) |
| [Open on GitHub](https://github.com/atom/open-on-github) | [](https://github.com/atom/open-on-github/actions) | [](https://david-dm.org/atom/open-on-github) |
| [Package Generator](https://github.com/atom/package-generator) | [](https://github.com/atom/package-generator/actions) | [](https://david-dm.org/atom/package-generator) |
| [Settings View](https://github.com/atom/settings-view) | [](https://github.com/atom/settings-view/actions) | [](https://david-dm.org/atom/settings-view) |
| [Snippets](https://github.com/atom/snippets) | [](https://github.com/atom/snippets/actions) | [](https://david-dm.org/atom/snippets) |
| [Spell Check](https://github.com/atom/spell-check) | [](https://github.com/atom/spell-check/actions) | [](https://david-dm.org/atom/spell-check) |
| [Status Bar](https://github.com/atom/status-bar) | [](https://github.com/atom/status-bar/actions) | [](https://david-dm.org/atom/status-bar) |
| [Styleguide](https://github.com/atom/styleguide) | [](https://github.com/atom/styleguide/actions) | [](https://david-dm.org/atom/styleguide) |
| [Symbols View](https://github.com/atom/symbols-view) | [](https://github.com/atom/symbols-view/actions) | [](https://david-dm.org/atom/symbols-view) |
| [Tabs](https://github.com/atom/tabs) | [](https://github.com/atom/tabs/actions) | [](https://david-dm.org/atom/tabs) |
| [Timecop](https://github.com/atom/timecop) | [](https://github.com/atom/timecop/actions) | [](https://david-dm.org/atom/timecop) |
| [Tree View](https://github.com/atom/tree-view) | [](https://github.com/atom/tree-view/actions) | [](https://david-dm.org/atom/tree-view) |
| [Update Package Dependencies](https://github.com/atom/atom/tree/master/packages/update-package-dependencies) | | [](https://david-dm.org/atom/update-package-dependencies) |
| [Welcome](https://github.com/atom/atom/tree/master/packages/welcome) | | [](https://david-dm.org/atom/welcome) |
| [Whitespace](https://github.com/atom/whitespace) | [](https://github.com/atom/whitespace/actions) | [](https://david-dm.org/atom/whitespace) |
| [Wrap Guide](https://github.com/atom/wrap-guide) | [](https://github.com/atom/wrap-guide/actions) | [](https://david-dm.org/atom/wrap-guide) |
## Libraries
| Library | Github Actions | Dependencies |
|---------|----------------|--------------|
| [Clear Cut](https://github.com/atom/clear-cut) | | [](https://david-dm.org/atom/clear-cut) |
| [Event Kit](https://github.com/atom/event-kit) | [](https://github.com/atom/event-kit/actions) | [](https://david-dm.org/atom/event-kit) |
| [First Mate](https://github.com/atom/first-mate) | [](https://github.com/atom/first-mate/actions) | [](https://david-dm.org/atom/first-mate) |
| [Fs Plus](https://github.com/atom/fs-plus) | [](https://github.com/atom/fs-plus/actions) | [](https://david-dm.org/atom/fs-plus) |
| [Grim](https://github.com/atom/grim) | [](https://github.com/atom/grim/actions) | [](https://david-dm.org/atom/grim) |
| [Jasmine Focused](https://github.com/atom/jasmine-focused) | | [](https://david-dm.org/atom/jasmine-focused) |
| [Keyboard Layout](https://github.com/atom/keyboard-layout) | [](https://github.com/atom/keyboard-layout/actions) | [](https://david-dm.org/atom/keyboard-layout) |
| [Oniguruma](https://github.com/atom/node-oniguruma) | [](https://github.com/atom/node-oniguruma/actions) | [](https://david-dm.org/atom/node-oniguruma) |
| [PathWatcher](https://github.com/atom/node-pathwatcher) | [](https://github.com/atom/node-pathwatcher/actions) | [](https://david-dm.org/atom/node-pathwatcher) |
| [Property Accessors](https://github.com/atom/property-accessors) | | [](https://david-dm.org/atom/property-accessors) |
| [Season](https://github.com/atom/season) | | [](https://david-dm.org/atom/season) |
| [Superstring](https://github.com/atom/superstring) | [](https://github.com/atom/superstring/actions) | [](https://david-dm.org/atom/superstring) |
| [TextBuffer](https://github.com/atom/text-buffer) | [](https://github.com/atom/text-buffer/actions) | [](https://david-dm.org/atom/text-buffer) |
| [Underscore-Plus](https://github.com/atom/underscore-plus) | [](https://github.com/atom/underscore-plus/actions) | [](https://david-dm.org/atom/underscore-plus) |
## Tools
| Language | Github Actions | Dependencies |
|----------|----------------|--------------|
| [AtomDoc](https://github.com/atom/atomdoc) | [](https://github.com/atom/atomdoc/actions) | [](https://david-dm.org/atom/atomdoc)
## Languages
| Language | Github Actions |
|----------|----------------|
| [C/C++](https://github.com/atom/language-c) | [](https://github.com/atom/language-c/actions) |
| [C#](https://github.com/atom/language-csharp) | [](https://github.com/atom/language-csharp/actions) |
| [Clojure](https://github.com/atom/language-clojure) | [](https://github.com/atom/language-clojure/actions) |
| [CoffeeScript](https://github.com/atom/language-coffee-script) | [](https://github.com/atom/language-coffee-script/actions) |
| [CSS](https://github.com/atom/language-css) | [](https://github.com/atom/language-css/actions) |
| [Git](https://github.com/atom/language-git) | [](https://github.com/atom/language-git/actions) |
| [GitHub Flavored Markdown](https://github.com/atom/language-gfm) | [](https://github.com/atom/language-gfm/actions) |
| [Go](https://github.com/atom/language-go) | [](https://github.com/atom/language-go/actions) |
| [HTML](https://github.com/atom/language-html) | [](https://github.com/atom/language-html/actions) |
| [Hyperlink](https://github.com/atom/language-hyperlink) | [](https://github.com/atom/language-hyperlink/actions) |
| [Java](https://github.com/atom/language-java) | [](https://github.com/atom/language-java/actions) |
| [JavaScript](https://github.com/atom/language-javascript) | [](https://github.com/atom/language-javascript/actions) |
| [JSON](https://github.com/atom/language-json) | [](https://github.com/atom/language-json/actions) |
| [Less](https://github.com/atom/language-less) | [](https://github.com/atom/language-less/actions) |
| [Make](https://github.com/atom/language-make) | [](https://github.com/atom/language-make/actions) |
| [Mustache](https://github.com/atom/language-mustache) | [](https://github.com/atom/language-mustache/actions) |
| [Objective-C](https://github.com/atom/language-objective-c) | [](https://github.com/atom/language-objective-c/actions) |
| [Pegjs](https://github.com/atom/language-pegjs) | |
| [Perl](https://github.com/atom/language-perl) | [](https://github.com/atom/language-perl/actions) |
| [PHP](https://github.com/atom/language-php) | [](https://github.com/atom/language-php/actions) |
| [Property-List](https://github.com/atom/language-property-list) | [](https://github.com/atom/language-property-list/actions) |
| [Puppet](https://github.com/atom/language-puppet) | [](https://github.com/atom/language-puppet/actions) |
| [Python](https://github.com/atom/language-python) | [](https://github.com/atom/language-python/actions) |
| [Ruby](https://github.com/atom/language-ruby) | [](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) | [](https://github.com/atom/language-sass/actions) |
| [Shellscript](https://github.com/atom/language-shellscript) | [](https://github.com/atom/language-shellscript/actions) |
| [Source](https://github.com/atom/language-source) | [](https://github.com/atom/language-source/actions) |
| [SQL](https://github.com/atom/language-sql) | [](https://github.com/atom/language-sql/actions) |
| [Text](https://github.com/atom/language-text) | [](https://github.com/atom/language-text/actions) |
| [TODO](https://github.com/atom/language-todo) | [](https://github.com/atom/language-todo/actions) |
| [TOML](https://github.com/atom/language-toml) | [](https://github.com/atom/language-toml/actions) |
| [TypeScript](https://github.com/atom/language-typescript) | [](https://github.com/atom/language-typescript/actions) |
| [XML](https://github.com/atom/language-xml) | [](https://github.com/atom/language-xml/actions) |
| [YAML](https://github.com/atom/language-yaml) | [](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

* 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.

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.

================================================
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.

================================================
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.

================================================
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.

================================================
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.

## 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.

## 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!

================================================
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 (
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
.
");
```
- [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
 |  |  |  | 
--- | --- | --- | --- | --- |
Ú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("
");
```
- [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
 |  |  |  | 
--- | --- | --- | --- | --- |
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("
");
// 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
 |  |  |  | 
--- | --- | --- | --- | --- |
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("
");
```
- [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)
## Поддержка браузеров
 |  |  |  | 
--- | --- | --- | --- | --- |
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 [](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
 |  |  |  | 
--- | --- | --- | --- | --- |
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("
");
```
- [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
 |  |  |  | 
--- | --- | --- | --- | --- |
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("
");
// 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
 |  |  |  | 
--- | --- | --- | --- | --- |
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
================================================

# 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
[](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(+QJvN1iHL
================================================
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 = (
`.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.

## 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 `